How does useMemo work in React?
The Short Answer
useMemo caches the result of an expensive computation between renders. It only recalculates when its dependencies change. This prevents redundant work — if the inputs haven't changed, React returns the previously computed value instead of running the calculation again. It's the value equivalent of useCallback (which memoizes functions).
The Problem It Solves
Every time a component re-renders, all code inside the function body runs again — including expensive computations. If you're filtering a 10,000-item list or doing complex math on every render, that work repeats even when the inputs haven't changed. useMemo lets you skip the recalculation when the result would be the same.
In the example below, filteredProducts is recalculated on every keystroke in the search input — even when the products array and filter criteria haven't changed. If the product list is large, this causes noticeable lag.
function ProductPage() {
const [searchQuery, setSearchQuery] = useState('');
const [sortBy, setSortBy] = useState('name');
const [products] = useState(generateLargeProductList()); // 10,000 items
// ❌ Runs on EVERY render — even when only searchQuery changed
// Sorting 10,000 items is expensive and unnecessary if products/sortBy didn't change
const sortedProducts = [...products].sort((a, b) => {
if (sortBy === 'name') return a.name.localeCompare(b.name);
return a.price - b.price;
});
// This filter is cheap, but it depends on the expensive sort above
const filteredProducts = sortedProducts.filter(p =>
p.name.toLowerCase().includes(searchQuery.toLowerCase())
);
return (
<div>
<input value={searchQuery} onChange={(e) => setSearchQuery(e.target.value)} />
<select value={sortBy} onChange={(e) => setSortBy(e.target.value)}>
<option value="name">Name</option>
<option value="price">Price</option>
</select>
<ProductList products={filteredProducts} />
</div>
);
}
The Solution: useMemo
By wrapping the expensive sort in useMemo, it only recalculates when products or sortBy actually change. Typing in the search input no longer triggers the sort — React returns the cached sorted array and only the cheap filter runs.
function ProductPage() {
const [searchQuery, setSearchQuery] = useState('');
const [sortBy, setSortBy] = useState('name');
const [products] = useState(generateLargeProductList());
// ✅ Only recalculates when products or sortBy changes
const sortedProducts = useMemo(() => {
console.log('Sorting...'); // Only logs when products/sortBy change
return [...products].sort((a, b) => {
if (sortBy === 'name') return a.name.localeCompare(b.name);
return a.price - b.price;
});
}, [products, sortBy]);
// Filter is cheap — no need to memoize (but you could if needed)
const filteredProducts = sortedProducts.filter(p =>
p.name.toLowerCase().includes(searchQuery.toLowerCase())
);
return (
<div>
<input value={searchQuery} onChange={(e) => setSearchQuery(e.target.value)} />
<select value={sortBy} onChange={(e) => setSortBy(e.target.value)}>
<option value="name">Name</option>
<option value="price">Price</option>
</select>
<ProductList products={filteredProducts} />
</div>
);
}
Now typing in the search input is instant — the expensive sort is skipped because its dependencies (products, sortBy) haven't changed. The sort only re-runs when the user changes the sort dropdown.
The Second Use Case: Stable References
useMemo also prevents creating new object/array references on every render. This matters when you pass computed objects as props to memoized children or as dependencies to other hooks. Without useMemo, a new reference is created every render — defeating React.memo or causing useEffect to re-run unnecessarily.
function Dashboard({ userId }: { userId: string }) {
const [refreshCount, setRefreshCount] = useState(0);
// ❌ New object every render — if passed to a memo'd child, it re-renders
const config = { userId, theme: 'dark', locale: 'en' };
// ✅ Same reference unless userId changes
const config = useMemo(
() => ({ userId, theme: 'dark', locale: 'en' }),
[userId]
);
// ✅ Stable array reference for useEffect dependency
const endpoints = useMemo(
() => [`/api/users/${userId}`, `/api/users/${userId}/stats`],
[userId]
);
useEffect(() => {
// Without useMemo on endpoints, this runs every render
endpoints.forEach(url => prefetch(url));
}, [endpoints]); // Stable reference — only re-runs when userId changes
return <SettingsPanel config={config} />;
}
useMemo vs useCallback
useMemo and useCallback are closely related — they both memoize something between renders. The difference is what they memoize: useMemo memoizes a computed value (the result of calling the function), while useCallback memoizes the function itself (without calling it).
// useMemo — caches the RESULT of the function
const sortedList = useMemo(() => {
return items.sort((a, b) => a.name.localeCompare(b.name));
}, [items]);
// sortedList is the sorted array
// useCallback — caches the FUNCTION itself
const handleSort = useCallback(() => {
return items.sort((a, b) => a.name.localeCompare(b.name));
}, [items]);
// handleSort is a function you can call later
// They're equivalent:
useCallback(fn, deps) === useMemo(() => fn, deps)
| Aspect | useMemo | useCallback |
|---|---|---|
| Memoizes | A computed value | A function reference |
| Returns | The result of calling the factory | The function itself |
| Use for | Expensive calculations, stable object references | Stable callback props for memo'd children |
| Equivalent | useMemo(() => value, deps) | useMemo(() => fn, deps) |
When NOT to Use useMemo
Memoization has overhead — React must store the previous value and compare dependencies on every render. For cheap computations, this overhead can exceed the cost of just recalculating. Don't memoize everything by default.
Skip useMemo when
- ❌The computation is trivial — simple math, string concatenation, or filtering a small array
- ❌The value isn't passed to a memoized child or used as a hook dependency
- ❌Dependencies change on every render — useMemo recalculates anyway, adding overhead for nothing
- ❌You haven't measured a performance problem — premature optimization adds complexity
Use useMemo when
- ✅The computation is genuinely expensive (sorting/filtering large arrays, complex math)
- ✅You need a stable object/array reference for React.memo children
- ✅You need a stable reference for useEffect/useCallback dependencies
- ✅You've profiled and confirmed the computation causes jank
Important Caveats
useMemo is a hint, not a guarantee
React may discard cached values in the future (e.g., for offscreen components or during memory pressure). Don't rely on useMemo for correctness — your code must still work if the value is recalculated. Use it only for performance optimization, never for preventing side effects.
Why Interviewers Ask This
This question tests whether you understand React's rendering performance model and can apply memoization judiciously. Interviewers want to see that you know what triggers recalculation, understand the difference between useMemo and useCallback, can identify when memoization helps vs when it's premature optimization, and understand the reference equality implications for child components and other hooks. It shows you think about performance with precision rather than cargo-culting optimization patterns.
Quick Revision Cheat Sheet
Purpose: Cache expensive computation results between renders
Recalculates when: Any dependency changes (Object.is comparison)
Second use: Stable object/array references for memo'd children or hook deps
vs useCallback: useMemo caches a value, useCallback caches a function
Don't overuse: Memoization has overhead — only use for genuinely expensive work
Not a guarantee: React may discard cached values — don't rely on it for correctness