ADRs
ADR 002 — Module-level stores + useSyncExternalStore
  • Date: 2026-05-20
  • Status: Accepted
  • Phase: 2 → 7

Context

Phase 2-7 нужен state с тремя свойствами:

  1. Шарится между routes (workflow ↔ screen edit ↔ preview) без unmount/remount
  2. Не зависит от Next.js App Router internals (testable в Node)
  3. Поддерживает per-piece subscriptions для persistence (per-screen save, не весь dataset)

Альтернативы рассмотренные:

  • Context + useReducer: unmount при navigation, нет global access из не-React контекстов
  • Zustand / Jotai: ещё одна зависимость, overkill для Phase 2-7
  • xyflow's useNodesState (для workflow): локален к компоненту, теряет state при navigation

Decision

Module-level singleton class (CompositionStore, LibraryStore, WorkflowStore, ...) с:

  • getSnapshot() — текущее immutable state
  • subscribe(fn) — listener registration
  • explicit mutator methods (addInstance, setLibrary, ...)

React-side: тонкий useSyncExternalStore wrapper в apps/web/src/lib/*.ts. Hook полностью SSR-safe (third argument getServerSnapshot обязателен для App Router).

Sub-bundle: stores без Reactpackages/editor/ (composition + library). Workflow store остаётся в apps/web/ потому что зависит от xyflow's applyNodeChanges / applyEdgeChanges.

Per-screen save: compositionStore.subscribeChanges(fn) диффит prev/next snapshot и вызывает fn(changedScreen) — без allocations на каждой мутации.

Consequences

Pros:

  • State persistsует между navigations (модуль не unmount)
  • Testable в Node без React renderer (vitest)
  • Hot module reload в dev сохраняет state (модуль reused)
  • Простая ментальная модель: один class = один store

Cons:

  • Singleton — тестам приходится использовать new CompositionStore(...) явно (или factory exports)
  • SSR snapshot должен совпадать с initial client snapshot (иначе hydration mismatch) — поэтому () => SSR_STATE возвращает empty/initial, не нагрузочные данные
  • Subscribers не gc'нутся если кто-то забудет unsubscribe (мы делаем через useEffect cleanup)

Related files

  • packages/editor/src/composition-store.ts
  • packages/editor/src/library-store.ts
  • apps/web/src/lib/composition-store.ts (hook)
  • apps/web/src/lib/library-store.ts (hook)
  • apps/web/src/lib/workflow-store.ts (xyflow-specific + hook)
  • apps/web/src/components/persistence-loader.tsx (auto-save subscriber)