Перейти к содержимому
GitHubXDiscord

Начало работы

За пять минут у тебя получится один файл со сценарием («пришло новое сообщение, у пользователя включены уведомления — показать тост») и три небольших компонента, которые к этому файлу подключаются. Ни один из компонентов не знает о других.

Пропустить установку — открыть пример счётчика в StackBlitz Пропустить установку — открыть пример счётчика в GitHub
  • Node 20+ для инструментов разработки.
  • Проект на React 18+/19, Solid 1.8+ или Vue 3.4+. (Если у тебя его пока нет, запусти pnpm dlx @triggery/cli create my-app, чтобы развернуть минимальный Vite-проект — см. @triggery/cli.)
  • TypeScript рекомендуется, но не обязателен. Примеры на этом сайте предполагают TS.

Понадобятся два пакета: @triggery/core (рантайм) и один биндинг под нужный фреймворк.

pnpm add @triggery/core @triggery/react

Peer-зависимости: react >= 18.0.0, react-dom >= 18.0.0.

Рантайм — то, что хранит реестр триггеров, условий и действий. В большинстве приложений создаётся один рантайм на корневом уровне.

src/main.tsx
import { createRuntime } from '@triggery/core';
import { TriggerRuntimeProvider } from '@triggery/react';
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { App } from './App';

const runtime = createRuntime();

createRoot(document.getElementById('root')!).render(
  <StrictMode>
    <TriggerRuntimeProvider runtime={runtime}>
      <App />
    </TriggerRuntimeProvider>
  </StrictMode>,
);

Провайдер можно вообще не ставить — тогда Triggery возьмёт общий рантайм, который создаётся по умолчанию при первом обращении. Явный провайдер нужен, чтобы изолировать рантаймы в тестах, микрофронтендах и мультиарендных приложениях.

Принятый стиль — один триггер на файл, с суффиксом .trigger.ts. Файл читается сверху вниз как спецификация.

src/triggers/message.trigger.ts
import { createTrigger } from '@triggery/core';

type Settings = { sound: boolean; notifications: boolean };
type Message  = { author: string; text: string; channelId: string };

export const messageTrigger = createTrigger<{
  events:     { 'new-message': Message };
  conditions: { settings: Settings; activeChannelId: string | null };
  actions:    { showToast: { title: string; body: string }; playSound: 'beep' };
}>({
  id: 'message-received',
  events: ['new-message'],
  required: ['settings'],
  handler({ event, conditions, actions, check }) {
    // Skip if the user is already looking at this conversation.
    if (conditions.activeChannelId === event.payload.channelId) return;

    // Skip if notifications are off — `check.is` narrows the value.
    if (!check.is('settings', s => s.notifications)) return;

    // Debounce sound so a burst of messages plays one beep.
    actions.debounce(800).playSound?.('beep');
    actions.showToast?.({
      title: event.payload.author,
      body: event.payload.text,
    });
  },
});

Один inline-обобщённый параметр описывает все порты триггера — три словаря с типами событий, условий и действий. Контекст обработчика ctx теперь строго типизирован: попробуй передать неверное имя события или несовместимый payload — TypeScript остановит на этапе компиляции.

Triggery разделяет «кто что делает» на три роли компонента:

  • Продьюсер — отправляет событие через useEvent.
  • Провайдер — отдаёт снимок состояния через useCondition, чтобы обработчик мог прочитать его лениво.
  • Реактор — регистрирует действие через useAction.

В настоящем чат-приложении каждая из этих ролей живёт в своём компоненте — в той части фичи, которой она естественно принадлежит.

src/features/SettingsPanel.tsx (provider)
import { useCondition } from '@triggery/react';
import { useState } from 'react';
import { messageTrigger } from '../triggers/message.trigger';

export function SettingsPanel() {
  const [settings, setSettings] = useState({ sound: true, notifications: true });
  useCondition(messageTrigger, 'settings', () => settings, [settings]);
  // …UI for editing settings
}
src/features/Chat.tsx (producer + provider for activeChannelId)
import { useEvent, useCondition } from '@triggery/react';
import { messageTrigger } from '../triggers/message.trigger';

export function Chat({ channelId }: { channelId: string }) {
  const fireMessage = useEvent(messageTrigger, 'new-message');
  useCondition(messageTrigger, 'activeChannelId', () => channelId, [channelId]);

  return (
    <button type="button" onClick={() => fireMessage({ author: 'Alice', text: 'hi', channelId: 'b' })}>
      send a fake message
    </button>
  );
}
src/features/NotificationLayer.tsx (reactor)
import { useAction } from '@triggery/react';
import { toast } from 'sonner';
import { messageTrigger } from '../triggers/message.trigger';

export function NotificationLayer() {
  useAction(messageTrigger, 'showToast', payload => {
    toast.success(payload.title, { description: payload.body });
  });
  useAction(messageTrigger, 'playSound', () => {
    new Audio('/beep.mp3').play().catch(() => {});
  });
  return null;
}

Размести три компонента где угодно в дереве под провайдером — они найдут друг друга через триггер, а не через контекст React/Solid/Vue.

Нажми «send a fake message» в компоненте Chat. Слой уведомлений напишет в консоль или покажет тост. Выключи settings.notifications — и он замолчит. Весь сценарий целиком — это один файл, и каждый компонент может расти независимо.