Must knowReact Performance · 01

Element Optimization

Hand React the same element, or props that compare equal, and it skips the render: reuse, element props, useMemo and memo.

On this page · 8 sections
  1. In one minute
  2. Why children re-render
  3. The example: a footer that ignores the quantity
  4. How it works
  5. Common mistakes
  6. Interview Q&A
  7. Related
  8. Sources

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:

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.

Inline
ProductPage.tsx
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}
What you see
Quantity: 1
Rendered this step
Page–
StoreFooter–
  1. eventClick Quantity. The quantity is Page’s state, so Page runs again.
  2. render phase<StoreFooter />JSX is a function call: every render builds a new element with a new, empty props object.
  3. render phaselog('render StoreFooter')React compares an ordinary component’s props by identity: a new object means “may have changed”, so it calls StoreFooter. Recorded: the footer rendered on both clicks.

0 / 0
A product page with a quantity counter, and a footer that has nothing to do with the quantity. Does the footer re-render? (All four versions recorded, two clicks each.)

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 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

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.

Sources

Primary source: kentcdodds.com/blog/optimize-react-re-renders