From a0188bd9b507a061996a661d5e1d02f566dd0a2c Mon Sep 17 00:00:00 2001 From: yyh Date: Tue, 27 Jan 2026 00:43:58 +0800 Subject: [PATCH] fix(workflow)!: add mounted guard to prevent ReactFlow operations after unmount When switching from graph view to skill view during an active preview run, SSE callbacks continue executing and attempt to update ReactFlow node/edge states. This could cause errors since the component is unmounted. Add optional `isMountedRef` parameter to `useNodesInteractionsWithoutSync` and `useEdgesInteractionsWithoutSync` hooks. When provided, operations are skipped if the component has unmounted, preventing potential errors while allowing the SSE connection to continue running in the background. BREAKING CHANGE: `useNodesInteractionsWithoutSync` and `useEdgesInteractionsWithoutSync` now accept an optional `isMountedRef` parameter. Existing callers are unaffected as the parameter is optional. --- .../use-edges-interactions-without-sync.ts | 8 ++++++-- .../use-nodes-interactions-without-sync.ts | 18 ++++++++++++++---- .../hooks/use-chat-flow-control.ts | 12 +++++++++--- 3 files changed, 29 insertions(+), 9 deletions(-) diff --git a/web/app/components/workflow/hooks/use-edges-interactions-without-sync.ts b/web/app/components/workflow/hooks/use-edges-interactions-without-sync.ts index 99673b70f8..83c43d5b3a 100644 --- a/web/app/components/workflow/hooks/use-edges-interactions-without-sync.ts +++ b/web/app/components/workflow/hooks/use-edges-interactions-without-sync.ts @@ -1,11 +1,15 @@ +import type { RefObject } from 'react' import { produce } from 'immer' import { useCallback } from 'react' import { useStoreApi } from 'reactflow' -export const useEdgesInteractionsWithoutSync = () => { +export const useEdgesInteractionsWithoutSync = (isMountedRef?: RefObject) => { const store = useStoreApi() const handleEdgeCancelRunningStatus = useCallback(() => { + if (isMountedRef && isMountedRef.current === false) + return + const { edges, setEdges, @@ -19,7 +23,7 @@ export const useEdgesInteractionsWithoutSync = () => { }) }) setEdges(newEdges) - }, [store]) + }, [store, isMountedRef]) return { handleEdgeCancelRunningStatus, diff --git a/web/app/components/workflow/hooks/use-nodes-interactions-without-sync.ts b/web/app/components/workflow/hooks/use-nodes-interactions-without-sync.ts index 0c343f4eb8..ed663081f2 100644 --- a/web/app/components/workflow/hooks/use-nodes-interactions-without-sync.ts +++ b/web/app/components/workflow/hooks/use-nodes-interactions-without-sync.ts @@ -1,12 +1,16 @@ +import type { RefObject } from 'react' import { produce } from 'immer' import { useCallback } from 'react' import { useStoreApi } from 'reactflow' import { NodeRunningStatus } from '../types' -export const useNodesInteractionsWithoutSync = () => { +export const useNodesInteractionsWithoutSync = (isMountedRef?: RefObject) => { const store = useStoreApi() const handleNodeCancelRunningStatus = useCallback(() => { + if (isMountedRef && isMountedRef.current === false) + return + const { getNodes, setNodes, @@ -20,9 +24,12 @@ export const useNodesInteractionsWithoutSync = () => { }) }) setNodes(newNodes) - }, [store]) + }, [store, isMountedRef]) const handleCancelAllNodeSuccessStatus = useCallback(() => { + if (isMountedRef && isMountedRef.current === false) + return + const { getNodes, setNodes, @@ -36,9 +43,12 @@ export const useNodesInteractionsWithoutSync = () => { }) }) setNodes(newNodes) - }, [store]) + }, [store, isMountedRef]) const handleCancelNodeSuccessStatus = useCallback((nodeId: string) => { + if (isMountedRef && isMountedRef.current === false) + return + const { getNodes, setNodes, @@ -52,7 +62,7 @@ export const useNodesInteractionsWithoutSync = () => { } }) setNodes(newNodes) - }, [store]) + }, [store, isMountedRef]) return { handleNodeCancelRunningStatus, diff --git a/web/app/components/workflow/panel/debug-and-preview/hooks/use-chat-flow-control.ts b/web/app/components/workflow/panel/debug-and-preview/hooks/use-chat-flow-control.ts index 54215e0a59..21af491126 100644 --- a/web/app/components/workflow/panel/debug-and-preview/hooks/use-chat-flow-control.ts +++ b/web/app/components/workflow/panel/debug-and-preview/hooks/use-chat-flow-control.ts @@ -1,4 +1,4 @@ -import { useCallback } from 'react' +import { useCallback, useEffect, useRef } from 'react' import { DEFAULT_ITER_TIMES, DEFAULT_LOOP_TIMES } from '../../../constants' import { useEdgesInteractionsWithoutSync } from '../../../hooks/use-edges-interactions-without-sync' import { useNodesInteractionsWithoutSync } from '../../../hooks/use-nodes-interactions-without-sync' @@ -19,8 +19,14 @@ export function useChatFlowControl({ const setHasStopResponded = useStore(s => s.setHasStopResponded) const setSuggestedQuestionsAbortController = useStore(s => s.setSuggestedQuestionsAbortController) const invalidateRun = useStore(s => s.invalidateRun) - const { handleNodeCancelRunningStatus } = useNodesInteractionsWithoutSync() - const { handleEdgeCancelRunningStatus } = useEdgesInteractionsWithoutSync() + + const isMountedRef = useRef(true) + useEffect(() => () => { + isMountedRef.current = false + }, []) + + const { handleNodeCancelRunningStatus } = useNodesInteractionsWithoutSync(isMountedRef) + const { handleEdgeCancelRunningStatus } = useEdgesInteractionsWithoutSync(isMountedRef) const { setIterTimes, setLoopTimes } = workflowStore.getState()