Claude-skill-registry effector-patterns
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.
git clone https://github.com/majiayu000/claude-skill-registry
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/effector-patterns" ~/.claude/skills/majiayu000-claude-skill-registry-effector-patterns && rm -rf "$T"
skills/data/effector-patterns/SKILL.mdEffector 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 |
|---|---|---|
| 17 | Flow list, active flow, metadata |
| 20 | Node CRUD, positions, dimensions |
| 23 | Edge connections, anchors, selection |
| 26 | Execution state, events, control |
| 29 | Node categories, filtering |
| 32 | Legacy port management |
| 35 | tRPC client instances |
| 38 | ArchAI integration |
| 41 | Port editor focus state |
| 44 | Drag & drop state |
| 47 | MCP server management |
| 50 | App initialization |
| 53 | Wallet integration |
| hotkeys/stores.ts | Keyboard shortcuts (not in domains.ts) |
| xyflow/domain.ts | XYFlow render (not in domains.ts) |
| perf-trace/domain.ts | Performance (not in domains.ts) |
| ports-v2/domain.ts:23 | Granular ports (not in domains.ts) |
Creating a Domain
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
.getState()This is the most common mistake. Found in 13+ files in the codebase.
// ❌ BAD: .getState() in reducer breaks reactivity const $compatiblePorts = portsDomain.createStore<string[] | null>(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:
bypasses Effector's dependency tracking.getState()- Updates to
won't trigger updates to$nodes$compatiblePorts - No subscription established - reads value once at call time
- Breaks the reactive data flow model
// ✅ 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
.getState()Only use
.getState() in these specific cases:
-
Inside effect handlers (when you truly need a snapshot):
const myEffectFx = createEffect(async (params) => { // OK: Effect runs once, needs current value const client = $trpcClient.getState() return client.mutation(params) }) -
Better: Use
instead:attach()// ✅ 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
sample()Use
sample() when you need to combine multiple sources reactively:
File:
apps/chaingraph-frontend/src/store/edges/stores.ts:126-151
// 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
attach()Use
attach() when effects need store values:
File:
apps/chaingraph-frontend/src/store/edges/stores.ts:46-74
// 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
combine()Use
combine() to create derived stores from multiple sources:
File:
apps/chaingraph-frontend/src/store/flow/stores.ts:300-308
// 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
sample()File:
apps/chaingraph-frontend/src/store/edges/stores.ts:335-393
// 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<EdgeRenderData> }> = [] 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
import { createEvent } from 'effector' export const globalReset = createEvent()
Usage in stores:
export const $edges = edgesDomain.createStore<EdgeData[]>([]) .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 for advanced patterns:
interval
- Time-based Events
intervalFile:
apps/chaingraph-frontend/src/store/flow/event-buffer.ts
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
spreadFile:
apps/chaingraph-frontend/src/store/ports-v2/buffer.ts
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
debugFile:
apps/chaingraph-frontend/src/store/ports-v2/domain.ts
import { debug } from 'patronum' // Enable in development (commented out in production) // debug(portsV2Domain)
React Integration
Using useUnit
(Recommended)
useUnitimport { 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 <div onClick={() => addNode(newNode)}>{/* ... */}</div> }
Avoid: useStore
and useEvent
separately
useStoreuseEvent// ❌ 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
// stores.ts import { sample, combine } from 'effector' import { myDomain } from '../domains' import { globalReset } from '../common' // ============ EVENTS ============ export const doSomething = myDomain.createEvent<Payload>() 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<State>(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 | | Reactive computation |
| Effect needs store value | | tRPC calls |
| Merge stores | | Error aggregation |
| Time-based batching | | Event buffer |
| Distribute to multiple targets | | Port updates |
| Reset on app state change | | All stores |
| Read store in component | | React integration |
Key Files
| File | Purpose |
|---|---|
| All domain definitions |
| globalReset event |
| Patronum interval example |
| Patronum spread example |
| sample/attach examples |
Related Skills
- Overall frontend structurefrontend-architecture
- How stores sync with backendsubscription-sync
- Optimistic UI patterns with Effectoroptimistic-updates