- Date: 2026-05-20
- Status: Accepted
- Phase: 2 → 7
Context
Phase 2-7 нужен state с тремя свойствами:
- Шарится между routes (workflow ↔ screen edit ↔ preview) без unmount/remount
- Не зависит от Next.js App Router internals (testable в Node)
- Поддерживает 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 statesubscribe(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 без React — packages/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 (мы делаем через
useEffectcleanup)
Related files
packages/editor/src/composition-store.tspackages/editor/src/library-store.tsapps/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)