Every non-trivial app eventually grows a modal mess: “Confirm delete” opens “Resolve conflict” opens “Reauthenticate” — all from different features, none of them wanting to know about each other. Triggery turns this into a one-liner per feature: fire modal:open, fire modal:close, listen if you care. Per-route stacks come for free with <TriggerScope>.
When “Delete account” is confirmed, it should close itself and open a re-auth modal. The first modal fires two events; the trigger maps each one to the right stack mutation.
src/features/modal/DeleteAccountModal.tsx
import { useEvent } from '@triggery/react';import { modalTrigger } from '../../triggers/modal.trigger';export function DeleteAccountModal({ id, active }: { id: string; active: boolean }) { const open = useEvent(modalTrigger, 'modal:open'); const close = useEvent(modalTrigger, 'modal:close'); return ( <dialog open={active}> <h2>Delete account</h2> <p>This cannot be undone. We'll ask you to re-enter your password to be sure.</p> <menu> <button type="button" onClick={() => close(id)}>Cancel</button> <button type="button" onClick={() => { close(id); open({ kind: 'reauth', id: 'reauth-1', reason: 'Confirm your password to delete the account.', }); }} > Continue </button> </menu> </dialog> );}
The cascade is two fire calls. The trigger runs once per event, the store updates twice, the host re-renders with the new top. No onConfirm={...} prop chain.
If multiple routes each need their own modal stack — e.g. “settings has its own dialogs, dashboard has its own, switching routes must dismiss the previous screen’s dialogs” — declare the trigger with a scope and wrap each route in a <TriggerScope> with the matching id.
import { TriggerScope } from '@triggery/react';import { ModalHost } from './features/modal/ModalHost';import { AccountSettings } from './features/account/AccountSettings';import { Dashboard } from './features/dashboard/Dashboard';export function App({ route }: { route: 'account' | 'dashboard' }) { return ( <TriggerScope id="dialog"> {/* Both routes share the same scope id, but only ONE is rendered at a time — unmounting the route's TriggerScope subtree also unregisters its modal host, which atomically drops the stack. */} {route === 'account' && <AccountSettings />} {route === 'dashboard' && <Dashboard />} <ModalHost /> </TriggerScope> );}
For parallel per-route stacks (e.g. modals embedded inside a multi-pane layout), give each pane a unique scope id (dialog/account, dialog/dashboard) and create a separate trigger per scope — registrations and triggers see only their matching bucket.