Spraxium logoSpraxium

Visão Geral dos Componentes

Entenda a arquitetura por trás dos componentes interativos do Discord no Spraxium. Aprenda o modelo baseado em decorators, a separação entre componente e handler, o contexto de fluxo, o ciclo de vida do dispatcher e os padrões compartilhados entre botões, selects, modais e embeds.

O que são componentes

O pacote @spraxium/components oferece um sistema orientado por decorators para montar elementos interativos de interface no Discord. Botões, menus de seleção, modais e embeds são definidos como classes decoradas, conectados por metadados e despachados automaticamente quando alguém interage com o bot. O pacote também traz um sistema de contexto de fluxo, que transporta dados com estado entre interações em várias etapas sem exigir gerenciamento manual de estado.

Em vez de trabalhar com builders crus do discord.js e montar tudo de forma imperativa, o Spraxium usa decorators de classe para declarar a aparência do componente, decorators separados para definir o que acontece na interação e serviços injetáveis para renderizar tudo em runtime. Essa separação deixa a definição reaproveitável, facilita testes nos handlers e mantém a lógica de negócio isolada da renderização.

O pacote vem com cinco famílias de componentes: botões, incluindo versões estáticas, de link e dinâmicas; menus de seleção, como string, user, role, mentionable e channel; modais, com 11 tipos de campo, validação e cache; embeds declarativos guiados por schema; e containers v2 para composição de layout. Todas seguem o mesmo padrão de decorator, handler e service, então o que você aprende em uma área se reaproveita direto nas outras.

Configuração do plugin

O módulo lê sua configuração a partir de uma entrada de plugin criada por defineComponents. Essa factory recebe um objeto ComponentsConfig que controla armazenamento de contexto, mensagens de erro e comportamentos específicos do sistema de componentes. O registro segue o mesmo padrão dos outros plugins do Spraxium: você chama a factory e adiciona o resultado ao array plugins em spraxium.config.ts.

config/components.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.',
  },
});

O campo context.storage escolhe o backend usado para persistir os contextos de fluxo. Existem quatro adapters prontos: memory para desenvolvimento, filesystem para bots de instância única, redis para ambientes com múltiplas instâncias e sqlite para persistência leve. Já o defaultTtl define, em segundos, o tempo de expiração dos contextos que não especificarem um TTL próprio.

CampoTipoPadrãoFinalidade
context.storagememory, filesystem, redis ou sqlitememory

Backend de persistência do contexto de fluxo. memory é volátil, os demais sobrevivem a reinícios.

context.defaultTtlnumber300

Tempo padrão de vida, em segundos, para contextos que não definem um TTL específico.

errorMessages.expiredstringMensagem interna em inglês

Texto de resposta quando alguém clica em um componente cujo contexto já expirou.

errorMessages.restrictedstringMensagem interna em inglês

Texto de resposta quando um usuário sem permissão tenta usar o componente.

O padrão decorator-handler

Todo componente interativo no Spraxium segue uma arquitetura de duas classes: uma define como o componente se parece, e a outra define o que acontece quando alguém interage com ele. Essa separação existe porque componentes do Discord são renderizados uma vez, mas podem ser clicados ou submetidos várias vezes, por pessoas diferentes. Quando a definição visual fica separada da lógica, o componente vira algo reaproveitável e o handler pode ser testado de forma isolada.

A classe do componente recebe um decorator como @Button, @StringSelect ou @ModalComponent, responsável por guardar metadados de aparência e identidade. A classe do handler recebe o decorator correspondente, como @ButtonHandler, @StringSelectHandler ou @ModalHandler, apontando para a classe do componente. Em runtime, o dispatcher lê os dois lados, encontra o handler certo para a interação recebida e chama o método handle com os parâmetros injetados.

import { Button } from '@spraxium/components';

@Button({
  customId: 'confirm_action',
  label: 'Confirm',
  style: 'success',
})
export class ConfirmButton {}

Ligação com o módulo

ComponentsModule é um módulo global que fornece todos os serviços de componente, como ButtonService, SelectService, ModalService, EmbedService, V2Service e ContextService. Você não precisa importá-lo diretamente. Basta adicionar as classes de handler ao array handlers do módulo da sua feature, e os serviços passam a ficar injetáveis automaticamente. Já as classes decoradas com @Button, @StringSelect, @ModalComponent ou @Embed existem apenas como metadados e nunca entram em módulo nenhum.

src/modules/settings/settings.module.ts
import { Module } from '@spraxium/common';
import { ConfirmButtonHandler } from './components/confirm-btn.handler';

@Module({
  handlers: [ConfirmButtonHandler],
})
export class SettingsModule {}

Só a classe do handler vai para o array handlers. A configuração do componente fica salva como metadado de construtor por meio de Reflect.defineMetadata. O dispatcher lê esse metadado direto da referência da classe vinculada ao decorator do handler, então não existe necessidade de instanciar a classe do componente via DI.

