Встроенные триггеры
Отдельный файл *.trigger.ts — правильный дом для любого правила, которое переиспользуется по кодовой базе, принадлежит фиче или имеет смысл показать не-инженеру. Большинство правил такой церемонии заслуживают. Некоторые — нет. Модалка, которая хочет закрыться по глобальному событию 'esc:pressed'. Панель, отправляющая аналитический пинг в момент, когда конкретная страница становится видимой. Dev-only-флаг, перезапускающий feature-flow по нажатию хоткея.
Для таких случаев правильный инструмент — useInlineTrigger: хук, объявляющий триггер прямо в файле компонента, которому принадлежит побочный эффект.
Три вещи:
- 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. Небольшие аналитические таппы»Компонент не вызывает действий; он просто хочет среагировать на одно событие, уже летающее по приложению.
2. Клей для стека модалок
Заголовок раздела «2. Клей для стека модалок»Модалка закрывает себя при смене маршрута. Правило локально для этого компонента — никакого сценария, достойного имени, нет.
Замыкание do автоматически захватывает свежий onClose через ref.
3. Эффекты по dev-only-флагу
Заголовок раздела «3. Эффекты по dev-only-флагу»Нажми Cmd-K, отправь 'devtools:open'. Dev-only-панель реагирует на это событие, без продакшен-стоимости.
4. Одноразовая интеграция с библиотекой
Заголовок раздела «4. Одноразовая интеграция с библиотекой»Новая библиотека, которую ты обкатываешь, отправляет событие, которое хочется перевести в триггер-style-сценарий на полдня — пока решаешь, стоит ли это полноценного .trigger.ts.
Прожило день — выноси в полноценный триггер.
Когда НЕ использовать — правило выноса
Заголовок раздела «Когда НЕ использовать — правило выноса»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-триггер часто срабатывает и ты ищешь его в панели — передай явный:
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-схему, вынеси её в тип:
В этот момент стоит спросить себя: это всё ещё inline, или пора заводить cta.trigger.ts и именованный хук useCtaClickEvent? По правилу большого пальца — да, но общий тип — вполне валидный промежуточный шаг.
Тонкость: поле on
Заголовок раздела «Тонкость: поле on»Внутри хук захватывает on на первом рендере и фиксирует. Смена on между рендерами выводит DEV-only-warning и не влияет на зарегистрированный триггер — побеждает старое имя события. Это намеренно: триггер, у которого имя события скачет между рендерами, создаёт churn регистраций, который не стоит поддерживать. Выбирай событие на месте вызова; если приходится переключаться — смонтируй два разных inline-триггера под условием.