Grokking React Advanced
Vote

0% completed

​

Memoization Using the useMemo Hook

The Running Example: A Product Search

Counting the Wasted Work

Adding useMemo

useMemo and memo Together

Getting the Dependencies Right

What useMemo Does Not Do

When to Use useMemo

Which work should a component repeat on every render, and which work could it keep from the last render? Most of what a component computes is cheap, and repeating it is the simplest correct choice. A few calculations are slow enough that repeating them with unchanged inputs wastes time the user can notice. The useMemo hook saves the result of one such calculation between renders and recomputes it only when its inputs change.

The store page now has a search box over a catalog of 20,000 products and a "Dark mode" checkbox. Every keystroke and every click of the checkbox re-renders ProductSearch.

import { useState } from 'react'; const CATEGORIES = ['Keyboards', 'Mice', 'Monitors', 'Cables', 'Headsets']; const products = Array.from({ length: 20000 }, (_, i) => ({ id: i, name: `${CATEGORIES[i % CATEGORIES.length]} item ${i}`, price: (i % 97) + 3, })); function filterProducts(products, query) { const q = query.trim().toLowerCase(); return products .filter((p) => p.name.toLowerCase().includes(q)) .sort((a, b) => a.price - b.price); } function ProductList({ items }) { return ( <ul> {items.slice(0, 20).map((p) => ( <li key={p.id}>{p.name} ${p.price}</li> ))} </ul> ); } export default function ProductSearch() { const [query, setQuery] = useState(''); const [isDark, setIsDark] = useState(false); const visible = filterProducts(products, query); return ( <div className={isDark ? 'dark' : 'light'}> <input aria-label="search" value={query} onChange={(e) => setQuery(e.target.value)} /> <label> <input type="checkbox" checked={isDark} onChange={(e) => setIsDark(e.target.checked)} /> Dark mode </label> <p>{visible.length} products</p> <ProductList items={visible} /> </div> ); }

filterProducts makes a lowercase copy of 20,000 names, keeps the matches and sorts them by price. We timed it in Node.js, taking the median of 20 calls:

filterProducts(20000, "mice") median ms: 2.43 result length: 4000

Repeated runs gave medians between 2.3 and 3.2 milliseconds. That is noticeable work for one render, and a browser on a slower device can take longer. React's documentation suggests that a calculation taking about 1 millisecond or more is a candidate for memoization. Before you memoize anything, measure the calculation in your own app, for example with console.time and console.timeEnd around the call.

Counting the Wasted Work

We rendered ProductSearch, entered "mice" in the search box as a single change, as pasting it would, and then clicked the "Dark mode" checkbox three times. A counter inside filterProducts recorded each call:

no useMemo | mount + type "mice": filterRuns 2 | after 3 dark-mode toggles: filterRuns 5

The first two calls are needed: one on mount and one for the new query. Typing the word letter by letter would need one call per letter instead, because each letter changes the query. The last three are not. Toggling dark mode changes isDark, which re-renders ProductSearch, and the component filters all 20,000 products again to get exactly the same 4,000 results.

Adding useMemo

useMemo takes two arguments: a function that computes the value, and a list of the values that the computation reads, called its dependencies. Replace the line that computes visible:

import { useMemo, useState } from 'react'; // inside ProductSearch const visible = useMemo(() => filterProducts(products, query), [query]);

On the first render, React calls the function and stores both the result and the dependency list. On every later render, it compares each dependency with its previous value using Object.is. If all of them are equal, React returns the stored result without calling the function. If any of them changed, it calls the function again and stores the new result.

products is not in the list because it is defined outside the component and never changes. Only values from inside the component, such as props, state and variables computed from them, need to be listed.

The same steps now give:

useMemo | mount + type "mice": filterRuns 2 | after 3 dark-mode toggles: filterRuns 2
Five renders of ProductSearch: filterProducts runs when query changes and is skipped on the three dark-mode toggles.
Five renders of ProductSearch: filterProducts runs when query changes and is skipped on the three dark-mode toggles.

Follow the five renders in the diagram from left to right. The two renders where query changed call filterProducts; the three renders where only isDark changed reuse the stored array of 4,000 products.

useMemo and memo Together

useMemo has a second use. In the run above, ProductList still rendered on every toggle, five times in all, because it is an ordinary component and its parent re-rendered.

The previous lesson showed that wrapping it in memo skips renders only if its props stay equal. Without useMemo, visible is a new array on every render, so memo alone does not help. We ran all four combinations:

VersionfilterProducts callsProductList renders
No useMemo, plain ProductList55
useMemo, plain ProductList25
No useMemo, ProductList wrapped in memo55
useMemo, ProductList wrapped in memo22

Only the last row skips both the calculation and the list render. useMemo returns the same array object whenever query is unchanged, and that stable reference is what lets memo see equal props.

Getting the Dependencies Right

Every value from the component that the calculation reads must be in the dependency list. If one is missing, React keeps returning a result computed from old values. We left the list empty by mistake:

const visible = useMemo(() => filterProducts(products, query), []);
deps [] after typing mice: 20000 products (should be 4000)

The calculation ran once, with an empty query, and never again. A lint rule named exhaustive-deps reports this mistake. In ESLint it is react-hooks/exhaustive-deps, from the eslint-plugin-react-hooks package. Vite's current React starter lints with oxlint instead, where the same check is react/exhaustive-deps; the starter's configuration does not list it, so add it to .oxlintrc.json if your linter does not already report it.

What useMemo Does Not Do

  • It does not make the first render faster. The calculation always runs on mount; useMemo only skips it on later renders.
  • It is not a place for side effects. The function must be pure: same inputs, same result, and nothing changed outside it. Fetching data belongs in an effect or in a data-fetching library such as React Query, covered in chapter 5.
  • It uses more memory, not less. React keeps the stored result alive between renders.
  • It is not a guarantee. React may discard the stored value, for example when you edit the component's file during development. Your code must still be correct if the calculation runs again. In Strict Mode, React also calls the function twice in development to help find impure code.

When to Use useMemo

useMemo is worth adding in three cases:

  1. The calculation is measurably slow, and its dependencies change less often than the component re-renders, as with filterProducts and the dark-mode toggle.
  2. The result is passed as a prop to a component wrapped in memo, as with ProductList.
  3. The result is used as a dependency of another hook, such as useEffect, and a new object on every render would make that hook run every time.

In other cases, useMemo adds code without saving work. A calculation over a few dozen items, for example, takes far less than a millisecond. React Compiler, when enabled, memoizes calculations like visible automatically, and the lesson "Automatic Memoization with React Compiler" describes how it changes this advice.

Reading Progress

0%


Vote for new content

On This Page

The Running Example: A Product Search

Counting the Wasted Work

Adding useMemo

useMemo and memo Together

Getting the Dependencies Right

What useMemo Does Not Do

When to Use useMemo