🎓 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
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).
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.
memo() — name and age are primitives, so React.memo's default shallow comparison just works.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.
Likes so far: 0. Toggle the second button, then click "Re-render parent" and watch whether the button's render count keeps climbing.
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.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.
#1 — Why 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.
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?"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.
<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.Common Mistakes
// ❌ 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} />// ❌ 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// ❌ 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)Best Practices
Pattern Reference
| Prop type | What memo needs |
|---|---|
| Primitives (string, number, boolean) | Nothing extra — default shallow check works |
| Functions | useCallback to keep the same reference |
| Objects / arrays | useMemo to keep the same reference |
| Object with irrelevant changing fields | A custom comparison function as memo's 2nd argument |
Practice Project
Build a small comment list that:
- Renders 50+
Commentrows, each wrapped inmemo() - Passes each row a stable
onDelete(id)handler viauseCallback - 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
lastSeenAtfield so rows don't re-render on that alone
What's Next?
- 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.