Memoization
Memoization means “remember the last result and reuse it if the inputs did not change”. React has three tools for it — useful in the right places, noise everywhere else.
On this page · 6 min · Intermediate
On this page
- Learning objectives
- Explanation
- Syntax
- Code example
- Step by step
- Real-world use
- Common mistakes
- Best practices
- Remember this
- Practice exercise
- Mini quiz
- Related topics
- My notes
- Reading time
- 6 min
- Difficulty
- Intermediate
- Updated
- Oct 28, 2026
Related tutorials
Learning objectives#
- Skip re-rendering a component with memo
- Cache an expensive calculation with useMemo
- Keep a function identity stable with useCallback
- Know when memoization is not worth it
Explanation#
memo(Component) re-renders the component only when its props change (compared shallowly with Object.is).
useMemo(() => compute(a, b), [a, b]) recalculates only when a or b changes. Use it for genuinely expensive work, like filtering 50,000 rows.
useCallback(fn, deps) returns the same function between renders. It matters when the function is a prop of a memo component or a dependency of an effect.
memo is defeated by new objects and functions created on every render: <Row style={{}} onClick={() => ...} /> always has “new” props. That is why memo, useMemo and useCallback are often used together.
The React Compiler (available for React 19) memoizes automatically at build time. In projects that use it, you rarely write these hooks by hand.
Syntax#
const Row = memo(function Row({ item, onSelect }) { ... });
const visible = useMemo(() => items.filter(matches), [items, query]);
const handleSelect = useCallback(id => setSelected(id), []);
Code example: Filtering a big list without wasted work#
import { memo, useCallback, useMemo, useState } from 'react';
const ITEMS = Array.from({ length: 5000 }, (_, i) => ({ id: i, name: `Product ${i}`, price: (i * 37) % 1000 }));
let rowRenders = 0;
const Row = memo(function Row({ item, selected, onSelect }) {
rowRenders += 1;
return (
<li onClick={() => onSelect(item.id)} style={{ fontWeight: selected ? 'bold' : 'normal' }}>
{item.name} — ₹{item.price}
</li>
);
});
export default function BigList() {
const [query, setQuery] = useState('');
const [selectedId, setSelectedId] = useState(null);
const [darkMode, setDarkMode] = useState(false);
const visible = useMemo( // re-filter only when query changes
() => ITEMS.filter(i => i.name.includes(query)).slice(0, 50),
[query],
);
const handleSelect = useCallback(id => setSelectedId(id), []); // same function every render
return (
<div style={{ background: darkMode ? '#111' : '#fff', color: darkMode ? '#eee' : '#111' }}>
<input value={query} onChange={e => setQuery(e.target.value)} aria-label="Filter" />
<label><input type="checkbox" checked={darkMode} onChange={e => setDarkMode(e.target.checked)} /> Dark</label>
<ul>
{visible.map(item => (
<Row key={item.id} item={item} selected={item.id === selectedId} onSelect={handleSelect} />
))}
</ul>
<small>Row renders so far: {rowRenders}</small>
</div>
);
}
The first render draws 50 rows. Toggling “Dark” re-renders BigList but none of the 50 Row components (their props did not change) and the filter does not re-run. Selecting a row re-renders only the two rows whose selected prop changed. Typing in the filter re-runs the filter once per change.
This example is illustrative. Copy or download it and run it in your own environment; the expected output is shown above.
Step-by-step explanation#
- Row is wrapped in memo, so it skips rendering when its props are unchanged.
- useMemo caches the filtered list; toggling dark mode does not recompute it.
- useCallback keeps handleSelect identical across renders, so memo sees the same onSelect prop.
- Without useCallback, a new arrow function every render would make every Row re-render.
Real-world use case#
A trading screen shows 2,000 instruments updating every second. Memoised rows that re-render only when their own price changes keep the UI smooth.
Common mistakes#
Each memo/useMemo has a cost and makes code harder to read. Use them where the Profiler shows a problem.
style={{}} or onClick={() => …} are new every render, so memo never skips. Stabilise them or restructure.
useMemo(() => filter(items, query), [items]) ignores query changes and shows stale data. List every input.
Best practices#
- Profile first, memoise second
- Combine memo with stable props
- Complete dependency arrays (use the ESLint rule)
- Consider the React Compiler
Remember this#
memo = skip re-render if props are equal.
useMemo = cache a value. useCallback = cache a function.
New object/function each render defeats memo.
Practice exercise#
Your task: A component computes the total of a 10,000-item cart on every render, even when only a "notes" text field changes. Cache the total with useMemo.
Need a hint?
Show solution
import { useMemo, useState } from 'react';
const CART = Array.from({ length: 10000 }, (_, i) => ({ id: i, price: 10, qty: 1 }));
export default function CartTotal() {
const [notes, setNotes] = useState('');
const total = useMemo(() => CART.reduce((sum, i) => sum + i.price * i.qty, 0), []);
return (
<>
<p>Total: ₹{total}</p> {/* ₹100000 */}
<textarea value={notes} onChange={e => setNotes(e.target.value)} aria-label="Notes" />
</>
);
}
Mini quiz#
What does useCallback return?
Want more? Practise all React MCQs · React interview questions
My notes#
Saved in this browser.