improvement(workflow): remove useEffect anti-patterns

This commit is contained in:
waleed
2026-01-27 13:05:11 -08:00
parent 6b412c578d
commit fe4fd47b9d
2 changed files with 40 additions and 42 deletions

View File

@@ -307,9 +307,8 @@ const WorkflowContent = React.memo(() => {
const isAutoConnectEnabled = useAutoConnect()
const autoConnectRef = useRef(isAutoConnectEnabled)
useEffect(() => {
autoConnectRef.current = isAutoConnectEnabled
}, [isAutoConnectEnabled])
// Keep ref in sync with latest value for use in callbacks (no effect needed)
autoConnectRef.current = isAutoConnectEnabled
// Panel open states for context menu
const isVariablesOpen = useVariablesStore((state) => state.isOpen)
@@ -448,11 +447,14 @@ const WorkflowContent = React.memo(() => {
)
/** Re-applies diff markers when blocks change after socket rehydration. */
const blocksRef = useRef(blocks)
const diffBlocksRef = useRef(blocks)
useEffect(() => {
if (!isWorkflowReady) return
if (hasActiveDiff && isDiffReady && blocks !== blocksRef.current) {
blocksRef.current = blocks
// Track if blocks actually changed (vs other deps triggering this effect)
const blocksChanged = blocks !== diffBlocksRef.current
diffBlocksRef.current = blocks
if (!isWorkflowReady || !blocksChanged) return
if (hasActiveDiff && isDiffReady) {
setTimeout(() => reapplyDiffMarkers(), 0)
}
}, [blocks, hasActiveDiff, isDiffReady, reapplyDiffMarkers, isWorkflowReady])
@@ -2160,6 +2162,8 @@ const WorkflowContent = React.memo(() => {
// Local state for nodes - allows smooth drag without store updates on every frame
const [displayNodes, setDisplayNodes] = useState<Node[]>([])
// Sync derivedNodes to displayNodes while preserving selection state
// This effect handles both normal sync and pending selection from paste/duplicate
useEffect(() => {
// Check for pending selection (from paste/duplicate), otherwise preserve existing selection
if (pendingSelection && pendingSelection.length > 0) {
@@ -2186,7 +2190,7 @@ const WorkflowContent = React.memo(() => {
selected: selectedIds.has(node.id),
}))
})
}, [derivedNodes, blocks, pendingSelection, clearPendingSelection])
}, [derivedNodes, blocks, pendingSelection, clearPendingSelection, syncPanelWithSelection])
// Phase 2: When displayNodes updates, check if pending zoom blocks are ready
// (Phase 1 is located earlier in the file where pendingZoomBlockIdsRef is defined)
@@ -2380,40 +2384,6 @@ const WorkflowContent = React.memo(() => {
resizeLoopNodesWrapper()
}, [derivedNodes, resizeLoopNodesWrapper, isWorkflowReady])
/** Cleans up orphaned nodes with invalid parent references after deletion. */
useEffect(() => {
if (!isWorkflowReady) return
// Create a mapping of node IDs to check for missing parent references
const nodeIds = new Set(Object.keys(blocks))
// Check for nodes with invalid parent references and collect updates
const orphanedUpdates: Array<{
id: string
position: { x: number; y: number }
parentId: string
}> = []
Object.entries(blocks).forEach(([id, block]) => {
const parentId = block.data?.parentId
// If block has a parent reference but parent no longer exists
if (parentId && !nodeIds.has(parentId)) {
logger.warn('Found orphaned node with invalid parent reference', {
nodeId: id,
missingParentId: parentId,
})
const absolutePosition = getNodeAbsolutePosition(id)
orphanedUpdates.push({ id, position: absolutePosition, parentId: '' })
}
})
// Batch update all orphaned nodes at once
if (orphanedUpdates.length > 0) {
batchUpdateBlocksWithParent(orphanedUpdates)
}
}, [blocks, batchUpdateBlocksWithParent, getNodeAbsolutePosition, isWorkflowReady])
/** Handles edge removal changes. */
const onEdgesChange = useCallback(
(changes: any) => {

View File

@@ -448,6 +448,34 @@ export const useWorkflowStore = create<WorkflowStore>()(
delete newBlocks[blockId]
})
// Clean up orphaned nodes - blocks whose parent was removed but weren't descendants
// This can happen in edge cases (e.g., data inconsistency, external modifications)
const remainingBlockIds = new Set(Object.keys(newBlocks))
Object.entries(newBlocks).forEach(([blockId, block]) => {
const parentId = block.data?.parentId
if (parentId && !remainingBlockIds.has(parentId)) {
// Parent was removed - convert to absolute position and clear parentId
// Calculate absolute position by traversing up the (now-deleted) parent chain
let absoluteX = block.position.x
let absoluteY = block.position.y
// Try to get parent's position from original blocks before deletion
let currentParentId: string | undefined = parentId
while (currentParentId && currentBlocks[currentParentId]) {
const parent = currentBlocks[currentParentId]
absoluteX += parent.position.x
absoluteY += parent.position.y
currentParentId = parent.data?.parentId
}
newBlocks[blockId] = {
...block,
position: { x: absoluteX, y: absoluteY },
data: { ...block.data, parentId: undefined },
}
}
})
const activeWorkflowId = useWorkflowRegistry.getState().activeWorkflowId
if (activeWorkflowId) {
const subBlockStore = useSubBlockStore.getState()