Skip to content

Commit b71ba64

Browse files
committed
perf(workflow): stabilize subflow edge selector
1 parent 094c027 commit b71ba64

2 files changed

Lines changed: 68 additions & 14 deletions

File tree

packages/workflow-renderer/src/subflow/subflow-node-view.tsx

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
Handle,
66
internalsSymbol,
77
Position,
8+
type ReactFlowState,
89
useStore as useReactFlowStore,
910
useStoreApi as useReactFlowStoreApi,
1011
useUpdateNodeInternals,
@@ -344,14 +345,21 @@ export function SubflowNodeView({
344345
const isPreviewSelected = data?.isPreviewSelected || false
345346

346347
const endHandleId = data.kind === 'loop' ? 'loop-end-source' : 'parallel-end-source'
347-
const displayedEdges = useReactFlowStore(
348-
useCallback((state) => (data.parentId ? state.edges : null), [data.parentId])
349-
)
350-
const showFixedEndPort = useMemo(
351-
() =>
352-
!displayedEdges ||
353-
displayedEdges.some((edge) => edge.source === id && edge.sourceHandle === endHandleId),
354-
[displayedEdges, endHandleId, id]
348+
const showFixedEndPort = useReactFlowStore(
349+
useMemo(() => {
350+
let previousEdges: ReactFlowState['edges'] | undefined
351+
let previousResult = !data.parentId
352+
353+
return (state: ReactFlowState) => {
354+
if (!data.parentId || state.edges === previousEdges) return previousResult
355+
356+
previousEdges = state.edges
357+
previousResult = state.edges.some(
358+
(edge) => edge.source === id && edge.sourceHandle === endHandleId
359+
)
360+
return previousResult
361+
}
362+
}, [data.parentId, endHandleId, id])
355363
)
356364
const BlockIcon = data.kind === 'loop' ? Repeat : Split
357365
const blockName = data.name || (data.kind === 'loop' ? 'Loop' : 'Parallel')

packages/workflow-renderer/src/workflow-block/workflow-block-border-mount.test.tsx

Lines changed: 52 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
* a knob-paint bug once threw only when a card had a coloured knob — invisible
66
* on an idle canvas, fatal on node creation.
77
*/
8-
import { act, useLayoutEffect } from 'react'
8+
import { act, Profiler, useLayoutEffect } from 'react'
99
import {
1010
normalizeWorkflowEdgeSourceHandle,
1111
normalizeWorkflowEdgeTargetHandle,
@@ -83,26 +83,26 @@ function mount(element: React.ReactElement) {
8383
return { host, root }
8484
}
8585

86-
const NO_EDGES: Edge[] = []
87-
8886
function SubflowMountFixture({
8987
id,
9088
kind,
9189
parentId,
92-
edges = NO_EDGES,
90+
edges,
91+
onRender,
9392
}: {
9493
id: string
9594
kind: 'loop' | 'parallel'
9695
parentId?: string
9796
edges?: Edge[]
97+
onRender?: () => void
9898
}) {
9999
const reactFlowStore = useReactFlowStoreApi()
100100

101101
useLayoutEffect(() => {
102-
reactFlowStore.setState({ edges })
102+
if (edges) reactFlowStore.setState({ edges })
103103
}, [edges, reactFlowStore])
104104

105-
return (
105+
const view = (
106106
<SubflowNodeView
107107
id={id}
108108
data={{ kind, name: kind === 'loop' ? 'Loop' : 'Parallel', parentId, isPreview: true }}
@@ -114,6 +114,14 @@ function SubflowMountFixture({
114114
onSelect={() => undefined}
115115
/>
116116
)
117+
118+
return onRender ? (
119+
<Profiler id={id} onRender={onRender}>
120+
{view}
121+
</Profiler>
122+
) : (
123+
view
124+
)
117125
}
118126

119127
function getSubflowSilhouette(host: HTMLElement, caseName: string) {
@@ -1077,6 +1085,44 @@ describe('WorkflowBlockBorder mount', () => {
10771085
}
10781086
)
10791087

1088+
it('does not rerender a nested subflow when unrelated edges change', () => {
1089+
const baselineRender = vi.fn()
1090+
const unrelatedEdgeRender = vi.fn()
1091+
const unrelatedEdges: Edge[] = [
1092+
{
1093+
id: 'unrelated-edge',
1094+
source: 'other-source',
1095+
sourceHandle: 'source',
1096+
target: 'other-target',
1097+
targetHandle: 'target',
1098+
},
1099+
]
1100+
1101+
mount(
1102+
<div>
1103+
<ReactFlowProvider>
1104+
<SubflowMountFixture
1105+
id='baseline-nested-loop'
1106+
kind='loop'
1107+
parentId='parent-loop'
1108+
onRender={baselineRender}
1109+
/>
1110+
</ReactFlowProvider>
1111+
<ReactFlowProvider>
1112+
<SubflowMountFixture
1113+
id='unrelated-edge-nested-loop'
1114+
kind='loop'
1115+
parentId='parent-loop'
1116+
edges={unrelatedEdges}
1117+
onRender={unrelatedEdgeRender}
1118+
/>
1119+
</ReactFlowProvider>
1120+
</div>
1121+
)
1122+
1123+
expect(unrelatedEdgeRender).toHaveBeenCalledTimes(baselineRender.mock.calls.length)
1124+
})
1125+
10801126
it('retracts a selected loop action swell after hover ends', () => {
10811127
vi.useFakeTimers()
10821128
vi.stubGlobal(

0 commit comments

Comments
 (0)