Claude Code Plugins

Community-maintained marketplace

Feedback
12
20

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.

Install Skill

Shared

Installs to .agents/skills, used by Codex, Amp, Warp, Cursor, OpenCode, and more.

CodexAmp
Warp
CursorOpenCode
Cline
Gemini CLI
GitHub Copilot
Personal

Available across projects.

$npx skills-installer add @chaingraphlabs/chaingraph/effector-patterns --client shared
Project

Writes to .agents/skills.

$npx skills-installer add @chaingraphlabs/chaingraph/effector-patterns -p --client shared
Note: Review the skill instructions before using it.

SKILL.md


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

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.

// ❌ 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:

  • .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
// ✅ 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):

    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:

    // ✅ 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

// 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

// 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

// 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

// 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

File: 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

File: 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

File: 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)

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 <div onClick={() => addNode(newNode)}>{/* ... */}</div>
}

Avoid: useStore and useEvent separately

// ❌ 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 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