Guide
Using Signals

Using Signals

Signals are a way to add states and basic reactivity to your commands. It is similar to SolidJS signals.

Creating a signal

Creating a signal is simple, you just need to call the createSignal function from commandkit. A signal in commandkit is a function that returns the current value of the signal. It also has a set function that can be used to update the value of the signal. The dispose function can be used to dispose the signal and all its subscribers.

signal.js
const { createSignal } = require('commandkit');
 
// createValue accepts one param (the initial value)
const [value, setValue, disposeValueSubscribers] = createSignal(0);
 
// Log the initial value
console.log(value()); // -> 0
 
// Update the value
setValue(1);
 
// Log the new value
console.log(value()); // -> 1
 
// Dispose the value's subscribers
disposeValueSubscribers();

Handling side effects

You can also handle side effects with signals, by using the createEffect function. Side effects are functions that run every time the signal value changes.

signal.js
const { createSignal, createEffect } = require('commandkit');
 
const [value, setValue, disposeValueSubscribers] = createSignal(0);
 
// This will run every time the value changes
createEffect(() => {
    console.log(`Current value is ${value()}`);
});
 
setValue(1); // Logs "Current value is 1"
setValue(2); // Logs "Current value is 2"
 
// Dispose the value's subscribers
disposeValueSubscribers();
 
// Logs nothing because subscribers were disposed
setValue(3);

Count command example

commands/counter.js
const {
    createSignal,
    createEffect,
    ButtonKit
} = require('commandkit');
const { ButtonStyle, ActionRowBuilder } = require('discord.js');
 
exports.data = {
    name: 'counter',
    description: 'A simple counter command',
};
 
function getButtons() {
    // Decrement button
    const dec = new ButtonKit()
        .setEmoji('➖')
        .setStyle(ButtonStyle.Primary)
        .setCustomId('decrement');
 
    // Reset button
    const reset = new ButtonKit()
        .setEmoji('0️⃣')
        .setStyle(ButtonStyle.Primary)
        .setCustomId('reset');
 
    // Increment button
    const inc = new ButtonKit()
        .setEmoji('➕')
        .setStyle(ButtonStyle.Primary)
        .setCustomId('increment');
 
    // Disposal button
    const trash = new ButtonKit()
        .setEmoji('🗑️')
        .setStyle(ButtonStyle.Danger)
        .setCustomId('trash');
 
    // Create an action row
    const row = new ActionRowBuilder()
        .addComponents(dec, reset, inc, trash);
 
    return { dec, reset, inc, trash, row };
}
 
exports.run = async ({ interaction }) => {
    // Create the signal & buttons
    const [count, setCount, disposeCountSubscribers] = createSignal(0);
    const { dec, reset, inc, trash, row } = getButtons();
 
    // Temporary variable to hold button interactions
    let inter;
 
    // Send the initial message with the buttons
    const message = await interaction.reply({
        content: `Count is ${count()}`,
        components: [row],
        fetchReply: true,
    });
 
    // Now, we subscribe to count signal and update the message every time the count changes
    createEffect(() => {
        // Make sure to "always" call the value function inside createEffect, otherwise the subscription will not occur
        const value = count();
 
        // Now udate the original message
        inter?.update(`Count is ${value}`);
    });
 
    // Handler to decrement the count
    dec.onClick((interaction) => {
        inter = interaction;
        setCount((prev) => prev - 1);
    }, { message });
 
    // Handler to reset the count
    reset.onClick((interaction) => {
        inter = interaction;
        setCount(0);
    }, { message });
 
    // Handler to increment the count
    inc.onClick((interaction) => {
        inter = interaction;
        setCount((prev) => prev + 1);
    }, { message });
 
    // Disposal handler
    trash.onClick(async (interaction) => {
        const disposed = row.setComponents(
            row.components.map((button) => {
                // Remove the 'onClick' handler and disable the button
                return button
                    .onClick(null)
                    .setDisabled(true);
            }),
        );
 
        // Dispose the signal's subscribers
        disposeCountSubscribers();
 
        // And finally: acknowledge the interaction
        await interaction.update({
            content: 'Finished counting!',
            components: [disposed],
        });
    }, { message });
}

Result