Kodokon kodokon.com

Memoization: memo, useMemo, useCallback

Learn when React.memo, useMemo, and useCallback genuinely speed up your app, and when they complicate it for nothing.

10 min · 3 questions

Open this lesson in Kodokon

When a component re-renders, all of its children re-render by default, whether their props changed or not. That is not a tragedy: a render only touches the DOM if the output changes. Memoization becomes relevant when frequent renders meet expensive subtrees. Three tools, three targets: React.memo skips a component's render if its props are shallowly equal, useMemo caches a computed value, useCallback caches a function. All three rely on reference equality.

JSX
import { memo } from "react";

const Row = memo(function Row({ item, onSelect }) {
  return (
    <li onClick={() => onSelect(item.id)}>{item.label}</li>
  );
});
Row only re-renders if item or onSelect changes reference.

The chain is only as strong as its weakest link. If a single prop receives a fresh reference on every render - an inline arrow function, an object or array literal - memo's comparison fails and the component re-renders anyway. useCallback stabilizes the function's reference so the comparison succeeds.

JSX
import { useCallback } from "react";

function List({ items }) {
  const handleSelect = useCallback((id) => {
    console.log("selected:", id);
  }, []);

  return (
    <ul>
      {items.map((item) => (
        <Row
          key={item.id}
          item={item}
          onSelect={handleSelect}
        />
      ))}
    </ul>
  );
}
handleSelect keeps the same reference on every render.

useMemo has two legitimate uses: avoiding a genuinely expensive computation (sorting or aggregating thousands of items), and stabilizing a reference consumed by a memoized child or listed in an effect's dependencies. For a trivial computation, the cache costs more than it saves: dependency comparison, retained memory, heavier reading.

JSX
import { useMemo } from "react";

function Stats({ orders }) {
  const total = useMemo(() => {
    return orders.reduce(
      (sum, order) => sum + order.amount,
      0
    );
  }, [orders]);

  return <p>Total: {total} €</p>;
}
The total is only recomputed if orders changes reference.

Knowledge check

Make sure you remember the key points of this lesson.

  1. A child is wrapped in React.memo, but its parent passes it an inline arrow function as a prop. Result?
    • Memoization works: functions are compared by their source code
    • Memoization is ineffective: the inline function creates a fresh reference on every render and the comparison fails
    • React raises a warning in development
  2. What does useCallback(fn, [a]) guarantee as long as a does not change?
    • That the function is never recreated in memory
    • That the function will only be executed once
    • That the same function reference is returned from one render to the next
  3. In which case is useMemo clearly justified?
    • An expensive computation, or a reference to stabilize for a memoized child or an effect dependency
    • Any derived value, as a precaution
    • Caching data across two mounts of the component