Command Palette

Search for a command to run...

Hectal
PHASE 16Advanced ~20 min· topic 3 of 10

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.

Card.jsxwhole filejsx
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.

React compares props before renderingdiagram
Rendering diagram…

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.

Fix.jsxwhole filejsx
// 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. 1

    Fix MemoStyled without useMemo

    In the first example, move the style object outside App as const TEAL = { color: "teal" }; and pass style={TEAL}. Predict the log for the count click, then run.

  2. 2

    Pass children to a memo component

    Change MemoCard to 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, and children is a prop.)

  3. 3

    Break the comparator

    Change samePoint to compare only x. Then add a button that changes y. Predict what the screen shows after clicking it. That stale screen is the danger of comparators.

Code & diagrams

memo skips, until a new object arrives New tab

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

Press Run to see the page.

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)
A custom comparator New tab

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

Press Run to see the page.

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

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

terminal
$ Console
── what you'll see ──
--- count changes (name is the same)
App rendered
PlainCard rendered
MemoCard rendered
MemoStyled rendered (new style object)

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 defaultProps becomes a SimpleMemoComponent fiber, 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 why children created by an owner that didn't render are skipped even without memo.

  • ▸

    memo returns 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-memoization lint rule and the compiler work together: the compiler keeps your existing memo/useMemo where it can, and the lint warns when it can't prove your manual memoisation is preserved.

Remember this

  1. 1

    memo is a function from react that 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 calling Card and its whole subtree. To memoise means to remember a result and reuse it while the inputs are the same.

  2. 2

    The comparison is shallow: React checks each prop with Object.is(oldProp, newProp), one level deep. Strings, numbers and booleans compare by value, so name="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. 3

    That's why memo silently 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 if memo weren'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 with useMemo and useCallback (Topic 16.4).

  4. 4

    memo accepts a second argument, a custom comparator: memo(Component, arePropsEqual) where arePropsEqual(prevProps, nextProps) returns true to 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. 5

    memo only 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. 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 manual memo calls become unnecessary because it memoises JSX automatically.

Explain it without notes

01

What does memo do, exactly?

02

Why can a memo component still render on every parent render?

03

When would you write a custom comparator, and what are the risks?

04

How do you decide whether a component should be wrapped in memo?

Practice

01

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.

02

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.