From e2f0def372a3fdbf5da8b0f57dea51fd7c0fab62 Mon Sep 17 00:00:00 2001 From: Hugo Gu Date: Mon, 25 May 2026 22:50:43 +0800 Subject: [PATCH] fix(web): fix stale closure in sigma event handlers breaking node selection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The sigma 'clickNode', 'clickStage', 'enterNode', and 'leaveNode' handlers are registered in a one-time useEffect (empty dep array). They captured options.onNodeClick via closure, so they always called the initial version of handleNodeClick — the one created before the graph loaded where `if (!graph) return` exits immediately. Consequence: clicking a node in the canvas never updated the app-level selectedNode state. This broke: - The Focus Depth filter (warning "Select a node to apply depth filter" persisted even after a canvas click) - The depth hop filter not applying (selectedNode was always null) - The code panel not opening on canvas node click Fix: store the three callback props in refs (onNodeClickRef, onNodeHoverRef, onStageClickRef) and update them synchronously on every render. The sigma event handlers now read from the refs, so they always invoke the latest version of the callbacks without needing to re-register. Co-authored-by: Claude AI-model: claude-sonnet-4-6 --- gitnexus-web/src/hooks/useSigma.ts | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/gitnexus-web/src/hooks/useSigma.ts b/gitnexus-web/src/hooks/useSigma.ts index f8a9bead8..15b97012a 100644 --- a/gitnexus-web/src/hooks/useSigma.ts +++ b/gitnexus-web/src/hooks/useSigma.ts @@ -238,6 +238,15 @@ export const useSigma = (options: UseSigmaOptions = {}): UseSigmaReturn => { const blastRadiusRef = useRef>(new Set()); const animatedNodesRef = useRef>(new Map()); const visibleEdgeTypesRef = useRef(null); + + // Keep callback refs fresh so the one-time sigma event handlers always + // call the latest version (avoids stale-closure bugs when graph loads). + const onNodeClickRef = useRef(options.onNodeClick); + const onNodeHoverRef = useRef(options.onNodeHover); + const onStageClickRef = useRef(options.onStageClick); + onNodeClickRef.current = options.onNodeClick; + onNodeHoverRef.current = options.onNodeHover; + onStageClickRef.current = options.onStageClick; const layoutTimeoutRef = useRef | null>(null); const effectsAnimationFrameRef = useRef(null); const treeLayoutFrameRef = useRef(null); @@ -665,23 +674,23 @@ export const useSigma = (options: UseSigmaOptions = {}): UseSigmaReturn => { sigma.on('clickNode', ({ node }) => { setSelectedNode(node); - options.onNodeClick?.(node); + onNodeClickRef.current?.(node); }); sigma.on('clickStage', () => { setSelectedNode(null); - options.onStageClick?.(); + onStageClickRef.current?.(); }); sigma.on('enterNode', ({ node }) => { - options.onNodeHover?.(node); + onNodeHoverRef.current?.(node); if (containerRef.current) { containerRef.current.style.cursor = 'pointer'; } }); sigma.on('leaveNode', () => { - options.onNodeHover?.(null); + onNodeHoverRef.current?.(null); if (containerRef.current) { containerRef.current.style.cursor = 'grab'; }