Topic 16.3
memo: Skipping a Component's Render
In one line
memo(Component) makes React skip rendering a component when its parent re-renders with props that are shallowly equal to last time; it breaks when you pass new objects, arrays or functions on every render, and it's worth it only for components that are slow to render or render very often.
Think of it like this
A shopkeeper copying a price list. Each morning the boss sends the list. A careful assistant first glances at it: "same prices as yesterday? Then I won't rewrite the shop board." The glance is quick, rewriting is slow. But if the boss prints the list on new paper every day and the assistant compares the paper instead of the prices, the assistant rewrites the board every day anyway. memo is the glance, and new objects are the new paper.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- memo
- A React function that wraps a component so React can skip rendering it when its props haven't changed.
- Memoise
- Remember a result and reuse it as long as the inputs are the same.
- Shallow comparison
- Comparing each prop with
Object.is, one level deep. Objects are equal only if they are the very same object. - Reference
- Which object in memory a variable points to. Two objects with the same contents are still two references.
- Inline object or function
- An object, array or arrow function written directly in JSX. It's created again on every render.
- Custom comparator
- Your own function, given to
memo, that decides whether old and new props count as equal.
Step by step
01Wrap the component
Give memo a component and use the result as normal. Naming the inner function keeps a readable name in DevTools and error messages.
import { memo } from "react";
const Card = memo(function Card({ name }) {
return <p>{name}</p>;
});
export default Card;02React compares props before rendering
When the parent renders, React looks at the memoised child: are all props Object.is-equal to last time? If yes, it reuses the last output and skips the subtree.
03New references break it
style={{ color: "teal" }} is a new object each render. So is onSave={() => save()}. Move fixed values out of the component, or stabilise them with useMemo/useCallback.
// Created once, outside the component: same reference forever.
const TEAL = { color: "teal" };
function App() {
return <MemoStyled name="Asha" style={TEAL} />;
}04A comparator for special cases
If the parent can't avoid building a new object but you know only some fields matter, a comparator can compare those fields. Return true to skip.
05Check that it works
Put a console.log in the component or use the React DevTools Profiler (Topic 16.6). A memoised component that still renders on every parent render is a sign of an unstable prop.
Try it yourself
- 1
Fix MemoStyled without useMemo
In the first example, move the style object outside
Appasconst TEAL = { color: "teal" };and passstyle={TEAL}. Predict the log for the count click, then run. - 2
Pass children to a memo component
Change
MemoCardto render{children}and use it as<MemoCard name={name}><b>hi</b></MemoCard>. Does it still skip on the count click? (No:<b>hi</b>is a new element object every render, andchildrenis a prop.) - 3
Break the comparator
Change
samePointto compare onlyx. Then add a button that changesy. Predict what the screen shows after clicking it. That stale screen is the danger of comparators.
Code & diagrams
When only the count changes, MemoCard is skipped. MemoStyled renders anyway: `style={{ color: "teal" }}` is a new object every time, so the shallow comparison fails.
Preview
Console
Nothing logged yet.Expected console output
App rendered
PlainCard rendered
MemoCard rendered
MemoStyled rendered (new style object)
--- count changes (name is the same)
App rendered
PlainCard rendered
MemoStyled rendered (new style object)
--- name changes
App rendered
PlainCard rendered
MemoCard rendered
MemoStyled rendered (new style object)The comparator returns true ("equal, skip") when x and y match, so a new `point` object with the same numbers doesn't render Marker. If Marker also took an `onClick`, the comparator would have to compare it too.
Preview
Console
Nothing logged yet.Expected console output
App rendered
Marker rendered at 1,5
--- tick (new object, same x and y)
App rendered
--- move (x changes)
App rendered
Marker rendered at 2,5Break it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Inline callback defeats memo
Give MemoCard an onSelect={() => console.log("selected")} prop and click the Count button.
Myth vs fact
Myth
Wrap every component in memo to be safe.
Fact
Each memo adds a props comparison and stored props. Where props change on most renders it's pure overhead, and it hides nothing if props are unstable. Use it where measurements show a benefit.
Myth
memo does a deep comparison of props.
Fact
It's shallow: Object.is per prop. Objects with the same contents but different identities are different.
Myth
A memo component never re-renders unless its props change.
Fact
Its own state changes and context changes still re-render it.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
Internally, a memo component with no custom comparator and no
defaultPropsbecomes aSimpleMemoComponentfiber, which React can bail out on very cheaply. The bail-out also works when the parent passes the same element object: React compares element identity first, which is whychildrencreated by an owner that didn't render are skipped even without memo. - ▸
memoreturns an exotic object ($$typeof: Symbol(react.memo)), not a function. Wrapping a component in memo changes its type, so switching between memoised and plain versions of a component at runtime would remount it. - ▸
In React 19 the
react-hooks/preserve-manual-memoizationlint rule and the compiler work together: the compiler keeps your existingmemo/useMemowhere it can, and the lint warns when it can't prove your manual memoisation is preserved.
Remember this
- 1
memois a function fromreactthat wraps a component:const Card = memo(function Card(props) { ... }). It returns a memoised component: when its parent re-renders, React compares the new props with the previous props, and if they're all equal it reuses the last result and skips callingCardand its whole subtree. To memoise means to remember a result and reuse it while the inputs are the same. - 2
The comparison is shallow: React checks each prop with
Object.is(oldProp, newProp), one level deep. Strings, numbers and booleans compare by value, soname="Asha"twice is equal. Objects, arrays and functions compare by reference (which object in memory), not by contents.{ color: "teal" }written in JSX creates a new object on every render, so it is never equal to the last one, even though it looks the same. - 3
That's why
memosilently stops working when a parent passes inline objects, arrays, functions or JSX:style={{...}},items={list.filter(...)},onClick={() => ...},icon={<Star />}. Every render produces new references, the comparison fails, and the child renders as ifmemoweren't there, while you still pay for the comparison. Fix it by creating those values once: move constants outside the component, or keep the same reference withuseMemoanduseCallback(Topic 16.4). - 4
memoaccepts a second argument, a custom comparator:memo(Component, arePropsEqual)wherearePropsEqual(prevProps, nextProps)returnstrueto skip the render. Use it rarely: it must compare every prop that affects the output, including functions (or you'll call stale callbacks), and a deep comparison can be slower than rendering. A wrong comparator causes a stale screen, a worse bug than a slow one. - 5
memoonly covers props. A memoised component still re-renders when its own state changes or a context it reads changes (Topic 16.1). It also doesn't help when a prop really changes every time, and it adds a small cost (storing props and comparing them) to every render where it fails. - 6
When is memo worth it? When a component (1) re-renders often with the same props, and (2) is noticeably expensive to render: a big chart, a long list, a rich editor. Measure first (Topic 16.6). Cheaper options often exist: move state down so the parent doesn't re-render, or pass the expensive part as
children. With the React Compiler (Topic 16.5), most manualmemocalls become unnecessary because it memoises JSX automatically.
Explain it without notes
What does memo do, exactly?
Why can a memo component still render on every parent render?
When would you write a custom comparator, and what are the risks?
How do you decide whether a component should be wrapped in memo?
Practice
A <Chart data={data} /> is memoised but renders on every keystroke in a search box in the same parent. data is computed as const data = rows.map(toPoint). Fix it.
Write a memo comparator for <Avatar user={user} size={size} /> that skips rendering when the user's id and photoUrl and the size are unchanged.
Trade-offs
- ↔
memo saves render time for expensive, stable-prop components, but adds comparison cost and memory to every render, and needs disciplined, stable props from every parent.
- ↔
Custom comparators can rescue awkward props but trade a slow screen for a possibly stale one.
Done when you can
I can wrap a component in memo and verify that it skips renders.
I can explain shallow comparison and why inline objects and functions break it.
I can write a correct custom comparator and explain its risk.
I know memo doesn't stop renders from state or context changes.
I can decide when memo is worth adding.