name: frontend-architecture description: Frontend package architecture for ChainGraph visual flow editor. Use when working on apps/chaingraph-frontend, React components, Effector stores, XYFlow integration, UI components, providers, or hooks. Covers directory structure, domain organization, provider hierarchy, component patterns. Triggers: frontend, react, component, provider, hook, ui, sidebar, flow editor, chaingraph-frontend, vite, tailwind.
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
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
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:
// 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
// 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
ChainGraphProvider
└─ RootProvider
├─ ThemeProvider
├─ ReactFlowProvider
│ ├─ ZoomProvider
│ ├─ DndContextProvider
│ └─ MenuPositionProvider
└─ [children]
Store Patterns
Creating a Store
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:
import { globalReset } from '../global'
$myStore.reset(globalReset)
Component Patterns
Using Effector in Components
import { useUnit } from 'effector-react'
import { $nodes, addNode } from '@/store/nodes'
function MyComponent() {
const [nodes, handleAddNode] = useUnit([$nodes, addNode])
// ...
}
Lazy Loading
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 = 300NODE_DIMENSIONS_DEBOUNCE_MS = 500NODE_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-patternsxyflow-patterns- XYFlow integration patternssubscription-sync- Real-time data synchronizationoptimistic-updates- Optimistic UI patternschaingraph-concepts- Core domain conceptstrpc-patterns- tRPC framework patternstrpc-flow-editing- Flow editing tRPC procedures