Memoize Smart

Session 10 — useMemo, useCallback & React.memo

60 minutes
Performance
🧪 Interactive
Scroll to begin
Part 2 · The Problem

Why Components Re-render

Before reaching for any memoization tool, you need to know what triggers a re-render in the first place. There are exactly three triggers:

1

Its own state changes

Calling setState (or a reducer's dispatch) always schedules a re-render of that component.

2

Its parent re-renders

By default, when a parent re-renders, every child re-renders too — even if that child's own props didn't change at all.

3

Context it consumes changes

As you saw last session — every consumer of a context re-renders when the Provider's value changes.

Trigger #2 is the one that surprises people. Here's the chain: a totally unrelated state update in a top-level component cascades down and re-renders dozens of components that have nothing to do with that state.

App — setState() called
↓ re-renders
Layout — props unchanged
↓ re-renders
ProductList — props unchanged
↓ re-renders
ProductCard ×50 — props unchanged

One state update at the top re-renders every component below it by default — regardless of whether their own props changed.

⚠️ Re-rendering Isn't Always Bad

A re-render that produces the same output is usually cheap — React still diffs the virtual DOM and skips real DOM updates. The problem is when a re-render triggers expensive work: a heavy calculation, a child tree with hundreds of nodes, or a function reference that breaks a child's own memoization. That's exactly what React.memo, useMemo, and useCallback exist to prevent.

Part 3 · Skipping Components

React.memo — Skip Re-renders for Unchanged Props

React.memo wraps a component and tells React: "before re-rendering this, do a shallow comparison of its new props against its old props. If every prop is the same, skip the re-render entirely and reuse the last result."

ProductCard.jsx
import { memo } from 'react';

function ProductCard({ name, price }) {
  // Imagine this renders a complex card with images, formatting, etc.
  return (
    <div className="card">
      <h4>{name}</h4>
      <p>${price}</p>
    </div>
  );
}

// Wrap the component — React now shallow-compares props before re-rendering
export default memo(ProductCard);

// Parent — only ProductCards whose own (name, price) changed actually re-render
function ProductList({ products }) {
  return products.map(p => <ProductCard key={p.id} name={p.name} price={p.price} />);
}
🧠 "Shallow Comparison" Is the Key Phrase

memo checks each prop with Object.is (basically ===). Primitives like strings and numbers compare by value, so price={9} stays equal across renders. But objects, arrays, and functions compare by reference — a brand-new {...}, [...], or () => {} created inline during render is a different reference every time, even if its contents look identical. That's the exact problem useMemo and useCallback solve.

pitfall-broken-memo.jsx — ⚠️ memo defeated
// ✗ memo is wasted — a new object AND a new function every render
function ProductList({ products }) {
  return products.map(p => (
    <ProductCard
      key={p.id}
      style={{ color: 'red' }}        {/* new object every render */}
      onSelect={() => select(p.id)}  {/* new function every render */}
    />
  ));
}
// Every ProductCard re-renders anyway — memo's shallow check always sees "changed" props
Part 4 · Memoizing Values

useMemo — Cache a Calculation Between Renders

useMemo(calculateValue, dependencies) runs calculateValue once, caches the result, and only reruns it when one of the values in the dependency array changes. On every other render, it just returns the cached value instantly.

PrimeFinder.jsx
import { useMemo, useState } from 'react';

function countPrimesBelow(limit) {
  let count = 0;
  for (let n = 2; n <= limit; n++) {
    let isPrime = true;
    for (let d = 2; d * d <= n; d++) {
      if (n % d === 0) { isPrime = false; break; }
    }
    if (isPrime) count++;
  }
  return count; // expensive for a large limit
}

function PrimeFinder({ limit }) {
  const [theme, setTheme] = useState('dark'); // unrelated state

  // Without useMemo, countPrimesBelow runs on EVERY render —
  // including when only `theme` changes and `limit` is identical.
  const primeCount = useMemo(() => countPrimesBelow(limit), [limit]);

  return (
    <div>
      <p>Primes below {limit}: {primeCount}</p>
      <button onClick={() => setTheme(t => t === 'dark' ? 'light' : 'dark')}>
        Toggle theme (unrelated re-render)
      </button>
    </div>
  );
}
💡 The Dependency Array Is a Promise

You're telling React "this value only needs to be recalculated when one of these dependencies changes." If you list [limit], the calculation reruns only when limit changes — every other render reuses the cached number, no matter what else updates in the component.

Part 5 · Live Demo

useMemo in Action — Prime Counter

This mirrors the code above. Switch modes, then click Force Re-render (simulating an unrelated state update, like the theme toggle) and watch whether the expensive calculation actually reruns. Then change the limit — a real dependency change — and see it rerun in both modes.

PrimeFinder — countPrimesBelow(limit)
Mode:
Renders
0
Calc Ran
0
Primes Found
Click a button to begin…
🔍 What to Notice

In "Without useMemo" mode, Calc Ran climbs on every single click — including "Force Re-render," which has nothing to do with the limit. In "With useMemo" mode, Calc Ran only climbs when you actually change the limit. The render count still goes up either way — only the expensive work is skipped.

Part 6 · Memoizing Functions

useCallback — Cache a Function's Identity

Functions are values, just like objects and arrays. Every time a component renders, any function defined inside it — including inline arrow functions passed as props — is a brand-new function reference, even if the code inside is identical. useCallback(fn, dependencies) returns the same function reference across renders, as long as the dependencies haven't changed.

Parent.jsx
import { useCallback, useState, memo } from 'react';

const Child = memo(function Child({ onIncrement }) {
  console.log('Child rendered');
  return <button onClick={onIncrement}>+1</button>;
});

function Parent() {
  const [count, setCount] = useState(0);
  const [step]  = useState(1);

  // Without useCallback, this is a NEW function every render —
  // Child's memo() sees a "changed" prop and re-renders anyway.
  const handleIncrement = useCallback(() => {
    setCount(c => c + step);
  }, [step]); // only recreated if `step` changes

  return (
    <>
      <p>Count: {count}</p>
      <Child onIncrement={handleIncrement} />
    </>
  );
}
💡 useCallback Is Just useMemo for Functions

useCallback(fn, deps) is equivalent to useMemo(() => fn, deps). It exists as its own hook purely because memoizing callback props is such a common pattern. Both are pointless without something downstream that actually cares about referential stability — typically a memo-wrapped child, or another hook's dependency array.

Part 7 · Live Demo

useCallback in Action — Parent & Memoized Child

The Child box below represents a memo-wrapped component that receives an onIncrement callback as a prop. Click Force Parent Re-render repeatedly and watch whether Child re-renders along with it. Then click Change Step — a real dependency change for the callback — and see Child react even in "with useCallback" mode.

Parent → Child(onIncrement) — Child is memo()'d
Mode:
Parent
0
renders
Child (memo)
0
renders
Step (callback dependency)
1
Click a button to begin…
🔍 What to Notice

In "Without useCallback" mode, the Child box flashes on every Parent re-render — even "Force Parent Re-render," which has nothing to do with the increment logic. In "With useCallback" mode, Child stays completely still during unrelated re-renders, and only flashes when "Change Step" actually changes the callback's dependency.

Part 8 · Decision Guide

When Memoization Actually Helps

All three tools have a cost: a comparison check, extra memory to hold the cached value, and more code to read. Reach for them when you can point to a specific, measurable problem — not by default.

React.memo

  • Component renders often via parent
  • Same props most of the time
  • Rendering itself is non-trivial
  • Large lists of sibling components

useMemo

  • Calculation is genuinely expensive
  • Filtering/sorting large arrays
  • Creating an object/array passed to a memo'd child
  • Value used as another hook's dependency

useCallback

  • Function passed to a memo'd child
  • Function used in another hook's dependency array
  • Function passed deep through Context
  • Stabilizing a custom hook's return value
SituationBest toolWhy
Filtering a 10-item list on every keystroke Nothing — just compute it inline Too cheap to matter; memoizing adds overhead for no gain
Sorting 50,000 rows derived from a prop useMemo Genuinely expensive; result only needs to change with the data
A button click handler passed to a plain (unmemoized) child Nothing The child re-renders anyway — useCallback buys nothing
That same handler passed to a memo-wrapped child useCallback Now reference stability decides whether the child skips work
A list of 200 complex cards re-rendering on every parent tick React.memo (+ stable props) Skips re-rendering entire subtrees that didn't actually change
🧠 Profile First, Memoize Second

Open React DevTools → Profiler, record an interaction, and look for components that re-render with identical output, or renders that take an unusually long time. Memoization is the fix for a problem you've found — not a habit to apply everywhere "just in case."

Part 9 · Common Mistakes

Memoization Pitfalls to Avoid

✗ Memoizing everything by default

Wrapping every component in memo and every value in useMemo "for performance." Each one adds a comparison cost — for cheap components and calculations, that cost can outweigh the savings.

✓ Memoize where you've measured a cost

Profile first. Reach for memoization on components that render often with stable props, or calculations that are visibly slow — not on a three-line component that renders in microseconds.

✗ Passing new objects/functions to a memo'd child

<Child style={{color:'red'}} onClick={() => ...} /> creates new references every render. memo on Child does nothing — its shallow prop check always sees "changed."

✓ Memoize the values you pass as props

Wrap the object in useMemo and the function in useCallback in the parent, so the same references survive across renders — that's what lets the child's memo actually skip work.

✗ Wrong or missing dependencies

useMemo(() => format(user), []) with an empty array caches forever — even after user changes, you keep seeing stale, outdated data.

✓ List every value the calculation reads

Include every prop, state, or variable read inside the memoized function. The ESLint react-hooks/exhaustive-deps rule catches this automatically — keep it enabled.

⚠️ useMemo Is a Hint, Not a Guarantee

React may discard a memoized value and recalculate it anyway in some situations (for example, to free memory). Never use useMemo for correctness — like skipping a required side effect. It's purely a performance optimization; your component must still produce correct output even if the memo "misses."

Part 10 · Wrap-Up

Recap & Quick Quiz

🔁

Default Re-renders

A parent re-rendering re-renders every child by default, regardless of whether that child's own props changed.

🛡️

React.memo

Skips re-rendering a component when its props are shallowly equal to last time. Broken by inline objects/functions as props.

🧮

useMemo

Caches the result of a calculation. Reruns only when a listed dependency changes.

🪝

useCallback

Caches a function reference. Same idea as useMemo, specialized for functions passed as props or dependencies.

🧪 Quiz — Question 1 of 4

By default, what happens to a child component when its parent re-renders?

🧪 Quiz — Question 2 of 4

A component is wrapped in memo, but its parent passes onClick={() => doThing()} inline. What happens?

🧪 Quiz — Question 3 of 4

When is useMemo actually worth using?

🧪 Quiz — Question 4 of 4

useMemo(() => formatUser(user), []) is called with an empty dependency array. What's the bug?
🎯 What's Next?

You now know how to spot unnecessary work and cut it out with confidence. In a future session, we'll look at extracting this kind of logic into custom hooks — reusable, named pieces of stateful behavior you can share across components.