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.
This commit is contained in:
yyh
2026-01-27 00:43:58 +08:00
parent 6b439b1699
commit a0188bd9b5
3 changed files with 29 additions and 9 deletions

View File

@@ -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<boolean>) => {
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,

View File

@@ -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<boolean>) => {
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,

View File

@@ -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()