0% completed
Memoization Using the useMemo Hook
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
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 Running Example: A Product Search
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
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:
| Version | filterProducts calls | ProductList renders |
|---|---|---|
No useMemo, plain ProductList | 5 | 5 |
useMemo, plain ProductList | 2 | 5 |
No useMemo, ProductList wrapped in memo | 5 | 5 |
useMemo, ProductList wrapped in memo | 2 | 2 |
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;
useMemoonly 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:
- The calculation is measurably slow, and its dependencies change less often than the component re-renders, as with
filterProductsand the dark-mode toggle. - The result is passed as a prop to a component wrapped in
memo, as withProductList. - 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%
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