useMemo
useMemo caches a computed value between renders until its dependencies change. Learn exactly how dependencies are compared, why it's a hint and not a guarantee, and the few situations where it actually pays off.
🎯 What you'll learn
- The basic API
- What actually gets compared
- useMemo is not guaranteed to skip work
- When it helps vs computing inline
📚 Before you start
1The Basic API
useMemo caches the result of an expensive calculation across re-renders, recomputing it only when one of its listed dependencies has changed:
import { useMemo } from "react";
function ProductList({ products, query }: { products: Product[]; query: string }) {
const filtered = useMemo(() => {
console.log("filtering..."); // only logs when products or query actually change
return products.filter((p) => p.name.toLowerCase().includes(query.toLowerCase()));
}, [products, query]);
return <ul>{filtered.map((p) => <li key={p.id}>{p.name}</li>)}</ul>;
}Without useMemo, products.filter(...) re-runs on every render of ProductList — including renders triggered by completely unrelated state changes (a parent re-rendering, a sibling's click handler, an unrelated useState in this same component). useMemo skips the recomputation and hands back the previously cached array as long as products and query are reference-equal (via Object.is) to what they were last time.
2What Actually Gets Compared
The dependency array is compared element by element, using the same Object.is semantics as every other hook's dependency array — not deep equality:
function Dashboard({ userId }: { userId: string }) {
// BAD: a new object literal is created every render, so this dependency
// is NEVER equal to the previous one, and useMemo recomputes every time —
// all the caching benefit is silently lost
const filters = useMemo(() => ({ userId, active: true }), [{ userId }]);
// ^^^^^^^^ new object every render
// GOOD: depend on the primitive values that actually determine the result
const summary = useMemo(() => computeSummary(userId, true), [userId]);
}Passing an object or array literal inline as a dependency defeats the whole point — it is never === to the previous render's literal. Depend on the primitive values (or on a stable reference produced by another useMemo/useCallback) instead.
3useMemo Is Not Guaranteed to Skip Work
React's docs are explicit that useMemo is a performance hint, not a semantic guarantee. React may discard a cached value and recompute it anyway — for instance, to free memory for offscreen components, or in future React versions that experiment with recomputing memoized values to prep for features like pre-warming component trees:
// This pattern is WRONG: it relies on useMemo to guarantee an object is only
// created once, using that assumption for correctness rather than performance
const cache = useMemo(() => new Map(), []); // do NOT rely on this Map surviving
// If correctness (not just performance) requires "only created once," use
// useRef or module state instead — useMemo alone doesn't promise it
const cacheRef = useRef<Map<string, number>>();
if (!cacheRef.current) cacheRef.current = new Map();The rule: write code that is correct even if useMemo recomputed on every render (i.e., as if it were plain inline code); use it purely to make that already-correct code faster.
4When useMemo Actually Helps
Three situations justify reaching for it:
// 1. A genuinely expensive computation (sorting/filtering large arrays, heavy math)
const sorted = useMemo(() => [...hugeArray].sort(compareFn), [hugeArray, compareFn]);
// 2. Preserving reference stability for a value passed to a memoized child
// (React.memo) so that child doesn't re-render just because a new object
// literal was created this render
const config = useMemo(() => ({ theme, locale }), [theme, locale]);
return <MemoizedPanel config={config} />;
// 3. Preserving reference stability for a value used as ANOTHER hook's dependency
const chartData = useMemo(() => transformForChart(rawData), [rawData]);
useEffect(() => {
renderChart(chartData); // stable dependency avoids re-running the effect every render
}, [chartData]);For anything cheap — string concatenation, simple arithmetic, a .map() over a handful of items — the overhead of running useMemo itself (storing the array, doing the comparison) usually costs more than just recomputing the value plainly. Don't wrap every computation in useMemo "just in case."
5useMemo vs Computing Inline
The default should be to compute values inline during render, not to reach for useMemo first:
function Greeting({ firstName, lastName }: { firstName: string; lastName: string }) {
// No useMemo needed — string concatenation is essentially free
const fullName = `${firstName} ${lastName}`;
return <h1>Hello, {fullName}</h1>;
}useMemo is an opt-in optimization for measured or clearly expensive work, not a default wrapper to apply everywhere "for performance." Reach for it after profiling shows a real cost, or when reference stability is specifically required by a downstream React.memo or dependency array — see the Performance optimization topic for the broader picture of when memoization actually pays off.
Under the HoodHow it works internally▾
React does not attach state to the component function — a function component is just a function, called fresh every render with no persistent memory of its own. Hook data has to live somewhere else: it lives on the fiber, React's internal object representing one component instance in the tree.
Each fiber holds a memoizedState pointer to the head of a singly linked list of hook objects, one node per hook call, built in the exact order those hooks are called and walked in that same order on every subsequent render. Matching is done purely by call order, which is why useMemo (like every hook) cannot be called conditionally — doing so shifts every later hook call to the wrong node in the list.
A useMemo hook node stores exactly two things in its memoizedState slot: the cached value from the last time the factory function ran, and the dependency array from that same run. When the component re-renders and execution reaches this useMemo call again, React does not simply call the factory — it first walks the new dependency array against the stored one, comparing each element pairwise with Object.is (the same identity comparison useEffect and useCallback use, exact for primitives, reference-only for objects/arrays/functions). If every element compares equal, React skips calling the factory entirely and returns the value already sitting in the node. If any element differs (or the array's length/identity changed, or this is the first render), React calls the factory function now, during this render, and overwrites the node's cached value and dependency array with the new results before returning the freshly computed value.
This is the load-bearing detail that separates useMemo from merely "caching": the factory function, when it does run, runs synchronously as part of the current render — not in an effect, not asynchronously. That's what makes it safe to use for values needed to render the current output (unlike useEffect, which always runs after commit). It's also why useMemo's optimization is fundamentally about avoiding redundant work across renders that don't need it, not about deferring work: on any render where dependencies actually changed, the cost is paid in full, in that render, exactly like it would be without useMemo at all.
React does not promise that a cached memoizedState slot survives forever. Internally, React may discard memoized values for fibers it needs to reclaim memory from (for example, a component tree kept offscreen by Suspense/Activity boundaries), and the React team has explicitly reserved the right to make useMemo "not guarantee" recomputation-skipping in future versions to enable upcoming optimizations. That reservation is exactly why component logic must be written so it stays correct if the factory reran on every single render — useMemo may only ever be trusted to make already-correct code faster, never to make otherwise-incorrect code work.
⚠️ Common Pitfalls
- Passing an inline object/array literal as a dependency — a new literal is created every render, so it's never
Object.is-equal to the previous one, and the memoization never hits; depend on the primitive fields instead, or memoize the object itself upstream. - Relying on
useMemofor correctness (e.g., "this only creates the Map once") rather than performance — React does not guarantee the cached value survives; useuseRefor module-level state for anything that must genuinely persist exactly once. - Wrapping cheap computations in
useMemo— the bookkeeping (storing the array, running theObject.iscomparisons) can cost more than just recomputing a trivial value inline; reserve it for measured, non-trivial cost. - Forgetting a dependency the factory function closes over — just like
useEffect, the factory closes over render-scope variables (see the closures material inlanguages/javascript); an incomplete dependency array means the memoized value can silently keep using a stale prior value instead of recomputing. - Expecting
useMemoto prevent a child from re-rendering by itself — reference stability fromuseMemoonly helps if the child is also wrapped inReact.memo; without that, the child re-renders regardless of whether its props are the same reference.
📋 Quick Reference
const value = useMemo(() => computeExpensive(a, b), [a, b])
→ recomputes only when a or b changes (Object.is per element)
→ returns the SAME cached reference otherwise
Use when: expensive computation, OR reference stability needed for
React.memo children or another hook's dependency array
Skip when: computation is cheap — just compute inline
NOT a correctness guarantee — React may discard the cache and recompute
anyway; write code that stays correct even if that happens.🧠 Key takeaways
useMemo(compute, deps)returns the cached value while deps are Object.is-equal.- It's a performance hint: React may discard the cache; code must stay correct without it.
- Worth it for expensive computations and referential stability (memo children, hook deps).
- Measure first; the React Compiler can add memoization automatically.
🎤 Interview Q&A
Q1What does useMemo do?core▾
Caches the result of a computation between renders, recomputing only when dependencies change: useMemo(() => expensive(a, b), [a, b]).
- Is it a guarantee? No; React may discard the cache. Treat it as a performance optimization.
Q2When should you use useMemo?core▾
For genuinely expensive computations (measured), to keep referential equality for objects/arrays passed to memoized children or used as effect dependencies.
- When not? Cheap computations: the overhead and complexity aren't worth it.
Q3How does useMemo relate to React.memo?core▾
React.memo skips re-rendering a component when props are shallow-equal; that only works if object/function props keep the same identity, which useMemo/useCallback provide.
- Common mistake? Wrapping a child in memo but passing a new inline object every render.
Q4How does the React Compiler change memoization?senior▾
It automatically memoizes components and values at build time when code follows the rules of React, reducing manual useMemo/useCallback.
- Requirement? Pure components and correct hook usage.
Q5What's a better fix than memoization sometimes?core▾
Restructuring: move state down closer to where it's used, lift expensive subtrees out as children so they don't re-render, or virtualize long lists.
- Why does passing children help? Elements created by the parent don't change when the wrapper's state changes.
🛠️ Practice exercises
Each exercise is a file in this topic folder. Code exercises are self-checking: run them and make the checks pass.
✅ Check yourself
Answer the questions — feedback is instant.
a + b is…