diff --git a/web/app/components/workflow/hooks/use-collaborative-workflow.ts b/web/app/components/workflow/hooks/use-collaborative-workflow.ts index 267bb2ecb3..c3d4ddc294 100644 --- a/web/app/components/workflow/hooks/use-collaborative-workflow.ts +++ b/web/app/components/workflow/hooks/use-collaborative-workflow.ts @@ -1,5 +1,5 @@ import type { Edge, Node } from '../types' -import { useCallback } from 'react' +import { useCallback, useMemo } from 'react' import { useStoreApi } from 'reactflow' import { collaborationManager } from '../collaboration/core/collaboration-manager' @@ -77,9 +77,9 @@ export const useCollaborativeWorkflow = () => { } }, [store, setNodes, setEdges]) - return { + return useMemo(() => ({ getState: collaborativeStore, setNodes, setEdges, - } + }), [collaborativeStore, setEdges, setNodes]) } diff --git a/web/app/components/workflow/nodes/_base/hooks/__tests__/use-node-crud.spec.ts b/web/app/components/workflow/nodes/_base/hooks/__tests__/use-node-crud.spec.ts new file mode 100644 index 0000000000..1ad0c38e8a --- /dev/null +++ b/web/app/components/workflow/nodes/_base/hooks/__tests__/use-node-crud.spec.ts @@ -0,0 +1,87 @@ +import type { CommonNodeType } from '@/app/components/workflow/types' +import { renderHook } from '@testing-library/react' +import { BlockEnum } from '@/app/components/workflow/types' +import useNodeCrud from '../use-node-crud' + +const mockHandleNodeDataUpdateWithSyncDraft = vi.hoisted(() => ({ + current: vi.fn(), +})) + +vi.mock('@/app/components/workflow/hooks', () => ({ + useNodeDataUpdate: () => ({ + handleNodeDataUpdateWithSyncDraft: mockHandleNodeDataUpdateWithSyncDraft.current, + }), +})) + +type TestNodeData = CommonNodeType<{ + value: string +}> + +const createData = (value = 'initial'): TestNodeData => ({ + type: BlockEnum.LLM, + title: 'Test Node', + desc: '', + value, +}) + +describe('useNodeCrud', () => { + beforeEach(() => { + vi.clearAllMocks() + mockHandleNodeDataUpdateWithSyncDraft.current = vi.fn() + }) + + it('keeps setInputs stable across rerenders when id does not change', () => { + const { result, rerender } = renderHook( + ({ id, data }) => useNodeCrud(id, data), + { + initialProps: { + id: 'node-1', + data: createData(), + }, + }, + ) + + const firstSetInputs = result.current.setInputs + + rerender({ + id: 'node-1', + data: createData('updated'), + }) + + expect(result.current.setInputs).toBe(firstSetInputs) + }) + + it('forwards node data updates with the current node id and latest updater', () => { + const { result, rerender } = renderHook( + ({ id, data }) => useNodeCrud(id, data), + { + initialProps: { + id: 'node-1', + data: createData(), + }, + }, + ) + + result.current.setInputs(createData('changed')) + + expect(mockHandleNodeDataUpdateWithSyncDraft.current).toHaveBeenCalledWith({ + id: 'node-1', + data: createData('changed'), + }) + + const nextUpdater = vi.fn() + mockHandleNodeDataUpdateWithSyncDraft.current = nextUpdater + + rerender({ + id: 'node-1', + data: createData('changed'), + }) + + result.current.setInputs(createData('latest')) + + expect(nextUpdater).toHaveBeenCalledWith({ + id: 'node-1', + data: createData('latest'), + }) + }) +}) diff --git a/web/app/components/workflow/nodes/_base/hooks/use-node-crud.ts b/web/app/components/workflow/nodes/_base/hooks/use-node-crud.ts index d1741f0bbb..fad9aa9c5e 100644 --- a/web/app/components/workflow/nodes/_base/hooks/use-node-crud.ts +++ b/web/app/components/workflow/nodes/_base/hooks/use-node-crud.ts @@ -1,15 +1,21 @@ import type { CommonNodeType } from '@/app/components/workflow/types' +import { useCallback, useEffect, useRef } from 'react' import { useNodeDataUpdate } from '@/app/components/workflow/hooks' const useNodeCrud = (id: string, data: CommonNodeType) => { const { handleNodeDataUpdateWithSyncDraft } = useNodeDataUpdate() + const updateRef = useRef(handleNodeDataUpdateWithSyncDraft) - const setInputs = (newInputs: CommonNodeType) => { - handleNodeDataUpdateWithSyncDraft({ + useEffect(() => { + updateRef.current = handleNodeDataUpdateWithSyncDraft + }, [handleNodeDataUpdateWithSyncDraft]) + + const setInputs = useCallback((newInputs: CommonNodeType) => { + updateRef.current({ id, data: newInputs, }) - } + }, [id]) return { inputs: data,