-
Notifications
You must be signed in to change notification settings - Fork 139
perf(calm-hub-ui): memoize visualiser node/edge components and cut hover re-allocations #3125
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -88,29 +88,28 @@ export function useGraphInteractions({ | |||||||||||||
| const handleNodeMouseEnter = useCallback( | ||||||||||||||
| (_event: React.MouseEvent, node: Node) => { | ||||||||||||||
| setNodes((nds) => | ||||||||||||||
| nds.map((n) => ({ | ||||||||||||||
| ...n, | ||||||||||||||
| style: { | ||||||||||||||
| ...n.style, | ||||||||||||||
| zIndex: n.id === node.id && !isGroupType(n.type) ? 1000 | ||||||||||||||
| : isGroupType(n.type) ? -1 | ||||||||||||||
| : 1, | ||||||||||||||
| }, | ||||||||||||||
| })) | ||||||||||||||
| nds.map((n) => { | ||||||||||||||
| const zIndex = n.id === node.id && !isGroupType(n.type) ? 1000 | ||||||||||||||
| : isGroupType(n.type) ? -1 | ||||||||||||||
| : 1; | ||||||||||||||
| // Only allocate a new node object when the value actually | ||||||||||||||
| // changes, so memoized node components (CustomNode, | ||||||||||||||
| // SystemGroupNode) don't re-render for untouched nodes. | ||||||||||||||
|
Comment on lines
+95
to
+97
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. (non-blocking nit) The mechanism is one layer up: ReactFlow's own
Suggested change
|
||||||||||||||
| if (n.style?.zIndex === zIndex) return n; | ||||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The reference-stability of untouched nodes is the whole optimisation, but nothing pins it: the hook tests don't cover the hover handlers, and the checklist says tests were added but the diff has none. This block in describe('useGraphInteractions hover z-index', () => {
const hoverNodes: Node[] = [
{ id: 'a', type: 'custom', position: { x: 0, y: 0 }, data: {}, style: { zIndex: 1 } },
{ id: 'b', type: 'custom', position: { x: 0, y: 0 }, data: {}, style: { zIndex: 1 } },
{ id: 'g', type: 'group', position: { x: 0, y: 0 }, data: {}, style: { zIndex: -1 } },
];
function setupHover() {
let updated: Node[] = [];
const setNodes = vi.fn((updater: (nodes: Node[]) => Node[]) => {
updated = updater(hoverNodes);
});
const { result } = renderHook(() =>
useGraphInteractions({ setNodes, onNodesChangeBase: vi.fn(), groupNodeTypes: ['group'] })
);
return { result, getUpdated: () => updated };
}
it('elevates only the hovered node and keeps other node references stable', () => {
const { result, getUpdated } = setupHover();
result.current.handleNodeMouseEnter({} as React.MouseEvent, hoverNodes[0]);
const [a, b, g] = getUpdated();
expect(a.style?.zIndex).toBe(1000);
expect(a).not.toBe(hoverNodes[0]);
expect(b).toBe(hoverNodes[1]);
expect(g).toBe(hoverNodes[2]);
});
it('resets only the elevated node on mouse leave', () => {
const { result, getUpdated } = setupHover();
result.current.handleNodeMouseEnter({} as React.MouseEvent, hoverNodes[0]);
const elevated = getUpdated();
const setNodes = vi.fn((updater: (nodes: Node[]) => Node[]) => updater(elevated));
const { result: r2 } = renderHook(() =>
useGraphInteractions({ setNodes, onNodesChangeBase: vi.fn(), groupNodeTypes: ['group'] })
);
r2.current.handleNodeMouseLeave();
const [a, b, g] = setNodes.mock.results[0].value as Node[];
expect(a.style?.zIndex).toBe(1);
expect(b).toBe(elevated[1]);
expect(g).toBe(elevated[2]);
});
}); |
||||||||||||||
| return { ...n, style: { ...n.style, zIndex } }; | ||||||||||||||
| }) | ||||||||||||||
| ); | ||||||||||||||
| }, | ||||||||||||||
| [setNodes, isGroupType] | ||||||||||||||
| ); | ||||||||||||||
|
|
||||||||||||||
| const handleNodeMouseLeave = useCallback(() => { | ||||||||||||||
| setNodes((nds) => | ||||||||||||||
| nds.map((n) => ({ | ||||||||||||||
| ...n, | ||||||||||||||
| style: { | ||||||||||||||
| ...n.style, | ||||||||||||||
| zIndex: isGroupType(n.type) ? -1 : 1, | ||||||||||||||
| }, | ||||||||||||||
| })) | ||||||||||||||
| nds.map((n) => { | ||||||||||||||
| const zIndex = isGroupType(n.type) ? -1 : 1; | ||||||||||||||
| if (n.style?.zIndex === zIndex) return n; | ||||||||||||||
| return { ...n, style: { ...n.style, zIndex } }; | ||||||||||||||
| }) | ||||||||||||||
| ); | ||||||||||||||
| }, [setNodes, isGroupType]); | ||||||||||||||
|
|
||||||||||||||
|
|
||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(non-blocking nit)
DecisionGroupNodeis registered alongside these inPatternGraph.tsxandDiffGraph.tsxbut isn't wrapped; worth memoising it too for consistency.