React Server Components
React Server Components (RSC) делят React-дерево на две половины — код, работающий на сервере и никогда не уезжающий в браузер, и код, который гидрируется и работает в браузере. Triggery твёрдо во второй половине: каждому хуку (useEvent, useCondition, useAction, useInspect, useInspectHistory) нужны useEffect и реальный DOM, а они есть только на клиенте.
Эта страница — практическое руководство: где проходит граница 'use client', как файлы триггеров уживаются с серверными компонентами и как соединить server action с клиентским триггером, чтобы запись в БД зажгла уведомление в трёх компонентах подальше.
Базовое правило
Заголовок раздела «Базовое правило»Всё, что вызывает хук Triggery, должно жить в 'use client'-файле. Файлы модулей триггеров (*.trigger.ts) — чистые модули — импортируют createTrigger, объявляют конфиг, экспортируют объект триггера. Но потребляются они клиентскими компонентами, поэтому на практике их тоже стоит помечать 'use client', чтобы бандлер не пытался выполнить их на сервере.
Директива 'use client' сверху — подсказка бандлеру, что модуль принадлежит клиентскому бандлу. Самому триггеру — данным плюс замыканию-обработчику — на директиву всё равно, но явная пометка предотвращает частую ошибку: серверный компонент пытается вызвать createTrigger на сервере (это сработает, но впустую тратит CPU и создаёт серверный рантайм, которым никто не пользуется).
Где ставится провайдер рантайма
Заголовок раздела «Где ставится провайдер рантайма»TriggerRuntimeProvider — клиентский компонент. Оберни его в своём файле и размести сразу под корневым layout:
Сам layout — серверный компонент. Внутри него граница TriggeryRoot помечает: «всё ниже — клиентское». Проп children при этом может всё ещё быть деревом серверных компонентов — и эти серверные компоненты могут содержать свои 'use client'-острова дальше вниз. RSC так и устроен; Triggery эту форму не меняет.
Компоненты, использующие хуки
Заголовок раздела «Компоненты, использующие хуки»Любой компонент, вызывающий useEvent, useCondition, useAction, useInspect или useInspectHistory, должен быть 'use client':
Серверный компонент, который рендерит один из таких, — это нормально: серверные компоненты могут компоновать клиентские как детей. Чего нельзя — это вызывать хук из серверного компонента.
Серверные компоненты, диспатчащие в триггеры
Заголовок раздела «Серверные компоненты, диспатчащие в триггеры»У серверных компонентов нет доступа к рантайму. Они не могут импортировать useEvent (на сервере он бы упал) и не могут добраться до инстанса клиентского рантайма. Мост — это обычный HTTP-fetch с клиента в Route Handler или server action и клиентский слушатель события, который запускает триггер, когда fetch вернётся.
Триггер срабатывает на клиенте, после подтверждения сервера. Обработчик читает условия, зарегистрированные другими клиентскими компонентами, и зовёт действия, зарегистрированные другими клиентскими реакторами. У сервера про триггер нулевая осведомлённость.
В этом и есть граница V1: сервер может попросить клиент что-то сделать; залезть в клиентский рантайм напрямую он не может. Клиент управляет своей оркестрацией сам.
Server actions (Next.js / React 19)
Заголовок раздела «Server actions (Next.js / React 19)»С server actions всё устроено так же, как с route handlers:
Форма та же — позови сервер, дождись результата, потом fire. Server action — это просто fetch с более приятной эргономикой.
Гибридные страницы: данные на сервере, оркестрация на клиенте
Заголовок раздела «Гибридные страницы: данные на сервере, оркестрация на клиенте»Типичная гибридная страница запрашивает данные в серверном компоненте и рендерит интерактивные кусочки как клиентские острова. Triggery встаёт чисто, потому что рантайм поднят выше островов:
Страница рендерится целиком на сервере: HTML для MessageList, Compose, NotificationLayer уезжает вниз. При гидрации каждый 'use client'-остров подключается к провайдеру TriggeryRoot, размещённому в layout. Друг друга они находят через триггер — Compose запускает событие, NotificationLayer реагирует, MessageList обновляется через своё React-состояние. Сервер ничего из этого не видит.
Паттерн одной схемой
Заголовок раздела «Паттерн одной схемой»Streaming и Suspense
Заголовок раздела «Streaming и Suspense»Streaming SSR в React 19 / Next App Router рендерит серверные компоненты потоком HTML-чанков; клиентские острова гидрируются по мере прихода их JS. С точки зрения Triggery:
- Провайдер монтируется, когда приходит его чанк. До этого хуки во вложенных островах не отрабатывали.
- После монтирования острова регистрируют условия и действия в своих
useEffect. Пока они этого не сделали, срабатывания либо пропускаются (requiredне выполнен), либо проходят без реактора. - Если запускаешь событие из клиентского компонента, который смонтирован раньше своего реактора, инспектор записывает «fired-but-unhandled». Это нормально — у триггера логика «нет реактора — ничего не делать». Следующее срабатывание после монтирования обоих запустит действия как обычно.
На практике это значит: размести <NotificationLayer />-реактор в layout, а не внутри маршрута страницы, который может уйти в Suspense. Реакторы дешёвые; пока действие не сработало, они ничего видимого не рендерят. Размещение в layout гарантирует, что они живы для каждого перехода между страницами.
Подводные камни
Заголовок раздела «Подводные камни»Server-only-модули
Заголовок раздела «Server-only-модули»'use server'-файл не может импортировать *.trigger.ts-модуль с 'use client' сверху — бандлер свалится. Если нужны общие типы между сервером и клиентом (например, интерфейс Message, который используется в payload’ах событий), положи их в types.ts без директивы и импортируй этот файл с обеих сторон.
getDefaultRuntime() на сервере
Заголовок раздела «getDefaultRuntime() на сервере»getDefaultRuntime() ленивый — создаёт глобальный рантайм при первом вызове. В RSC-окружении это значит, что серверный процесс тоже имеет рантайм по умолчанию — и он общий между запросами. Не регистрируй триггеры против рантайма по умолчанию в серверном коде; протечёт между запросами. Лекарство — то же, что и в тестах: передавай явный рантайм в createTrigger(config, runtime) или вызывай createTrigger только из 'use client'-модулей.
Рассинхроны гидрации
Заголовок раздела «Рассинхроны гидрации»useInspect и useInspectHistory возвращают undefined / [] на сервере и на первом клиентском рендере — одно и то же значение с обеих сторон, рассинхрона нет. Если делаешь дебаг-панель, которая показывает «no runs yet», пока список пуст, и таблицу — когда непуст, серверный HTML и первый гидрационный HTML идентичны. Обновления начинают приходить после первого commit’а эффектов; это клиентское обновление, а не рассинхрон гидрации.
Обработчики действий, трогающие server-only-API
Заголовок раздела «Обработчики действий, трогающие server-only-API»Так делать не надо. Обработчик действия — это замыкание, зарегистрированное через useAction, живёт в клиентском компоненте и работает в браузере. Если нужно писать в БД из обработчика — обработчик вызывает server action (или fetch), ждёт результат и передаёт его в следующий шаг. Server-only-API живут внутри server action.