Memoization and the React Compiler

React · Advanced · 16 min

Memoization fixes real slowdowns, but misapplied it adds complexity; the Compiler changes when you need it by hand.

Because re-rendering cascades, you sometimes want to skip work. React offers three tools:

  • `memo(Component)` skips re-rendering a component when its props are shallowly equal (Object.is per prop) to last time.
  • `useMemo(() => compute(a, b), [a, b])` caches a computed value between renders until a dependency changes.
  • `useCallback(fn, [deps])` caches a function identity; it's useMemo(() => fn, deps).

They work together. memo is defeated if a parent passes a new object or arrow function each render, since {} !== {}. So you stabilise those props with useMemo/useCallback. The same applies to values in effect dependencies and context values.

Use them when you've measured a problem: an expensive calculation (filtering thousands of rows), a large subtree that re-renders on every keystroke, or a value that must be stable for an effect. Memoizing everything costs memory and readability, and one unstable prop silently breaks the chain.

Often a structural fix is better: move state down into the component that uses it, or lift expensive content up and pass it as children so it isn't re-created when the wrapper's state changes.

The React Compiler (stable since v1.0) is a build-time plugin (babel-plugin-react-compiler, enabled through the Vite React plugin or Next.js config) that analyses your components and Hooks and inserts fine-grained memoization automatically, including for JSX and callbacks, and even after early returns. With it, most manual memo/useMemo/useCallback becomes unnecessary. It relies on your code following the Rules of React (pure render, no mutation of props/state, Hooks at top level) and skips components it can't prove safe; the eslint-plugin-react-hooks rules flag those. Manual memoization remains as an escape hatch for precise control, such as effect dependencies.

Example

import { memo, useCallback, useMemo, useState } from "react";

const List = memo(function List({ items, onSelect }) {
  console.log("List rendered");
  return <ul>{items.map(i => <li key={i.id} onClick={() => onSelect(i.id)}>{i.name}</li>)}</ul>;
});

export default function Products({ products }) {
  const [query, setQuery] = useState("");
  const [theme, setTheme] = useState("light");

  // Recomputed only when products or query change, not when theme changes
  const visible = useMemo(
    () => products.filter(p => p.name.toLowerCase().includes(query.toLowerCase())),
    [products, query]
  );
  // Stable identity so memo(List) can skip re-rendering
  const handleSelect = useCallback(id => console.log("selected", id), []);

  return (
    <div className={theme}>
      <input value={query} onChange={e => setQuery(e.target.value)} />
      <button onClick={() => setTheme(t => (t === "light" ? "dark" : "light"))}>Theme</button>
      <List items={visible} onSelect={handleSelect} />
    </div>
  );
}
// With the React Compiler enabled, the useMemo/useCallback/memo above are unnecessary.

Remember

  • memo skips renders on shallow-equal props; useMemo caches values; useCallback caches functions
  • A single new object or function prop defeats memo; measure before memoizing
  • The React Compiler memoizes automatically for code that follows the Rules of React

Watch out for

  • Wrapping a component in memo but passing an inline object or arrow function, so it re-renders anyway.

Check yourself

  1. A memo-wrapped Chart receives options={{ color: 'red' }} written inline in the parent. The parent re-renders. Does Chart re-render?
  2. What does the React Compiler require of your code to optimise it safely?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: react.dev/learn/react-compiler