React.memo & Avoiding Re-renders

React.memo is a higher-order component that tells React to skip re-rendering a component when its props haven't changed. It does a shallow comparison of props by reference. Paired with useMemo / useCallback , it prevents wasted renders in large trees. By the end you'll know exactly why a memoized component sometimes still re-renders — and how to fix it.

Learn React.memo & Avoiding Re-renders in our free React course — a beginner-friendly interactive lesson with runnable examples, a practice exercise and a…

Part of the free React course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.

1️⃣ The Default: Children Re-render Too

By default, when a component re-renders, all of its children re-render — even if their props didn't change. Usually that's harmless and fast. But for an expensive child, or a child rendered many times, those wasted renders add up. React.memo wraps a component so React compares its props and skips the render when they're shallowly equal.

The demo models that shallow prop comparison so you can see what "props didn't change" really means:

Your turn — complete the per-key check that decides whether props are equal:

2️⃣ Why memo "Doesn't Work": Unstable References

The most common surprise: you wrap a child in memo , but it still re-renders every time. The cause is almost always a prop that's a new reference on every render — an inline object, array, or arrow function. Shallow comparison sees a different reference and gives up.

The demo proves it: an inline object is never equal across renders, but a stable reference is:

3️⃣ Custom Comparison Functions

memo accepts an optional second argument: a function . Return true to skip the render (props are "equal"), false to re-render. Use it when shallow comparison isn't enough — but keep it cheap, since it runs every render.

Beware: the return value is inverted from what you might expect. true means "they match, don't re-render." A common bug is returning the opposite and re-rendering constantly.

These lines define a memoized child and the stable props its parent must pass. Put them in a correct order:

1) A memoized child gets {'style={ }'} inline. Does memo skip its render?

No. The inline object is a new reference every render, so the shallow comparison fails and the child re-renders.

2) In a custom comparison function, what does returning true mean?

"Props are equal — skip the re-render." It is the inverse of what people often expect.

3) Does React.memo compare props shallowly or deeply by default?

Shallowly — each prop is compared with Object.is . Nested objects with equal contents but different references count as different.

📋 Quick Reference

Use the shallow comparison to decide whether memo would skip a re-render. Compare two prop objects where one shares a reference and one is inline. Predict each result first.

Practice quiz

What does React.memo do?

  • Caches a component's state
  • Memoizes a value like useMemo
  • Skips re-rendering when props are shallowly equal to the previous props
  • Prevents all re-renders forever

Answer: Skips re-rendering when props are shallowly equal to the previous props. memo wraps a component so React reuses its last output when props haven't changed (shallowly).

How does React.memo compare props by default?

  • Shallowly, with Object.is per prop
  • Deeply, recursively
  • By JSON.stringify
  • By component name

Answer: Shallowly, with Object.is per prop. It compares each prop with Object.is; nested objects with equal content but new references count as different.

By default, when a component re-renders, its children…

  • Never re-render
  • Re-render only if memoized
  • Unmount and remount
  • Re-render too, even if their props didn't change

Answer: Re-render too, even if their props didn't change. Children re-render with their parent by default; memo lets React skip that when props are unchanged.

Why does a memoized child still re-render every time here: <Avatar style={{ borderRadius: 8 }} />?

  • memo only works on classes
  • The inline object is a new reference each render, so shallow comparison fails
  • style isn't a valid prop
  • It needs a key

Answer: The inline object is a new reference each render, so shallow comparison fails. Inline objects/arrays/functions create a new reference every render, breaking the shallow comparison.

How do you stabilize an object prop so memo can skip?

useMemo returns a stable reference across renders so the shallow comparison passes.

How do you stabilize a function prop passed to a memoized child?

  • useMemo
  • useState
  • useCallback
  • bind it inline

Answer: useCallback. useCallback keeps the same function identity across renders.

In a custom comparison function passed to memo, returning true means…

  • Re-render the component
  • Props are equal — skip the re-render
  • Throw an error
  • Force a deep compare

Answer: Props are equal — skip the re-render. memo's compare returns true to skip (props 'equal') and false to re-render — the inverse of what many expect.

What is the signature of memo's optional second argument?

  • (state, action) => state
  • (a, b) => number
  • () => void
  • (prevProps, nextProps) => boolean

Answer: (prevProps, nextProps) => boolean. It's a comparison function (prevProps, nextProps) => boolean controlling whether to skip the render.

Should you wrap every component in React.memo?

  • Yes, always
  • No — the comparison has a cost; use it for components that render often or are expensive, and profile first
  • Yes, for performance
  • Only class components

Answer: No — the comparison has a cost; use it for components that render often or are expensive, and profile first. memo adds a comparison each render; apply it selectively and measure before optimizing.

Why does a memo'd wrapper around {children} rarely skip rendering?

  • children can't be a prop
  • memo ignores children
  • JSX children are usually a new reference each render
  • children are always equal

Answer: JSX children are usually a new reference each render. Children passed as JSX are typically new each render, so the shallow comparison fails.