В любом нетривиальном приложении со временем заводится модалочный хаос: «Подтвердить удаление» открывает «Разрешить конфликт» открывает «Войти повторно» — всё из разных фич, и ни одна не хочет знать про остальные. Triggery превращает это в одну строчку на фичу: запускай modal:open, запускай modal:close, слушай, если интересно. Отдельные стеки на маршрут — через <TriggerScope>.
Когда «Удалить аккаунт» подтверждено, модалка должна закрыть себя и открыть модалку повторного входа. Первая модалка запускает два события; триггер маппит каждое в нужную мутацию стека.
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> );}
Каскад — это два вызова fire. Триггер прогоняется один раз на событие, стор обновляется дважды, хост перерендеривается с новой верхушкой. Никаких цепочек onConfirm={...} через пропсы.
5. Отдельные стеки на маршрут через <TriggerScope>
Если нескольким маршрутам нужен свой стек модалок — например «у настроек свои диалоги, у дашборда свои, при смене маршрута диалоги предыдущего экрана должны исчезнуть» — объяви триггер со scope и оберни каждый маршрут в <TriggerScope> с соответствующим id.
src/triggers/modal.trigger.ts (вариант со скоупом)
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> );}
Для параллельных стеков по маршрутам (например, модалки внутри многопанельного layout) задай каждой панели уникальный scope id (dialog/account, dialog/dashboard) и сделай отдельный триггер на каждый скоуп — регистрации и триггеры видят только свой бакет.