Appesto Software Solutions
React

Stop unnecessary re-renders in React: memo, useMemo, useCallback – and when not to

August 9, 2026 · 3 min read ·

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.
Keep reading

Related articles