Встроенные триггеры
Отдельный файл *.trigger.ts — правильный дом для любого правила, которое переиспользуется по кодовой базе, принадлежит фиче или имеет смысл показать не-инженеру. Большинство правил такой церемонии заслуживают. Некоторые — нет. Модалка, которая хочет закрыться по глобальному событию 'esc:pressed'. Панель, отправляющая аналитический пинг в момент, когда конкретная страница становится видимой. Dev-only-флаг, перезапускающий feature-flow по нажатию хоткея.
Для таких случаев правильный инструмент — useInlineTrigger: хук, объявляющий триггер прямо в файле компонента, которому принадлежит побочный эффект.
import { useInlineTrigger } from '@triggery/react';
export function CtaBanner() {
useInlineTrigger<{ events: { 'cta:click': { id: string; placement: string } } }>({
on: 'cta:click',
do: ({ event }) => {
analytics.track('cta_click', event.payload);
},
});
return <button onClick={() => fireCta()}>Sign up</button>;
}Три вещи:
- Schema-generic такой же, как у
createTrigger, — карты events / conditions / actions. Большинство inline-триггеров используют толькоevents, поэтому остальные две обычно опущены. on— имя события (должно совпадать с одним из ключей вevents). Должно быть стабильным между рендерами.do— обработчик. Принимает тот же аргументctx, что и обычный триггер (event,conditions,actions,check,signal,meta).
Хук работает по принципу fire-and-forget: триггер создаётся при монтировании, регистрируется в активном рантайме и удаляется при размонтировании. Пока компонент в дереве — правило живо.
Что хук делает за тебя
Заголовок раздела «Что хук делает за тебя»- Стабильный id. Если не передал, хук генерирует debug-id (
inline:<counter>) при первом запуске и фиксирует его на время жизни компонента. Этот id появляется в записях инспектора, чтобы можно было опознать, какой inline-триггер сработал. - Стабильная ссылка на обработчик. Внутри хук держит ref на последний обратный вызов
do. Сам объект триггера создаётся один раз; последующие перерендеры обновляют обработчик без снятия регистрации. Поэтому замыкание над локальным состоянием — нормально: запускается самое свежее замыкание. - Cleanup при размонтировании. При размонтировании триггер снимается с регистрации (
trigger.dispose()). Любой выполняющийся async-запуск прерывается черезsignal.aborted = true. - Наследование скоупа. Если хук смонтирован внутри
<TriggerScope id="…">, inline-триггер автоматически наследует этот скоуп.
Когда использовать
Заголовок раздела «Когда использовать»Четыре самых частых случая:
1. Небольшие аналитические таппы
Заголовок раздела «1. Небольшие аналитические таппы»Компонент не вызывает действий; он просто хочет среагировать на одно событие, уже летающее по приложению.
function PricingPage() {
useInlineTrigger<{ events: { 'route:visible': { path: string } } }>({
on: 'route:visible',
do: ({ event }) => {
if (event.payload.path === '/pricing') analytics.page('pricing');
},
});
return <PricingContent />;
}2. Клей для стека модалок
Заголовок раздела «2. Клей для стека модалок»Модалка закрывает себя при смене маршрута. Правило локально для этого компонента — никакого сценария, достойного имени, нет.
function ConfirmDialog({ onClose }: Props) {
useInlineTrigger<{ events: { 'route:change': void } }>({
on: 'route:change',
do: onClose,
});
return <Dialog>…</Dialog>;
}Замыкание do автоматически захватывает свежий onClose через ref.
3. Эффекты по dev-only-флагу
Заголовок раздела «3. Эффекты по dev-only-флагу»Нажми Cmd-K, отправь 'devtools:open'. Dev-only-панель реагирует на это событие, без продакшен-стоимости.
function DevToolbar() {
const [open, setOpen] = useState(false);
useInlineTrigger<{ events: { 'devtools:open': void } }>({
on: 'devtools:open',
do: () => setOpen(true),
});
if (!import.meta.env.DEV) return null;
return open ? <DevPanel onClose={() => setOpen(false)} /> : null;
}4. Одноразовая интеграция с библиотекой
Заголовок раздела «4. Одноразовая интеграция с библиотекой»Новая библиотека, которую ты обкатываешь, отправляет событие, которое хочется перевести в триггер-style-сценарий на полдня — пока решаешь, стоит ли это полноценного .trigger.ts.
useInlineTrigger<{
events: { 'fancy-lib:event': { kind: string; data: unknown } };
}>({
on: 'fancy-lib:event',
do: ({ event, signal }) => {
if (event.payload.kind !== 'ready') return;
if (signal.aborted) return;
initializeIntegration(event.payload.data);
},
});Прожило день — выноси в полноценный триггер.
Когда НЕ использовать — правило выноса
Заголовок раздела «Когда НЕ использовать — правило выноса»useInlineTrigger — запасной выход, не дефолт. Выноси в файл *.trigger.ts, как только верно хотя бы одно:
- У правила есть
conditionsилиactions. Inline-триггеры технически могут использовать обе карты, но в этот момент ты прячешь сценарную логику внутри UI-компонента. Вынеси. - Больше одного компонента захочет читать это правило. Как только появилась мысль «извлеку-ка» — извлекай.
- Обработчик вырастает за ~10 строк. Inline-триггеры задуманы визуально маленькими. Большее превращает файл компонента в файл сценария под прикрытием.
- Нужны именованные хуки. Именованные хуки опираются на отдельный модуль триггера —
createNamedHooks(trigger)к inline-триггерам не применим. - Нужно видеть его в статическом
runtime.graph(). Inline-триггеры регистрируются во время монтирования и не видны build-time-экстракторам графа. - Нужно тестировать без рендера React. Inline-триггеры живут внутри хука — их тестирование требует рендера host-компонента. Отдельный
.trigger.tsтестируется напрямую.
Другими словами: если правилу нужно хоть что-то из «имя, узнаваемое продакт-менеджерами», «тесты», «переиспользование», «именованные хуки», «статический граф» — это правило для createTrigger, а не для useInlineTrigger.
Кастомный id для стабильных записей инспектора
Заголовок раздела «Кастомный id для стабильных записей инспектора»По умолчанию хук авто-генерирует inline:<counter>. Два ре-монта одного компонента получают один и тот же id в пределах сессии — счётчик не сбрасывается; между сессиями id не стабилен. Если нужен осмысленный id в инспекторе — полезно, когда inline-триггер часто срабатывает и ты ищешь его в панели — передай явный:
useInlineTrigger({
id: 'cta-banner:cta-click',
on: 'cta:click',
do: ({ event }) => analytics.track('cta_click', event.payload),
});Id всё равно должны быть уникальны в пределах рантайма. Один и тот же id, смонтированный дважды, активирует поведение last-mount-wins: второй монт молча заменяет первый. Полезно для двойных монтов под React StrictMode, но в проде это значит, что два разных <CtaBanner> будут наступать друг другу на ноги — выбирай id, включающий дискриминатор инстанса компонента, либо оставляй авто-id.
Что хук НЕ делает
Заголовок раздела «Что хук НЕ делает»- Не авто-обнаруживает
*.trigger.ts. Это работа@triggery/vite. Inline-триггеры всегда регистрируются при монтировании, точка. - Не принимает
required,schedule,concurrencyилиscopeв V1. Триггер работает с дефолтами активного рантайма (microtask-расписание,take-latest-параллелизм, безrequired, скоуп наследуется от окружающего<TriggerScope>). Если нужны эти настройки — это ещё один сигнал к выносу в полноценныйcreateTrigger-файл. - Не дедуплицирует. Два компонента, вызывающих
useInlineTriggerс одинаковым auto-id-паттерном в одном дереве рендера, каждый создают свой триггер. Оба сработают на совпадающее событие.
TypeScript: не переписывай схему везде
Заголовок раздела «TypeScript: не переписывай схему везде»Если два компонента делят одну inline-схему, вынеси её в тип:
type CtaEvents = { events: { 'cta:click': { id: string; placement: string } } };
function CtaBanner() {
useInlineTrigger<CtaEvents>({ on: 'cta:click', do: ({ event }) => {/* … */} });
// …
}
function CtaFooter() {
useInlineTrigger<CtaEvents>({ on: 'cta:click', do: ({ event }) => {/* … */} });
// …
}В этот момент стоит спросить себя: это всё ещё inline, или пора заводить cta.trigger.ts и именованный хук useCtaClickEvent? По правилу большого пальца — да, но общий тип — вполне валидный промежуточный шаг.
Тонкость: поле on
Заголовок раздела «Тонкость: поле on»Внутри хук захватывает on на первом рендере и фиксирует. Смена on между рендерами выводит DEV-only-warning и не влияет на зарегистрированный триггер — побеждает старое имя события. Это намеренно: триггер, у которого имя события скачет между рендерами, создаёт churn регистраций, который не стоит поддерживать. Выбирай событие на месте вызова; если приходится переключаться — смонтируй два разных inline-триггера под условием.