What are components
The @spraxium/components package provides a decorator-driven system for building interactive Discord UI elements. Buttons, select menus, modals, and embeds are all defined as decorated classes, wired through metadata, and dispatched automatically when users interact with your bot. The package also includes a flow context system that threads stateful data across multi-step interactions without manual state management.
Unlike raw discord.js builders where you construct every object imperatively, Spraxium components use class decorators to declare what a component looks like, separate handler decorators to define what happens when a user interacts, and injectable services to render components at runtime. This separation keeps component definitions reusable, handlers testable, and rendering decoupled from business logic.
The package ships five component families: buttons (static, link, and dynamic), select menus (string, user, role, mentionable, channel), modals (with 11 field types, validation, and caching), embeds (declarative schema-driven), and v2 containers (composable layout blocks). Each family follows the same decorator-handler-service pattern, so learning one transfers directly to the others.
Plugin configuration
The module reads its configuration from a plugin entry produced by defineComponents. This factory accepts a ComponentsConfig object that controls context storage, error messages, and per-component behavior. Registration follows the same pattern as every Spraxium plugin: call the factory, add the result to the plugins array in spraxium.config.ts.
import { defineComponents } from '@spraxium/components';
export const componentsPlugin = defineComponents({
context: {
storage: 'memory',
defaultTtl: 300,
},
errorMessages: {
expired: 'This interaction has expired.',
restricted: 'You are not allowed to use this component.',
},
});The context.storage field selects the persistence backend for flow contexts. Four adapters are available: 'memory' for development, 'filesystem' for single-instance bots, 'redis' for multi-instance deployments, and 'sqlite' for lightweight persistence. The defaultTtl sets the expiration in seconds for contexts that do not specify their own TTL.
| Field | Type | Default | Purpose |
|---|---|---|---|
| context.storage | memory, filesystem, redis, or sqlite | memory | Persistence backend for flow contexts. Memory is volatile, others survive restarts. |
| context.defaultTtl | number | 300 | Default time-to-live for contexts in seconds. Individual contexts can override this. |
| errorMessages.expired | string | Built-in English message | Reply text when a user clicks a component whose context has expired. |
| errorMessages.restricted | string | Built-in English message | Reply text when a user restricted from using a component tries to interact. |
The decorator-handler pattern
Every interactive component in Spraxium follows a two-class architecture: one class defines what the component looks like, and a separate class defines what happens when the user interacts with it. This separation exists because Discord components are rendered once but can be clicked many times by different users. Keeping the visual definition apart from the interaction logic makes components reusable across multiple messages and handlers independently testable.
The component class carries a decorator like @Button, @StringSelect, or @ModalComponent that stores metadata about appearance and identity. The handler class carries a corresponding decorator like @ButtonHandler, @StringSelectHandler, or @ModalHandler that references the component class. At runtime, the dispatcher reads both sets of metadata, matches incoming interactions to the correct handler, and invokes the handle method with injected parameters.
import { Button } from '@spraxium/components';
@Button({
customId: 'confirm_action',
label: 'Confirm',
style: 'success',
})
export class ConfirmButton {}Module wiring
The ComponentsModule is a global module that provides all component services (ButtonService, SelectService, ModalService, EmbedService, V2Service, ContextService). You do not import it directly. Add your handler classes to the handlers array of your feature module and the services become injectable automatically. Component classes decorated with @Button, @StringSelect, @ModalComponent, or @Embed are metadata-only and are never registered in any module.
import { Module } from '@spraxium/common';
import { ConfirmButtonHandler } from './components/confirm-btn.handler';
@Module({
handlers: [ConfirmButtonHandler],
})
export class SettingsModule {}Only the handler class goes in the handlers array. Component classes store their configuration as constructor-level metadata via Reflect.defineMetadata. The dispatcher reads that metadata directly from the class reference stored inside each handler's decorator, so no DI instantiation of component classes is ever needed.
Bootstrap lifecycle
Instance scanning
When the module loader instantiates each provider and handler, an instance scanner registered by ComponentsModule pushes every instance into a pendingInstances buffer. This happens during the standard dependency injection phase, before any lifecycle hooks fire. The scanner collects all classes, not just component-related ones, because filtering happens in the next phase.
Configuration loading
The ComponentLifecycle.onBoot hook reads the plugin configuration from ConfigStore using the defineComponents factory. If configuration is present, it sets the dispatcher config and initializes the selected context storage adapter with the configured TTL.
Handler registration
The dispatcher drains the pendingInstances buffer and checks each class for handler metadata. Classes decorated with @ButtonHandler, @SelectHandler, or @ModalHandler are registered with their respective sub-dispatchers. The sub-dispatcher reads the linked component's metadata to extract the customId or prefix used for routing.
Event binding
If at least one handler was registered, the dispatcher attaches a listener to the Discord client's interactionCreate event. When an interaction arrives, the dispatcher parses the customId, identifies the correct sub-dispatcher, resolves the flow context if present, and invokes the handler's handle method with injected parameters.
Custom ID anatomy
Every interactive Discord component carries a customId string that identifies it when the user interacts. Spraxium uses a structured format to encode both the component identity and optional flow context into this string. The tilde character (~) separates the base ID from the context ID.
| Pattern | Example | Meaning |
|---|---|---|
| Static button | confirm_action | Fixed ID, matched by exact equality. |
| Static with context | confirm_action~a1b2c3 | Static ID + context UUID after tilde. Context is resolved before handler. |
| Dynamic button | user_ban:12345 | Prefix + colon + data suffix. Matched by prefix, data extracted for handler. |
| Dynamic with context | user_ban:12345~a1b2c3 | Dynamic prefix + data + context UUID. |
The splitCustomId helper parses these patterns internally. When a tilde is present, the dispatcher extracts the context ID, retrieves the SpraxiumContext from the configured storage adapter, checks expiration and user restrictions, and injects it via the @FlowContext parameter decorator.
Parameter injection
Handler handle methods receive their arguments through parameter decorators. The dispatcher resolves each parameter at invocation time based on the decorator metadata attached to the method signature. This system is consistent across all component types, so the same decorators work in button handlers, select handlers, and modal handlers.
| Decorator | Injects | Available in |
|---|---|---|
| @Ctx() | The raw Discord interaction object (ButtonInteraction, AnySelectMenuInteraction, ModalSubmitInteraction). | All handlers |
| @FlowContext() | The SpraxiumContext object with typed data and metadata. Only available when the component was rendered with a context. | All handlers |
| @Selected() | The full select menu interaction. Useful for accessing values, user, member, and other select-specific data. | Select handlers |
| @SelectedValues() | The | Select handlers |
| @Field(id) | A single modal field value extracted by its field ID. | Modal handlers |
Rendering components
Each component family has an injectable service that reads decorator metadata and produces discord.js builder objects. You inject the service into your command handlers, listeners, or other services and call its build method to produce action rows ready for message replies.
import { Ctx, SlashCommandHandler } from '@spraxium/common';
import { ButtonService, ContextService } from '@spraxium/components';
import type { ChatInputCommandInteraction } from 'discord.js';
import { ConfirmButton } from '../components/confirm-btn.component';
import { CancelButton } from '../components/cancel-btn.component';
import { SettingsCommand } from './settings.command';
@SlashCommandHandler(SettingsCommand)
export class SettingsCommandHandler {
constructor(
private readonly buttons: ButtonService,
private readonly context: ContextService,
) {}
async handle(@Ctx() interaction: ChatInputCommandInteraction): Promise<void> {
const ctx = await this.context.create({ guildId: interaction.guildId });
const row = this.buttons.rowWithContext(ctx, ConfirmButton, CancelButton);
await interaction.reply({
content: 'Are you sure you want to reset settings?',
components: [row],
});
}
}The build method accepts one or more component classes and returns an ActionRowBuilder. The rowWithContext variant attaches a context ID to every button in the row. For select menus, SelectService.build handles both static and dynamic options. For modals, ModalService.build constructs the full modal from schema metadata.
Shared concepts across families
Several architectural patterns recur across buttons, selects, modals, and embeds. Understanding these shared concepts once means you can apply them to any component type without relearning the fundamentals. The decorator-handler separation, metadata-driven rendering, context threading, and parameter injection all work identically regardless of the component family.
The documentation in this category covers buttons, selects, embeds, and the context system in detail. Modals have their own dedicated category because their field system, validation pipeline, caching mechanism, and dynamic schema building introduce concepts that are unique to form-like interactions and warrant deeper coverage.