useMemo and useCallback, When They Help
- react
- javascript
- typescript
- webdev
- frontend
- hooks
- softwareengineering
- performance
The previous article in this series was about useEffect and the discipline of not reaching for it as a default tool. This one is about a related instinct: reaching for useMemo and useCallback on every value and every function, "just to be safe." That instinct is understandable. It's also almost always wrong, for the exact reason you think it's right.
This is part of the React & Next.js Complete Guide, and it builds on the re-render model from the first article in the series. If you haven't internalized that a component re-renders whenever its parent re-renders, regardless of whether its own props changed, the rest of this article won't click, because that's the exact behavior useMemo and useCallback exist to work around.
I've reviewed pull requests where every callback in a component was wrapped in useCallback and every derived value in useMemo, with no measurement behind any of it. The code wasn't wrong, but it wasn't faster either, and it was harder to read. Memoization is a tool for a specific, identifiable cost. Used without that cost in mind, it's just overhead with extra syntax.
What useMemo and useCallback actually do
Both hooks solve the same underlying problem: on every re-render, a component function runs from top to bottom again, which means every value and every inline function inside it is recreated from scratch. Most of the time that's cheap and irrelevant. Two problems make it matter:
- Some computations are genuinely expensive: sorting a large array, running a regex over a big string, doing client-side aggregation over a dataset.
- Recreated objects and functions have a new identity on every render, which breaks referential equality checks that things like
React.memo, dependency arrays, and context consumers rely on.
useMemo caches a computed value across renders and only recalculates it when its dependencies change:
type Order = { id: string; amount: number; status: "paid" | "pending" | "refunded" };
function OrderSummary({ orders, status }: { orders: Order[]; status: Order["status"] }) {
const filtered = useMemo(
() => orders.filter((order) => order.status === status),
[orders, status],
);
const total = useMemo(
() => filtered.reduce((sum, order) => sum + order.amount, 0),
[filtered],
);
return (
<div>
<p>{filtered.length} orders, total ${total.toFixed(2)}</p>
</div>
);
}
useCallback does the same thing for functions: it returns the same function reference across renders as long as its dependencies haven't changed, instead of a brand new function every time.
function OrderList({ orders, onSelect }: { orders: Order[]; onSelect: (id: string) => void }) {
const handleSelect = useCallback(
(id: string) => {
onSelect(id);
},
[onSelect],
);
return (
<ul>
{orders.map((order) => (
<OrderRow key={order.id} order={order} onSelect={handleSelect} />
))}
</ul>
);
}
Neither hook prevents the parent component from re-executing. They only prevent a value or function reference from changing when the underlying inputs haven't changed. That distinction is the whole reason people misuse them: they expect these hooks to stop re-renders, and that's not what they do on their own.
useMemo for expensive computation: the case it actually solves
The clearest justification for useMemo is a computation whose cost you can point to: filtering thousands of rows, computing a derived chart dataset, running an expensive string transformation. If OrderSummary above re-renders because some unrelated piece of parent state changed (a modal opening, for instance), recalculating filtered and total from scratch is wasted work. useMemo skips it, because orders and status haven't changed.
The failure mode I see most often is subtler: using useMemo where the dependency array quietly defeats the whole point. If orders is itself created inline in a parent render (orders={data.filter(...)} for instance, instead of a memoized or stable reference), the array has a new identity every render, so useMemo recalculates every time anyway. You've paid for the memoization machinery and gotten none of the benefit. Fixing that usually means memoizing further up the tree, or restructuring so the data doesn't get recreated inline in the first place.
useCallback for referential stability: mostly matters with memo
useCallback on its own, without a memoized child component downstream, does very little. If OrderRow in the example above is a regular component (no React.memo), it re-renders whenever OrderList re-renders regardless of whether handleSelect is a new function each time, because plain components don't check prop equality at all. useCallback only pays off when it's paired with something that actually checks whether props changed:
const OrderRow = React.memo(function OrderRow({
order,
onSelect,
}: {
order: Order;
onSelect: (id: string) => void;
}) {
return (
<li>
{order.id} — ${order.amount.toFixed(2)}
<button onClick={() => onSelect(order.id)}>View</button>
</li>
);
});
Now OrderRow skips its own re-render if order and onSelect are referentially the same as last time. Without useCallback wrapping handleSelect in the parent, onSelect would be a new function on every parent render, React.memo would see a changed prop, and the whole memoization would be pointless. This is the pairing that actually matters: React.memo (or a dependency array elsewhere) checking equality, and useCallback/useMemo making sure that equality check can actually pass.
That's also why useCallback in isolation, on a component with no memoized children and no dependency arrays downstream consuming the function, is close to useless. It still allocates a hook slot, still runs a dependency comparison on every render, and buys nothing. I've seen codebases where every event handler was wrapped in useCallback out of habit, with zero memoized children anywhere in the tree. That's pure overhead with no corresponding benefit, and it makes the component harder to read for no reason.
Production pitfalls
A few patterns come up often enough in review that they're worth calling out directly.
Objects and arrays created inline as dependencies or props defeat memoization silently. Passing filters={{ status, search }} inline as a prop creates a new object every render, so any useMemo or React.memo downstream that depends on it will never see a stable reference. The fix is either memoizing the object itself or passing primitives (status, search) directly instead of bundling them.
Stale closures inside memoized callbacks. If useCallback's dependency array is missing a value the function actually reads, the callback keeps referencing an old version of that value, similar to the classic stale closure problem in useEffect. The ESLint react-hooks/exhaustive-deps rule catches most of these; don't disable it without a specific reason written in a comment.
Memoizing something that's already cheap. Wrapping const label = \${firstName} ${lastName}`inuseMemocosts more than the string concatenation it's protecting.useMemoanduseCallback` aren't free: they allocate a hook slot, run a comparison, and add a line of indirection. For trivial computations, that overhead usually costs more than the thing it's avoiding.
Reaching for memoization before measuring anything. The React DevTools Profiler will show you exactly which components re-render and roughly how long each render takes. Use it before adding memoization, not after something feels slow. Adding useMemo everywhere and hoping it helps is a common way to end up with code that's both slower to read and no faster to run.
Forgetting that useMemo is not a guarantee. React may, in some circumstances, discard a memoized value and recompute it (for instance, to free memory for offscreen components). Don't rely on useMemo for correctness, only for performance. If a computation must run exactly once for correctness reasons, that's a job for useRef or a proper effect, not memoization.
Key takeaways
- `useMemo` and `useCallback` don't prevent a component from re-rendering; they preserve the identity of a value or function across renders when its dependencies haven't changed.
- `useMemo` earns its cost when the computation is genuinely expensive; for trivial calculations, the memoization overhead can exceed the cost it's avoiding.
- `useCallback` mostly matters when paired with `React.memo` or another equality check downstream. Without that pairing, it does very little.
- Objects and arrays created inline as props or dependencies get a new identity every render, which silently defeats memoization further down the tree.
- Measure with the React DevTools Profiler before adding memoization. Optimizing without a measured problem tends to produce harder-to-read code with no performance gain.
- `useMemo` is a performance optimization, not a correctness guarantee; React can discard and recompute a memoized value under some circumstances.
Frequently asked questions
Does useMemo prevent a component from re-rendering?
No. It only preserves the referential identity of the computed value across renders when its dependencies are unchanged. The component itself still re-renders and re-runs its function body; only the specific memoized value is reused instead of recalculated.
Is useCallback ever necessary without React.memo?
It rarely helps on its own. It matters when the callback is a dependency for another hook (like `useEffect`) where a changing reference would cause unwanted re-runs, or when it's passed to a memoized child. Without either of those, it mostly adds overhead.
Why does my memoized child still re-render even though I used useCallback?
Check whether any other prop passed to the child is being recreated inline, such as an object or array literal. `React.memo`'s shallow comparison checks every prop, and a single unstable prop is enough to invalidate the memoization for the whole component.
Should I memoize every function and value by default?
No. Default to plain functions and values, and add memoization when you've identified a specific, measured cost: an expensive computation or a re-render you've confirmed is wasteful. Blanket memoization adds cognitive overhead without a proven benefit.
How do I decide if a computation is expensive enough to memoize?
Profile it. The React DevTools Profiler shows render duration per component. If a computation runs over a large dataset, does nontrivial string or number processing, or you can otherwise point to real cost, it's a reasonable candidate. If you can't point to a specific reason, it probably isn't yet.
Can useMemo cause bugs if the dependency array is wrong?
Yes. A missing dependency means the memoized value can become stale, silently returning outdated data after props or state change. An extra, unnecessary dependency just defeats the memoization by recalculating too often. Let the `react-hooks/exhaustive-deps` ESLint rule guide the dependency array rather than guessing.
Explore more on

About the author
I'm Aman Kumar Singh, a software engineer in Noida, India building scalable full-stack products with React, Next.js, Node.js, NestJS, PostgreSQL, Redis, and AWS. I write about backend engineering, distributed systems, and system design.