React.memo lets React skip a child when its props are the same as last time. See how it compares props, what breaks it, and when to use it.
In Part 18 we saw the main rule of rendering. When a parent renders, its children render too. Part 18 also showed two ways out. React skips a child whose element is the same object as last time. And it skips a render when a set function gets the value the state already has.
Part 19 used the first way. It moved parts of the page around so that fewer children render. This part is about a third way out: memo.
memo lets you mark a component so that React skips it when its props are the same as last time. We’ll see how React decides “the same”. We’ll see what breaks it, how to fix that, and when it is worth using at all.
Try this first
Read this code. Don’t press Run yet.
import { memo, useState } from 'react'
function PlainCard({ label }: { label: string }) {
console.log('PlainCard renders')
return <p>{label}</p>
}
const MemoCard = memo(function MemoCard({ label }: { label: string }) {
console.log('MemoCard renders')
return <p>{label}</p>
})
export default function App() {
const [clicks, setClicks] = useState(0)
return (
<div>
<button onClick={() => setClicks(clicks + 1)}>Clicks: {clicks}</button>
<PlainCard label="I am plain." />
<MemoCard label="I am wrapped in memo." />
</div>
)
}
PlainCard renders
PlainCard renders
MemoCard renders
MemoCard renders
PlainCard renders
PlainCard renders
Both cards get a label that never changes. The only difference is that MemoCard is wrapped in memo(...).
Make a guess. When you click the button, App renders again. Which cards will render with it?
Now press Run, click the button once, and look at the Console.
The first four lines are the first render. Each line appears twice because of Strict Mode. As Part 2 showed, React calls each component twice in development to find bugs.
After the click, only PlainCard renders appears, twice. MemoCard did not run. Its label was the same as last time, so React skipped it.
What memo gives back
memo is a function from React. You give it a component, and it gives back a new component. You use the new one in your JSX, like any other component.
import { memo } from 'react'
function Card() {
return <p>I am a card.</p>
}
const MemoCard = memo(Card)
export default function App() {
return (
<ul>
<li>typeof Card: {typeof Card}</li>
<li>typeof MemoCard: {typeof MemoCard}</li>
<li>MemoCard.type is Card: {String(MemoCard.type === Card)}</li>
<li><MemoCard /></li>
</ul>
)
}
Run it. Card is a function. MemoCard is not. It is a small object, and its type points back at Card. React’s docs say memo doesn’t change the component you give it. Card stays the same function. The object tells React to check the props before it calls Card.
You will see both memo and React.memo in code. They are the same function. We checked: React.memo === memo is true.
The name comes from memoization. That means keeping the result of some work. Then you don’t have to do the work again for the same input. A component wrapped in memo is called memoized. React’s docs use both words.
How memo compares props
Say the parent writes the JSX for the child itself. Then each render makes a new element, with a new props object. So React can’t just ask “is it the same props object?” The answer would always be no. (Part 19 showed the exception. An element passed in from above stays the same.)
Instead, React looks at the props one by one. React’s docs say it “will compare each prop with Object.is“. You met Object.is in Part 5. It is the same test React uses for state. Here is what it says about the kinds of values props usually hold:
| Compare | Object.is says |
|---|---|
3 and 3 |
true |
'Ana' and 'Ana' |
true |
true and true |
true |
| one function and that same function | true |
{} and {} |
false |
[] and [] |
false |
() => {} and () => {} |
false |
Numbers, text and true/false are compared by value. For objects, arrays and functions, the question is different. Is this the very same one? Two objects with the same fields are still two objects.
If the props have the same names and every prop gives true, React skips the component. If even one gives false, React renders it.
A component with no props passes every time. So memo always skips it when only the parent renders. We checked: a memo Footer with no props ran 0 more times while its parent rendered 3 more times.
How memo decides. Choose a case, then press play or step through it.
Here is the figure in words. A Card gets three props: label, count and onPick.
Every prop the same:
- The parent renders again. It makes a new
<Card>element. memocompareslabel:'Ana'and'Ana'. Same.- It compares
count:3and3. Same. - It compares
onPick. The parent passed its set function, which is the same function both times. Same. - Every prop is the same, so React skips
Card.
One prop different:
Steps 1 to 3 are the same. In step 4, the parent passed () => setN(0). The parent writes that arrow function inside its own code, so each render makes a new one. It looks the same, but Object.is says it is different. So in step 5, React calls Card again.
We tested both cases. The parent rendered 3 more times, with Strict Mode off. With the set function, Card ran 0 more times. With the arrow function, it ran 3 more times.
When a prop really changes, memo can’t skip, and it shouldn’t:
import { memo, useState } from 'react'
const Score = memo(function Score({ points }: { points: number }) {
console.log('Score renders')
return <p>Points: {points}</p>
})
export default function App() {
const [points, setPoints] = useState(0)
return (
<div>
<button onClick={() => setPoints(points + 1)}>Add a point</button>
<Score points={points} />
</div>
)
}
points is different after each click, so Score renders every time. We clicked 3 times: Score ran 3 more times with Strict Mode off, and 6 with it on.
Shallow: memo doesn’t look inside
React’s docs call this kind of check shallow. It means React compares each prop, but it never looks inside a prop that is an object. If the object is the same one as last time, React stops there, even if something inside it changed.
So changing an object in place, as Part 5 warned, can leave a memoized child showing old data:
import { memo, useState } from 'react'
type User = { name: string }
const NameTag = memo(function NameTag({ user }: { user: User }) {
console.log('NameTag renders')
return <p>Name: {user.name}</p>
})
const user: User = { name: 'Ana' }
export default function App() {
const [clicks, setClicks] = useState(0)
function rename() {
user.name = 'Ben'
setClicks(clicks + 1)
}
return (
<div>
<button onClick={rename}>New name ({clicks})</button>
<NameTag user={user} />
</div>
)
}
Run it and click “New name”. App renders, because clicks changed. But the page still says “Name: Ana”. user is the same object as before, so NameTag was skipped. The new name sits inside the object, and nobody reads it.
The fix is the one from Part 5: make a new object for the new name, and keep it in state.
The other side: parts you didn’t change stay the same
Part 5 promised something here. When you update state with spread, the parts you didn’t change stay the same objects. A memoized child that gets one of those parts can skip.
import { memo, useState } from 'react'
type Profile = { name: string; city: string }
const ProfileCard = memo(function ProfileCard({ profile }: { profile: Profile }) {
console.log('ProfileCard renders')
return <p>{profile.name} lives in {profile.city}.</p>
})
export default function App() {
const [data, setData] = useState({
profile: { name: 'Ana', city: 'Lima' },
settings: { dark: false },
})
function toggleDark() {
setData({ ...data, settings: { dark: !data.settings.dark } })
}
return (
<div>
<button onClick={toggleDark}>Dark: {String(data.settings.dark)}</button>
<ProfileCard profile={data.profile} />
</div>
)
}
ProfileCard renders
ProfileCard renders
Click “Dark” a few times. ProfileCard renders never appears again after the first render. { ...data } made a new outer object and a new settings. But data.profile is still the same object.
We also tried structuredClone(data), which copies every level. Then profile was a new object each time, and ProfileCard ran on every click: 3 times for 3 clicks, with Strict Mode off.
An everyday example
A teacher checks a pile of homework. The pile is the props, and each sheet is one prop. For each sheet, she asks one question. Is this the same sheet of paper as yesterday? If every sheet is the same, she doesn’t grade anything again.
The exact version
The teacher never reads the sheets. That is the shallow part. If a student wrote a new answer on yesterday’s sheet, she won’t notice. That is the NameTag bug.
The example breaks in one place. For text and numbers, Object.is does compare the values. Two separate 'Ana' strings are “the same sheet” to React.
A hint, not a promise
React’s docs say a memoized component “will usually not be re-rendered” when its props are the same. Then they add: “memoization is a performance optimization, not a guarantee.”
Performance means how fast the app is. An optimization is a change that makes it faster. So memo is a way to save work. React may still decide to render your component. Your code must be correct either way.
The docs say it plainly: “If your code doesn’t work without it, find the underlying problem and fix it first.” A test is easy. Remove memo. If the app breaks, the bug was there all along.
State and context still render a memo component
memo only looks at props from the parent. A component also renders when its own state changes, or when a context it reads changes.
Context lets a parent hand a value to any component below it. You don’t pass props through each level. Part 23 covers it in depth. Here, all you need is this: useContext(ThemeContext) reads the value, and <ThemeContext value={theme}> gives it.
import { createContext, memo, useContext, useState } from 'react'
const ThemeContext = createContext('light')
const Badge = memo(function Badge({ name }: { name: string }) {
const [likes, setLikes] = useState(0)
const theme = useContext(ThemeContext)
console.log('Badge renders')
return (
<button onClick={() => setLikes(likes + 1)}>
{name}: {likes} likes, {theme}
</button>
)
})
export default function App() {
const [clicks, setClicks] = useState(0)
const [theme, setTheme] = useState('light')
return (
<ThemeContext value={theme}>
<button onClick={() => setClicks(clicks + 1)}>Parent: {clicks}</button>
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>Switch theme</button>
<Badge name="Ana" />
</ThemeContext>
)
}
Badge renders
Badge renders
Badge renders
Badge renders
Badge renders
Badge renders
Run it, then press the three buttons in order. Watch the Console after each one.
- Parent: nothing.
Apprendered, butnameis still"Ana", soBadgewas skipped. - Ana: “Badge renders”, twice.
Badgechanged its own state. - Switch theme: “Badge renders”, twice. The context value
Badgereads has changed.
So the six lines are two for the first render, two for the “Ana” click and two for “Switch theme”. React’s docs say the same: “Even with memo, your component will re-render if its own state changes or if a context that it’s using changes.”
We also tried a memo child that does not read the context. The context changed 3 times, and that child ran 0 more times.
What breaks memo
memo only helps if the props are the same. The easy way to break it is to make a new object, array or function during the parent’s render. Here is one example with all of them:
import { memo, useState, type ReactNode } from 'react'
type Props = {
name: string
style?: { color: string }
items?: number[]
onPick?: () => void
children?: ReactNode
}
const Child = memo(function Child({ name }: Props) {
console.log(name, 'renders')
return <p>{name}</p>
})
export default function App() {
const [clicks, setClicks] = useState(0)
return (
<div>
<button onClick={() => setClicks(clicks + 1)}>Render App again ({clicks})</button>
<Child name="only text" />
<Child name="object" style={{ color: 'teal' }} />
<Child name="array" items={[1, 2, 3]} />
<Child name="function" onPick={() => {}} />
<Child name="text child">Hello</Child>
<Child name="JSX child"><b>Hello</b></Child>
</div>
)
}
Press Run. The Console shows each child twice for the first render. Then click the button once. You’ll see each of these lines twice:
object renders
array renders
function renders
JSX child renders
style={{ color: 'teal' }}makes a new object on every render ofApp.items={[1, 2, 3]}makes a new array.onPick={() => {}}makes a new function.<b>Hello</b>is an element, and an element is an object. JSX between the tags becomes thechildrenprop, sochildrenis a new object each time.
“only text” and “text child” were skipped. Hello between the tags is just text, and text is compared by value.
React’s docs warn that one value that is always new is “enough to break memoization for an entire component”. They also say memo “is completely useless if the props passed to your component are always different”.
Spreading props
{...user} passes every field of user as a prop. That includes fields the child never uses.
import { memo, useState } from 'react'
const NameTag = memo(function NameTag({ name }: { name: string }) {
console.log('NameTag renders')
return <p>Name: {name}</p>
})
export default function App() {
const [user, setUser] = useState({ name: 'Ana', visits: 0 })
return (
<div>
<button onClick={() => setUser({ ...user, visits: user.visits + 1 })}>
Visits: {user.visits}
</button>
<NameTag {...user} />
</div>
)
}
NameTag shows only name. But {...user} also passes visits, and visits changes on every click. So NameTag renders on every click. We clicked 3 times: it ran 3 more times with Strict Mode off.
The fix is to pass only what the child needs: <NameTag name={user.name} />. With that change, it ran 0 more times.
How to keep props the same
There are a few simple fixes. Try them before anything else.
Move fixed values out of the component. Code at the top level of the file runs once. A value made there is the same object forever. That works for objects, arrays and functions:
import { memo, useState } from 'react'
function sayHello() {
console.log('Hello!')
}
const HelloButton = memo(function HelloButton({ onHello }: { onHello: () => void }) {
console.log('HelloButton renders')
return <button onClick={onHello}>Say hello</button>
})
export default function App() {
const [clicks, setClicks] = useState(0)
return (
<div>
<button onClick={() => setClicks(clicks + 1)}>Clicks: {clicks}</button>
<HelloButton onHello={sayHello} />
</div>
)
}
HelloButton renders
HelloButton renders
The two lines are from the first render. Clicking “Clicks” doesn’t add any more, because sayHello is the same function every time.
Pass plain values instead of objects. React’s docs suggest passing “the minimum necessary information”. Pass name={user.name}, not the whole user. Text and numbers are compared by value.
Pass set functions and dispatch as they are. React’s docs say the set function from useState “has a stable identity”. That means it is the same function on every render. So is dispatch from useReducer, as Part 15 showed.
import { memo, useState } from 'react'
type AddProps = { onAdd: (update: (n: number) => number) => void }
const AddButton = memo(function AddButton({ onAdd }: AddProps) {
console.log('AddButton renders')
return <button onClick={() => onAdd(n => n + 1)}>Add one</button>
})
export default function App() {
const [count, setCount] = useState(0)
console.log('App renders')
return (
<div>
<p>Count: {count}</p>
<AddButton onAdd={setCount} />
</div>
)
}
App renders
App renders
AddButton renders
AddButton renders
App renders
App renders
The click renders App, but not AddButton. onAdd is setCount, the same function as last time. We checked this too, in this app. Over 3 clicks, setCount was the same function every time App ran, with Strict Mode on or off.
Now change onAdd={setCount} to onAdd={update => setCount(update)}. It does the same job. But the arrow is new on every render, so AddButton runs on every click: 3 times for 3 clicks, with Strict Mode off.
Pass JSX from above. Part 19 showed this. Say a parent gets an element as children from above. Then that element is the same object on each of the parent’s renders.
Sometimes none of these fit. The object must be made from state, or the function must read the latest props. Then you need useMemo and useCallback, which keep a value or a function the same between renders. That is Part 21. And Part 22 shows the React Compiler, which can add this kind of memoization for you. React’s docs say it “automatically applies the equivalent of memo to all components”.
Your own compare function
memo takes a second, optional argument: your own compare function. React calls it with the old props and the new props.
- Return
trueif they are equal. React skips the render. - Return
falseif they are different. React renders.
Notice that true means “skip”. The name React’s docs use for it, arePropsEqual, helps you remember.
We checked when React calls it. It was not called for the first render. After that, it was called once per parent update, with Strict Mode on or off.
React’s docs say you usually won’t need one. Here is why. This one compares only label, and forgets about onReport:
import { memo, useState } from 'react'
type ReportProps = { label: string; onReport: () => void }
const ReportButton = memo(
function ReportButton({ label, onReport }: ReportProps) {
return <button onClick={onReport}>{label}</button>
},
(oldProps, newProps) => oldProps.label === newProps.label,
)
export default function App() {
const [count, setCount] = useState(0)
const [message, setMessage] = useState('')
return (
<div>
<button onClick={() => setCount(count + 1)}>Count: {count}</button>
<ReportButton label="Report" onReport={() => setMessage('Count was ' + count)} />
<p>{message}</p>
</div>
)
}
Run it. Click “Count” three times, then click “Report”. The page says “Count: 3” and “Count was 0”.
Here is what happened. label never changes, so the compare function always returned true. React never rendered ReportButton again. So its button still has the onReport from the first render. That function was made when count was 0, and it still sees 0. A value like this, old and never updated, is called stale.
React’s docs warn about exactly this. With your own compare function, they say, “you must compare every prop, including functions”. If you don’t, the component keeps seeing “the props and state from a previous render”.
There are two fixes. Remove the compare function, or compare onReport too. Both make the page say “Count was 3”. Here, both also mean ReportButton renders on every click, because onReport is new each time. We counted: 3 clicks on “Count” rendered it 3 times with Strict Mode off, with either fix. To keep it the same, you need useCallback, from Part 21.
The docs give two more warnings. First, check that your compare function is “actually faster than re-rendering the component”. Second, avoid deep checks that walk through every level of an object. Those “can freeze your app for many seconds” when the data gets big. The one exception they give: data with “a known limited depth”, where you know how many levels there are.
When memo helps, and when it doesn’t
React’s docs say when memo is worth it. It “is only valuable when your component re-renders often with the same exact props, and its re-rendering logic is expensive”. Expensive here means it takes a lot of work.
A list is a good case. When you pick one item, only two items really change: the old pick and the new one.
import { memo, useState } from 'react'
const FRUITS = ['Apple', 'Banana', 'Cherry', 'Orange']
type RowProps = {
name: string
selected: boolean
onSelect: (name: string) => void
}
const Row = memo(function Row({ name, selected, onSelect }: RowProps) {
console.log('Row renders:', name)
return (
<li>
<button onClick={() => onSelect(name)}>
{selected ? '[x] ' : '[ ] '}
{name}
</button>
</li>
)
})
export default function App() {
const [selected, setSelected] = useState('Apple')
return (
<ul>
{FRUITS.map(fruit => (
<Row
key={fruit}
name={fruit}
selected={fruit === selected}
onSelect={setSelected}
/>
))}
</ul>
)
}
Row renders: Apple
Row renders: Apple
Row renders: Banana
Row renders: Banana
Row renders: Cherry
Row renders: Cherry
Row renders: Orange
Row renders: Orange
Row renders: Apple
Row renders: Apple
Row renders: Cherry
Row renders: Cherry
Run it and click “Cherry”. After the first render, only Apple and Cherry render. Their selected prop changed. Banana and Orange got the same three props, so React skipped them.
Two small choices make this work. Each row gets selected, a true or false, not the whole selected name. And onSelect is the set function, which never changes.
We tried the same list with 50 rows, with Strict Mode off. Picking one item ran Row 2 times with memo, and 50 times without it.
That is a count of renders, not a count of time. We did not time it. A row this small is fast either way. React’s memo page says to measure speed with React “running in the production mode”. Part 36 shows how to find which components are slow.
memo doesn’t help in two cases:
- The props change every time.
Scoreabove got a newpointson each click, so it rendered every time anyway. Then React does the comparing as well as the rendering. - The component is cheap. If it renders fast, skipping it saves almost nothing.
What about using memo everywhere, just in case? The docs say “There is no significant harm to doing that either”. But they name a cost: the code gets harder to read. And one always-new prop breaks it. When something feels slow, the docs suggest a tool called the profiler. It shows “which components would benefit the most from memoization”.
memo and TypeScript
The props type of your component flows through memo. TypeScript checks the memoized component the same way:
import { memo } from 'react'
const Greeting = memo(function Greeting({ name }: { name: string }) {
return <p>Hello, {name}!</p>
})
const ok = <Greeting name="Ana" />
const wrong = <Greeting name={5} />
TypeScript stops at name={5} with Type 'number' is not assignable to type 'string'.
The compare function gets the same types. In the ReportButton example, we changed oldProps.label to oldProps.lable. TypeScript said Property 'lable' does not exist on type 'Readonly<ReportProps>'. Did you mean 'label'?
One more small habit. Pass memo a function with a name, memo(function Fruits() { ... }), as React’s docs do. Then React’s warnings can name it. We forgot the key in a list inside Fruits. With a named function, the warning told us to check the render method of Fruits. With an arrow function, it did not name the component.
Common mistakes
Adding memo before you know it helps
Most components are cheap, or get new props every time. For them, memo adds code and saves nothing. It isn’t a bug: React’s docs say there is “no significant harm” in it. The cost is code that is harder to read. Try the simple fixes from Part 19 first. Add memo where you have seen a part of the app render too often with the same props.
New objects and functions in the props
import { memo } from 'react'
type ChartProps = { options: { color: string }; onPick: () => void }
const Chart = memo(function Chart({ options }: ChartProps) {
return <p style={options}>A chart</p>
})
function Page({ setPicked }: { setPicked: (picked: boolean) => void }) {
return <Chart options={{ color: 'teal' }} onPick={() => setPicked(true)} />
}
Chart is memoized, but Page still renders it every time. Both props are made again in each render. Move options out of the component, or pass plain values. For the function, keep it the same with useCallback from Part 21. Or, if the child can pass the value itself, pass the set function directly.
Expecting memo to stop every render
memo only compares props. A component still renders when its own state changes, or when a context it reads changes. That is correct, not a bug. If a context changes too often, Part 25 shows how to read only part of it.
A compare function that skips a function prop
If your compare function ignores a function prop, the child keeps the function from an old render. The ReportButton example showed “Count was 0” when the count was 3. Compare every prop, or don’t write a compare function.
Calling memo inside a component
import { memo, useState } from 'react'
function Counter() {
const [count, setCount] = useState(0)
return <button onClick={() => setCount(count + 1)}>Counter: {count}</button>
}
export default function App() {
const [clicks, setClicks] = useState(0)
const MemoCounter = memo(Counter)
return (
<div>
<button onClick={() => setClicks(clicks + 1)}>App: {clicks}</button>
<MemoCounter />
</div>
)
}
Run it. Click “Counter” twice, then “App”. The counter goes back to 0.
Each call to memo makes a new object. We checked: memo(Counter) === memo(Counter) is false. The element <MemoCounter /> has that object as its type, not Counter. So each render of App gives the element a new type. The object’s own .type is still Counter, but React compares the outer object. As Part 2 showed, a new type means React removes the old component, with its state, and adds a new one.
The fix: call memo once, at the top level of the file. Then the counter keeps its 2.
Practice
Press Edit on the examples above and try these. Strict Mode is on, so count each render as two lines.
- In “Try this first”, click the button 3 times. After the first render, how many
PlainCard renderslines appear? How manyMemoCard renders? - In the “What breaks
memo” example, make the “object” child stop rendering on each click. Don’t touchChild. - Fix the
ReportButtonbug, so “Report” shows the real count. - In the fruit list, change
onSelect={setSelected}toonSelect={name => setSelected(name)}. Click “Cherry”. How many “Row renders” lines appear after the first render?
Answers
- 6 lines of
PlainCard renders: 3 clicks, and Strict Mode doubles each one. 0 lines ofMemoCard renders. - Move the object out of
App. Putconst teal = { color: 'teal' }aboveApp, and passstyle={teal}. Now one click logs only “array”, “function” and “JSX child”, twice each. - Delete the compare function, so the end is
})with nothing after the component. Or compare both props:oldProps.label === newProps.label && oldProps.onReport === newProps.onReport. Either way, three clicks on “Count” and one on “Report” show “Count was 3”. - 8 lines: all four rows, twice each. The arrow is a new function on every render, so every row’s
onSelectis different, and every row renders.
Interview questions
Try to answer each one out loud before you open the answer.
What does React.memo do?
It wraps a component and gives back a memoized one. When the parent renders again, React compares the new props with the old ones. If they are all the same, React skips the component and keeps what it had on the page. If any prop is different, it renders as usual.
A strong answer adds that memo doesn’t change the component you give it. It returns a new component. And React’s docs call it “a performance optimization, not a guarantee”. The app must work the same without it.
How does memo compare props? What does “shallow” mean?
It compares each prop with its old value using Object.is. Numbers, text and true/false are compared by value. Objects, arrays and functions are compared by asking if it is the very same one. “Shallow” means React doesn’t look inside an object prop. A changed field inside the same object is not noticed. A new object with the same fields counts as different.
A strong answer gives an example: Object.is({}, {}) is false, so style={{ color: 'teal' }} breaks memo. It may also name the two places where Object.is differs from ===, as Part 5 noted. Object.is(NaN, NaN) is true, and Object.is(0, -0) is false.
A child is wrapped in memo, but it still renders every time its parent does. Why?
Usually one of its props is new on every render. Common causes:
- an object or array written in the JSX;
- an arrow function made in the parent;
- JSX passed as
children; {...props}passing a value that keeps changing.
To fix it, move fixed values out of the component. Pass plain values. Pass set functions and dispatch as they are. Or keep the value the same with useMemo or useCallback.
A strong answer also checks the other causes. The child may have its own state that changes, or it may read a context that changes.
Does memo stop a component from rendering when its state or context changes?
No. memo only looks at props from the parent. A memoized component still renders when its own state changes, or when a context it reads changes. In our test, a memo Badge was skipped when the parent rendered. It rendered when it changed its own state, and when the theme context changed.
A strong answer gives the docs’ fix for a context that changes too often. Split the component in two. The outer one reads the context. It passes only the part it needs, as a prop, to a memoized inner one.
What is the second argument to memo, and what can go wrong with it?
It is your own compare function, arePropsEqual(oldProps, newProps). Return true to skip the render, false to render. The danger is leaving out a prop, most often a function. Then the child keeps a function from an old render, which still sees old state. In our test, the page showed “Count was 0” when the count was 3. React’s docs say you “must compare every prop, including functions”. They also warn against deep checks, which can get very slow.
A strong answer adds two details. React doesn’t call it for the first render; in our test it was called once per parent update after that. And true means “skip”, which is the opposite of the old class method shouldComponentUpdate. There, returning false tells React “that re-rendering can be skipped”.
Should you wrap every component in memo?
Usually no. React’s docs say it is only worth it in one case. The component “re-renders often with the same exact props, and its re-rendering logic is expensive”. For cheap components, or props that change every time, it saves nothing. The docs say wrapping more has no “significant harm”. But it makes code harder to read, and one always-new prop breaks it.
A strong answer says: first try composition and keeping state low. Then measure, in production mode, before adding memo.
Why can you pass a set function to a memo child without useCallback?
React keeps them the same function on every render. React’s docs say the set function “has a stable identity”. So Object.is says the prop is the same, and the child can skip. In our test, a memo child given setCount ran 0 more times over 3 parent renders. Given update => setCount(update), it ran 3 more times.
A strong answer adds the updater form. The child calls onAdd(n => n + 1), so it can change the count without knowing the current value. That is why the parent can pass setCount as it is, with no wrapper function.
Do you still need memo with the React Compiler?
React’s docs say: “When you enable React Compiler, you typically don’t need React.memo anymore.” The compiler adds this kind of memoization for you. Part 22 shows what it does.
A strong answer adds that the idea doesn’t go away. The docs show the compiler keeping a child’s element when its props haven’t changed. They call this “exactly what React.memo does”. So the rules in this part still explain what the compiler is doing.
Sources
- memo, react.dev: what
memoreturns,Object.isfor each prop, “not a guarantee”, state and context, minimizing props changes,arePropsEqualand its pitfall, whenmemois worth it, and the React Compiler. - useState, react.dev: the set function “has a stable identity”.
- useReducer, react.dev:
dispatchis stable too. - useContext, react.dev: components that read a context render again when it changes.
- Introduction to React Compiler, react.dev: what the compiler does, for Part 22.
- Object.is(), MDN: how
Object.iscompares values. - The
memosource in React 19.3.0 (react/cjs/react.development.js):memoreturns an object withtypeandcompare. - Every log, render count, page text, warning and TypeScript error above comes from running React 19.3.0 and TypeScript 7.0.2 for this post, with Strict Mode on and off.
- This part follows the React.memo kata in react-katas.