Синхронизация выделения: диаграмма ⇄ таблица
Одна и та же сущность рисуется в интерфейсе дважды — узлом в диаграмме и строкой в таблице. При наведении или клике в одной панели нужно подсветить совпадение в другой. Классическая UI-задача, и в любом фреймворке она решается через одно из трёх:
- Поднять состояние в общий родитель — диаграмма и таблица навсегда связаны через него.
- Пробрасывать
selectedId+onSelectпропсами через все компоненты между ними — бойлерплейт, утечка ответственности. - Общий стор, подключённый к каждому листу — со временем превращается в god-object, когда добавляются ещё три синхронизируемые панели.
Triggery делит задачу пополам:
- правило («событие ховера → “эта сущность теперь под мышкой”») живёт в одном файле триггера,
- результирующее состояние живёт в крошечном сторе рядом,
- две панели — чистые потребители, они не знают друг о друге, только о сторе.
Файловая раскладка
Заголовок раздела «Файловая раскладка»- README.md обзорное описание
- index.html точка входа Vite
Директорияsrc/
- App.tsx продьюсеры + реакторы
- main.tsx точка входа
- store.ts крошечный стор — реактор пишет, обе панели читают
Директорияtriggers/
- index.ts правило — события, условия, действия, обработчик
Сценарий
Заголовок раздела «Сценарий»Маленькая доменная модель — Order, Invoice, Customer, Product, Shipment — рисуется в двух независимых панелях:
- Диаграмма на SVG с узлами и связями.
- Таблица с одной строкой на сущность.
Требуемое поведение:
- Навёл на строку — соответствующий узел подсвечивается.
- Навёл на узел — соответствующая строка подсвечивается.
- Кликнул в любой панели — выделение фиксируется.
- Панели монтируются независимо. Если на странице только таблица — всё работает.
Триггер
Заголовок раздела «Триггер»Два события, два действия — всё правило целиком.
Крошечный стор
Заголовок раздела «Крошечный стор»useAction в v0.10 — аддитивный: каждый компонент, подписанный на одну (trigger, name), срабатывает на каждый emit. Можно было бы повесить useAction на каждый Row/Node. Но fan-out на N строк = N перерисовок на emit, плюс churn от cleanup при перемонтаже. Поэтому: один реактор пишет в стор, каждая строка/узел читает из стора через useSyncExternalStore.
В большом приложении это был бы Zustand / Redux / Jotai / Signals. Здесь оставляем без сторонних зависимостей.
Связываем триггер со стором — один раз
Заголовок раздела «Связываем триггер со стором — один раз»Один монтаж. Конкурирующих реакторов нет. Положи это где угодно под провайдером рантайма.
Обе панели потребляют стор
Заголовок раздела «Обе панели потребляют стор»Каждая панель отправляет события на mouse-in / click и читает выделение через useSelection(). Друг с другом не разговаривают.
Две панели, без общего родителя, без проброса selectedId / onSelect пропсами. Удалить таблицу ничего не сломает: событие entity:hover всё равно отправляется, стор всё равно обновляется, никто не читает — жизнь продолжается.
Что здесь даёт именно Triggery
Заголовок раздела «Что здесь даёт именно Triggery»Сам стор-паттерн можно было бы сделать и без Triggery. Что добавляет Triggery:
- Один файл хранит правило. Сегодня это «событие → запись в стор». Завтра логика может ветвиться — отправить аналитику на
entity:select, игнорировать ховер при включённом DND, заглушить события из таблицы пока диаграмма в полноэкране. Каждое новое условие — строка в обработчике, а не россыпь по компонентам. - Код продьюсера не меняется при изменении правила.
<Node>умеет только одно — отправитьentity:hoverсо своим id. Любые новые поведения (debounce, скоупы по вкладкам, перенаправление в другой стор) для продьюсера невидимы. - Скоупированные варианты бесплатно. Если та же пара диаграмма + таблица живёт в двух соседних дашбордах, оборачиваем каждый в
<TriggerScope id="left">/<TriggerScope id="right">, и триггер читает события только из своего скоупа. См. Области видимости. - Триггер тестируется независимо. Отправил
entity:hover, проверил, что mock стора вызван с нужным id. Без DOM, без рендера, без провайдеров — см. Модульные тесты.
Варианты
Заголовок раздела «Варианты»- Мультивыделение — меняем тип payload на
Array<string>. Хелпер записи в стор сам решает, как объединять значения. - Навигация с клавиатуры — отправляем
entity:hoverизkeydown-обработчика на диаграмме или таблице; обе панели обновляются, не зная откуда событие пришло. - Третий UI (график, JSON-инспектор) — читаем
useSelection()в этом компоненте. Существующие панели не трогаем.