Борьба со спагетти
Triggery хорош в оркестрации. Он не хорош в “любых побочных эффектах где попало”. Тот же createTrigger, который чётко выражает “новое сообщение + notifications включены + не активный канал ⇒ тост + бипл + бейдж”, превращается в шум, когда ты используешь его для “увеличить счётчик при клике на кнопку”. Эта страница — про границу.
Лакмус-тест
Заголовок раздела «Лакмус-тест»Прежде чем тянуться к createTrigger, задай три вопроса по порядку.
- Пересекает ли побочный эффект границы фичей — владеет ли входами или выходами чей-то ещё компонент?
- Имеет ли он форму сценария — узнает ли в нём не-инженер одно правило на продуктовом языке?
- Если бы ты писал это хуками, разъехалось бы это на три или больше
useEffect’ов в трёх или больше компонентах?
Если ответил да на все три — это триггер.
Если ответил нет хоть на один — оставайся с useState + useEffect (или эквивалентом твоего стора). Сэкономишь время, реестр триггеров останется читаемым, а твои *.trigger.ts файлы будут читаться как спецификации, когда ты их откроешь.
Что значит “форма сценария”
Заголовок раздела «Что значит “форма сценария”»У сценария есть хотя бы по одной из этих форм:
- Несколько входов из несвязанных мест. Settings живут в одной фиче, активный канал в другой, текущий пользователь в третьей. Правилу нужны все.
- Несколько выходов в несвязанных местах. Тост в корне layout, бипл в аудио-системе, число в сайдбаре.
- Причина пропустить. “Если пользователь уже смотрит на этот канал — нет”. Сценарии собирают причины, не только шаги.
Счётчик — ни одно из этого. Один вход (клик), один выход (счётчик), ноль условий пропуска. useState ровно правильного размера.
Триггер vs useEffect: разбор примера
Заголовок раздела «Триггер vs useEffect: разбор примера»Первому нечего делать в виде триггера. Второму нечего делать в виде трёх useEffect’ов.
DEV warn-эвристики
Заголовок раздела «DEV warn-эвристики»Дефолты Triggery предполагают, что сценарии остаются читабельными. Два ESLint-правила из @triggery/eslint-plugin ловят самые частые способы, которыми *.trigger.ts потихоньку превращается в скрипт.
max-handler-size
Заголовок раздела «max-handler-size»Считает top-level выражения в теле обработчика (control-flow блоки — один выражение каждый). Если у тебя 50+ выражений, ты либо выражаешь два сценария в одном триггере, либо делаешь вычисления, которые принадлежат обычной функции — и её надо импортировать.
max-ports-per-trigger
Заголовок раздела «max-ports-per-trigger»Ограничивает количество портов. Триггер, реагирующий на 12 разных событий — уже не сценарий, это event-брокер, и твоя команда начнёт побаиваться трогать файл. Разделяй.
prefer-named-hook
Заголовок раздела «prefer-named-hook»Когда файл делает четыре или больше port-вызовов, эргономика именованных хуков (useNewMessageEvent вместо useEvent(trigger, 'new-message')) начинает доминировать. Правило подталкивает к ним — см. Именованные хуки для механики.
Разделение жирного триггера
Заголовок раздела «Разделение жирного триггера»Когда max-handler-size или max-ports-per-trigger начинают срабатывать, лечение редко “подними лимит”. Это “тут два сценария, дай им два id”.
Форма, которую надо разбить
Заголовок раздела «Форма, которую надо разбить»Оба триггера реагируют на то же событие. Ни один не знает о другом. Сценарий уведомления можно поставить на паузу feature-флагом, не трогая аналитику. Аналитику можно заменить целиком (Segment → PostHog), не перечитывая правило уведомления.
Антипаттерны и правильный фикс
Заголовок раздела «Антипаттерны и правильный фикс»Антипаттерн: “триггер делает всё внутри одной фичи”
Заголовок раздела «Антипаттерн: “триггер делает всё внутри одной фичи”»Это useState, надевший восемь украшений. Фича владеет своим входом и своим выходом; никому больше нет дела. Фикс:
Триггеры — не состояние. Не заставляй их играть в состояние на фиче, где нет ко-актёров.
Антипаттерн: “три триггера запускают по порядку, чтобы сделать одно дело”
Заголовок раздела «Антипаттерн: “три триггера запускают по порядку, чтобы сделать одно дело”»Ты написал пайплайн. Triggery может его пронести через каскады (см. Каскад), но трёхшаговая цепочка — это функция под прикрытием, и файловая когезия пропала. Фикс: консолидируй в один триггер и вызывай шаги инлайн.
Резервируй каскады для fan-out поперёк фичей, не для “шаг A потом шаг B”.
Антипаттерн: “триггер читает выход другого триггера через глобальный стор”
Заголовок раздела «Антипаттерн: “триггер читает выход другого триггера через глобальный стор”»Ты шлёшь скрытый канал. Триггер B больше не реагирует на что-то читаемое; он реагирует на мутацию в сторе, которую триггер A случайно сделал на стороне, и эта проводка не живёт ни в одном из двух файлов. Фикс: сделай каскад явным.
Каскады появляются в инспекторе с parent runId. Побочные каналы через стор — нет. Последние делают кросс-фичную проводку untraceable; первые — задача на один grep.
Где заканчивается ответственность Triggery
Заголовок раздела «Где заканчивается ответственность Triggery»Не тянись к триггеру, когда:
- Побочный эффект локален одному компоненту.
useEffectкороче и яснее. - Тебе нужен state machine. XState моделирует допустимые переходы; Triggery моделирует сценарии. Используй оба — обработчики могут вызывать сервисы.
- Тебе нужен stream-пайплайн. RxJS даёт тебе операторы во времени. Обёртывай их выходы как условия.
- Тебе нужна кросс-таб / кросс-window оркестрация. Это территория
BroadcastChannel+ (со временем)@triggery/server. - “Сценарий” — это один вход, один выход, без пропусков. Ни один файл в реестре не стоит нуля условий и нуля альтернатив.
Чек-лист ревьюера
Заголовок раздела «Чек-лист ревьюера»Когда ревьюишь PR, вводящий триггер, на этом diff стоит остановиться.
- Id триггера читается как имя сценария (
notification-on-message), а не имя события (new-message). - Хотя бы одно
requiredусловие или хотя бы одна причина пропустить в обработчике — иначе обработчик стреляет на каждый вызов. - Нет вызова
useEventвнутри телаuseActionв том же файле (правилоno-event-cascadeего поймает, но глаза быстрее). - Тело обработчика — под
max-handler-sizeпроекта. Если нет — спроси: это один сценарий или два? - Дженерик схемы — inline в вызове
createTrigger<{...}>, не вынесен в удалённыйtype(вынесение безвредно, но делает файл сложнее читать end-to-end). - Поверхность портов достаточно мала, чтобы ты мог назвать каждое событие, условие и действие вслух на одном вдохе.
Если отклоняешь PR, самый полезный однострочник: “Какая третья фича читает или пишет это? Если ответ — никакая, оставь в компоненте.”