cd ../blog
January 21, 2026Code Atlas

React Context Fixed My Infinite Re-render Loop

Storing selection state in React Flow node data caused an infinite re-render loop. Moving to React Context kept node objects stable and fixed it.

reactperformancecontextreact-flow

I wanted path highlighting in Code Atlas: right-click a node, see its connections light up. My first approach stored selection state in the node data objects. The browser locked up on every click. Claude identified it as a re-render loop, and the fix was moving selection to React Context so node objects stay stable.

What I Was Building

Code Atlas uses React Flow for interactive call graphs. I wanted a "highlight connections" feature:

  • Yellow = selected node
  • Blue = connected nodes (callers and callees)
  • Dimmed = everything else

Seemed simple. It wasn't.

The Symptom

Clicking any node froze the browser. React DevTools showed rapid-fire re-renders. I described this to Claude, who immediately identified it as a re-render loop, not a layout or data issue. I hadn't considered that my state placement could cause this.

The Broken Approach

My first attempt put selection state directly in the node data:

// Selection state was embedded in node data
interface FunctionNodeData {
  label: string;
  isSelected: boolean;      // This caused the problem
  isHighlighted: boolean;   // This too
  // ...
}

// This memo created new node objects on every selection change
const filteredNodes = useMemo(() => {
  return nodes.map(node => ({
    ...node,
    data: {
      ...node.data,
      isSelected: node.id === selectedId,
      isHighlighted: connectedIds.has(node.id),
    }
  }));
}, [nodes, selectedId, connectedIds]); // Re-runs on every click

This looked fine to me. The memo recalculates when selection changes, nodes get their updated visual state. What's wrong with that?

Why It Looped

The sequence went like this:

  1. Click a node, selectedId changes
  2. filteredNodes memo re-runs (depends on selectedId)
  3. New node objects are created (spread operator creates new references)
  4. React Flow sees "new" nodes (different object references)
  5. Layout effect triggers because "nodes changed"
  6. Layout calls setNodes internally
  7. That triggers the memo again
  8. Loop

The problem wasn't the dependency array. It was that I kept creating new node objects on every selection change, and React Flow couldn't tell they were "the same" nodes.

The Fix: React Context

Claude suggested separating "what the node is" from "how it looks right now." Selection state moved to Context:

// SelectionContext.tsx
export interface SelectionContextValue {
  selectedId: string | null;
  connectedIds: Set<string>;
  onSelect: (nodeId: string) => void;
}

export const SelectionContext = createContext<SelectionContextValue>({
  selectedId: null,
  connectedIds: new Set(),
  onSelect: () => {},
});

export function useSelection() {
  return useContext(SelectionContext);
}

The node component reads from context instead of props:

function FunctionNode({ data }: { data: FunctionNodeData }) {
  const { selectedId, connectedIds, onSelect } = useSelection();

  // Selection state derived at render time, not stored in data
  const isSelected = selectedId === data.nodeId;
  const isHighlighted = connectedIds.has(data.nodeId);
  const isDimmed = selectedId !== null && !isSelected && !isHighlighted;

  // ... render with appropriate colors
}

And the filteredNodes memo no longer depends on selection:

// Node data is now stable - only contains structural info
const { filteredNodes, filteredEdges } = useMemo(() => {
  // ... map over nodes, but don't include selection state
  return {
    ...node,
    data: {
      label: node.name,
      isExported: node.isExported,
      nodeId: sanitizeId(node.id),
      // No isSelected, no isHighlighted
    },
  };
}, [data, filters]); // selectedId is NOT a dependency anymore

Why It Works

  • Node objects are stable (same reference unless the actual graph changes)
  • filteredNodes only re-runs when the graph structure changes
  • Context updates re-render only the nodes that consume it
  • Layout effect only triggers when nodes actually change

The key insight: React Flow's layout engine needs stable node references. When selection changes, only the node component re-renders to apply different colors, not the entire layout system.

Connection to a Vercel Principle

I read somewhere that "performance work fails because it starts too low in the stack." This felt like that. The fix wasn't tweaking the useMemo dependency array or adding more memoization. It was choosing the right abstraction level for where state lives.

If I'd tried to fix this with React.memo on the node component, or careful dependency management, I'd still have the fundamental problem: new object references triggering layout recalculations. The architecture was wrong, not the optimization.

What I Learned

  • Re-render loops often mean state is in the wrong place, not that you need more memoization
  • Context decouples "what changes frequently" (selection) from "what should stay stable" (node structure)
  • When React DevTools shows rapid re-renders, my first question now is "where does this state live?"
  • Sometimes the AI catches things faster than I would have, I was focused on the wrong layer

The path highlighting works now. Yellow for selected, blue for connected, dimmed for everything else. No more browser lockups.