--- name: effector-patterns description: Effector state management patterns and CRITICAL anti-patterns for ChainGraph frontend. Use when writing Effector stores, events, effects, samples, or any reactive state code. Contains anti-patterns to AVOID like $store.getState(). Covers domains, patronum utilities, global reset. Triggers: effector, store, createStore, createEvent, createEffect, sample, combine, attach, domain, $, useUnit, getState, anti-pattern, patronum. --- # Effector Patterns for ChainGraph This skill covers Effector state management patterns used in the ChainGraph frontend, including **CRITICAL anti-patterns** that agents MUST avoid. ## Domain Organization ChainGraph uses domain-based store organization. All domains are defined in: **File**: `apps/chaingraph-frontend/src/store/domains.ts` ### All Domains | Domain | Line | Purpose | |--------|------|---------| | `flowDomain` | 17 | Flow list, active flow, metadata | | `nodesDomain` | 20 | Node CRUD, positions, dimensions | | `edgesDomain` | 23 | Edge connections, anchors, selection | | `executionDomain` | 26 | Execution state, events, control | | `categoriesDomain` | 29 | Node categories, filtering | | `portsDomain` | 32 | Legacy port management | | `trpcDomain` | 35 | tRPC client instances | | `archaiDomain` | 38 | ArchAI integration | | `focusedEditorsDomain` | 41 | Port editor focus state | | `dragDropDomain` | 44 | Drag & drop state | | `mcpDomain` | 47 | MCP server management | | `initializationDomain` | 50 | App initialization | | `walletDomain` | 53 | Wallet integration | | `hotkeysDomain` | hotkeys/stores.ts | Keyboard shortcuts (not in domains.ts) | | `xyflowDomain` | xyflow/domain.ts | XYFlow render (not in domains.ts) | | `perfTraceDomain` | perf-trace/domain.ts | Performance (not in domains.ts) | | `portsV2Domain` | ports-v2/domain.ts:23 | Granular ports (not in domains.ts) | ### Creating a Domain ```typescript import { createDomain } from 'effector' // Naming: {feature}Domain with kebab-case internal name export const myFeatureDomain = createDomain('my-feature') ``` --- ## CRITICAL Anti-Patterns ### Anti-Pattern #1: Using `.getState()` in Store Reducers **This is the most common mistake.** Found in 13+ files in the codebase. ```typescript // ❌ BAD: .getState() in reducer breaks reactivity const $compatiblePorts = portsDomain.createStore(null) .on($draggingEdgePort, (state, draggingEdgePort) => { // This ONLY reads $nodes at call time, NOT reactively const nodes = Object.values($nodes.getState()) // ← ANTI-PATTERN // ... return compatiblePorts }) ``` **Why it's wrong:** - `.getState()` bypasses Effector's dependency tracking - Updates to `$nodes` won't trigger updates to `$compatiblePorts` - No subscription established - reads value once at call time - Breaks the reactive data flow model ```typescript // ✅ GOOD: Use sample() for reactive derivation const $compatiblePorts = sample({ source: { nodes: $nodes, draggingPort: $draggingEdgePort }, clock: $draggingEdgePort, fn: ({ nodes, draggingPort }) => { if (!draggingPort) return null const nodeList = Object.values(nodes) // ... compute compatible ports return compatiblePorts }, }) ``` ### Where `.getState()` IS Acceptable Only use `.getState()` in these specific cases: 1. **Inside effect handlers** (when you truly need a snapshot): ```typescript const myEffectFx = createEffect(async (params) => { // OK: Effect runs once, needs current value const client = $trpcClient.getState() return client.mutation(params) }) ``` 2. **Better: Use `attach()` instead**: ```typescript // ✅ BEST: Explicit dependency via attach() const myEffectFx = attach({ source: $trpcClient, effect: async (client, params) => { return client.mutation(params) }, }) ``` --- ## Correct Patterns ### Pattern 1: `sample()` - Reactive Derivation Use `sample()` when you need to combine multiple sources reactively: **File**: `apps/chaingraph-frontend/src/store/edges/stores.ts:126-151` ```typescript // Derive dragging port data from nodes and dragging edge const $draggingEdgePortUpdated = sample({ source: $nodes, // Reactive source clock: $draggingEdge, // When to sample fn: (nodes, draggingEdge) => { // Transform function if (!draggingEdge?.nodeId || !draggingEdge?.handleId) { return null } const node = nodes[draggingEdge.nodeId] if (!node) return null const draggingPort = node.getPort(draggingEdge.handleId) return draggingPort ? { draggingEdge, draggingPort } : null }, }) ``` ### Pattern 2: `attach()` - Effect with Source Use `attach()` when effects need store values: **File**: `apps/chaingraph-frontend/src/store/edges/stores.ts:46-74` ```typescript // Effect that needs tRPC client const addEdgeFx = attach({ source: $trpcClient, effect: async (client, event: AddEdgeEventData) => { if (!client) { throw new Error('TRPC client is not initialized') } return client.flow.connectPorts.mutate({ flowId: event.flowId, sourceNodeId: event.sourceNodeId, sourcePortId: event.sourcePortId, targetNodeId: event.targetNodeId, targetPortId: event.targetPortId, }) }, }) ``` ### Pattern 3: `combine()` - Merge Stores Use `combine()` to create derived stores from multiple sources: **File**: `apps/chaingraph-frontend/src/store/flow/stores.ts:300-308` ```typescript // Combine multiple error states export const $allFlowsErrors = combine( $flowsError, $createFlowError, $updateFlowError, $deleteFlowError, $forkFlowError, (loadError, createError, updateError, deleteError, forkError) => loadError || createError || updateError || deleteError || forkError, ) // Object syntax (creates named object) export const $flowSubscriptionState = combine({ status: $flowSubscriptionStatus, error: $flowSubscriptionError, isSubscribed: $isFlowSubscribed, }) ``` ### Pattern 4: Advanced `sample()` with Multiple Clocks **File**: `apps/chaingraph-frontend/src/store/edges/stores.ts:335-393` ```typescript // React to multiple events with named source object sample({ clock: [$portConfigs, $portUI, setEdges, setEdge, $xyflowNodesList], source: { edgeMap: $edgeRenderMap, portConfigs: $portConfigs, portUI: $portUI, xyflowNodes: $xyflowNodesList, }, fn: ({ edgeMap, portConfigs, portUI, xyflowNodes }) => { const changes: Array<{ edgeId: string, changes: Partial }> = [] for (const [edgeId, edge] of edgeMap) { const sourceKey = toPortKey(edge.source, edge.sourceHandle) const sourceConfig = portConfigs.get(sourceKey) // ... compute changes } return { changes } }, target: edgeDataChanged, }) ``` --- ## Global Reset Pattern All stores should support global reset for clean state transitions: **File**: `apps/chaingraph-frontend/src/store/common.ts` ```typescript import { createEvent } from 'effector' export const globalReset = createEvent() ``` **Usage in stores:** ```typescript export const $edges = edgesDomain.createStore([]) .on(setEdges, (source, edges) => [...source, ...edges]) .on(removeEdge, (edges, event) => edges.filter(e => e.edgeId !== event.edgeId)) .reset(resetEdges) // Domain-specific reset .reset(globalReset) // Global reset (ALWAYS add this) ``` --- ## Patronum Utilities ChainGraph uses [patronum](https://patronum.effector.dev/) for advanced patterns: ### `interval` - Time-based Events **File**: `apps/chaingraph-frontend/src/store/flow/event-buffer.ts` ```typescript import { interval } from 'patronum' // Create periodic ticker for event batching const ticker = interval({ timeout: 50, // 50ms interval start: tickerStart, // Event to start ticker stop: tickerStop, // Event to stop ticker }) // Auto-start when buffer gets first event sample({ clock: flowEventReceived, source: $flowEventBuffer, filter: buffer => buffer.length === 1, // First event target: tickerStart, }) // Auto-stop when buffer is empty sample({ clock: $flowEventBuffer, filter: buffer => buffer.length === 0, target: tickerStop, }) ``` ### `spread` - Distribute Events **File**: `apps/chaingraph-frontend/src/store/ports-v2/buffer.ts` ```typescript import { spread } from 'patronum' // Spread port updates to multiple targets sample({ clock: portUpdatesReceived, fn: processPortUpdates, target: spread({ valueUpdates: applyValueUpdates, uiUpdates: applyUIUpdates, configUpdates: applyConfigUpdates, connectionUpdates: applyConnectionUpdates, }), }) ``` ### `debug` - Development Debugging **File**: `apps/chaingraph-frontend/src/store/ports-v2/domain.ts` ```typescript import { debug } from 'patronum' // Enable in development (commented out in production) // debug(portsV2Domain) ``` --- ## React Integration ### Using `useUnit` (Recommended) ```typescript import { useUnit } from 'effector-react' function MyComponent() { // ✅ GOOD: Destructure stores and events together const [nodes, selectedIds, selectNode] = useUnit([ $nodes, $selectedNodeIds, selectNode, ]) // Or with object syntax const { nodes, addNode } = useUnit({ nodes: $nodes, addNode: addNodeEvent, }) return
addNode(newNode)}>{/* ... */}
} ``` ### Avoid: `useStore` and `useEvent` separately ```typescript // ❌ AVOID: Separate hooks (less efficient) const nodes = useStore($nodes) const addNode = useEvent(addNodeEvent) // ✅ PREFER: Combined useUnit const [nodes, addNode] = useUnit([$nodes, addNodeEvent]) ``` --- ## Store Organization Pattern ### Standard Store File Structure ```typescript // stores.ts import { sample, combine } from 'effector' import { myDomain } from '../domains' import { globalReset } from '../common' // ============ EVENTS ============ export const doSomething = myDomain.createEvent() export const reset = myDomain.createEvent() // ============ EFFECTS ============ export const doSomethingFx = myDomain.createEffect(async (payload: Payload) => { // async logic }) // Or with attach for source dependency export const doSomethingFx = attach({ source: $dependency, effect: async (dep, payload) => { // async logic with dep }, }) // ============ STORES ============ export const $myStore = myDomain.createStore(initialState) .on(doSomething, (state, payload) => newState) .on(doSomethingFx.doneData, (state, result) => newState) .reset(reset) .reset(globalReset) // ============ DERIVED STORES ============ export const $derivedStore = combine($myStore, $otherStore, (my, other) => { // compute derived state }) // ============ WIRING ============ sample({ clock: someEvent, source: $myStore, filter: (state) => state.shouldTrigger, target: doSomethingFx, }) ``` --- ## Quick Reference | Need | Pattern | Example | |------|---------|---------| | Derive from multiple stores | `sample({ source, clock, fn })` | Reactive computation | | Effect needs store value | `attach({ source, effect })` | tRPC calls | | Merge stores | `combine(stores, fn)` | Error aggregation | | Time-based batching | `interval({ timeout, start, stop })` | Event buffer | | Distribute to multiple targets | `spread({ ... })` | Port updates | | Reset on app state change | `.reset(globalReset)` | All stores | | Read store in component | `useUnit([$store, event])` | React integration | --- ## Key Files | File | Purpose | |------|---------| | `src/store/domains.ts` | All domain definitions | | `src/store/common.ts` | globalReset event | | `src/store/flow/event-buffer.ts` | Patronum interval example | | `src/store/ports-v2/buffer.ts` | Patronum spread example | | `src/store/edges/stores.ts` | sample/attach examples | --- ## Related Skills - `frontend-architecture` - Overall frontend structure - `subscription-sync` - How stores sync with backend - `optimistic-updates` - Optimistic UI patterns with Effector