perf(web): use imperative store access in action-only hooks - #4208
perf(web): use imperative store access in action-only hooks#4208claude[bot] wants to merge 1 commit into
Conversation
Several hooks subscribed reactively to `state.nodes` and `state.edges` but only consumed them inside callbacks invoked on user action. Every node drag frame changed the array references, re-rendering every component that mounted these hooks — even though the data was only needed at action time, not at render time. Switch to `useNodeStoreRef()` + `store.getState()` inside the callbacks so the components no longer re-render on drag/pan/zoom. Hooks fixed: useDuplicate, useAlignNodes, useGroupIntoSubgraph, useSelectConnected, useNodeContextMenu, CommandMenu (WorkflowCommands). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
georgi
left a comment
There was a problem hiding this comment.
CI is failing — test-app and quality gate are red (typecheck passes). The useSelectConnected hook removes the connectedNodeCount property from its return type, but existing tests or consumers may still reference it.
The perf optimization (switching from reactive subscriptions to imperative store.getState() for action-only hooks) is the right pattern. Please fix the test failures and push an update.
Generated by Claude Code
georgi
left a comment
There was a problem hiding this comment.
CI is failing — 14 test failures in src/hooks/nodes/__tests__/useNodeContextMenu.test.ts. Main is currently green, so these are introduced by this PR.
Root cause: The switch from reactive Zustand selectors to imperative store.getState() means the test mocks (which set up state for hook rendering) aren't reaching the imperative code path. Functions like updateNodeData, setSelectedNodes, duplicateNode, and runInline are never called, and conditions like isInGroup and isBypassed read stale defaults.
The tests need to be updated to mock findNode (the new O(1) lookup replacing nodes.find()) and to set up the store state so that getState() returns the expected values at call time.
Also note: PR #4238 (Set optimization in useDuplicate.ts) has been merged to main. This PR also modifies useDuplicate.ts, so you'll need to rebase and resolve the merge conflict there.
Generated by Claude Code
georgi
left a comment
There was a problem hiding this comment.
The performance optimization approach (imperative getState() for action-only hooks) is correct and the connectedNodeCount removal is verified safe.
However, CI fails with 14 test failures in useNodeContextMenu.test.ts. The root cause:
The hook was changed from nodes.find(n => n.id === nodeId) to findNode(nodeId), but the test's mockFindNode is a bare jest.fn() that returns undefined by default. The test relied on mockNodes.find(...) working implicitly.
Fix needed in web/src/hooks/nodes/__tests__/useNodeContextMenu.test.ts:
- In
beforeEach(~line 136), add:mockFindNode.mockImplementation((id: string) => mockNodes.find(n => n.id === id)) - Update per-test overrides (lines ~584, ~620) that use modified nodes (
groupedNode,bypassedNode) to configuremockFindNodeto return the correct node.
Once the test mock is fixed, this should be ready to merge.
Generated by Claude Code
georgi
left a comment
There was a problem hiding this comment.
The imperative store access pattern is correct and well-motivated — reading nodes/edges via store.getState() inside callbacks rather than subscribing at render time is the right approach for action-only hooks.
However, CI is red: test-app and quality jobs failed. Since main branch CI is currently green, this failure was introduced by this PR. Please investigate and fix the test-app failure before merge.
Generated by Claude Code
georgi
left a comment
There was a problem hiding this comment.
The Zustand pattern change (using useNodeStoreRef() + store.getState() for action-only callbacks) is correct and idiomatic. However, CI is failing with 14 test failures in useNodeContextMenu.test.ts, directly caused by this PR.
The source change in useNodeContextMenu.ts switches from state.nodes.find(...) to state.findNode(nodeId), but the test file's mockFindNode was never given a working implementation — it always returns undefined. Before this PR, the mock was dead code (production code used nodes.find()); now it's load-bearing and broken.
Fix: Give mockFindNode a working implementation in beforeEach, e.g.:
mockFindNode.mockImplementation((id) => nodeContextState.nodes.find(n => n.id === id));Also note: this PR modifies useGroupIntoSubgraph.ts, which is also modified by PR #4289 (type annotations). Once #4289 merges, this PR will need a rebase to resolve the conflict on that file.
Generated by Claude Code
georgi
left a comment
There was a problem hiding this comment.
This PR is 8 days stale and has CI failures (test-app). The pattern is sound (moving from reactive subscriptions to imperative store.getState() inside callbacks), but it needs a rebase onto current main before it can be merged. The test failures are likely due to codebase drift since July 10.
Generated by Claude Code
georgi
left a comment
There was a problem hiding this comment.
This PR has merge conflicts (mergeable_state: dirty) and CI failures (test-app, quality gate). The performance optimization approach is correct — replacing reactive store subscriptions with imperative store.getState() in action-only callbacks — but the branch needs to be rebased on current main and the test failures resolved before it can be merged.
The failing test-app job likely indicates the test suite encountered changes on main that conflict with this PR's modifications to the hook files.
Generated by Claude Code
georgi
left a comment
There was a problem hiding this comment.
The imperative store.getState() pattern is correct for action-only hooks — these subscriptions were only read inside callbacks, not at render time, so removing them eliminates unnecessary re-renders during drag/pan/zoom.
However, the test-app CI job fails on this branch while it passes on current main (verified against PR #4372 which ran against a recent main SHA). This failure is PR-introduced, not pre-existing.
The connectedNodeCount removal from useSelectConnected is safe (no external consumers reference it), but please:
- Investigate and fix the
test-appfailure — check the job logs to see which test(s) break. - Rebase onto current
main— the base SHA is 10 days stale.
The code changes are sound; just needs green CI.
Generated by Claude Code
georgi
left a comment
There was a problem hiding this comment.
The imperative store access pattern is sound and worth landing, but the PR needs fixes before it can merge:
-
CI failing (
test-app).useNodeContextMenu.tswas changed to callstate.findNode(nodeId), but the test'smockFindNodeis a barejest.fn()that returnsundefined. Before this PR, the mock was dead code (production usednodes.find()directly); now it's load-bearing and breaks ~14 tests. Fix: givemockFindNodea working implementation that returns the expected node. -
Merge conflicts.
mergeable_stateisdirty. BothuseDuplicate.tsanduseGroupIntoSubgraph.tshave since diverged on main (PRs #4238 and #4289 modified the same files). A rebase is required. -
Missing test for
useNodeContextMenu. All other modified hooks had their tests updated, butuseNodeContextMenu.test.tswas not — this is the direct cause of the CI failure.
Please rebase onto current main, fix the mockFindNode implementation in the test, and resolve merge conflicts.
Generated by Claude Code
Summary
state.nodes/state.edgessubscriptions withuseNodeStoreRef()+store.getState()inside callbacks. These hooks only need graph data at action time (duplicate, align, group, select-connected), not at render time — but the reactive subscription caused every host component to re-render on every drag frame (~60fps).nodes/edgessubscription used only inrunWorkflowcallback. The command menu no longer re-renders during drag/pan/zoom.nodes.find(n => n.id === nodeId)(O(n) scan on every store update) withfindNode(nodeId)(O(1) cached Map lookup, no subscription to full nodes array).connectedNodeCountreactive computation (no consumer ever read it).Each hook now only subscribes to stable store actions (functions), avoiding re-renders when
nodes/edgesarray references change during drag, pan, or zoom operations.Test plan
useSelectConnected,useDuplicate,useAlignNodes,useGroupIntoSubgraph)🤖 Generated with Claude Code