In one minute
When a component re-renders, React also re-renders its children, because every JSX expression creates a new element object with a new props object. React only skips a child when it can prove nothing changed:
- A plain component is skipped when it gets the very same element as last time (
oldProps === newProps). - A
memocomponent is skipped when every prop isObject.is-equal to last time.
So there are two families of fixes. Hand React the same element: create it once, create it in a parent that re-renders less, or cache it with useMemo. Or wrap the component in memo and keep its props stable. Neither stops a component from re-rendering when its own state or a context it reads changes.
You’ll be able to: say exactly when React skips a child, apply the four element techniques and memo, and spot the prop that silently breaks memo.
1function StoreFooter({ currency = 'USD', onSubscribe }) {2 log('render StoreFooter')3 return <footer>Prices in {currency} …</footer>4}56function Page() {7 const [qty, setQty] = useState(1)8 return (<>9 <QtyButton qty={qty} setQty={setQty} />10 <StoreFooter />11 </>)12}- eventClick Quantity. The quantity is
Page’s state, soPageruns again. - render phase
<StoreFooter />JSX is a function call: every render builds a new element with a new, emptypropsobject. - render phase
log('render StoreFooter')React compares an ordinary component’s props by identity: a new object means “may have changed”, so it callsStoreFooter. Recorded: the footer rendered on both clicks.
1function StoreFooter({ currency = 'USD', onSubscribe }) {2 log('render StoreFooter')3 return <footer>Prices in {currency} …</footer>4}56const footer = <StoreFooter /> // created once78function Page() {9 const [qty, setQty] = useState(1)10 return (<>11 <QtyButton qty={qty} setQty={setQty} />12 {footer}13 </>)14}- eventClick Quantity. The quantity is
Page’s state, soPageruns again. - render phase
{footer}Pagereturns the hoistedfooter: the very same object as last time. - render phase
const footer = <StoreFooter /> // created onceSame element, same props object, so React bails out without callingStoreFooter. Recorded: onlyPagerendered. (Passing the element in from a parent that doesn’t re-render works the same way.)
1function StoreFooter({ currency = 'USD', onSubscribe }) {2 log('render StoreFooter')3 return <footer>Prices in {currency} …</footer>4}56const MemoFooter = memo(StoreFooter)78function Page() {9 const [qty, setQty] = useState(1)10 const [currency, setCurrency] = useState('USD')11 return (<>12 <QtyButton qty={qty} setQty={setQty} />13 <MemoFooter currency={currency} />14 </>)15}- eventClick Quantity. The quantity is
Page’s state, soPageruns again. - render phase
<MemoFooter currency={currency} />A new element, butMemoFooteris wrapped inmemo… - render phase
const MemoFooter = memo(StoreFooter)…so React compares the props key by key withObject.is:"USD" === "USD". All equal, so it skips the footer. Recorded: onlyPage; and when the currency really changed, the footer rendered.
1function StoreFooter({ currency = 'USD', onSubscribe }) {2 log('render StoreFooter')3 return <footer>Prices in {currency} …</footer>4}56const MemoFooter = memo(StoreFooter)78function Page() {9 const [qty, setQty] = useState(1)10 return (<>11 <QtyButton qty={qty} setQty={setQty} />12 <MemoFooter currency="USD"13 onSubscribe={() => log('subscribed')} />14 </>)15}- eventClick Quantity. The quantity is
Page’s state, soPageruns again. - render phase
onSubscribe={() => log('subscribed')} />The arrow function is created anew on every render. - render phase
const MemoFooter = memo(StoreFooter)currencyis equal, butonSubscribeisn’t: one unstable prop is enough.memonow costs a comparison and the render. Recorded: the footer rendered on both clicks.
Why children re-render
JSX is a function call that returns a plain object:
const a = <StoreFooter />
const b = <StoreFooter />
log('a === b →', a === b, '· a === a →', a === a)
log('a.props === b.props →', a.props === b.props)
log('a →', { type: (a.type as { name: string }).name, key: a.key, props: a.props })
a === b → false · a === a → true
a.props === b.props → false
a → {"type":"StoreFooter","key":null,"props":{}}
Two <StoreFooter /> elements look identical but are two different objects. During reconciliation React compares the previous props with the new ones by identity for an ordinary component. A new object means “may have changed”, so React calls the component. It never compares the contents unless you ask for it with memo.
The example: a footer that ignores the quantity
The product page has a quantity button (state in Page) and a store footer that has nothing to do with the quantity:
function StoreFooter({ currency = 'USD', onSubscribe }: { currency?: string; onSubscribe?: () => void }) {
log(' render StoreFooter')
return (
<footer>
Prices in {currency} · <button onClick={onSubscribe}>Subscribe</button>
</footer>
)
}
Every version below was clicked twice on “Quantity”.
1. Inline (the default)
function PageInline() {
const [qty, setQty] = useState(1)
log('render Page')
return (
<>
<QtyButton qty={qty} setQty={setQty} />
<StoreFooter />
</>
)
}
render Page
render StoreFooter
render Page
render StoreFooter
The footer rendered on both clicks.
2. Reuse the element
const footer = <StoreFooter /> // created once, when the module loads
function PageReuse() {
const [qty, setQty] = useState(1)
log('render Page')
return (
<>
<QtyButton qty={qty} setQty={setQty} />
{footer}
</>
)
}
render Page
render Page
The element is created once, when the module loads. Every render of Page hands React the same object, so its props are === and React bails out. This only works for elements that don’t depend on the component’s props or state.
3. Pass the element in
function App() {
return <PageWithFooter footer={<StoreFooter />} /> // App doesn't re-render
}
function PageWithFooter({ footer }: { footer: ReactNode }) {
const [qty, setQty] = useState(1)
log('render Page')
return (
<>
<QtyButton qty={qty} setQty={setQty} />
{footer}
</>
)
}
render Page
render Page
An element’s identity follows the component that creates it. App created the footer and doesn’t re-render when the quantity changes, so Page keeps getting the same element. This is the same trick as children (Composition and Layout Components).
4. Cache the element with useMemo
When the element needs a value from the re-rendering component (here, a currency), cache it by that value:
function PageMemoElement() {
const [qty, setQty] = useState(1)
const [currency, setCurrency] = useState('USD')
log('render Page')
const footer = useMemo(() => <StoreFooter currency={currency} />, [currency])
return (
<>
<QtyButton qty={qty} setQty={setQty} />
<button id="currency" onClick={() => setCurrency('EUR')}>Show EUR</button>
{footer}
</>
)
}
Quantity clicks, then “Show EUR”:
render Page
render Page
render Page
render StoreFooter
It works, but it’s unusual: memo is the conventional tool for the same job.
5. memo the component
const MemoFooter = memo(StoreFooter)
function PageMemo() {
const [qty, setQty] = useState(1)
const [currency, setCurrency] = useState('USD')
log('render Page')
return (
<>
<QtyButton qty={qty} setQty={setQty} />
<button id="currency" onClick={() => setCurrency('EUR')}>Show EUR</button>
<MemoFooter currency={currency} />
</>
)
}
render Page
render Page
render Page
render StoreFooter
The element is new each time, but memo compares currency with Object.is: "USD" === "USD" → skip. When the currency really changed, it rendered.
6. One inline function breaks it
function PageMemoBroken() {
const [qty, setQty] = useState(1)
log('render Page')
return (
<>
<QtyButton qty={qty} setQty={setQty} />
<MemoFooter currency="USD" onSubscribe={() => log('subscribed')} />
</>
)
}
render Page
render StoreFooter
render Page
render StoreFooter
The arrow function is a new function every render, so the comparison fails every time: memo now costs a comparison and the render.
How it works
React’s check for an ordinary component is literally current.memoizedProps !== workInProgress.pendingProps (Reconciliation, The Work Loop). For a memo component it runs shallowEqual(prevProps, nextProps) instead: same keys, each value Object.is-equal.
Prop passed to a memo component |
Equal next render? |
|---|---|
currency="USD", count={3}, name={name} |
✅ primitives compare by value |
setX from useState, dispatch |
✅ stable by guarantee |
style={{ color }}, items={[1, 2]} |
❌ new object or array |
onClick={() => …} |
❌ new function (recorded above) |
JSX children <span /> |
❌ new element |
a value from useMemo / useCallback |
✅ while the deps don’t change |
useMemo on an element and memo on a component are the same idea in two places: useMemo caches the element in the parent and compares a dependency array; memo caches the result on the component and compares the props object.
Common mistakes
- Wrapping everything in
memo. If any prop is new each render, it’s pure overhead. - Memoizing before restructuring. Moving state down or passing elements in (techniques 2 and 3) often removes the re-render with no extra code.
- Expecting it to stop context or state updates. A reused element or
memocomponent still re-renders for its own state and the contexts it reads. - Optimizing without measuring. “Sometimes you may inadvertently make things slower when applying a performance optimization.” With the React Compiler enabled, much of this memoization is done for you.
Interview Q&A
Why does a child re-render when its parent re-renders, even with the same props?
The parent’s JSX runs again and creates a new element with a new props object. React compares an ordinary component’s props by identity, so a new object means it calls the component. Recorded: a === b → false for two <StoreFooter />s.
How can you avoid a re-render without memo?
Give React the same element object: hoist a static element out of the component, create it in a parent that re-renders less (props or children), or cache it with useMemo. Recorded: with the element reused or passed in, two quantity clicks rendered only Page.
How does memo decide?
It compares each prop with Object.is (shallowEqual). Primitives compare by value; objects, arrays and functions by identity.
What silently breaks memo?
Any prop created during render: inline objects, arrays, arrow functions, JSX children. Recorded: adding onSubscribe={() => …} made the memoized footer render on every click again.
Does memo stop all re-renders?
No. A memoized component still re-renders when its own state changes or a context it reads changes.
useMemo on an element vs memo on a component?
useMemo caches one element inside the parent, by a dependency array. memo caches the component’s last result wherever it’s rendered, by its props. memo is the conventional choice.
Related
- React Re-rendering: what triggers a render.
- Optimize Context: the same identity rules for context values.
- Optimize Rendering:
memoon 500 list rows. - Reconciliation: where React compares props.
Sources
- Kent C. Dodds: One simple trick to optimize React re-renders, What is JSX?
- react.dev:
memo,useMemo - Topic order inspired by Kent C. Dodds’ EpicReact React Performance workshop; the example app and code here are this handbook’s own.
Primary source: kentcdodds.com/blog/optimize-react-re-renders