What Is the Difference Between useMemo and useCallback?
Advertisement
🧩 Scenario
Architecture Walkthrough
What Each Hook Actually Caches
useMemo and useCallback share the same structure: both accept a factory and a dependency array, and both recompute only when a dependency changes. The distinction is what gets stored.
useMemo runs the factory function and caches its return value. Use it when the return value is expensive to produce — filtering or sorting large arrays, aggregating data, running a regex over a large string. useCallback caches the function reference itself. It is equivalent to useMemo(() => yourFunction, deps) — a fact that clarifies the division: if you need a stable function to pass as a prop, use useCallback; if you need to memoize what a function returns, use useMemo.
Why useCallback Requires React.memo to Be Useful
useCallback exists to prevent child re-renders by keeping function prop references stable. But a component only skips a re-render when something explicitly tells React to skip it, and that mechanism is React.memo. Without React.memo on the child, React re-renders it unconditionally regardless of whether the prop reference changed. Without useCallback, React.memo compares references that always differ and re-renders anyway. useCallback without React.memo adds comparison overhead with zero benefit.
The Dependency Array Is the Most Dangerous Part
Both hooks depend on dependency array accuracy. A missing dependency causes a stale closure — the factory captures a value that no longer reflects current state. React's eslint-plugin-react-hooks catches most omissions and should be treated as a hard requirement.
The opposite error — listing too many dependencies — invalidates the cache too frequently and eliminates the benefit. Object and array dependencies are particularly dangerous: if filters is created inline on every render, Object.is sees a new reference each time and the cache never hits. Stabilizing dependencies is often as important as the memoization itself.
Key Code Explained
import { useState, useMemo, useCallback, memo } from 'react';
interface Product {
id: string;
name: string;
category: string;
}
interface ProductCardProps {
product: Product;
onAdd: (id: string) => void;
}
// React.memo required — otherwise useCallback on parent adds overhead for nothing
const ProductCard = memo(function ProductCard({ product, onAdd }: ProductCardProps) {
console.log(`Rendering: ${product.name}`);
return (
<div>
<span>{product.name}</span>
<button onClick={() => onAdd(product.id)}>Add</button>
</div>
);
});
function ProductList({
allProducts,
filters,
}: {
allProducts: Product[];
filters: string[];
}) {
const [cartCount, setCartCount] = useState(0);
// useMemo: solves a COMPUTATION problem
// expensiveFilter runs once when filters change; skipped on cartCount updates
const filteredProducts = useMemo(
() => allProducts.filter((p) => filters.includes(p.category)),
[allProducts, filters] // recompute only when these change
);
// useCallback: solves a REFERENCE STABILITY problem
// dispatch from useReducer is stable by contract; safe to list as dep
// Without useCallback: new function reference every render → ProductCard re-renders despite React.memo
const handleAddToCart = useCallback(
(productId: string) => {
setCartCount((c) => c + 1);
// addToCart(productId)...
},
[] // no external deps — setCartCount updater form avoids closing over state
);
return (
<>
<p>Cart: {cartCount}</p>
{filteredProducts.map((p) => (
<ProductCard key={p.id} product={p} onAdd={handleAddToCart} />
))}
</>
);
}
// Sorting example — the spread matters
const sortedProducts = useMemo(
() => [...allProducts].sort((a, b) => a.name.localeCompare(b.name)),
[allProducts] // only re-sorts when products array changes
// [...allProducts] is required: Array.sort mutates in place
// mutating allProducts would corrupt React's internal reference tracking
);
// The equivalence: these are identical
const stableFn = useCallback(() => doSomething(), []);
const stableFnViaUseMemo = useMemo(() => () => doSomething(), []);
// useCallback exists only for readability — the intent is clearer
The two hooks solve two entirely different problems in ProductList. useMemo solves a computation problem: expensiveFilter is slow, so cache its result and re-run only when filters changes. useCallback solves a reference-stability problem: handleAddToCart would be a new function instance on every render, causing every ProductCard (memoized) to re-render. They are not interchangeable — you cannot use useCallback to prevent an expensive filter from running, and useMemo returns a value, not a function suitable for an event handler.
Tradeoffs
| Hook | Caches | Solves | Requires pairing with |
|---|---|---|---|
useMemo | Factory return value | Redundant expensive computation | Nothing required |
useCallback | Function reference | Unstable function prop causing child re-renders | React.memo on the child |
| Neither | Nothing | Nothing (simplest code) | N/A — correct for cheap components |
What Interviewers Actually Check
- Whether you can state without hesitation what each hook caches
- Whether you know
useCallback(fn, deps)equalsuseMemo(() => fn, deps)— signals deep API understanding - Whether you identify the
useCallback+React.memopairing requirement without being prompted - Whether you can explain the stale closure bug from missing deps vs the cache-miss bug from unstable object deps
- Whether you can describe a concrete scenario: profiling, identifying the problem type, choosing the right hook
Follow-Up Questions
- You have a
useCallbackwith a dependency that changes on every render because it's an object created in the parent's render body. How do you break the cycle? - React's documentation says
useCallbackis equivalent touseMemo(() => fn, deps). Why doesuseCallbackexist as a separate hook? - A teammate adds
useMemoto every derived value in a large component "for safety." What is the argument against this? - You use
useMemoto cache a filtered list depending on afiltersobject from a URL query string parsed fresh on every render. The filter still runs every time. Why, and how do you fix it? - The React Profiler shows 200 components re-rendering on every keystroke in a search input. Walk through your diagnosis and fix strategy.
Common Candidate Mistakes
- Reaching for
useCallbackwhen the problem is computation cost — that isuseMemo - Reaching for
useMemoto stabilize a function prop —useCallbackis clearer and identical in effect - Using
useCallbackwithoutReact.memoon the child — adds memoization overhead for zero re-render savings - Passing an inline object as a dependency to either hook —
Object.isreports inequality every render and the cache never hits - Omitting a variable the factory closes over — produces stale results that are hard to reproduce in development
Interview Readiness Checklist
Before you leave this question, make sure you can answer:
- Can you state what each hook caches and give a one-sentence use case for each?
- Can you explain the
useCallback(fn, deps) = useMemo(() => fn, deps)equivalence? - Can you identify which hook solves a computation problem vs a reference-stability problem?
- Can you describe the paired requirement:
useCallbackonly benefits when the child hasReact.memo? - Can you describe the dependency array failure modes: missing dep (stale closure) vs unstable dep (cache never hits)?
Summary
useMemo caches the return value of an expensive factory function, recomputing only when listed dependencies change via Object.is. useCallback caches the function reference itself and is equivalent to useMemo(() => fn, deps) — it exists as a separate hook for readability. The practical rule: useMemo for computation problems (avoid re-running a slow derivation), useCallback for reference-stability problems (prevent a memoized child from re-rendering due to a new function reference). useCallback without React.memo on the receiving child is pure overhead. Both hooks share the same dependency array failure modes: a missing dependency causes a stale closure, and an unstable object or array dependency causes the cache to never hit. Profile before applying either hook, and treat them as surgical fixes rather than defaults applied to every function and derived value in a component.
Is useCallback just useMemo for functions?
Yes. useCallback(fn, deps) is exactly equivalent to useMemo(() => fn, deps). useCallback exists as a separate hook for readability — it makes the intent (stabilize a function reference) explicit without the extra wrapping arrow function.
Advertisement
Stay Updated
Get the latest frontend challenges, interview questions and tutorials delivered to your inbox.
Advertisement