Stop unnecessary re-renders in React: memo, useMemo, useCallback – and when not to
First, the reassurance: re-renders are usually fine. React re-rendering a component does not mean the DOM changed – reconciliation throws away most of that work cheaply. The problem is re-renders that are both frequent AND expensive: a 500-row table re-rendering on every keystroke in an unrelated search box. Fix those; ignore the rest.
Step 1: measure before touching anything
Open React DevTools → Profiler, record the slow interaction, and look at the flame graph. Enable 'Highlight updates when components render' to see the blast radius live. The question is never 'how do I stop re-renders' – it's 'which specific component is slow, and why is it rendering when nothing it shows has changed?'
Step 2: composition fixes beat memoization
// ❌ Typing re-renders the ENTIRE page because state lives too high
function Page() {
const [query, setQuery] = useState('');
return (<>
<SearchBox value={query} onChange={setQuery} />
<ExpensiveTable rows={rows} />
</>);
}
// ✅ Move the state down - the table no longer cares
function Page() {
return (<>
<Search /> {/* state lives inside */}
<ExpensiveTable rows={rows} />
</>);
}
The two structural moves – push state down into the component that uses it, or lift expensive children up as a `children` prop so they're created by a parent that doesn't re-render – eliminate most 'everything re-renders' complaints with zero memoization. React skips reconciling children whose element identity hasn't changed.
Step 3: memoize the survivors
const Row = memo(function Row({ item, onSelect }: RowProps) {
return <tr onClick={() => onSelect(item.id)}>...</tr>;
});
// memo is defeated by unstable props - stabilize them:
const onSelect = useCallback((id: string) => setSelected(id), []);
const sorted = useMemo(() => [...rows].sort(byDate), [rows]);
memo compares props by reference, so a fresh inline callback or a .sort() result created each render silently disables it. That's the real job of useCallback and useMemo: keeping references stable so memo works. Memoizing values nobody compares is pure overhead – every useMemo costs a comparison and cache space on every render.
The React Compiler changes the default
The React Compiler (stable since late 2025) auto-memoizes components and values at build time, making most hand-written memo/useCallback/useMemo redundant in codebases that follow the Rules of React. If you're starting fresh, enable it and write plain code. Structural fixes still matter – the compiler can't move your state down a level – but the memoization boilerplate era is ending.
Checklist for a janky interaction
- Profile first – name the slow component before changing code.
- State too high? Push it down. Expensive child re-created? Pass it as children.
- Lists: stable keys plus virtualization (TanStack Virtual) beat memoizing 1,000 rows.
- Context splitting: a context whose value changes often shouldn't also carry values that rarely change – every consumer re-renders on any change.
- Only then: memo the component, and stabilize every prop it receives.

