Qué son los componentes
El paquete @spraxium/components ofrece un sistema orientado a decorators para construir elementos interactivos de interfaz en Discord. Botones, menús de selección, modales y embeds se definen como clases decoradas, se conectan por metadatos y se despachan automáticamente cuando alguien interactúa con el bot. El paquete también incluye un sistema de contexto de flujo que transporta datos con estado entre interacciones de varias etapas sin requerir gestión manual del estado.
En lugar de trabajar con builders crudos de discord.js y montar todo de forma imperativa, Spraxium usa decorators de clase para declarar la apariencia del componente, decorators separados para definir qué ocurre en la interacción y servicios inyectables para renderizar todo en runtime. Esta separación hace la definición reutilizable, facilita pruebas en handlers y mantiene la lógica de negocio aislada del renderizado.
El paquete incluye cinco familias de componentes: botones, incluyendo versiones estáticas, de enlace y dinámicas; menús de selección, como string, user, role, mentionable y channel; modales, con 11 tipos de campo, validación y caché; embeds declarativos guiados por schema; y contenedores v2 para composición de layout. Todas siguen el mismo patrón de decorator, handler y service, por lo que lo que aprendes en una área se reutiliza directamente en las demás.
Configuración del plugin
El módulo lee su configuración desde una entrada de plugin creada con defineComponents. Esta factory recibe un objeto ComponentsConfig que controla almacenamiento de contexto, mensajes de error y comportamientos específicos del sistema de componentes. El registro sigue el mismo patrón de los demás plugins de Spraxium: llamas a la factory y agregas el resultado al array plugins en 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.',
},
});El campo context.storage elige el backend usado para persistir los contextos de flujo. Hay cuatro adapters listos: memory para desarrollo, filesystem para bots de instancia única, redis para entornos con múltiples instancias y sqlite para persistencia ligera. Ya defaultTtl define, en segundos, el tiempo de expiración de contextos que no especifiquen un TTL propio.
| Campo | Tipo | Predeterminado | Finalidad |
|---|---|---|---|
| context.storage | memory, filesystem, redis o sqlite | memory | Backend de persistencia del contexto de flujo. memory es volátil; los demás sobreviven a reinicios. |
| context.defaultTtl | number | 300 | Tiempo de vida predeterminado, en segundos, para contextos que no definen un TTL específico. |
| errorMessages.expired | string | Mensaje interno en inglés | Texto de respuesta cuando alguien hace clic en un componente cuyo contexto ya expiró. |
| errorMessages.restricted | string | Mensaje interno en inglés | Texto de respuesta cuando un usuario sin permiso intenta usar el componente. |
El patrón decorator-handler
Todo componente interactivo en Spraxium sigue una arquitectura de dos clases: una define cómo se ve el componente y la otra define qué pasa cuando alguien interactúa con él. Esta separación existe porque los componentes de Discord se renderizan una vez, pero pueden ser clicados o enviados varias veces, por personas distintas. Cuando la definición visual queda separada de la lógica, el componente se vuelve reutilizable y el handler puede probarse de forma aislada.
La clase del componente recibe un decorator como @Button, @StringSelect o @ModalComponent, responsable de guardar metadatos de apariencia e identidad. La clase del handler recibe el decorator correspondiente, como @ButtonHandler, @StringSelectHandler o @ModalHandler, apuntando a la clase del componente. En runtime, el dispatcher lee ambos lados, encuentra el handler correcto para la interacción recibida y llama al método handle con parámetros inyectados.
import { Button } from '@spraxium/components';
@Button({
customId: 'confirm_action',
label: 'Confirm',
style: 'success',
})
export class ConfirmButton {}Integración con el módulo
ComponentsModule es un módulo global que provee todos los servicios de componente, como ButtonService, SelectService, ModalService, EmbedService, V2Service y ContextService. No necesitas importarlo directamente. Basta agregar las clases de handler al array handlers del módulo de tu feature, y los servicios quedan inyectables automáticamente. Las clases decoradas con @Button, @StringSelect, @ModalComponent o @Embed existen solo como metadatos y nunca entran en ningún módulo.
import { Module } from '@spraxium/common';
import { ConfirmButtonHandler } from './components/confirm-btn.handler';
@Module({
handlers: [ConfirmButtonHandler],
})
export class SettingsModule {}Solo la clase del handler va al array handlers. La configuración del componente se guarda como metadato de constructor por medio de Reflect.defineMetadata. El dispatcher lee ese metadato directamente desde la referencia de clase vinculada al decorator del handler, por lo que no hay necesidad de instanciar la clase del componente vía DI.
Ciclo de vida en el bootstrap
Escaneo de instancias
Cuando el loader de módulos instancia providers y handlers, un scanner registrado por ComponentsModule empuja cada instancia a un buffer llamado pendingInstances. Esto ocurre durante la fase normal de inyección de dependencias, antes de que se dispare cualquier hook de ciclo de vida. El scanner recopila todas las clases, y el filtrado específico de componentes queda para la etapa siguiente.
Lectura de configuración
El hook ComponentLifecycle.onBoot lee la configuración del plugin desde ConfigStore usando la factory defineComponents. Cuando la configuración existe, aplica la config en el dispatcher e inicializa el adapter de almacenamiento de contexto con el TTL definido.
Registro de handlers
El dispatcher vacía el buffer pendingInstances y verifica cada clase en busca de metadatos de handler. Clases decoradas con @ButtonHandler, @SelectHandler o @ModalHandler se registran en sus respectivos sub-dispatchers. Cada sub-dispatcher lee el metadato del componente vinculado para extraer el customId o el prefix usado en el ruteo.
Binding de eventos
Si al menos un handler fue registrado, el dispatcher adjunta un listener al evento interactionCreate del cliente de Discord. Cuando llega una interacción, el dispatcher interpreta el customId, identifica el sub-dispatcher correcto, resuelve el contexto de flujo si existe e invoca el método handle con los parámetros inyectados.
Anatomía del custom ID
Todo componente interactivo de Discord lleva una string customId para que la interacción pueda identificarse después. En Spraxium, ese valor sigue un formato estructurado que codifica la identidad del componente y, opcionalmente, la referencia del contexto de flujo. El carácter til (~) separa el ID base del ID de contexto.
| Patrón | Ejemplo | Significado |
|---|---|---|
| Botón estático | confirm_action | ID fijo, comparado por igualdad exacta. |
| Estático con contexto | confirm_action~a1b2c3 | ID estático más el UUID del contexto después del til. El contexto se resuelve antes del handler. |
| Botón dinámico | user_ban:12345 | Prefijo, dos puntos y sufijo de datos. El matching ocurre por prefijo, y los datos se extraen para el handler. |
| Dinámico con contexto | user_ban:12345~a1b2c3 | Prefijo dinámico más datos y UUID de contexto. |
El helper splitCustomId interpreta estos formatos internamente. Cuando encuentra un til, el dispatcher extrae el ID del contexto, busca el SpraxiumContext en el adapter configurado, verifica expiración y restricciones de usuario, e inyecta el resultado con el decorator de parámetro @FlowContext.
Inyección de parámetros
Los métodos handle de los handlers reciben sus argumentos mediante decorators de parámetro. El dispatcher resuelve cada parámetro en el momento de la invocación con base en los metadatos unidos a la firma del método. El comportamiento es consistente en todos los tipos de componente, por eso los mismos decorators aparecen en handlers de botón, select y modal.
| Decorador | Inyecta | Disponible en |
|---|---|---|
| @Ctx() | El objeto bruto de interacción de Discord, como ButtonInteraction, AnySelectMenuInteraction o ModalSubmitInteraction. | Todos los handlers |
| @FlowContext() | El objeto SpraxiumContext con data tipado y metadatos. Solo aparece cuando el componente fue renderizado con contexto. | Todos los handlers |
| @Selected() | La interacción completa del select menu. Útil para acceder a values, user, member y otros datos específicos de selects. | Handlers de select |
| @SelectedValues() | El array de strings proveniente de interaction.values. Es un atajo práctico para handlers de string select. | Handlers de select |
| @Field(id) | Un valor individual de campo de modal extraído por el ID del campo. | Handlers de modal |
Renderizado de componentes
Cada familia de componentes tiene un servicio inyectable que lee metadatos de decorators y produce builders de discord.js. Inyectas ese servicio en command handlers, listeners u otros servicios y llamas al método build para generar action rows listas para responder mensajes.
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],
});
}
}El método build acepta una o más clases de componente y devuelve un ActionRowBuilder. La variante rowWithContext adjunta un ID de contexto a cada botón de la fila. En selects, SelectService.build maneja tanto opciones estáticas como dinámicas. En modales, ModalService.build compone el modal completo a partir del schema descrito en los metadatos.
Conceptos compartidos entre familias
Varios patrones de arquitectura se repiten entre botones, selects, modales y embeds. Cuando entiendes estos conceptos una vez, puedes aplicar el mismo razonamiento en cualquier tipo de componente sin reaprender la base desde cero. La separación entre decorator y handler, el renderizado guiado por metadatos, el encadenamiento de contexto y la inyección de parámetros funcionan igual en todas las familias.
La documentación de esta categoría cubre en mayor detalle botones, selects, embeds y el sistema de contexto. Los modales tienen una categoría propia porque el sistema de campos, la validación, la caché y la construcción dinámica de schema introducen conceptos muy particulares de interacciones tipo formulario.