Skip to content

Commit 5cc657b

Browse files
authored
refactor(agentflow): Simplify ReactFlow and Context state sync (#5827)
* Fix biderectional sync * addede utility function to sync states * Fix comment * Normalize nodes before updating states
1 parent 7a3b253 commit 5cc657b

2 files changed

Lines changed: 50 additions & 71 deletions

File tree

packages/agentflow/src/Agentflow.tsx

Lines changed: 5 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ function AgentflowCanvas({
5151
renderHeader?: AgentflowProps['renderHeader']
5252
renderNodePalette?: AgentflowProps['renderNodePalette']
5353
}) {
54-
const { state, setNodes, setEdges, setDirty, setReactFlowInstance, closeEditDialog } = useAgentflowContext()
54+
const { state, setNodes, setEdges, setDirty, setReactFlowInstance, closeEditDialog, registerLocalStateSetters } = useAgentflowContext()
5555
const { isDarkMode } = useConfigContext()
5656
const agentflow = useAgentflow()
5757
const reactFlowWrapper = useRef<HTMLDivElement>(null)
@@ -75,71 +75,20 @@ function AgentflowCanvas({
7575
// Load available nodes
7676
const { availableNodes } = useFlowNodes()
7777

78-
// TODO: Refactor bidirectional state sync architecture
79-
// Current implementation uses refs and setTimeout to prevent circular updates between
80-
// ReactFlow's local state (useNodesState/useEdgesState) and AgentflowContext state.
81-
// This approach is brittle and prone to race conditions.
82-
//
83-
// Problem: We have TWO sources of truth:
84-
// 1. ReactFlow local state (nodes/edges) - updated by drag/drop, user interactions
85-
// 2. AgentflowContext state - updated by edit dialog, delete, duplicate operations
86-
//
87-
// Current sync flow:
88-
// - Local ReactFlow → Context (for drag/drop) ✓
89-
// - Context → Local ReactFlow (for edit dialog updates) ⚠️ Brittle with refs/setTimeout
90-
//
91-
// Better solution (for future refactor):
92-
// Option A: Make context operations (updateNodeData, deleteNode, etc.) also call
93-
// setLocalNodes/setLocalEdges directly, then remove reverse sync entirely.
94-
// Option B: Use deep equality checks (e.g., lodash.isEqual) instead of refs/setTimeout.
95-
// Option C: Consolidate to single source of truth (context only, ReactFlow reads from it).
96-
//
97-
// See: https://github.com/FlowiseAI/Flowise/pull/XXX for discussion
98-
//
99-
// Use refs to prevent circular sync updates
100-
const syncingToContext = useRef(false)
101-
const syncingFromContext = useRef(false)
78+
// Register local state setters with context on mount
79+
useEffect(() => {
80+
registerLocalStateSetters(setLocalNodes, setLocalEdges)
81+
}, [registerLocalStateSetters, setLocalNodes, setLocalEdges])
10282

10383
// Sync local ReactFlow state to context (when user interacts with canvas)
10484
useEffect(() => {
105-
if (syncingFromContext.current) return
106-
syncingToContext.current = true
10785
setNodes(nodes as FlowNode[])
108-
// Use setTimeout to reset the flag after the sync completes
109-
setTimeout(() => {
110-
syncingToContext.current = false
111-
}, 0)
11286
}, [nodes, setNodes])
11387

11488
useEffect(() => {
115-
if (syncingFromContext.current) return
116-
syncingToContext.current = true
11789
setEdges(edges as FlowEdge[])
118-
setTimeout(() => {
119-
syncingToContext.current = false
120-
}, 0)
12190
}, [edges, setEdges])
12291

123-
// Sync context state back to local ReactFlow (when updated via edit dialog, etc.)
124-
useEffect(() => {
125-
if (syncingToContext.current) return
126-
syncingFromContext.current = true
127-
setLocalNodes(state.nodes)
128-
setTimeout(() => {
129-
syncingFromContext.current = false
130-
}, 0)
131-
}, [state.nodes, setLocalNodes])
132-
133-
useEffect(() => {
134-
if (syncingToContext.current) return
135-
syncingFromContext.current = true
136-
setLocalEdges(state.edges)
137-
setTimeout(() => {
138-
syncingFromContext.current = false
139-
}, 0)
140-
}, [state.edges, setLocalEdges])
141-
// END TODO: Bidirectional state sync
142-
14392
// Flow handlers
14493
const { handleConnect, handleNodesChange, handleEdgesChange, handleAddNode } = useFlowHandlers({
14594
nodes: nodes as FlowNode[],

packages/agentflow/src/infrastructure/store/AgentflowContext.tsx

Lines changed: 45 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { createContext, Dispatch, ReactNode, useCallback, useContext, useReducer } from 'react'
1+
import { createContext, Dispatch, ReactNode, useCallback, useContext, useReducer, useRef } from 'react'
22
import type { ReactFlowInstance } from 'reactflow'
33

44
import type { AgentflowAction, AgentflowState, FlowConfig, FlowData, FlowEdge, FlowNode, InputParam, NodeData } from '@/core/types'
@@ -32,6 +32,9 @@ export interface AgentflowContextValue {
3232
//Dialog operations
3333
openEditDialog: (nodeId: string, data: NodeData, inputParams: InputParam[]) => void
3434
closeEditDialog: () => void
35+
36+
// Register ReactFlow local state setters
37+
registerLocalStateSetters: (setLocalNodes: (nodes: FlowNode[]) => void, setLocalEdges: (edges: FlowEdge[]) => void) => void
3538
}
3639

3740
const AgentflowContext = createContext<AgentflowContextValue | null>(null)
@@ -48,6 +51,34 @@ export function AgentflowStateProvider({ children, initialFlow }: AgentflowState
4851
edges: initialFlow?.edges || []
4952
})
5053

54+
// Store ReactFlow local state setters in refs which are populated by AgentflowCanvas
55+
const localNodesSetterRef = useRef<((nodes: FlowNode[]) => void) | null>(null)
56+
const localEdgesSetterRef = useRef<((edges: FlowEdge[]) => void) | null>(null)
57+
58+
const registerLocalStateSetters = useCallback(
59+
(setLocalNodes: (nodes: FlowNode[]) => void, setLocalEdges: (edges: FlowEdge[]) => void) => {
60+
localNodesSetterRef.current = setLocalNodes
61+
localEdgesSetterRef.current = setLocalEdges
62+
},
63+
[]
64+
)
65+
66+
// Helper function to synchronize state updates between context and ReactFlow
67+
const syncStateUpdate = useCallback(({ nodes, edges }: { nodes?: FlowNode[]; edges?: FlowEdge[] }) => {
68+
if (nodes !== undefined) {
69+
const normalizedNodes = normalizeNodes(nodes)
70+
dispatch({ type: 'SET_NODES', payload: normalizedNodes })
71+
localNodesSetterRef.current?.(normalizedNodes)
72+
}
73+
if (edges !== undefined) {
74+
dispatch({ type: 'SET_EDGES', payload: edges })
75+
localEdgesSetterRef.current?.(edges)
76+
}
77+
if (nodes !== undefined || edges !== undefined) {
78+
dispatch({ type: 'SET_DIRTY', payload: true })
79+
}
80+
}, [])
81+
5182
// Convenience setters
5283
const setNodes = useCallback((nodes: FlowNode[]) => {
5384
dispatch({ type: 'SET_NODES', payload: nodes })
@@ -74,11 +105,10 @@ export function AgentflowStateProvider({ children, initialFlow }: AgentflowState
74105
(nodeId: string) => {
75106
const newNodes = state.nodes.filter((node) => node.id !== nodeId)
76107
const newEdges = state.edges.filter((edge) => edge.source !== nodeId && edge.target !== nodeId)
77-
dispatch({ type: 'SET_NODES', payload: newNodes })
78-
dispatch({ type: 'SET_EDGES', payload: newEdges })
79-
dispatch({ type: 'SET_DIRTY', payload: true })
108+
109+
syncStateUpdate({ nodes: newNodes, edges: newEdges })
80110
},
81-
[state.nodes, state.edges]
111+
[state.nodes, state.edges, syncStateUpdate]
82112
)
83113

84114
const duplicateNode = useCallback(
@@ -96,10 +126,10 @@ export function AgentflowStateProvider({ children, initialFlow }: AgentflowState
96126
data: { ...nodeToDuplicate.data }
97127
}
98128

99-
dispatch({ type: 'SET_NODES', payload: [...state.nodes, newNode] })
100-
dispatch({ type: 'SET_DIRTY', payload: true })
129+
const newNodes = [...state.nodes, newNode]
130+
syncStateUpdate({ nodes: newNodes })
101131
},
102-
[state.nodes]
132+
[state.nodes, syncStateUpdate]
103133
)
104134

105135
const updateNodeData = useCallback(
@@ -113,20 +143,19 @@ export function AgentflowStateProvider({ children, initialFlow }: AgentflowState
113143
}
114144
return node
115145
})
116-
dispatch({ type: 'SET_NODES', payload: newNodes })
117-
dispatch({ type: 'SET_DIRTY', payload: true })
146+
147+
syncStateUpdate({ nodes: newNodes })
118148
},
119-
[state.nodes]
149+
[state.nodes, syncStateUpdate]
120150
)
121151

122152
// Edge operations
123153
const deleteEdge = useCallback(
124154
(edgeId: string) => {
125155
const newEdges = state.edges.filter((edge) => edge.id !== edgeId)
126-
dispatch({ type: 'SET_EDGES', payload: newEdges })
127-
dispatch({ type: 'SET_DIRTY', payload: true })
156+
syncStateUpdate({ edges: newEdges })
128157
},
129-
[state.edges]
158+
[state.edges, syncStateUpdate]
130159
)
131160

132161
// Dialog operations
@@ -179,7 +208,8 @@ export function AgentflowStateProvider({ children, initialFlow }: AgentflowState
179208
openEditDialog,
180209
closeEditDialog,
181210
getFlowData,
182-
reset
211+
reset,
212+
registerLocalStateSetters
183213
}
184214

185215
return <AgentflowContext.Provider value={value}>{children}</AgentflowContext.Provider>

0 commit comments

Comments
 (0)