0% completed
Memoization Using the useCallback Hook
On This Page
The Problem in One Example
Adding useCallback
useCallback Needs memo
Dependencies and Stale Values
A Function Used by an Effect
A Context Value That Holds a Function
useCallback and useMemo
Two functions with exactly the same code are not equal in JavaScript, as this snippet shows.
const a = () => console.log('Button clicked!'); const b = () => console.log('Button clicked!'); console.log(a === b, Object.is(a, b)); // false false
A component creates new functions in the same way every time it renders. Every arrow function or function declaration inside the component body produces a new function object on each render. For a child wrapped in memo, such a function prop is a changed prop, and the memo lesson's run showed the child rendering on every click because of an inline onWave function. The useCallback hook keeps the same function object between renders until its dependencies change.
The Problem in One Example
ChildComponent is wrapped in memo and receives one prop, a click handler. The parent has two pieces of state that the child does not use:
import { memo, useState } from 'react'; const ChildComponent = memo(function ChildComponent({ onClick }) { console.log('Rendering ChildComponent'); return <button onClick={onClick}>Click Me</button>; }); export default function ParentComponent() { const [count, setCount] = useState(0); const [otherState, setOtherState] = useState(false); const handleClick = () => { console.log('Button clicked!'); }; return ( <div> <h1>Count: {count}</h1> <ChildComponent onClick={handleClick} /> <button onClick={() => setCount(count + 1)}>Increment Count</button> <button onClick={() => setOtherState(!otherState)}>Toggle Other State</button> </div> ); }
We rendered it and clicked "Increment Count", "Toggle Other State" and "Click Me" once each, recording what the console printed after each step:
memo, no useCallback | mount: [ 'Rendering ChildComponent' ] | Increment: [ 'Rendering ChildComponent' ] | Toggle: [ 'Rendering ChildComponent' ] | Click Me: [ 'Button clicked!' ]
The child rendered after both state changes, although nothing it shows had changed. Each render of ParentComponent creates a new handleClick, memo compares the old and the new function with Object.is, finds them different, and renders the child.
Adding useCallback
useCallback takes the function and a dependency list, and returns a function. Replace the handleClick definition:
import { memo, useCallback, useState } from 'react'; // inside ParentComponent const handleClick = useCallback(() => { console.log('Button clicked!'); }, []);
On the first render, React stores the function you passed and returns it. On later renders, React compares the dependencies with their previous values. If they are all equal, it returns the stored function and ignores the new one you just created; if any changed, it stores and returns the new one.
React does not call your function at any point. It only hands it back, and the child calls it when the button is clicked.
The empty list means the function never needs to change, because it reads no props or state. The same three steps now print:
memo + useCallback | mount: [ 'Rendering ChildComponent' ] | Increment: [] | Toggle: [] | Click Me: [ 'Button clicked!' ]
The diagram places two renders side by side. Look at which function object the child receives on the second render: a different object makes the child render again, and the same object lets React skip it.
useCallback Needs memo
useCallback by itself skips nothing. We kept useCallback but removed memo from ChildComponent:
useCallback, no memo | mount: [ 'Rendering ChildComponent' ] | Increment: [ 'Rendering ChildComponent' ] | Toggle: [ 'Rendering ChildComponent' ] | Click Me: [ 'Button clicked!' ]
The child rendered on every click again. A component without memo re-renders whenever its parent does, whatever its props are. A stable function only helps a component, or a hook, that compares it with the previous one.
Dependencies and Stale Values
A memoized function keeps the values it saw when it was created. If it reads state and that state is missing from the dependency list, it keeps using the old value. Here the child's Save button records the current count, but the list is empty:
const handleSave = useCallback(() => { saved.push(count); }, []);
deps [] after 3 increments, Save recorded: [ 0 ]
The count on the screen was 3, but the stored function was created during the first render, when count was 0. A function that keeps reading values from the render where it was created is said to have a stale closure. There are two ways to fix it.
List the dependency. With [count], React stores a new function whenever count changes:
const handleSave = useCallback(() => { saved.push(count); }, [count]);
deps [count]: Save recorded [ 3 ] | child renders after mount+3 increments: 4 | after 2 note keystrokes: 4
The correct value is recorded. The child now renders once for each change of count, because each change creates a new function, but it was still skipped while the user typed into an unrelated note field in the same parent.
Use an updater function when the new state depends on the old. A function that only changes state can avoid reading it at all, so it needs no dependency:
const handleAdd = useCallback(() => { setCount((c) => c + 1); }, []);
updater + []: Count: 3 | AddButton renders: 1
Three clicks raised the count to 3, and the memoized AddButton that receives handleAdd rendered only once, on mount.
A Function Used by an Effect
The other common reason for useCallback is an effect that lists a function as a dependency. A function created during rendering is new every time, so the effect runs after every render. We counted the runs over a mount and three re-renders that did not change query:
const search = useCallback(() => { return fetchResults(query); }, [query]); useEffect(() => { search(); }, [search]);
effect runs over mount + 3 re-renders: plain function 4 | useCallback 1
With useCallback, the effect ran only on mount and would run again only when query changes. When the function is used only by that one effect, a simpler fix is to define it inside the effect, so that the effect lists query directly.
A Context Value That Holds a Function
The theme toggle of chapter 1 gave its provider an object, { theme, toggleTheme }, and the lesson "Context API Use Cases and Best Practices" noted that such an object is new on every render. With memo and the two hooks, you can now measure that cost and remove it.
App below owns the theme and a search query. Page is wrapped in memo, so its parent's renders cannot make it render, and only a change of the context value can make ThemeButton render.
import { createContext, memo, useContext, useState } from 'react'; const ThemeContext = createContext(null); function ThemeButton() { const { theme, toggleTheme } = useContext(ThemeContext); return <button onClick={toggleTheme}>Theme: {theme}</button>; } const Page = memo(function Page() { return <section><ThemeButton /></section>; }); export default function App() { const [theme, setTheme] = useState('light'); const [query, setQuery] = useState(''); const value = { theme, toggleTheme: () => setTheme((t) => (t === 'light' ? 'dark' : 'light')), }; return ( <ThemeContext value={value}> <input aria-label="search" value={query} onChange={(e) => setQuery(e.target.value)} /> <Page /> </ThemeContext> ); }
Typing three letters re-renders App three times. Each render creates a new value object and a new toggleTheme function. A context is not compared prop by prop as memo compares props: React compares the whole old value with the whole new value using Object.is. Two different objects are never equal, so React re-renders ThemeButton three times, although the theme stayed "light". memo on Page stops Page itself from rendering, but it cannot stop ThemeButton: a component that reads a context re-renders whenever the context value changes, as the memo lesson showed with Label.
Wrapping only the object in useMemo is not enough. The object lists toggleTheme as a dependency, and an inline toggleTheme is a new function on every render, so useMemo builds a new object every time. Both pieces need to stay the same: useCallback keeps the function, and useMemo keeps the object until theme or toggleTheme changes. They replace the value lines:
import { useCallback, useMemo } from 'react'; // inside App, in place of the value object above const toggleTheme = useCallback(() => { setTheme((t) => (t === 'light' ? 'dark' : 'light')); }, []); const value = useMemo(() => ({ theme, toggleTheme }), [theme, toggleTheme]);
toggleTheme needs no dependencies because it uses the updater form of setTheme, as handleAdd did above. We typed three letters into each version and then clicked the theme button once:
inline object: after typing 3 letters: ThemeButton 3 | after toggling theme: ThemeButton 1, Theme: dark useMemo only: after typing 3 letters: ThemeButton 3 | after toggling theme: ThemeButton 1, Theme: dark useMemo + useCallback: after typing 3 letters: ThemeButton 0 | after toggling theme: ThemeButton 1, Theme: dark
Compare the two rows of the diagram. Both versions update the button correctly when the theme changes; the memoized version only stops the renders that showed nothing new.
This fix matters only when the provider's component re-renders for reasons other than the context value, as App does here on every keystroke. In a small app where the provider re-renders rarely, the inline object is fine.
useCallback and useMemo
useCallback(fn, deps) gives the same result as useMemo(() => fn, deps). useMemo calls the function you pass and saves its result; useCallback saves the function itself. useCallback exists because saving a function is common enough to deserve a shorter form.
| Tool | You pass | React saves | Typical use |
|---|---|---|---|
memo | A component | Its last output | Skip a child when its props are equal |
useMemo | A calculation | The calculation's result | Skip slow work; keep an object or array stable |
useCallback | A function | The function | Keep a function prop or effect dependency stable |
All three tools need you to decide where memoization helps and to keep each dependency list correct. React Compiler, a build tool, can add the same memoization automatically. The lesson "Automatic Memoization with React Compiler", at the end of this chapter, shows how to turn it on and how it changes the advice of this chapter.
Reading Progress
0%
On This Page
The Problem in One Example
Adding useCallback
useCallback Needs memo
Dependencies and Stale Values
A Function Used by an Effect
A Context Value That Holds a Function
useCallback and useMemo