React path · Rendering less · Topic 8 of 12

Memoisation: memo, useMemo, useCallback

Not started

By default, when a component renders, everything below it renders too, and every expression in every component body runs again. Usually that's fine. When it isn't, memoisation lets React skip work whose inputs haven't changed: memo skips rendering a component, useMemo skips recomputing a value, and useCallback keeps a function the same between renders. All three compare inputs with Object.is, so they only work if the inputs keep their identity.

Why it matters: Memoisation sprinkled everywhere makes code harder to read and often saves nothing; missing in the one hot spot, it leaves a list re-rendering on every keystroke. The React Compiler now does much of it for you.

  1. 1memo skips a component

    A component wrapped in `memo` is not rendered again when its parent renders, if every prop is `Object.is`-equal to last time.

  2. 2useMemo and useCallback keep values stable

    `useMemo` caches a computed value and `useCallback` a function, until a dependency changes. They help when the value is expensive, or when its identity is a prop of a memoised child or a dependency of an effect.

  3. 3Identity is the whole game

    A new object, array or arrow function created during render is a new value every time, which defeats `memo` and re-runs effects. That's what the hooks are for.

  4. 4The compiler does it automatically

    React Compiler (1.0 since October 2025) memoises components and values at build time. In a project that uses it, most hand-written memoisation is unnecessary.

1

How it works

The explanation, with examples you can run.

memo: skip a child

import { memo, useCallback, useState } from "react" type Item = { id: string; label: string } const Row = memo(function Row({ item, onSelect }: { item: Item; onSelect: (id: string) => void }) {  return <li onClick={() => onSelect(item.id)}>{item.label}</li>}) export function List({ items }: { items: Item[] }) {  const [query, setQuery] = useState("")  const [selected, setSelected] = useState<string | null>(null)   const handleSelect = useCallback((id: string) => setSelected(id), [])   return (    <>      <input value={query} onChange={(e) => setQuery(e.target.value)} />      <p>Selected: {selected}</p>      <ul>        {items.map((item) => (          <Row key={item.id} item={item} onSelect={handleSelect} />        ))}      </ul>    </>  )}

Typing in the input re-renders List. Each Row gets the same item object and the same handleSelect function as before, so memo skips it. Without useCallback, handleSelect would be a new function on every keystroke and every row would render again, memo or not. Note that memo only looks at props: a memoised component still renders when its own state changes or a context it reads changes.

useMemo: skip a calculation

import { useMemo } from "react" type Product = { name: string; price: number } export function Results({ products, query }: { products: Product[]; query: string }) {  const matches = useMemo(    () => products.filter((p) => p.name.toLowerCase().includes(query.toLowerCase())),    [products, query],  )  return <p>{matches.length} matches</p>}

Worth it if products has tens of thousands of entries and the component renders often for unrelated reasons. For a hundred items, the filter takes microseconds and the memoisation adds code for nothing. Measure before you decide: wrap the calculation in console.time, or use the Profiler in React DevTools.

When memoisation helps

  • A component that is slow to render, re-rendering often with the same props.
  • A value or function passed to a memoised child, where a new identity would defeat its memo.
  • A value used as an effect dependency, where a new identity would re-run the effect.
  • A calculation that is measurably expensive.

Before reaching for it, check the structural fixes. Move state down into the component that uses it, so fewer components render on each change. Pass components as children: if a parent's state changes, elements it received as children were created by its own parent and don't render again.

The React Compiler

React Compiler, stable since version 1.0 in October 2025, is a build-time plugin that analyses components and inserts the equivalent of memo, useMemo and useCallback where they are safe. It relies on your code following the rules of React (pure render, hooks at the top level, no mutation of props and state); code that breaks them is skipped. In a project that uses it, write plain code and keep manual memoisation for the rare case the compiler can't handle. Either way, memoisation is an optimisation: React may discard cached values, so the program must behave correctly without them.

2

Check yourself

3 questions. Get them all right to finish this topic.

  1. 1.`Row` is wrapped in `memo`. The parent renders `<Row item={item} onSelect={() => select(item.id)} />`. Does `memo` skip Row's re-renders?
  2. 2.When does `useMemo(() => items.filter(fn), [items, fn])` actually save time?
  3. 3.If your app breaks when you remove a `useMemo`, what does that tell you?

0 of 3 right so far. Get all three to finish this topic.