memo, useMemo and useCallback
Skip React work with memo, useMemo and useCallback: how props are compared, why new objects and functions break memo, and how to measure before you memoize.
- 01Skip a component with memoWrap a component in
memo, and explain how React compares each prop withObject.isbefore it skips the component. - 02Keep a value or a function between rendersCache a slow calculation with
useMemo, keep the same function withuseCallback, and say when each one helps. - 03Know when memoizing pays offSpot the new objects, functions and children that break
memo, and measure a calculation before you decide to cache it.
A list that renders again for nothing
When a component's state changes, React renders it again, and every component inside it too. When React re-renders showed why that's usually fine: a render is a call to your function, and React only changes the page where the result is different. But some renders are slow, and some happen for nothing. Here a shop page has a note box and a list of teas. The result is live: click in the box, type a word, and watch the small counter next to Teas.
import { useState } from "react";import RenderCount from "./RenderCount.jsx"; const teas = ["Green tea", "Black tea", "Mint tea"]; function TeaList({ items }) { return ( <section> <h3>Teas <RenderCount /></h3> <ul> {items.map((tea) => ( <li key={tea}>{tea}</li> ))} </ul> </section> );} export default function App() { const [note, setNote] = useState(""); return ( <> <input value={note} onChange={(event) => setNote(event.target.value)} placeholder="A note for the shop" /> <TeaList items={teas} /> </> );}Line 4 makes the list of teas once, outside any component. TeaList (lines 6 to 17) draws it. Line 9 puts a <RenderCount /> next to the heading: a small helper, in the RenderCount.jsx tab, that shows how many renders of TeaList React has put on the page. It's the counter from When React re-renders, without the blue flash, and you don't need to read it to use it. Line 20 keeps the note in state, and line 25 updates it on every key. Line 28 renders TeaList with the same teas every time.
Type hi. The counter says renders: 3: the first render, plus one for each key. Yet the list never changed. Each key updates App's state, so React renders App, and with it TeaList, which gives back the same three teas. Now tell React to skip TeaList when nothing it gets has changed:
import { memo, useState } from "react";import RenderCount from "./RenderCount.jsx"; const teas = ["Green tea", "Black tea", "Mint tea"]; const TeaList = memo(function TeaList({ items }) { return ( <section> <h3>Teas <RenderCount /></h3> <ul> {items.map((tea) => ( <li key={tea}>{tea}</li> ))} </ul> </section> );}); export default function App() { const [note, setNote] = useState(""); return ( <> <input value={note} onChange={(event) => setNote(event.target.value)} placeholder="A note for the shop" /> <TeaList items={teas} /> </> );}Two lines changed. Line 1 imports memo from React, and line 6 passes TeaList to it: const TeaList = memo(function TeaList(…) {…}), closed with }); on line 17. Type hi again. The counter stays at renders: 1. Before it renders TeaList, React now checks whether its props are the same as last time. items is the same teas array, so React skips TeaList and keeps what it showed before.
App again. Without memo, TeaList renders with it. With memo, React first compares TeaList's props with last time. They're the same, so it skips TeaList and keeps what it showed before.Keeping a result and reusing it while its inputs stay the same is called memoization. The Memoization lesson in the JavaScript course does it for plain functions. React gives you three tools for it, and this lesson covers each: memo for a component, useMemo for a value you work out, and useCallback for a function.
A busy barista keeps the last drink she made on the counter, with its order slip clipped to it. When a request comes in, she glances at the slip. If it's the same slip as before, she hands over the drink on the counter. If it's a new slip, she makes a fresh drink, even when someone has copied out the same order word for word. She never reads the slips: she only checks whether it's the same piece of paper.
- In real life: The drink the barista made last time
- In React: The component's last rendered result
- In real life: The order slip that comes with each request
- In React: The props a component gets on each render
- In real life: She checks whether it's the very same slip, not whether the words match
- In React: React checks each prop with
Object.is, not by its contents - In real life: The same slip again: she hands over the drink she already made
- In React: The same props: React skips the render and keeps the last result
- In real life: A new slip, even with the same order written on it: she makes the drink again
- In React: A new object or function, even if it looks the same: the component renders again
Memoizing means keeping a result and reusing it while its inputs stay the same. memo keeps a component's last render and skips it when every prop is the same. useMemo keeps a calculated value, and useCallback keeps a function, while their dependencies are the same. "The same" means the same value by Object.is: a new object or function never counts, even when it looks alike.
memo skips a component when its props are the same
memo takes your component and gives back a new one that renders the same thing, with one difference: before each render after the first, React compares the new props with the old ones. Here TeaList gets two props, and a button changes one of them:
import { memo, useState } from "react";import RenderCount from "./RenderCount.jsx"; const teas = [ { name: "Green tea", price: 3 }, { name: "Black tea", price: 2 }, { name: "Mint tea", price: 4 },]; const TeaList = memo(function TeaList({ items, showPrices }) { return ( <section> <h3>Teas <RenderCount /></h3> <ul> {items.map((tea) => ( <li key={tea.name}> {tea.name} {showPrices && ` $${tea.price}`} </li> ))} </ul> </section> );}); export default function App() { const [note, setNote] = useState(""); const [showPrices, setShowPrices] = useState(false); return ( <> <input value={note} onChange={(event) => setNote(event.target.value)} placeholder="A note for the shop" /> <button onClick={() => setShowPrices(!showPrices)}>{showPrices ? "Hide prices" : "Show prices"}</button> <TeaList items={teas} showPrices={showPrices} /> </> );}Lines 4 to 8 make the teas once, with their prices. Line 10 wraps TeaList in memo; it takes items and showPrices. Line 18 adds the price to each tea when showPrices is true. In App, line 28 keeps showPrices in state, line 32's button flips it, and line 33 passes both props.
Type a note: the counter stays at renders: 1. Press Show prices: the prices appear and the counter says renders: 2. Type again: it stays at 2. Each time App renders, React takes the props that line 33 gives TeaList and checks them one at a time against last time's:
items: the sameteasarray from line 4, every time.showPrices:falseandfalsewhile you type;falseandtrueafter the button.
If every prop is the same, React skips TeaList. If even one differs, it renders TeaList with the new props. The check it uses for "the same" is Object.is, which works like === (the Equality lesson covers the two small cases where they differ). Run this to see what it says about the kinds of values props hold:
const teas = ["Green tea", "Black tea"];
console.log(Object.is(teas, teas));
console.log(Object.is(["Green tea", "Black tea"], ["Green tea", "Black tea"]));
console.log(Object.is(3, 3), Object.is("Tea", "Tea"));
console.log(Object.is(() => {}, () => {}));
console.log(Object.is({ inStock: true }, { inStock: true }));Line 3 prints true: teas is compared with itself. Line 4 prints false: two arrays with the same teas are still two arrays. Line 5 prints true true: numbers and strings are compared by their value. Lines 6 and 7 print false: two functions, or two objects, are different values even when they're written the same way. Objects, arrays and functions are compared by which one they are, not by what's inside (the Objects and references lesson explains why).
memo takes each prop in turn and asks Object.is(old, new). Typing a note changes neither prop, so React skips TeaList. Show prices changes showPrices, so TeaList renders.This kind of check is called shallow: React compares each prop as a whole and never looks inside it. So if code changes an array in place, say with teas.push(…), memo sees the same array and skips the render, and the list on screen falls behind. That's one more reason state updates make new arrays and objects, as Updating arrays in state showed. (Without memo, React only checks whether the whole props object is the same object, which it never is after the parent renders. The Reconciliation and the diffing algorithm lesson, in the Under the hood stage, shows that check in React's code.)
Three more things to know about memo:
- It only looks at props. A memo'd component still renders when its own state changes, or when a context it reads with
useContextchanges. - It's a speed-up, not a promise. React's docs say React may still render a memo'd component now and then, so your page must show the right thing either way.
memo(TeaList, arePropsEqual)takes your own comparison as a second argument, a function that gets the old and new props and returnstrueto skip. You rarely need it, and it must compare every prop, functions included, or the component can show old data. A deep comparison that walks through big objects can be slower than the render it skips.
useMemo keeps a slow calculation's result
memo skips a whole component. Sometimes the slow part is one calculation inside a component that has to render anyway. Here a tea search filters its list with filterTeas, in the teas.js tab. It's slow on purpose: lines 13 to 16 of that tab wait 20 ms before it filters, and line 18 logs how long it took. Open the console under the result.
import { useState } from "react";import { filterTeas, teas } from "./teas.js"; export default function App() { const [query, setQuery] = useState(""); const [large, setLarge] = useState(false); const visible = filterTeas(teas, query); return ( <div style={{ fontSize: large ? 20 : 15 }}> <input value={query} onChange={(event) => setQuery(event.target.value)} placeholder="Find a tea" /> <button onClick={() => setLarge(!large)}>Larger text</button> <ul> {visible.map((tea) => ( <li key={tea}>{tea}</li> ))} </ul> </div> );}Line 7 calls filterTeas during every render. Line 6 keeps a second piece of state, large, which line 10 uses for the text size and line 12's button flips. It has nothing to do with the teas.
The console already shows filterTeas("") took 20 ms, with a small 2 beside it: in development, StrictMode renders every component twice to help you find mistakes, so each render logs twice, and the console counts the same line instead of repeating it. Press Larger text: the 2 becomes a 4. That's two more runs, 40 ms of filtering (20 ms in a production build, without StrictMode) to make the text bigger, for a list that didn't change. That's your "before" measurement. Now keep the result with useMemo:
import { useMemo, useState } from "react";import { filterTeas, teas } from "./teas.js"; export default function App() { const [query, setQuery] = useState(""); const [large, setLarge] = useState(false); const visible = useMemo(() => filterTeas(teas, query), [query]); return ( <div style={{ fontSize: large ? 20 : 15 }}> <input value={query} onChange={(event) => setQuery(event.target.value)} placeholder="Find a tea" /> <button onClick={() => setLarge(!large)}>Larger text</button> <ul> {visible.map((tea) => ( <li key={tea}>{tea}</li> ))} </ul> </div> );}Line 1 imports useMemo, and line 7 wraps the call: useMemo(() => filterTeas(teas, query), [query]). It takes two things:
- A function with no arguments that works out the value:
() => filterTeas(teas, query). - A dependency array,
[query]: every value from the component that the calculation reads and that can change between renders.
On the first render, React calls the function and keeps two things: the result, and the dependencies it was worked out from. On each later render, it compares every dependency with last time, using Object.is again. All the same: it hands back the kept result without calling anything. One changed: it calls the function, and keeps the new result instead.
Press Larger text: the count stays at 2, because filterTeas didn't run. That's your "after": 0 ms of filtering per click. Type g: filterTeas("g") took 20 ms appears, with a 2. query changed, so React works the list out again (and in development StrictMode calls your function twice, throwing one result away, to catch impure code). Typing costs exactly what it did before. useMemo doesn't make a calculation faster; it skips the repeats.
useMemo keeps the last result and the query it was worked out from. When the query is the same as last time, it hands back the kept list; when it changed, it calls filterTeas again and keeps the new list.You met useMemo once already, in You might not need an effect, as the right way to cache a filter instead of an effect. A few more rules:
teas isn't in the array because it's made once, outside the component, and never changes. If the teas came in as a prop, they could change between renders, so they'd be a dependency too:
function TeaSearch({ teas, query }) {
const visible = useMemo(() => filterTeas(teas, query), [teas, query]);
// …
}This is part of a component, not a whole example, so it doesn't run here. Line 2 lists both values the calculation reads.
- The function runs during the render, so it must be pure: work out a value and change nothing outside it (see Keeping components pure). The lint rule
react-hooks/exhaustive-depsfrom Rules of Hooks checks the array for you. - The kept result is a speed-up, not storage. React can throw it away, for example while you edit a file in development, so the page must be right even when the function runs again.
useCallback keeps the same function
Props can be functions too, like a handler that adds a tea to the cart. Here TeaList is wrapped in memo and gets one. Type a note and watch its counter:
import { memo, useState } from "react";import RenderCount from "./RenderCount.jsx"; const teas = ["Green tea", "Black tea", "Mint tea"]; const TeaList = memo(function TeaList({ items, onAdd }) { return ( <section> <h3>Teas <RenderCount /></h3> {items.map((tea) => ( <button key={tea} onClick={() => onAdd(tea)}> Add {tea} </button> ))} </section> );}); export default function App() { const [note, setNote] = useState(""); const [cart, setCart] = useState([]); function addTea(tea) { setCart([...cart, tea]); } return ( <> <input value={note} onChange={(event) => setNote(event.target.value)} placeholder="A note for the shop" /> <p>Items in cart: {cart.length}</p> <TeaList items={teas} onAdd={addTea} /> </> );}Line 6 wraps TeaList in memo, and line 11 calls onAdd(tea) when you press a button. In App, lines 23 to 25 declare addTea, and line 31 passes it. Type a word: the counter goes up with every key, even though memo is there. A function declared inside a component is made again on every render, so each render of App passes a new addTea. Functions are objects (see Function objects), and Object.is never calls two of them the same.
import { memo, useCallback, useState } from "react";import RenderCount from "./RenderCount.jsx"; const teas = ["Green tea", "Black tea", "Mint tea"]; const TeaList = memo(function TeaList({ items, onAdd }) { return ( <section> <h3>Teas <RenderCount /></h3> {items.map((tea) => ( <button key={tea} onClick={() => onAdd(tea)}> Add {tea} </button> ))} </section> );}); export default function App() { const [note, setNote] = useState(""); const [cart, setCart] = useState([]); const addTea = useCallback((tea) => { setCart([...cart, tea]); }, [cart]); return ( <> <input value={note} onChange={(event) => setNote(event.target.value)} placeholder="A note for the shop" /> <p>Items in cart: {cart.length}</p> <TeaList items={teas} onAdd={addTea} /> </> );}Line 1 imports useCallback. Lines 23 to 25 pass the function to it, with a dependency array, [cart], on line 25. On the first render React keeps the function. On later renders, while cart is the same, it hands back that same function instead of the new one. Type a word: the counter stays at renders: 1. Press Add Green tea twice: it says renders: 3 and the cart has 2 items. Adding changes cart, so addTea must be a new function that sees the new cart, and TeaList renders with it. That's right, not a bug.
Press Edit & run and change line 24 to setCart((items) => [...items, tea]); and line 25 to }, []);. With an updater function (from Queueing state updates), addTea no longer reads cart at all, so it has no dependencies and stays the same forever. Now adding teas leaves the counter at renders: 1 too.
useCallback is useMemo for a function. These two lines do the same thing:
const addTea = useCallback((tea) => {
setCart([...cart, tea]);
}, [cart]);
// does the same as:
const addTea = useMemo(() => (tea) => {
setCart([...cart, tea]);
}, [cart]);Lines 1 to 3 keep the function itself. Lines 6 to 8 keep the result of a function that returns it, which is the same thing written the long way. useCallback never calls your function; it only keeps it. So it helps in exactly two places, where code compares functions:
- A prop of a component wrapped in
memo, as here. - A dependency of another Hook, like an effect that should only run again when the function really changes:
const loadTeas = useCallback(() => {
return fetch(`/api/teas?q=${query}`);
}, [query]);
useEffect(() => {
loadTeas();
}, [loadTeas]);Line 3 makes loadTeas change only when query does, and line 7 lists it as the effect's dependency, so the effect runs again only for a new query. Often a cleaner fix is to move the function inside the effect, as Removing effect dependencies showed. Anywhere else, like onClick on a plain <button>, nothing compares the function, and useCallback only adds work. How to read the table: each row is one tool, what React keeps for it, and when React hands that back instead of doing the work again.
| What you write | What React keeps | When React reuses it |
|---|---|---|
memo(TeaList) | The component's last rendered result. | When every prop is the same as last time, by Object.is. |
useMemo(() => filterTeas(teas, query), [query]) | The value the function returned last time. | When every dependency in the array is the same as last time. |
useCallback((tea) => …, [cart]) | The function itself, made in an earlier render. | When every dependency in the array is the same as last time. |
Why memo often doesn't help
memo is easy to add and easy to break without noticing. Here are four memo'd components side by side, each with a counter. Type a word in the box:
import { memo, useState } from "react";import RenderCount from "./RenderCount.jsx"; const Title = memo(function Title({ text }) { return <h3>{text} <RenderCount /></h3>;}); const Filters = memo(function Filters({ options }) { return <p>In stock only: {options.inStock ? "yes" : "no"} <RenderCount /></p>;}); const AddButton = memo(function AddButton({ onAdd }) { return <button onClick={onAdd}>Add tea <RenderCount /></button>;}); const Panel = memo(function Panel({ children }) { return <div>{children} <RenderCount /></div>;}); export default function App() { const [note, setNote] = useState(""); const [cart, setCart] = useState(0); return ( <> <input value={note} onChange={(event) => setNote(event.target.value)} placeholder="A note for the shop" /> <Title text="Teas" /> <Filters options={{ inStock: true }} /> <AddButton onAdd={() => setCart(cart + 1)} /> <Panel> <p>Fresh today</p> </Panel> <p>Cart: {cart}</p> </> );}Lines 4 to 18 wrap four small components in memo. Only Title stays at renders: 1. The other three climb with every key, and each for the same reason: App makes one of their props again on every render.
- Line 26 passes
text="Teas", a string. Strings are compared by value, so it's always the same. - Line 27 passes
options={{ inStock: true }}. The outer braces put a JavaScript value into JSX; the inner ones make a new object, on every render. - Line 28 passes
onAdd={() => setCart(cart + 1)}: a new arrow function on every render. - Lines 29 to 31 put
<p>Fresh today</p>betweenPanel's tags. JSX is a call that makes an object (see Writing JSX), and that object reachesPanelas itschildrenprop, new on every render.
App runs { inStock: true } and () => setCart(cart + 1) again, so it makes a new object and a new function. They look like last time's, but they're different values, and Object.is asks whether two values are the same one, not whether they look alike.The fix is to pass the same value each time. Press Edit & run, add const inStockOnly = { inStock: true }; above App (line 20), and change the Filters line (line 27) to pass options={inStockOnly}: the object is now made once, and Filters stays at renders: 1. An object that depends on state gets useMemo instead, a function gets useCallback, and JSX children can be kept with useMemo or moved into the memo'd component itself (Exercise 6 does this).
In the playground, App passes a memo'd TeaList its props in one of five ways. Pick one, press Render App again a few times, and read what memo compared, prop by prop.
import { memo, useState } from "react";import RenderCount from "./RenderCount.jsx"; const TeaList = memo(function TeaList(props) { return <p>TeaList <RenderCount /></p>;}); export default function App() { const [clicks, setClicks] = useState(0); const [picked, setPicked] = useState(null); return ( <> <button onClick={() => setClicks(clicks + 1)}>Render App again</button> <TeaList title="Teas" count={3} /> </> );}App 1 · TeaList 0- App has rendered
- 1
- TeaList has rendered
- 0
No comparison yet. memo compares props from the second render of App on.
Press Render App again. App renders, and memo compares TeaList's new props with its old ones.
Only plain values, and values kept by useMemo and useCallback, let memo skip. One prop that is new on every render is enough to make memo do its comparison and then render anyway, which is a little slower than no memo at all.
When to bother: measure first
Memoizing isn't free. Each tool costs something on every render, skipped or not:
- React compares each prop or dependency with last time.
- React keeps the old props, values and functions in memory.
- Your code gets longer, and every dependency array must stay right. A wrong one is a bug: Exercise 5 shows a cart that stops counting.
So before you memoize a calculation, measure it. Note the time before and after with performance.now(), as this example does, and look at the console:
import { useState } from "react"; const products = Array.from({ length: 1000 }, (_, index) => ({ name: `Tea ${index + 1}`, price: (index % 9) + 1,})); export default function App() { const [large, setLarge] = useState(false); const start = performance.now(); const cheap = products.filter((product) => product.price <= 3); const time = performance.now() - start; console.log(`Filtering ${products.length} products took ${time.toFixed(2)} ms`); return ( <div style={{ fontSize: large ? 20 : 15 }}> <button onClick={() => setLarge(!large)}>Larger text</button> <p>{cheap.length} teas cost $3 or less.</p> </div> );}Lines 3 to 6 make 1,000 products. Line 11 notes the time, line 12 filters the cheap ones, and line 13 works out how long it took, in milliseconds. Line 14 logs it. On most computers it says well under 1 ms. Press Larger text a few times to measure again. React's docs suggest caching only work that adds up to about 1 ms or more; this filter isn't worth a useMemo. (In your own browser's console, console.time and console.timeEnd measure the same way.)
Measure the way your users will see it:
- A development build is slower than a production build, with StrictMode's extra renders and extra checks, so check a production build before you decide.
- Your laptop is faster than many phones. The Performance panel in Chrome's developer tools can slow its CPU down on purpose ("CPU throttling") to show what a slower device feels.
- To find which components render and how long they take, use the Profiler in React Developer Tools, which Profiling React apps covers.
How to read the table: each row is something you notice, and whether memoizing is worth it then.
| What you notice | Is memoizing worth it? |
|---|---|
| A calculation you measured takes 1 ms or more. | Yes. useMemo saves that time on every render that doesn't change its inputs. |
| A slow component renders often with the same props. | Yes. memo lets React skip it and keep what it showed. |
| The component gets a new object, function or JSX children on every render. | Not yet. memo can't skip it until those props stay the same. |
| The work is a few items or a quick sum, under a millisecond. | No. The time saved is too small to notice, while the dependency check, the kept value, and the longer code cost something on every render. |
| The component is quick, or rarely renders again. | No. Nothing slow is saved, and the code gets harder to read. |
Often a change in structure beats memoizing. If the note box lived in its own component with its own state, typing would render only that component, and TeaList would never render at all, with no memo needed. That's "moving state down", from When React re-renders, and Performance patterns adds more fixes like it.
The React Compiler, a build tool from the React team whose stable 1.0 release came out in October 2025, adds this kind of memoization to your components when you build your app. It keeps values, functions and JSX the same between renders where it can, so you write plain code and rarely need memo, useMemo or useCallback yourself. The next lesson, The React Compiler, shows what it does and how to turn it on. Knowing these three tools still matters: they explain what the compiler does, and they're the escape hatch when you need exact control.
A shop page, memoized where it counts
Here the three tools work together on a page with a search box, a note box and a cart. The product grid is the slow part: each card in the ProductGrid.jsx tab waits 2 ms on purpose (lines 5 to 8 of that tab), and the grid is wrapped in memo on line 17 of that tab.
import { useCallback, useMemo, useState } from "react";import ProductGrid from "./ProductGrid.jsx"; const products = [ { name: "Green tea", price: 3 }, { name: "Black tea", price: 2 }, { name: "Mint tea", price: 4 }, { name: "Lemon tea", price: 3 }, { name: "Jasmine tea", price: 5 },]; export default function App() { const [query, setQuery] = useState(""); const [note, setNote] = useState(""); const [cart, setCart] = useState([]); const visible = useMemo( () => products.filter((product) => product.name.toLowerCase().includes(query.toLowerCase())), [query], ); const addToCart = useCallback((product) => { setCart((items) => [...items, product]); }, []); return ( <> <input value={query} onChange={(event) => setQuery(event.target.value)} placeholder="Find a tea" /> <input value={note} onChange={(event) => setNote(event.target.value)} placeholder="A note for the shop" /> <p>Items in cart: {cart.length}</p> <ProductGrid products={visible} onAdd={addToCart} /> </> );}Lines 17 to 20 keep the filtered products with useMemo, worked out again only when query changes. Lines 22 to 24 keep addToCart with useCallback. Its updater function on line 23 doesn't read cart, so its dependency array is empty and it never changes. Line 31 passes both to the grid.
Type in the note box: the Products counter stays at renders: 1. Press an Add button: the cart count goes up, and the counter still says renders: 1, because neither visible nor addToCart changed. Type le in the search box: the grid shows Lemon tea and the counter goes up, because the list really changed. Sort each card below: when App renders again, does memo skip TeaList? Assume teas is an array made once outside App, pick and sorted come from useCallback and useMemo with no dependencies, choose is a function declared inside App, and clicks is the state that just changed.
<TeaList items={teas} /><TeaList items={[...teas]} /><TeaList onPick={pick} /><TeaList onPick={(tea) => pick(tea)} /><TeaList onPick={choose} /><TeaList title="Teas" count={3} /><TeaList style={{ gap: 8 }} /><TeaList items={sorted} /><TeaList count={clicks} /><TeaList>Fresh today</TeaList><TeaList><b>Fresh</b></TeaList>
App just rendered again. Sort each way of rendering the memo'd TeaList: does memo skip it, or does it render again?
Common mix-ups
- "
memomakes a component faster." No. It skips a render when every prop is the same. When the component does render, it takes just as long, plus the time to compare. - "
memocompares objects by what's inside them." No. It compares each prop withObject.is. Two objects with the same contents are still two objects. - "
useCallbackmakes a function run faster." No. It keeps the same function between renders. That only matters to a memo'd component or a dependency array that compares it. - "
useMemomakes the first render faster." No. The first render always does the work.useMemoonly skips it on later renders whose dependencies didn't change. - "Wrap everything in
memo, just in case." No. Every memo compares props on every render, and it skips nothing while a prop is new each time. Memoize what you measured as slow. - "React notices a value I left out of the dependency array." No. It hands back the old result or the old function, which reads old values: the cart that stops at 1. The
exhaustive-depslint rule catches this. - "A memo'd component never renders again." No. It renders when a prop changes, when its own state changes, and when a context it reads changes.
- "My page needs
useMemoto work." No. Memoizing is only for speed, and React may throw a kept value away. Your page must show the right thing without it.
Practice exercises
The exercises use the same RenderCount helper and the same slow filterTeas, in their tabs. Change the code with Edit & run; the result updates as you type.
You click the button three times. What does TeaList's counter say? Answer, then press Edit & run and click to check.
import { memo, useState } from "react";
import RenderCount from "./RenderCount.jsx";
const teas = ["Green tea", "Black tea"];
const TeaList = memo(function TeaList({ items, title }) {
return <p>{title}: {items.length} <RenderCount /></p>;
});
export default function App() {
const [count, setCount] = useState(0);
return (
<>
<button onClick={() => setCount(count + 1)}>Clicked {count} times</button>
<TeaList items={teas} title="Teas" />
</>
);
}import { memo, useState } from "react";import RenderCount from "./RenderCount.jsx"; const teas = ["Green tea", "Black tea"]; const TeaList = memo(function TeaList({ items, title }) { return <p>{title}: {items.length} <RenderCount /></p>;}); export default function App() { const [count, setCount] = useState(0); return ( <> <button onClick={() => setCount(count + 1)}>Clicked {count} times</button> <TeaList items={teas} title="Teas" /> </> );}Each click renders App, but items is the same teas array and title is the same string, so memo skips TeaList every time. The counter stays at renders: 1.
The same page, with one more prop. You click the button three times. What does the counter say now?
import { memo, useState } from "react";
import RenderCount from "./RenderCount.jsx";
const teas = ["Green tea", "Black tea"];
const TeaList = memo(function TeaList({ items, style }) {
return <p style={style}>Teas: {items.length} <RenderCount /></p>;
});
export default function App() {
const [count, setCount] = useState(0);
return (
<>
<button onClick={() => setCount(count + 1)}>Clicked {count} times</button>
<TeaList items={teas} style={{ fontWeight: "bold" }} />
</>
);
}import { memo, useState } from "react";import RenderCount from "./RenderCount.jsx"; const teas = ["Green tea", "Black tea"]; const TeaList = memo(function TeaList({ items, style }) { return <p style={style}>Teas: {items.length} <RenderCount /></p>;}); export default function App() { const [count, setCount] = useState(0); return ( <> <button onClick={() => setCount(count + 1)}>Clicked {count} times</button> <TeaList items={teas} style={{ fontWeight: "bold" }} /> </> );}style={{ fontWeight: "bold" }} makes a new object on every render of App. memo finds style different each time and renders TeaList: 1 first render plus 3 clicks makes renders: 4.
Each tea in the TeaList.jsx tab waits 30 ms on purpose, so typing in the note box feels sticky. Make React skip the list while you type. What does the counter say after two letters?
import { useState } from "react";import TeaList from "./TeaList.jsx"; const teas = ["Green tea", "Black tea", "Mint tea"]; export default function App() { const [note, setNote] = useState(""); return ( <> <input value={note} onChange={(event) => setNote(event.target.value)} placeholder="A note for the shop" /> <TeaList items={teas} /> </> );}import { useState } from "react";import TeaList from "./TeaList.jsx"; const teas = ["Green tea", "Black tea", "Mint tea"]; export default function App() { const [note, setNote] = useState(""); return ( <> <input value={note} onChange={(event) => setNote(event.target.value)} placeholder="A note for the shop" /> <TeaList items={teas} /> </> );}App.jsx stays the same. In the TeaList.jsx tab, line 1 imports memo and line 12 exports memo(function TeaList({ items }) { … }), closed with });. items is the same teas array on every key, so React skips the slow list, typing feels instant, and the counter stays at renders: 1.
Press Dark mode and read the console: the slow filter runs for a list that didn't change. Keep its result with useMemo. After your change, how many more times does filterTeas run when you press Dark mode?
import { useState } from "react";import { filterTeas, teas } from "./teas.js"; export default function App() { const [query, setQuery] = useState(""); const [dark, setDark] = useState(false); const visible = filterTeas(teas, query); return ( <div style={{ background: dark ? "#222" : "#fff", color: dark ? "#eee" : "#222", padding: 8 }}> <input value={query} onChange={(event) => setQuery(event.target.value)} placeholder="Find a tea" /> <button onClick={() => setDark(!dark)}>Dark mode</button> <ul> {visible.map((tea) => ( <li key={tea}>{tea}</li> ))} </ul> </div> );}import { useMemo, useState } from "react";import { filterTeas, teas } from "./teas.js"; export default function App() { const [query, setQuery] = useState(""); const [dark, setDark] = useState(false); const visible = useMemo(() => filterTeas(teas, query), [query]); return ( <div style={{ background: dark ? "#222" : "#fff", color: dark ? "#eee" : "#222", padding: 8 }}> <input value={query} onChange={(event) => setQuery(event.target.value)} placeholder="Find a tea" /> <button onClick={() => setDark(!dark)}>Dark mode</button> <ul> {visible.map((tea) => ( <li key={tea}>{tea}</li> ))} </ul> </div> );}Before, every Dark mode click ran filterTeas twice more: the count beside filterTeas("") took 20 ms went up by 2. With useMemo and [query], the theme doesn't change query, so React hands back the kept list and filterTeas doesn't run. Typing still filters, for each new query.
Someone added useCallback so the memo'd list skips renders. Now the cart never goes past one item. Find why and fix it without making addTea change on every add. What does the page say after three clicks on Add Green tea?
import { memo, useCallback, useState } from "react"; const teas = ["Green tea", "Black tea"]; const TeaList = memo(function TeaList({ items, onAdd }) { return items.map((tea) => ( <button key={tea} onClick={() => onAdd(tea)}> Add {tea} </button> ));}); export default function App() { const [cart, setCart] = useState([]); const addTea = useCallback((tea) => { setCart([...cart, tea]); }, []); return ( <> <p>Items in cart: {cart.length}</p> <TeaList items={teas} onAdd={addTea} /> </> );}import { memo, useCallback, useState } from "react"; const teas = ["Green tea", "Black tea"]; const TeaList = memo(function TeaList({ items, onAdd }) { return items.map((tea) => ( <button key={tea} onClick={() => onAdd(tea)}> Add {tea} </button> ));}); export default function App() { const [cart, setCart] = useState([]); const addTea = useCallback((tea) => { setCart((items) => [...items, tea]); }, []); return ( <> <p>Items in cart: {cart.length}</p> <TeaList items={teas} onAdd={addTea} /> </> );}With [], useCallback keeps the function from the first render, and in it cart is always [], so every click sets the cart to one tea. The updater setCart((items) => [...items, tea]) gets the newest cart from React, so the function can stay the same and still count: three clicks make 3.
Panel is wrapped in memo, but its counter climbs with every key. Fix it so typing leaves Panel alone. What does its counter say after you type?
import { memo, useState } from "react";import RenderCount from "./RenderCount.jsx"; const teas = ["Green tea", "Black tea", "Mint tea"]; const Panel = memo(function Panel({ title, children }) { return ( <section> <h3>{title} <RenderCount /></h3> {children} </section> );}); export default function App() { const [note, setNote] = useState(""); return ( <> <input value={note} onChange={(event) => setNote(event.target.value)} placeholder="A note for the shop" /> <Panel title="Teas"> <ul> {teas.map((tea) => ( <li key={tea}>{tea}</li> ))} </ul> </Panel> </> );}import { memo, useMemo, useState } from "react";import RenderCount from "./RenderCount.jsx"; const teas = ["Green tea", "Black tea", "Mint tea"]; const Panel = memo(function Panel({ title, children }) { return ( <section> <h3>{title} <RenderCount /></h3> {children} </section> );}); export default function App() { const [note, setNote] = useState(""); const list = useMemo( () => ( <ul> {teas.map((tea) => ( <li key={tea}>{tea}</li> ))} </ul> ), [], ); return ( <> <input value={note} onChange={(event) => setNote(event.target.value)} placeholder="A note for the shop" /> <Panel title="Teas">{list}</Panel> </> );}The <ul> between Panel's tags is a new element object on every render, so children always changes. Lines 17 to 26 keep it with useMemo (it reads only teas, made outside App, so there are no dependencies), and line 30 passes the kept list. Panel stays at renders: 1.
Check your understanding
For each question, ask what is being compared, and whether it's the same value as last time.
Question 1 of 7After the first render, the button is clicked twice. How many more times does
TeaListrender?Read the code, then predictJSXconst teas = ["Green tea", "Black tea"]; const TeaList = memo(function TeaList({ items }) { console.log("TeaList renders"); return <p>{items.length} teas</p>; }); export default function App() { const [count, setCount] = useState(0); return ( <> <button onClick={() => setCount(count + 1)}>{count}</button> <TeaList items={teas} /> </> ); }Choose an answer to see the explanation.
Question 2 of 7What does
memocompare to decide whether to skip a component?Choose an answer to see the explanation.
Question 3 of 7The Add tea button is clicked twice. Does
memoskipAddButton?Read the code, then predictJSXconst AddButton = memo(function AddButton({ onAdd }) { console.log("AddButton renders"); return <button onClick={onAdd}>Add tea</button>; }); export default function App() { const [cart, setCart] = useState(0); return ( <> <p>Cart: {cart}</p> <AddButton onAdd={() => setCart(cart + 1)} /> </> ); }Choose an answer to see the explanation.
Question 4 of 7What does
useMemodo on a component's first render?Choose an answer to see the explanation.
Question 5 of 7The button is clicked three times. What does it say?
Read the code, then predictJSXexport default function App() { const [cart, setCart] = useState(0); const addTea = useCallback(() => { setCart(cart + 1); }, []); return <button onClick={addTea}>Cart: {cart}</button>; }Choose an answer to see the explanation.
Question 6 of 7When does
useCallbackhelp?Choose an answer to see the explanation.
Question 7 of 7You measure a filter: it takes 0.02 ms per render. What should you do?
Choose an answer to see the explanation.
Key takeaways
- A parent's render renders its children.
memo(Component)skips a child when every prop is the same as last time. - "The same" means
Object.is, one prop at a time. Strings and numbers compare by value; objects, arrays, functions and JSX compare by which one they are. useMemo(() => work, deps)keeps a calculated value until a dependency changes. It skips repeats; it doesn't speed up the first run.useCallback(fn, deps)keeps a function. It only helps a memo'd component or a dependency array.- A new object, function or JSX child made during each render breaks
memo. - Measure first: memoize work that takes about 1 ms or more, or a slow component that renders often with the same props.
- The React Compiler adds this memoization for you at build time.
Remember the one-liner.
React reuses what it kept only while every input is the very same value, so keep the inputs the same, and only bother where you measured something slow.
Coming next: The React Compiler. It adds memo, useMemo and useCallback's work to your components at build time, so you can write plain code.