React

React.memo — Skipping Unnecessary Re-renders

Thirdy Gayares
12 min read

🎓 What You Will Learn

  • Why children re-render by default: a parent re-render cascades downward
  • React.memo basics: skipping a re-render when props are shallowly equal
  • Function props: why memo needs useCallback to actually help
  • Object/array props: why memo needs useMemo for the same reason
  • Custom comparison: memo's second argument for fine-grained control
  • When NOT to use it: cheap components, unstable props, the children prop trap
1

Why Children Re-render by Default

When a component re-renders, React re-renders every child underneath it too — by default, regardless of whether that child's own props actually changed. For most components that's cheap and invisible. For a component that does real work on every render (a big list row, a chart, an expensive layout), re-rendering it for no reason wastes time.

memo(Component) wraps a component so React skips re-rendering it when its props are the same as last time — checked with a fast, shallow comparison (Object.is on each prop).

2

Basic React.memo — Primitive Props

The simplest case: a component whose props are all primitives (strings, numbers, booleans). Primitives compare by value, so a shallow check works perfectly — no extra setup needed.

Ada Lovelace, 36 — rendered 1 time(s)

name and age never change, so the memoized card doesn't re-render even as the parent's click count climbs.

Nothing special was needed here beyond wrapping the component in memo()name and age are primitives, so React.memo's default shallow comparison just works.
3

Function Props Need useCallback

Functions are objects. An inline arrow function () => setLikes(l => l + 1) written directly in the parent's render is a brand-new function every single render — never === to the previous one. Without useCallback, memo on the child is completely defeated.

Rendered 1 time(s)

Likes so far: 0. Toggle the second button, then click "Re-render parent" and watch whether the button's render count keeps climbing.

memo() and useCallback() are a pair. Wrapping a component in memo without stabilizing the function props you pass it accomplishes nothing — the child re-renders exactly as often as it would have without memo at all.
4

Custom Comparison Functions

memo accepts a second argument: a function that decides whether to skip the re-render. This is useful when an object prop has fields that update constantly but don't affect what the component renders.

#1Why React.memo Matters

Rendered 1 time(s)

A new post object is created every click (different lastViewedAt), but the custom comparator ignores that field — the card doesn't re-render.

Return true to skip, false to re-render. That's the opposite of a typical equality function — it reads as "are these props the same?" not "should I update?"
5

When NOT to Reach for React.memo

Every memo'd component pays for a props comparison on every parent render — for a cheap component, that comparison can cost more than just re-rendering it would have. And if you don't also stabilize function/object props with useCallback/useMemo, memo does nothing but add overhead.

The children prop trap: <Memoized><Child /></Memoized> creates a new children element on every render of the parent, exactly like a function prop would — memo can't help unless children is memoized too.
6

Common Mistakes

1Wrapping in memo() without stabilizing props
mistake-unstable-props.jsx
// ❌ Pointless: memo does nothing because onSave is a new function every render
const Form = memo(function Form({ onSave }) { /* ... */ });
<Form onSave={() => save(data)} />

// ✅ GOOD: stabilize with useCallback first
const handleSave = useCallback(() => save(data), [data]);
<Form onSave={handleSave} />
2Memoizing components that always receive new props
mistake-always-changing.jsx
// ❌ BAD: "timestamp" changes every render — memo's check always fails,
// so you pay for the comparison AND the re-render
const Row = memo(function Row({ item, timestamp }) { /* ... */ });

// ✅ GOOD: don't pass props that always change, or use a custom comparator
// that ignores the field that doesn't affect rendering
3Memoizing trivially cheap components
mistake-cheap-component.jsx
// ❌ BAD: comparing props costs more than just re-rendering this
const Label = memo(function Label({ text }) {
  return <span>{text}</span>;
});

// ✅ GOOD: reserve memo for components with real render cost
// (large lists, charts, heavy layout/computation)
7

Best Practices

Measure first — apply memo to components that actually show up as slow
Always pair with useCallback/useMemo for any function or object props
Reach for it on expensive components — large lists, charts, heavy layout
Use a custom comparator when a prop has fields that update but don't affect rendering
Skip it for cheap, simple components — the comparison isn't free either
8

Pattern Reference

Prop typeWhat memo needs
Primitives (string, number, boolean)Nothing extra — default shallow check works
FunctionsuseCallback to keep the same reference
Objects / arraysuseMemo to keep the same reference
Object with irrelevant changing fieldsA custom comparison function as memo's 2nd argument
9

Practice Project

Build a small comment list that:

  • Renders 50+ Comment rows, each wrapped in memo()
  • Passes each row a stable onDelete(id) handler via useCallback
  • Adds an unrelated "new comment count" badge elsewhere and confirms typing/clicking it doesn't re-render every row
  • Adds a custom comparator that ignores a lastSeenAt field so rows don't re-render on that alone
10

What's Next?

🚀 Next Learning Topics:
  • useMemo: revisit stabilizing object/array props that feed memoized children
  • useCallback: revisit stabilizing the function props this post depends on
  • Code Splitting: reduce what loads at all, not just what re-renders
  • Custom Hooks: wrap the memo + useCallback pairing into a reusable pattern

Keep practicing! 💪 React.memo, useMemo, and useCallback are one system, not three separate tricks — reach for all three together once you've found a component that's genuinely worth optimizing.

About the Author

TG

Thirdy Gayares

Passionate developer creating custom solutions for everyone. I specialize in building user-friendly tools that solve real-world problems while maintaining the highest standards of security and privacy.