Ciclo de vida no bootstrap

Varredura de instâncias

Quando o loader de módulos instancia providers e handlers, um scanner registrado por ComponentsModule empurra cada instância para um buffer chamado pendingInstances. Isso acontece durante a fase normal de injeção de dependência, antes de qualquer hook de ciclo de vida disparar. O scanner coleta todas as classes, e a filtragem específica de componentes fica para a etapa seguinte.

Leitura da configuração

O hook ComponentLifecycle.onBoot lê a configuração do plugin a partir do ConfigStore usando a factory defineComponents. Quando a configuração existe, ele aplica a config no dispatcher e inicializa o adapter de armazenamento de contexto com o TTL definido.

Registro dos handlers

O dispatcher esvazia o buffer pendingInstances e verifica cada classe em busca de metadados de handler. Classes decoradas com @ButtonHandler, @SelectHandler ou @ModalHandler são registradas em seus respectivos sub-dispatchers. Cada sub-dispatcher lê o metadado do componente vinculado para extrair o customId ou o prefix usado no roteamento.

Binding de eventos

Se ao menos um handler tiver sido registrado, o dispatcher anexa um listener ao evento interactionCreate do cliente do Discord. Quando uma interação chega, o dispatcher interpreta o customId, identifica o sub-dispatcher correto, resolve o contexto de fluxo se ele existir e invoca o método handle com os parâmetros injetados.

Anatomia do custom ID

Todo componente interativo do Discord carrega uma string customId para que a interação seja identificada depois. No Spraxium, esse valor segue um formato estruturado que codifica a identidade do componente e, opcionalmente, a referência do contexto de fluxo. O caractere til (~) separa o ID base do ID do contexto.

PadrãoExemploSignificado
Botão estáticoconfirm_actionID fixo, comparado por igualdade exata.
Estático com contextoconfirm_action~a1b2c3

ID estático mais o UUID do contexto após o til. O contexto é resolvido antes do handler.

Botão dinâmicouser_ban:12345

Prefixo, dois-pontos e sufixo de dados. O matching acontece pelo prefixo, e os dados são extraídos para o handler.

Dinâmico com contextouser_ban:12345~a1b2c3Prefixo dinâmico mais dados e UUID do contexto.

O helper splitCustomId interpreta esses formatos internamente. Quando encontra um til, o dispatcher extrai o ID do contexto, busca o SpraxiumContext no adapter configurado, verifica expiração e restrições de usuário e injeta o resultado pelo decorator de parâmetro @FlowContext.

Injeção de parâmetros

Os métodos handle dos handlers recebem seus argumentos por meio de decorators de parâmetro. O dispatcher resolve cada parâmetro no momento da invocação com base nos metadados presos à assinatura do método. O comportamento é consistente em todos os tipos de componente, então os mesmos decorators aparecem em handlers de botão, select e modal.

DecoradorInjetaDisponível em
@Ctx()

O objeto bruto de interação do Discord, como ButtonInteraction, AnySelectMenuInteraction ou ModalSubmitInteraction.

Todos os handlers
@FlowContext()

O objeto SpraxiumContext com data tipado e metadados. Só aparece quando o componente foi renderizado com contexto.

Todos os handlers
@Selected()

A interação completa do select menu. Útil para acessar values, user, member e outros dados específicos de selects.

Handlers de select
@SelectedValues()

O array de strings vindo de interaction.values. É um atalho prático para handlers de string select.

Handlers de select
@Field(id)

Um valor individual de campo de modal extraído pelo ID do campo.

Handlers de modal

Renderização dos componentes

Cada família de componentes tem um serviço injetável que lê os metadados dos decorators e produz builders do discord.js. Você injeta esse serviço em command handlers, listeners ou outros serviços e chama o método build para gerar action rows prontas para responder mensagens.

src/commands/settings.command-handler.ts
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],
    });
  }
}

O método build aceita uma ou mais classes de componente e devolve um ActionRowBuilder. A variante rowWithContext anexa um ID de contexto a cada botão da linha. Em selects, SelectService.build cuida tanto de opções estáticas quanto dinâmicas. Em modais, ModalService.build monta o modal completo a partir do schema descrito nos metadados.

Conceitos compartilhados entre as famílias

Vários padrões de arquitetura se repetem entre botões, selects, modais e embeds. Quando você entende esses conceitos uma vez, consegue aplicar o mesmo raciocínio em qualquer tipo de componente sem reaprender a base do zero. A separação entre decorator e handler, a renderização guiada por metadados, o encadeamento de contexto e a injeção de parâmetros funcionam do mesmo jeito em todas as famílias.

A documentação desta categoria cobre com mais detalhe botões, selects, embeds e o sistema de contexto. Já os modais têm uma categoria própria, porque o sistema de campos, a validação, o cache e a construção dinâmica de schema introduzem conceitos bem particulares de interações em formato de formulário.