ChainGraph Frontend Architecture
This skill provides architectural guidance for the @badaitech/chaingraph-frontend package - a React-based visual programming interface for designing computational graphs.
Package Overview
Location: apps/chaingraph-frontend/
Purpose: Visual flow editor for ChainGraph using XYFlow
Stack: React 19 + Vite + TypeScript + Effector + XYFlow + Tailwind + Radix UI
Directory Structure
code
apps/chaingraph-frontend/src/ ├── main.tsx # Entry point, store initialization ├── App.tsx # Router setup ├── FlowLayout.tsx # Layout: Sidebar + Flow ├── config.ts # Environment configuration │ ├── components/ │ ├── flow/ # Main flow editor (see below) │ ├── ui/ # Radix UI components (31 files) │ ├── sidebar/ # Navigation & flow list │ ├── dnd/ # Drag-and-drop (dnd-kit) │ ├── debug/ # Debug tools │ └── theme/ # Theme switching │ ├── store/ # Effector state (15 domains) │ ├── domains.ts # Domain definitions │ ├── initialization.ts # App init orchestration │ ├── nodes/ # Node CRUD, positions │ ├── edges/ # Edge connections, anchors │ ├── flow/ # Active flow, metadata │ ├── ports-v2/ # Port values, configs, UI │ ├── execution/ # Execution state │ ├── trpc/ # tRPC clients (2 servers) │ └── ... # 20+ more domains │ ├── hooks/ # Global custom hooks └── providers/ # Provider hierarchy
Flow Component Structure
code
components/flow/
├── Flow.tsx # Main XYFlow container (13.6KB)
├── nodes/
│ ├── ChaingraphNode/ # Main node component (17 files)
│ │ ├── ChaingraphNodeOptimized.tsx # Perf-optimized
│ │ └── ports/ # ArrayPort, ObjectPort
│ ├── AnchorNode/ # Edge anchor visualization
│ └── GroupNode/ # Group nodes
├── edges/
│ ├── components/ # Edge UI
│ └── utils/ # Edge calculations
└── hooks/ # 18 flow-specific hooks
├── useNodeDragHandling.ts
├── useNodeSelection.ts
├── useFlowCopyPaste.ts
├── useKeyboardShortcuts.ts
└── ...
Key Architectural Patterns
1. Two tRPC Clients
The app manages TWO separate WebSocket connections:
typescript
// Main Server (flows, nodes, edges) $trpcClient → ws://localhost:3001 // Executor Server (execution events) $trpcClientExecutor → ws://localhost:4021
2. Effector Domain-Based Stores
15 domains organized by feature:
| Domain | Purpose |
|---|---|
nodes | Node CRUD, positions, dimensions |
edges | Connections, anchors, selection |
flow | Active flow, metadata |
ports-v2 | Port values, configs, UI state |
execution | Execution state & events |
trpc | tRPC client instances |
xyflow | XYFlow render optimization |
3. Optimistic Updates + Debouncing
typescript
// Pattern: Local update → Debounce → Server sync updateNodePositionLocal(event) // Immediate UI update // After NODE_POSITION_DEBOUNCE_MS (300ms)... updateNodePositionFx(event) // Send to server
4. Provider Hierarchy
code
ChainGraphProvider
└─ RootProvider
├─ ThemeProvider
├─ ReactFlowProvider
│ ├─ ZoomProvider
│ ├─ DndContextProvider
│ └─ MenuPositionProvider
└─ [children]
Store Patterns
Creating a Store
typescript
import { nodesDomain } from '../domains'
// Events (commands)
export const addNode = nodesDomain.createEvent<INode>()
export const removeNode = nodesDomain.createEvent<string>()
// Effects (async operations)
export const addNodeFx = nodesDomain.createEffect(async (params) => {
const client = $trpcClient.getState()
return client.flow.addNode.mutate(params)
})
// Stores
export const $nodes = nodesDomain.createStore<Record<string, INode>>({})
.on(addNode, (nodes, node) => ({ ...nodes, [node.id]: node }))
.on(removeNode, (nodes, id) => {
const { [id]: _, ...rest } = nodes
return rest
})
// Derived stores
export const $selectedNodeIds = combine($nodes, (nodes) =>
Object.keys(nodes).filter(id => nodes[id].isSelected)
)
Global Reset Pattern
All stores support global reset:
typescript
import { globalReset } from '../global'
$myStore.reset(globalReset)
Component Patterns
Using Effector in Components
typescript
import { useUnit } from 'effector-react'
import { $nodes, addNode } from '@/store/nodes'
function MyComponent() {
const [nodes, handleAddNode] = useUnit([$nodes, addNode])
// ...
}
Lazy Loading
typescript
const LazyComponent = lazy(() => import('./HeavyComponent'))
// In render
<Suspense fallback={<Loading />}>
<LazyComponent />
</Suspense>
Performance Considerations
- •Node Position Interpolation - Smooth animations via
position-interpolation-advanced.ts - •XYFlow Store - Combined render data per node to reduce re-renders
- •Effector Sampling - Accumulate rapid updates, sample at intervals
- •Debounce Constants:
- •
NODE_POSITION_DEBOUNCE_MS = 300 - •
NODE_DIMENSIONS_DEBOUNCE_MS = 500 - •
NODE_UI_DEBOUNCE_MS = 1000
- •
Key Files Reference
| File | Purpose |
|---|---|
src/main.tsx | App bootstrap |
src/store/initialization.ts | Init orchestration |
src/store/domains.ts | 13 Effector domains |
src/components/flow/Flow.tsx | Main flow component |
src/providers/ChainGraphProvider.tsx | Top-level provider |
Best Practices
- •Use domain-based stores - Create stores within appropriate domains
- •Optimistic updates first - Update local store immediately, sync to server with debounce
- •Avoid direct tRPC calls in components - Use Effector effects
- •Use
useUnit- NotuseStoreoruseEventseparately - •Reset on
globalReset- All stores should support global reset - •Lazy load heavy components - Especially documentation/tooltips
- •Use ports-v2 - Legacy
ports/is being phased out
Related Skills
- •
effector-patterns- Effector state management anti-patterns - •
xyflow-patterns- XYFlow integration patterns - •
subscription-sync- Real-time data synchronization - •
optimistic-updates- Optimistic UI patterns - •
chaingraph-concepts- Core domain concepts - •
trpc-patterns- tRPC framework patterns - •
trpc-flow-editing- Flow editing tRPC procedures