useMemo keeps the answer of a calculation between renders, and useCallback keeps a function. Learn how they decide, the three real reasons to use them, and when to leave them out.
In Part 18 you saw that a render runs every line of a component again. Usually that is fast. Part 20 showed React.memo, which lets a child skip a render when its props are the same as last time.
This part covers two hooks that keep a value the same between renders: useMemo and useCallback. We’ll see what they do, and count what they save. We’ll also see the three cases where React’s docs say they help, and the many cases where they don’t.
There is also a tool, the React Compiler, that can add this for you. Part 22 is about it. So the title says “when you still need them”. The last main section gives that list.
Try this first
Read this code. Don’t press Run yet.
import { useState } from 'react'
const items: string[] = []
for (let i = 1; i <= 5000; i++) {
items.push('Item ' + i)
}
function filterItems(query: string) {
console.log('filterItems runs')
return items.filter(item => item.includes(query))
}
export default function App() {
const [query, setQuery] = useState('')
const [note, setNote] = useState('')
const shown = filterItems(query)
return (
<div>
<input placeholder="Search" value={query} onChange={e => setQuery(e.target.value)} />
<input placeholder="Note" value={note} onChange={e => setNote(e.target.value)} />
<p>Matches: {shown.length}</p>
</div>
)
}
filterItems runs
filterItems runs
filterItems runs
filterItems runs
filterItems runs
filterItems runs
The page has a list of 5,000 items, made once at the top of the file. The Search box keeps only the items that contain what you type. The Note box is for a note. It has nothing to do with the search.
Make a guess. You type two letters in the Note box. Does filterItems run?
Press Run. Type “hi” in the Note box, and look at the Console.
Six lines. Two came when the page loaded. Then two more came for each letter. The search never changed, but the filter ran again on every key.
Each key changes note, so React renders App again. A render runs every line of App, and that includes filterItems(query). The lines come in pairs because of Strict Mode, as in Part 2.
Is a filter over 5,000 items slow? We can’t tell by looking. Later we’ll see how to measure it before you decide. For now, see the waste. Say the work was slow. Then every key in the Note box would wait for a filter that gives the same answer as last time.
What memoization means
A calculation is code that works out an answer from some values. The filter is one. Give it the same query, and it gives the same answer. So we could keep the answer, and use it again while query stays the same.
Keeping an answer to use again is called caching. The place where you keep it is a cache.
When the answer comes from a function, this kind of caching has its own name: memoization. To memoize a calculation means to remember its answer for the inputs it got. A memoized value is an answer that was kept like this. It sounds like “memo”, a short note you write so you don’t forget. That’s a good way to remember it. React’s docs say this is “also known as memoization, which is why this Hook is called useMemo.”
useMemo: keep the answer of a calculation
Here is the same app with one line changed:
import { useMemo, useState } from 'react'
const items: string[] = []
for (let i = 1; i <= 5000; i++) {
items.push('Item ' + i)
}
function filterItems(query: string) {
console.log('filterItems runs')
return items.filter(item => item.includes(query))
}
export default function App() {
const [query, setQuery] = useState('')
const [note, setNote] = useState('')
const shown = useMemo(() => filterItems(query), [query])
return (
<div>
<input placeholder="Search" value={query} onChange={e => setQuery(e.target.value)} />
<input placeholder="Note" value={note} onChange={e => setNote(e.target.value)} />
<p>Matches: {shown.length}</p>
</div>
)
}
filterItems runs
filterItems runs
filterItems runs
filterItems runs
filterItems runs
filterItems runs
Run it. Type “hi” in the Note box. Then type “42” in the Search box.
The Console has six lines again, but they came at different times. Two came when the page loaded. The Note box added none. The Search box added two for each key, because each key changed query. The page ends with “Matches: 199”.
useMemo takes two things:
- A calculation. It is a function with no arguments that returns the value you want:
() => filterItems(query). - A dependency array. It lists every value from the component that the calculation uses:
[query]. You met dependency arrays in Part 11.
On the first render, React calls the calculation and keeps the answer. On each later render, React compares each dependency with its value from the last render. It uses Object.is, as Effects do. If they are all the same, React gives back the kept answer and doesn’t call the calculation. If one is different, React calls the calculation again, keeps the new answer, and gives it back.
What useMemo does on a render. Choose a case, then press play or step through it.
Here are the two cases in words.
You type in the Note box. The Search box already shows 42.
- React renders
Appagain, and reaches theuseMemoline. - React takes the dependencies from last time:
['42']. - It compares them with this render’s,
['42'], usingObject.is. - They are all the same, so React gives back the kept answer.
filterItemsdoes not run. The page shows 199 matches.
You type 2 in the Search box. It showed 4 before.
- React renders
Appagain, and reaches theuseMemoline. - React takes the dependencies from last time:
['4']. - It compares them with this render’s,
['42'], usingObject.is. queryis different, so React calls your calculation.- React keeps the new answer, 199 matches, in place of the old one, 2,084. The page shows 199.
An everyday example
A shop worker gets asked, “How much are 12 apples?” She works it out and writes the answer on a note. The next customer asks the same question. She reads the note. Then someone asks about 13 apples. The note is no help, so she works it out again and writes a new note.
The question is the dependency. The note is the cache.
The exact version
React keeps only one answer for each useMemo: the one from the last time it ran the calculation. It doesn’t keep a list of old answers. Type 4, then 2, then delete the 2. The box shows 4 again, but the answer for 4 is gone. We checked with Strict Mode off: the calculation ran once for each of the three changes.
Also, the note may be thrown away. React’s docs give reasons it may do this. We come back to that below, in A cache, not a promise.
Strict Mode calls the calculation twice
We counted calls to App and to filterItems in the useMemo app, with Strict Mode on and off. We added a console.log('App renders') to count App.
| What happened | App (on) |
filterItems (on) |
App (off) |
filterItems (off) |
|---|---|---|---|---|
| The page loads | 2 | 2 | 1 | 1 |
| One key in the Note box | 2 | 0 | 1 | 0 |
| One key in the Search box | 2 | 2 | 1 | 1 |
With Strict Mode on, React called the calculation twice whenever a dependency changed. React’s docs say it does this to help you find code that is not pure. A pure calculation gives the same answer every time and changes nothing outside itself (Part 11). If a calculation changes something, running it twice makes that easier to see. The docs add: “The result from one of the calls will be ignored.”
Strict Mode’s extra calls happen only in development. With Strict Mode off, we got the numbers in the two “off” columns.
useCallback: keep a function
Part 11 and Part 16 showed that a function made inside a component is a new function on every render. It does the same job, but Object.is says it is a different function.
useCallback keeps a function the same between renders, until a dependency changes. Here are three ways to make a function: a plain one, one with useCallback, and one with useMemo.
import { useCallback, useMemo, useRef, useState } from 'react'
export default function App() {
const [clicks, setClicks] = useState(0)
const plain = () => console.log('hi')
const withCallback = useCallback(() => console.log('hi'), [])
const withMemo = useMemo(() => () => console.log('hi'), [])
const first = useRef({ plain, withCallback, withMemo })
function compare() {
console.log('plain is the same:', first.current.plain === plain)
console.log('useCallback is the same:', first.current.withCallback === withCallback)
console.log('useMemo is the same:', first.current.withMemo === withMemo)
}
return (
<div>
<button onClick={() => setClicks(clicks + 1)}>Render again: {clicks}</button>
<button onClick={compare}>Compare</button>
</div>
)
}
plain is the same: false
useCallback is the same: true
useMemo is the same: true
useRef keeps the three functions from the first render, as in Part 14. compare checks them against this render’s.
Run it. Click “Render again” twice, then “Compare”. The plain function is new. The other two are the same functions as on the first render.
Look at the useMemo line closely. () => () => console.log('hi') is a calculation that returns a function. useMemo keeps that returned function. That is all useCallback does too. React’s docs put it this way, as a short version of useCallback inside React:
function useCallback(fn, dependencies) {
return useMemo(() => fn, dependencies);
}
Here is our own example of the same idea:
import { useCallback, useMemo } from 'react'
export function Demo({ room }: { room: string }) {
const a = useCallback(() => console.log(room), [room])
const b = useMemo(() => () => console.log(room), [room])
return <button onClick={() => { a(); b() }}>Log</button>
}
a and b behave the same way. The docs say the only benefit of useCallback is that “it lets you avoid writing an extra nested function inside. It doesn’t do anything else.”
So there is one difference to remember:
useMemocalls your function and keeps what it returns.useCallbackdoes not call your function. It keeps the function itself, for you to call later.
The three reasons to use them
React’s docs list a few cases where useMemo is worth it. For useCallback, the list is the same without the first case.
- The calculation is slow, and its dependencies rarely change.
- You pass the value to a child wrapped in
memo. - The value is a dependency of another hook, like an Effect.
The docs then say: “There is no benefit to wrapping a calculation in useMemo in other cases.” Here is each case.
Reason 1: a slow calculation
That was “Try this first”. Without useMemo, a key in the Note box ran the filter again. With it, the filter ran only when query changed. The next section shows how to find out if a calculation is really slow.
Reason 2: a child wrapped in memo
memo lets a child skip a render when every prop is the same as last time, by Object.is (Part 20). Here FruitList is wrapped in memo. But it gets an array and a function, and App makes both while it renders:
import { memo, useState } from 'react'
const fruits = ['apple', 'banana', 'cherry', 'grape', 'mango']
const FruitList = memo(function FruitList({ items, onPick }: { items: string[]; onPick: (fruit: string) => void }) {
console.log('FruitList renders')
return (
<ul>
{items.map(fruit => (
<li key={fruit}>
<button onClick={() => onPick(fruit)}>{fruit}</button>
</li>
))}
</ul>
)
})
export default function App() {
const [query, setQuery] = useState('')
const [picked, setPicked] = useState('nothing')
const [clicks, setClicks] = useState(0)
const shown = fruits.filter(fruit => fruit.includes(query))
function handlePick(fruit: string) {
setPicked(query === '' ? fruit : fruit + ', found with ' + query)
}
return (
<div>
<input placeholder="Search" value={query} onChange={e => setQuery(e.target.value)} />
<button onClick={() => setClicks(clicks + 1)}>Clicks: {clicks}</button>
<p>Picked: {picked}</p>
<FruitList items={shown} onPick={handlePick} />
</div>
)
}
FruitList renders
FruitList renders
FruitList renders
FruitList renders
Run it and click “Clicks” once. Two lines came on load, and two more came from the click. FruitList rendered again, even with memo.
filter makes a new array on every render. handlePick is a new function on every render. So memo sees new props each time, and can’t skip anything.
Now keep both the same between renders:
import { memo, useCallback, useMemo, useState } from 'react'
const fruits = ['apple', 'banana', 'cherry', 'grape', 'mango']
const FruitList = memo(function FruitList({ items, onPick }: { items: string[]; onPick: (fruit: string) => void }) {
console.log('FruitList renders')
return (
<ul>
{items.map(fruit => (
<li key={fruit}>
<button onClick={() => onPick(fruit)}>{fruit}</button>
</li>
))}
</ul>
)
})
export default function App() {
const [query, setQuery] = useState('')
const [picked, setPicked] = useState('nothing')
const [clicks, setClicks] = useState(0)
const shown = useMemo(() => fruits.filter(fruit => fruit.includes(query)), [query])
const handlePick = useCallback((fruit: string) => {
setPicked(query === '' ? fruit : fruit + ', found with ' + query)
}, [query])
return (
<div>
<input placeholder="Search" value={query} onChange={e => setQuery(e.target.value)} />
<button onClick={() => setClicks(clicks + 1)}>Clicks: {clicks}</button>
<p>Picked: {picked}</p>
<FruitList items={shown} onPick={handlePick} />
</div>
)
}
FruitList renders
FruitList renders
Run it and click “Clicks”. Only the two lines from loading the page. FruitList skipped the click.
handlePick reads query, so query is in its array. It also uses setPicked, but React keeps a set function the same on every render (Part 4). So that one doesn’t need to be listed.
If the handler only called setPicked(fruit), you wouldn’t need useCallback at all. You could pass onPick={setPicked}, as Part 20 suggests, because a set function never changes. This handler does more: it also records what you searched for. A function made in App that reads state is new on every render. That is the case useCallback is for.
We tried all four mixes, with Strict Mode off. We clicked “Clicks” 3 times, then picked “banana”, then typed “an” in the Search box, one key at a time. We counted the times FruitList rendered after the page loaded.
items |
onPick |
3 clicks | Pick a fruit | Type 2 keys |
|---|---|---|---|---|
| plain | plain | 3 | 1 | 2 |
useMemo |
plain | 3 | 1 | 2 |
| plain | useCallback |
3 | 1 | 2 |
useMemo |
useCallback |
0 | 0 | 2 |
Only the last row skips anything. One prop that is new each time is enough to make memo render the child. React’s docs say it like this: “a single value that’s “always new” is enough to break memoization for an entire component.”
Typing still renders FruitList. That is right: query changed, so the list is really different. With Strict Mode on, every number in the table doubles.
Reason 3: a dependency of an Effect
Here the function is used by an Effect. The Effect pretends to connect to a chat room. It only logs, but in a real app this could open a connection to a server.
import { useEffect, useState } from 'react'
export default function App() {
const [room, setRoom] = useState('music')
const [clicks, setClicks] = useState(0)
function makeOptions() {
return { room: room, retries: 3 }
}
useEffect(() => {
const options = makeOptions()
console.log('connect to ' + options.room)
return () => console.log('disconnect from ' + options.room)
}, [makeOptions])
return (
<div>
<button onClick={() => setClicks(clicks + 1)}>Clicks: {clicks}</button>
<button onClick={() => setRoom('travel')}>Go to travel</button>
<p>Room: {room}</p>
</div>
)
}
connect to music
disconnect from music
connect to music
disconnect from music
connect to music
disconnect from music
connect to music
Run it and click “Clicks” twice. The first three lines came when the page loaded: setup, cleanup, setup, because of Strict Mode (Part 11). Then each click logged disconnect from music and connect to music again, though the room never changed.
makeOptions is a new function on every render. So [makeOptions] always looks changed, and the Effect runs after every render. With Strict Mode off, we saw the same two lines on each click.
The tools from Part 11 that check your code see this. The Part 10 project’s Oxlint 1.87.0 printed this first line, and the rest of its warning shows the code:
! react-hooks(exhaustive-deps): React hook useEffect depends on `makeOptions`, which changes every render
Its last line was a hint: help: Try memoizing this variable with `useRef` or `useCallback`. ESLint with eslint-plugin-react-hooks 7.1.1 printed a longer message:
The 'makeOptions' function makes the dependencies of useEffect Hook (at line 15) change on every render. Move it inside the useEffect callback. Alternatively, wrap the definition of 'makeOptions' in its own useCallback() Hook.
The warning gives two fixes. Here is the second one, useCallback:
import { useCallback, useEffect, useState } from 'react'
export default function App() {
const [room, setRoom] = useState('music')
const [clicks, setClicks] = useState(0)
const makeOptions = useCallback(() => {
return { room: room, retries: 3 }
}, [room])
useEffect(() => {
const options = makeOptions()
console.log('connect to ' + options.room)
return () => console.log('disconnect from ' + options.room)
}, [makeOptions])
return (
<div>
<button onClick={() => setClicks(clicks + 1)}>Clicks: {clicks}</button>
<button onClick={() => setRoom('travel')}>Go to travel</button>
<p>Room: {room}</p>
</div>
)
}
connect to music
disconnect from music
connect to music
disconnect from music
connect to travel
Run it. Click “Clicks” twice, then “Go to travel”. The clicks print nothing now. makeOptions changes only when room changes, so the Effect runs again only then.
But the first fix in the warning is better. React’s docs say “it’s even better to remove the need for a function dependency.” Move the function inside the Effect:
import { useEffect, useState } from 'react'
export default function App() {
const [room, setRoom] = useState('music')
const [clicks, setClicks] = useState(0)
useEffect(() => {
function makeOptions() {
return { room: room, retries: 3 }
}
const options = makeOptions()
console.log('connect to ' + options.room)
return () => console.log('disconnect from ' + options.room)
}, [room])
return (
<div>
<button onClick={() => setClicks(clicks + 1)}>Clicks: {clicks}</button>
<button onClick={() => setRoom('travel')}>Go to travel</button>
<p>Room: {room}</p>
</div>
)
}
We got exactly the same five lines as with useCallback, with Strict Mode on. Now the Effect depends on room, a piece of text. Text can’t become “new” by accident the way a function can. And there is no hook to get wrong.
The same is true for objects. Part 11 made an object inside the Effect for the same reason. Make the value inside the Effect first. Use useMemo or useCallback when the value must be shared, like a function that a custom hook returns.
A function that a custom hook returns
Part 16 ended with a problem. Its useCounter hook returned a new increase function on every render. An Effect that listed increase removed its key listener and added it again on every click.
The Effect is in the component that uses the hook, so the hook can’t move the function inside it. This is where useCallback belongs. Here is Part 16’s example with one change, inside useCounter:
import { useCallback, useEffect, useState } from 'react'
function useCounter() {
const [count, setCount] = useState(0)
const increase = useCallback(() => {
setCount(c => c + 1)
}, [])
return { count, increase }
}
export default function App() {
const { count, increase } = useCounter()
useEffect(() => {
function handleKeyDown(e: KeyboardEvent) {
if (e.key === '+') {
increase()
}
}
console.log('add the key listener')
window.addEventListener('keydown', handleKeyDown)
return () => {
console.log('remove the key listener')
window.removeEventListener('keydown', handleKeyDown)
}
}, [increase])
return <button onClick={increase}>Count: {count}</button>
}
add the key listener
remove the key listener
add the key listener
Run it and click the button three times. Those three lines all came when the page loaded, because of Strict Mode. The clicks added none. In Part 16’s version, each click added two more lines. We counted with Strict Mode on and off: with useCallback, the clicks added none in both.
increase lists no dependencies, []. It uses only setCount, and the updater c => c + 1, so it never needs a newer value. React’s docs suggest this for custom hooks: wrap the functions a hook returns in useCallback. Then the people who use your hook can put them in an Effect’s array safely.
How to tell if a calculation is slow
Don’t guess. Measure. React’s docs show a simple way, with the browser’s console.time:
import { useState } from 'react'
const items: string[] = []
for (let i = 1; i <= 5000; i++) {
items.push('Item ' + i)
}
function filterItems(query: string) {
return items.filter(item => item.includes(query))
}
export function Search() {
const [query, setQuery] = useState('')
console.time('filter')
const shown = filterItems(query)
console.timeEnd('filter')
return (
<div>
<input placeholder="Search" value={query} onChange={e => setQuery(e.target.value)} />
<p>Matches: {shown.length}</p>
</div>
)
}
console.time('filter') starts a timer named “filter”. console.timeEnd('filter') stops it, and prints the name and the time in between. Time is shown in milliseconds (ms). There are 1,000 milliseconds in one second, as Part 13 said.
Use this in a project on your own computer, like the one from Part 10. Read the lines in the browser’s developer tools, in the Console. The playground’s Console area on this page shows only console.log, console.info, console.warn and console.error.
Then do what a user does, like typing in the box, and add up the times. React’s docs give an example line, filter array: 0.15ms. They suggest memoizing when the total for one action is “significant”. As a rough guide, they give “say, 1ms or more”.
We don’t give a time for this filter here. The number depends on the computer, and on many other things. The docs warn about three of them:
- “your machine is probably faster than your users’”. Chrome’s developer tools have an option that slows the page down on purpose, to test this.
- Times measured in development are not exact. For example, Strict Mode renders each component twice. For the true times, build the app for production and test it on a device like your users have.
- “useMemo won’t make the first render faster.” It only saves work on later renders.
After you add useMemo, measure again. Keep it only if the total went down.
When not to use them
Most calculations are fast. React’s docs say: “unless you’re creating or looping over thousands of objects, it’s probably not expensive.” “Expensive” here means slow.
Wrapping a cheap calculation doesn’t break anything. The docs say there is “no significant harm” in it. But the code gets harder to read, and every dependency array is one more thing to keep right.
And useMemo must never be what makes your code correct. The docs say: “You should only rely on useMemo as a performance optimization. If your code doesn’t work without it, find the underlying problem and fix it first.”
The docs also list ways to need less memoization in the first place:
- Pass JSX as
children, so a wrapper’s state change doesn’t render them (Part 19). - Keep state as low in the tree as you can (Part 9).
- Keep rendering pure.
- Avoid Effects that set state when you could work the value out while rendering (Part 11).
- Move objects and functions inside the Effect that uses them, as above.
A cache, not a promise
React usually keeps the answer. But it doesn’t promise to. The docs say React “will not throw away the cached value unless there is a specific reason to do that”. Then they give reasons:
- In development, React throws the cache away when you edit your component’s file.
- In development and production, React throws it away in one more case. The component has to wait for something, like data, the first time it is added. Part 31 covers waiting with Suspense.
- React may add new features later that throw the cache away.
So useMemo only makes code faster. It is not a promise to keep the value. If an Effect depends on a useMemo value and React throws it away, the Effect runs again. That is one more reason to move objects inside the Effect.
If you need a value that must stay, don’t use useMemo. The docs say “a state variable or a ref may be more appropriate.” The useCallback page says the same.
When you still need them
The React Compiler is a tool that changes your code when you build the app. React’s docs say it “automatically memoizes values and functions, reducing the need for manual useMemo calls”. Part 22 shows what it does.
It is not built into React itself. You add it to your project. The playground on this page doesn’t use it. The Part 10 project doesn’t either: Vite offered “TypeScript + React Compiler”, and we picked plain “TypeScript”.
So you still need useMemo and useCallback in three cases:
- Your project has no compiler. Then you add memoization yourself, for the three reasons above.
- The compiler skipped your code. It works only for code that follows the Rules of React. These are rules from React’s docs, like “don’t change props or state in place”. Part 22 lists them. Sometimes the compiler can see that code breaks them. Then the docs say it “safely skips optimization rather than risk changing your app’s behavior”. Code that breaks them in ways it can’t see may be compiled anyway, and then behave differently.
- You need exact control, often for an Effect. React’s docs say the hooks “can continue to be used with React Compiler as an escape hatch”. They add: “A common use-case for this is if a memoized value is used as an effect dependency”.
For existing code, the docs suggest leaving memoization in place when you add the compiler. They say removing it can change what the compiler makes. Test carefully if you do remove it.
Common mistakes
Wrapping everything
import { useMemo } from 'react'
function Total({ price, count }: { price: number; count: number }) {
const total = useMemo(() => price * count, [price, count])
return <p>Total: {total}</p>
}
Working out price * count is cheap. And total is a number, so Object.is treats a new one with the same value as the same. No child or Effect gains anything here.
The fix: const total = price * count.
Leaving out a dependency
import { useMemo, useState } from 'react'
const fruits = ['apple', 'banana', 'cherry', 'grape', 'mango']
export default function App() {
const [query, setQuery] = useState('')
const shown = useMemo(() => fruits.filter(fruit => fruit.includes(query)), [])
return (
<div>
<input placeholder="Search" value={query} onChange={e => setQuery(e.target.value)} />
<p>{shown.join(', ')}</p>
</div>
)
}
Run it and type “an”. The list doesn’t change. It still shows all five fruits.
The array [] never changes, so React never runs the calculation again. The calculation saw query only on the first render, when it was empty. Its answer is stale: old, and nothing updates it, as in Part 11. TypeScript is happy with this code, and the playground shows no warning.
Both linter tools catch it. The Part 10 project’s Oxlint 1.87.0 started its warning with this line:
! react-hooks(exhaustive-deps): React Hook useMemo has a missing dependency: 'query'
ESLint with eslint-plugin-react-hooks 7.1.1 printed:
React Hook useMemo has a missing dependency: 'query'. Either include it or remove the dependency array.
The fix is [query]. Then typing “an” shows banana, mango. We checked both, with Strict Mode on and off.
Forgetting the array
import { useMemo } from 'react'
function Matches({ query }: { query: string }) {
const shown = useMemo(() => ['Item 1', 'Item 2'].filter(item => item.includes(query)))
return <p>Matches: {shown.length}</p>
}
With no array, React has nothing to compare, so the calculation runs on every render. TypeScript stops it: “Expected 2 arguments, but got 1.” The playground doesn’t check types. We tried the same mistake in the useMemo version of the app, where types are not checked. The filter ran again for every key in the Note box. With Strict Mode on, you’ll see three lines for each key, not two. With it off, one.
Changing things inside the calculation
The calculation runs while React renders, so it must be pure. This one changes the array it was given:
import { useMemo } from 'react'
const todos = ['Buy milk']
function TodoList({ items }: { items: string[] }) {
const shown = useMemo(() => {
items.push('Go for a walk')
return items
}, [items])
return <p>{shown.join(', ')}</p>
}
export default function App() {
return <TodoList items={todos} />
}
Run it. “Go for a walk” appears twice. Strict Mode ran the calculation twice, and each run added one more item to the same array. With Strict Mode off, we saw it once. That doesn’t mean the code is right. It only means the bug is hidden. The linter printed nothing for this code.
The fix: make a new array, and leave the one you got alone.
import { useMemo } from 'react'
const todos = ['Buy milk']
function TodoList({ items }: { items: string[] }) {
const shown = useMemo(() => {
return [...items, 'Go for a walk']
}, [items])
return <p>{shown.join(', ')}</p>
}
export default function App() {
return <TodoList items={todos} />
}
The same goes for any side effect in the calculation. Setting a timer, sending data to a server and changing the page title are side effects. useMemo is not “run this once”. Code that should run because the component is on the page goes in an Effect. Code that should run because of a click goes in the event handler.
Expecting useCallback to make the function faster
useCallback doesn’t change what your function does, or how fast it runs. It doesn’t even stop the function from being made. React’s docs say: “useCallback does not prevent creating the function.”
We checked. On each render, we compared the arrow function we wrote with the one useCallback gave back. After a click, they were different functions. The new one was made, and React gave us the old one instead.
So useCallback helps only when something compares the function: a memo child, or a dependency array. Anywhere else, it adds code and saves nothing.
Practice
Press Edit on any example above and try these.
- In “Try this first” (no
useMemo), type “abc” in the Note box. How many lines does the Console show in total? - In the
useMemoversion, press Run, type “4” in the Search box, then “x” in the Note box. How many lines in total? What does the page say? - In the
useCallbackexample, press Run, then click “Compare” without clicking “Render again” first. What does the line forplainsay? Why? - In the
FruitListexample with both hooks, changehandlePickback to a plain function. Keep theuseMemo. Run it and click “Clicks” twice. How manyFruitList renderslines are there in total?
Answers
- 8 lines. Two when the page loads, then two for each of the three letters. Strict Mode doubles each render.
- 4 lines. Two on load and two for the “4”. The Note box added none. The page says “Matches: 2084”.
plain is the same: false, even with no clicks. Strict Mode calledApptwice on load.useRefkept what the first call made. The second call made a newplain. But in React 19,useMemoanduseCallbackuse the first call’s results again during Strict Mode’s second call. React’s upgrade guide for React 19 says so. So they handed back the first call’s functions. With Strict Mode off, we gottruefor all three.- 6 lines: two on load, then two for each click.
itemsstays the same, butonPickis new each time, somemocan’t skip.
Interview questions
Try to answer each one out loud before you open the answer.
What does useMemo do, and how does React decide to run the calculation again?
useMemo(calculate, deps) keeps the result of a calculation between renders. On the first render, React calls calculate and stores the result. On later renders, it compares each dependency with the last render’s value, using Object.is. All the same: React returns the stored result without calling calculate. Any different: it calls calculate again and stores the new result.
A strong answer adds that useMemo keeps only the latest result, not a list of old ones. It doesn’t make the first render faster. And in Strict Mode, React calls the calculation twice in development.
What is the difference between useMemo and useCallback?
useMemo calls your function and keeps what it returns. useCallback keeps the function itself and doesn’t call it. useCallback(fn, deps) is the same as useMemo(() => fn, deps). React’s docs show that as a short version of how useCallback works.
A strong answer adds that useCallback doesn’t make the function faster. A new function is still made on every render. React just hands back the kept one when the dependencies are the same.
When is it worth using useMemo or useCallback?
React’s docs give three cases. The calculation is clearly slow, and its dependencies rarely change. The value goes to a child wrapped in memo. Or the value is a dependency of another hook, such as an Effect. Otherwise, there’s no benefit.
A strong answer adds two things. First, measure before you add it. Use console.time, or the profiler in React Developer Tools (Part 36). Measure a production build if you can. Second, the memo case needs every prop to stay the same. In our test, a memo child with a kept array but a new function still rendered on every click.
Can you rely on useMemo to keep a value?
No. React’s docs say it is only there to make code faster. It is not a promise. React may throw the cache away. In development, it does so when you edit the file. In development and production, it does so in one more case. The component waits for something, like data, the first time it is added. Future features may do it too. Your code must still work if the calculation runs again.
A strong answer says what to use instead. If a value must stay, keep it in state or a ref. If an object is only used by an Effect, make it inside the Effect.
Why does my useMemo calculation run twice?
Strict Mode, in development only. React calls the calculation twice to find code that is not pure, and uses only one result. In our test, Strict Mode ran the calculation twice on load and twice for each change of a dependency. With it off, once each. When no dependency changed, it ran zero times in both.
A strong answer shows the kind of bug this finds. A calculation that pushed an item into a prop array showed the item twice with Strict Mode on. The fix is to make a new array instead.
An Effect depends on a function made in the component, and runs after every render. What can you do?
The function is new on every render, so Object.is always says the dependency changed. The best fix is to move the function inside the Effect. Then depend on the plain values it uses, like room. Another is to wrap it in useCallback with the right dependencies. React’s docs prefer the first: “it’s even better to remove the need for a function dependency.”
A strong answer mentions the exhaustive-deps linter rule. For this code, ESLint’s message suggests both fixes. A third fix is useEffectEvent, added in React 19.2 (Part 11). It lets an Effect call the newest version of a function without listing it. It may also mention custom hooks. React’s docs suggest wrapping the functions a hook returns in useCallback. Then callers can use them safely in Effects.
With the React Compiler, do you still need useMemo and useCallback?
Less often. The compiler memoizes components and values for you when you build the app. But it is a separate tool you add to your project. It skips code that it can see breaks the Rules of React. Code that breaks them in ways it can’t see may be compiled anyway, and behave differently. React’s docs also keep the hooks as a way out, for when you need exact control. A common case is a value used as an Effect’s dependency.
A strong answer adds the docs’ advice for existing code. Leave the existing memoization in place, because removing it can change what the compiler outputs.
Sources
- useMemo, react.dev: what it does and returns,
Object.is, “also known as memoization”, Strict Mode calling the calculation twice, the reasons React throws the cache away, “only rely on useMemo as a performance optimization”,console.time, “say, 1ms or more”, production timing, the three cases, “a single value that’s “always new””, ways to need less memoization, “not a semantic guarantee”, “It doesn’t do anything else”, moving objects inside the Effect, the changed-prop example and the missing array. - useCallback, react.dev: caching a function, the “Simplified implementation” with
useMemo, “useCallback does not prevent creating the function”, the two cases for functions, moving the function inside the Effect, and custom hooks wrapping returned functions. - memo, react.dev: a
memochild skips a render when its props are the same. - StrictMode, react.dev: the functions passed to
useMemoare called twice in development. - React Compiler, Debugging and Troubleshooting and React Compiler v1.0, react.dev: a build-time tool, what it memoizes, “safely skips optimization”, the hooks as an escape hatch for Effect dependencies, and leaving existing memoization in place.
- Removing Effect Dependencies and useEffect, react.dev: objects and functions as Effect dependencies.
- React 19 Upgrade Guide, react.dev: in Strict Mode,
useMemoanduseCallback“will reuse the memoized results from the first render during the second render”. - console.time() and Object.is(), MDN.
- The playground’s Console area:
wp/snippets/react-playground.phpcopieslog,info,warnanderror. The Part 10 project’s “TypeScript” choice comes from thecreate-viterun recorded for Part 10. - Every log, count, table row and page text above comes from running React 19.3.0 for this post, in jsdom, with Strict Mode on and off. The type errors come from TypeScript 7.0.2. The linter messages come from ESLint 10.12.0 with
eslint-plugin-react-hooks7.1.1, and from Oxlint 1.87.0 in the Part 10 project. - This part follows the Memoization (When You Need It) kata in react-katas. The kata says React 19 brings the compiler. The compiler is a separate build tool that you add to a project. React 19 doesn’t turn it on for you.