Skip to content

ActionChannel

Stable · since 0.10.0

A typed multi-subscriber channel for one action of a trigger. Returned by trigger.action(name) (cached per (trigger, name)). Replaces the v0.9 pattern of hand-rolled Set<callback> + for-of fan-out wired through runtime.registerAction.

import type { ActionChannel } from '@triggery/core';
type ActionChannel<P> = {
  /** Subscribe to emits. Returns an unsubscribe function. */
  subscribe(cb: (payload: P) => void): () => void;
  /** Number of currently active subscribers — for DevTools and tests. */
  readonly subscribed: number;
};
import { createTriggerfunction createTrigger<S extends TriggerSchema>(runtime?: Runtime): TriggerBuilder<S>
Returns a chainable `TriggerBuilder<S>`. See module docs for examples.
} from '@triggery/core/builder';
type Schema
type Schema = {
    events: {
        tick: number;
    };
    actions: {
        showToast: {
            title: string;
        };
    };
}
= {
events
events: {
    tick: number;
}
: { ticktick: number: number };
actions
actions: {
    showToast: {
        title: string;
    };
}
: { showToast
showToast: {
    title: string;
}
: { titletitle: string: string } };
}; const tconst t: Trigger<Schema> = createTriggercreateTrigger<Schema>(runtime?: Runtime): TriggerBuilder<Schema>
Returns a chainable `TriggerBuilder<S>`. See module docs for examples.
<Schema
type Schema = {
    events: {
        tick: number;
    };
    actions: {
        showToast: {
            title: string;
        };
    };
}
>()
.idfunction id(id: string): TriggerBuilder<Schema, never>
Set the unique trigger id (mandatory before `.handle`).
('demo')
.eventsfunction events(events: readonly "tick"[]): TriggerBuilder<Schema, never>
Declare the event keys this trigger listens for (mandatory before `.handle`).
(['tick'])
.handlefunction handle(handler: TriggerHandler<Schema, never>): Trigger<Schema>
Finalize the trigger with the given handler. The handler sees `conditions.<key>` as `NonNullable<…>` for every key passed to `.require`. Returns the live `Trigger<S>`, already registered with the runtime.
(({ actions
actions: ActionsCtx<{
    showToast: {
        title: string;
    };
}>
}) => {
actions
actions: ActionsCtx<{
    showToast: {
        title: string;
    };
}>
.showToast
showToast?: ((payload: {
    title: string;
}) => void) | undefined
?.({ titletitle: string: 'tick' });
}); const toast
const toast: ActionChannel<{
    title: string;
}>
= tconst t: Trigger<Schema>.action
action<"showToast">(name: "showToast"): ActionChannel<{
    title: string;
}>
Get (or create) the typed multi-subscriber channel for an action. Cached per (trigger, name): repeat calls return the same channel.
('showToast');
const unsubAconst unsubA: () => void = toast
const toast: ActionChannel<{
    title: string;
}>
.subscribe
function subscribe(cb: (payload: {
    title: string;
}) => void): () => void
Subscribe to emits. Returns an unsubscribe function.
((p
p: {
    title: string;
}
) => consolevar console: Console.logConsole.log(...data: any[]): void
The **`console.log()`** static method outputs a message to the console. [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/log_static)
('A', p
p: {
    title: string;
}
));
const unsubBconst unsubB: () => void = toast
const toast: ActionChannel<{
    title: string;
}>
.subscribe
function subscribe(cb: (payload: {
    title: string;
}) => void): () => void
Subscribe to emits. Returns an unsubscribe function.
((p
p: {
    title: string;
}
) => consolevar console: Console.logConsole.log(...data: any[]): void
The **`console.log()`** static method outputs a message to the console. [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/log_static)
('B', p
p: {
    title: string;
}
));
// toast.subscribed === 2 unsubAconst unsubA: () => void(); // toast.subscribed === 1

Channel subscribers and any runtime.registerAction(...) handler for the same key both fire on every action emit — the channel is additive, not a replacement.

The channel is cached per (trigger, name). Repeat trigger.action(name) calls return the same channel object.

trigger.action('showToast') === trigger.action('showToast') // true

This means you can use t.action('X') as a stable reference for hooks and effects without memoising.

// Old path — still works
runtime.registerAction(trigger.id, 'showToast', (p) => toastA(p));

// New path — coexists, both fire on every emit
trigger.action('showToast').subscribe((p) => toastB(p));

The runtime.registerAction path remains last-write-wins (a single slot per action — newer writes overwrite older). Channel subscribers run additively in addition to that slot’s handler. This means:

  • v0.10 React/Solid/Vue useAction internally uses the channel path → multiple components mounting the same useAction(trigger, 'name', fn) now all run on each emit. (This is the one semantic change in v0.10 — see the migration guide.)
  • Adapters that need exclusive ownership can keep using runtime.registerAction(...). The two paths don’t interfere.
import { useAction } from '@triggery/react';
import { useState } from 'react';

function ToastSlot() {
  const [toasts, setToasts] = useState<ToastPayload[]>([]);
  useAction(messageTrigger, 'showToast', (p) => setToasts((cur) => [...cur, p]));
  return <ul>{toasts.map((t) => <li key={t.id}>{t.title}</li>)}</ul>;
}

useAction is a thin wrapper over trigger.action('showToast').subscribe(...) — every component that mounts it gets called on every emit.

The prefer-action-channel rule (enabled as warn in the strict preset) flags the v0.9 hand-rolled Set + for-of fan-out pattern and suggests the channel API.