React.memo
React.memo wraps a component so it skips re-rendering when its props are shallow-equal to the previous render. Pair it with useMemo (caches a computed value) and useCallback (caches a function reference) and you have the three knobs for hand-tuning re-renders. The trap: most components do not need them; profile first.
memo, useMemo, useCallback with a real bottleneck
EXAMPLE
import React, { memo, useMemo, useCallback, useState } from 'react';
// 1) A child that is expensive to render — pretend each row paints a chart
function ExpensiveRow({ item, onSelect }) {
// Pretend-heavy work
let acc = 0;
for (let i = 0; i < 200_000; i++) acc += Math.sqrt(item.id + i);
return (
<li onClick={() => onSelect(item.id)}>
{item.label} ({acc.toFixed(0)})
</li>
);
}
// 2) Wrap with memo — re-renders ONLY when props change by Object.is
const MemoRow = memo(ExpensiveRow);
// 3) Use useCallback so onSelect is the SAME reference across renders.
// Without it, every render makes a new function -> memo's check fails -> all rows re-render.
export default function List({ items }) {
const [selected, setSelected] = useState(null);
const onSelect = useCallback((id) => setSelected(id), []);
// 4) useMemo for derived data — recompute only when items change
const sorted = useMemo(
() => [...items].sort((a, b) => a.label.localeCompare(b.label)),
[items],
);
return (
<ul>
{sorted.map((item) => (
<MemoRow key={item.id} item={item} onSelect={onSelect} />
))}
</ul>
);
}
// 5) memo with a custom comparator for deep-ish checks
const DeepRow = memo(ExpensiveRow, (prev, next) => {
return prev.item.id === next.item.id
&& prev.item.label === next.item.label; // ignore other fields
});
// 6) When NOT to use memo
// - Component is cheap to render. Hash + compare is not free either.
// - Props are usually new every render (inline objects/arrays, anonymous funcs).
// Either lift them to useMemo/useCallback or skip memo.
// - You are reaching for memo to fix a perf bug — profile first. A million memos
// do not fix a missing key, a single huge re-render, or N+1 child re-renders.
// 7) The React Profiler (DevTools) is the right starting point
// - Open DevTools -> Profiler -> Record an interaction
// - The flamechart shows which components render and why
// - Look for unexpected wide rows under low-frequency updates
// 8) React Compiler (RC at the time of writing) automates much of this
// If you can adopt the compiler, you write fewer useMemo/useCallback by hand —
// the compiler does the same analysis and only memoizes what actually helps.
Why it matters
Reach for memo only after the React Profiler shows a measurable re-render cost. Default-memoising every component is a real net loss — each comparator runs on every parent render, the memory overhead is non-zero, and you trade obvious render-when-state-changes semantics for "why did this not update".
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
const Row = React.memo(function Row({ user }) { return <div>{user.name}</div>; });
Try it Yourself »
Discussion
Loading…