AcademyReact
React.memo and Unnecessary Re-renders
A component re-renders whenever its parent does, props unchanged or not — usually fine, and React.memo is the opt-out for when it measurably is not.
4 min readUpdated 3 exercises
The default: children re-render with their parent
React's baseline behavior surprises people the first time they see it: when a component re-renders, every child it returns re-renders too, whether or not that child's own props actually changed.
function Parent() {
const [count, setCount] = useState(0);
return (
<>
<button onClick={() => setCount((c) => c + 1)}>{count}</button>
<ExpensiveChart data={staticData} />
</>
);
}Clicking the button re-renders Parent, which re-renders ExpensiveChart too, even though staticData never changed and never will. Nothing is broken here — this is React working as specified.
Why that default is fine almost all the time
"Re-render" does not mean "repaint the DOM." A re-render calls the component function again, builds a fresh element tree, and hands it to reconciliation — which diffs it against the last tree and only touches the real DOM where something actually differs. For ExpensiveChart above, if its output does not depend on anything that changed, reconciliation bails out at the diff step and nothing is written to the DOM. The wasted work is calling the function and building the throwaway object tree, which for the overwhelming majority of components costs microseconds.
This is why "prevent re-renders" is the wrong starting instinct. React was built assuming components re-render often and cheaply; treating every re-render as a bug to eliminate turns a non-problem into a maintenance burden, usually for a performance win nobody would ever measure.
What React.memo actually changes
React.memo wraps a component and adds one check before it runs: compare this render's props to last render's props, shallowly (Object.is per key, one level deep — not a deep equality check), and skip calling the component entirely if they are all equal.
const ExpensiveChart = React.memo(function ExpensiveChart({ data }) {
return renderChart(data); // assume this is genuinely costly
});Now when Parent re-renders and data is the same reference as last time, ExpensiveChart is skipped — React reuses the element tree from the previous render for that subtree without calling the function again. That is the entire feature: a shallow-equality gate in front of a component call.
Shallow equality is the whole catch
"Shallow" is doing a lot of work in that sentence, and it is where React.memo quietly stops helping. An object, array, or function created fresh inside the render is a new reference every time — even when its contents are identical to last render's — and Object.is sees two different references as unequal:
function Parent() {
const [count, setCount] = useState(0);
return (
<>
<button onClick={() => setCount((c) => c + 1)}>{count}</button>
{/* New array literal every render — React.memo can never see this as "unchanged" */}
<ExpensiveChart data={[1, 2, 3]} />
</>
);
}[1, 2, 3] is a brand-new array on every call to Parent, regardless of whether the numbers ever change. React.memo's shallow comparison correctly reports "the data prop changed" — it did, by reference — and re-renders ExpensiveChart every single time, making the memo wrapper pure overhead: one extra comparison per render, for a skip that never happens. The same trap catches inline object literals (style={{ color: 'red' }}) and inline arrow functions (onClick={() => doThing()}) passed as props to a memoized child — each is a fresh reference, so memoization never triggers unless the value crossing the boundary was itself produced by useMemo or useCallback upstream.
When it is actually worth reaching for
Worth memoizing when BOTH are true:
1. The component is expensive to render (measured, not assumed)
2. It re-renders often with props that are frequently unchangedA component that renders in under a millisecond gains nothing from skipping that millisecond — the comparison itself costs something, so a cheap component wrapped in React.memo can end up marginally slower, not faster. The components worth wrapping are the ones you can point to: a chart library render, a large virtualized list, anything doing real computation in its own body — combined with a parent that re-renders on unrelated state changes (a keystroke in a sibling input, a websocket tick) while this component's own props stay put.
The practical order of operations: build the feature without React.memo first. Reach for it only after the React DevTools profiler shows a specific component re-rendering often for no prop-related reason, and only after confirming the props it receives are stable (or wrapping them in useMemo/useCallback upstream so the shallow comparison can actually succeed).
Now practice it
2 challenges · 1 quizReading this page is the cheap half. These are the exercises that make you use it.
- Shallow EqualProImplements the exact comparison React.memo runs on props — the mechanism this whole page is about.ChallengeSolve · EasyChallengeSolve · Easy
- Selector MemoizationProApplies the same shallow-equality reasoning to derived state instead of props, where the same reference trap shows up.ChallengeSolve · MediumChallengeSolve · Medium
- Performance · MemoizationRe-render questions at the level where "it re-rendered" and "it repainted the DOM" stop being interchangeable.QuizTake · MediumQuizTake · Medium
Read next
What useMemo and useCallback Memoize
useMemo caches a value, useCallback caches a function reference — the same mechanism underneath, aimed at two different bugs.
4 min
Reconciliation and Why Keys Matter
The heuristic React uses to decide what to reuse and what to rebuild, and why the wrong key silently attaches state to the wrong row.
4 min