fix(web): address React rendering review — stale closure, ref dep, O(1) lookups

- MarkdownRenderer: wrap handleLinkClick in useCallback, add to
  markdownComponents useMemo deps (fixes stale closure)
- GraphCanvas: remove sigmaRef from useEffect deps (ref identity
  never changes), extract handleToggleAIHighlights to useCallback
- CodeReferencesPanel: add nodeById Map for O(1) focus-in-graph
  lookup (was O(N) graph.nodes.find on every click)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
jreakin 2026-03-23 09:41:31 -05:00
parent 29795bb86e
commit 83ec256cec
3 changed files with 24 additions and 16 deletions

View file

@ -3,6 +3,7 @@ import { Code, PanelLeftClose, PanelLeft, Trash2, X, Target, FileCode, Sparkles,
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism';
import { useAppState } from '../hooks/useAppState';
import type { GraphNode } from '../core/graph/types';
import { NODE_COLORS } from '../lib/constants';
/** Map file extension to Prism syntax highlighter language identifier */
@ -75,6 +76,11 @@ export const CodeReferencesPanel = ({ onFocusNode }: CodeReferencesPanelProps) =
codeReferenceFocus,
} = useAppState();
const nodeById = useMemo(() => {
if (!graph) return new Map<string, GraphNode>();
return new Map(graph.nodes.map(n => [n.id, n]));
}, [graph]);
const [isCollapsed, setIsCollapsed] = useState(false);
const [glowRefId, setGlowRefId] = useState<string | null>(null);
const panelRef = useRef<HTMLElement | null>(null);
@ -421,7 +427,7 @@ export const CodeReferencesPanel = ({ onFocusNode }: CodeReferencesPanelProps) =
const nodeId = ref.nodeId!;
// Sync selection + focus graph
if (graph) {
const node = graph.nodes.find((n) => n.id === nodeId);
const node = nodeById.get(nodeId);
if (node) setSelectedNode(node);
}
onFocusNode(nodeId);

View file

@ -82,6 +82,17 @@ export const GraphCanvas = forwardRef<GraphCanvasHandle>((_, ref) => {
setSelectedNode(null);
}, [setSelectedNode]);
const handleToggleAIHighlights = useCallback(() => {
if (isAIHighlightsEnabled) {
clearAIToolHighlights();
clearAICitationHighlights();
clearBlastRadius();
setSelectedNode(null);
setSigmaSelectedNode(null);
}
toggleAIHighlights();
}, [isAIHighlightsEnabled, clearAIToolHighlights, clearAICitationHighlights, clearBlastRadius, setSelectedNode, toggleAIHighlights]);
const {
containerRef,
sigmaRef,
@ -154,7 +165,8 @@ export const GraphCanvas = forwardRef<GraphCanvasHandle>((_, ref) => {
filterGraphByDepth(sigmaGraph, appSelectedNode?.id || null, depthFilter, visibleLabels);
sigma.refresh();
}, [visibleLabels, depthFilter, appSelectedNode, sigmaRef]);
// eslint-disable-next-line react-hooks/exhaustive-deps -- sigmaRef identity never changes
}, [visibleLabels, depthFilter, appSelectedNode]);
// Sync app selected node with sigma
useEffect(() => {
@ -312,17 +324,7 @@ export const GraphCanvas = forwardRef<GraphCanvasHandle>((_, ref) => {
{/* AI Highlights toggle - Top Right */}
<div className="absolute top-4 right-4 z-20">
<button
onClick={() => {
if (isAIHighlightsEnabled) {
// Turning off — clear AI highlights and selection (preserve user query highlights)
clearAIToolHighlights();
clearAICitationHighlights();
clearBlastRadius();
setSelectedNode(null);
setSigmaSelectedNode(null);
}
toggleAIHighlights();
}}
onClick={handleToggleAIHighlights}
className={
isAIHighlightsEnabled
? 'w-10 h-10 flex items-center justify-center bg-cyan-500/15 border border-cyan-400/40 rounded-lg text-cyan-200 hover:bg-cyan-500/20 hover:border-cyan-300/60 transition-colors'

View file

@ -90,13 +90,13 @@ export const MarkdownRenderer: React.FC<MarkdownRendererProps> = ({
return parts.join('```');
};
const handleLinkClick = (e: React.MouseEvent<HTMLAnchorElement>, href: string) => {
const handleLinkClick = React.useCallback((e: React.MouseEvent<HTMLAnchorElement>, href: string) => {
if (href.startsWith('code-ref:') || href.startsWith('node-ref:')) {
e.preventDefault();
onLinkClick?.(href);
}
// External links open in new tab (default behavior)
};
}, [onLinkClick]);
const formattedContent = React.useMemo(() => formatMarkdownForDisplay(content), [content]);
@ -176,7 +176,7 @@ export const MarkdownRenderer: React.FC<MarkdownRendererProps> = ({
);
},
pre: ({ children }: any) => <>{children}</>,
}), [onLinkClick]); // Removed handleLinkClick dependency as it is defined inside component but depends on onLinkClick
}), [handleLinkClick]);
return (
<div className="text-text-primary text-sm">