useMemo is a React hook that caches the result of a computation
between renders. It re-runs the factory function only when one of its
listed dependencies changes; otherwise it returns the previously
cached value.
function ProductList({ products, filterText }) {
// Without useMemo: filtered every render even if products/filterText
// haven't changed
const filtered = useMemo(
() => products.filter(p => p.name.includes(filterText)),
[products, filterText] // only re-compute when these change
)
return filtered.map(p => <ProductRow key={p.id} product={p} />)
}
The problem it solves is twofold. First, it avoids re-running
expensive computations on every render. Second, it preserves
referential identity of objects and arrays so downstream
React.memo comparisons and useEffect dependency checks don't
fire unnecessarily.
Rule of thumb: Only reach for useMemo when the computation is
measurably expensive or when referential stability of the returned
value is required by a consumer — not as a default wrapping strategy.
useCallback memoizes a function reference so the same function
object is returned across renders as long as its dependencies stay the
same. It is syntactic sugar over useMemo returning a function.
// These two are equivalent:
const handleClick = useCallback(() => {
doSomething(id)
}, [id])
const handleClick = useMemo(() => () => {
doSomething(id)
}, [id])
The key difference in intent:
useMemo— memoize a computed value (object, array, number…)useCallback— memoize a function definition so its reference stays stable
Both return a cached result; useCallback is just the idiomatic form
when that result is a function.
Rule of thumb: Use useCallback when you need a stable function
reference to pass as a prop to a memoized child or as a dependency
inside a useEffect — not to speed up the call itself.
The dependency array is the second argument to both hooks. React
compares each element of the array to its previous value using
Object.is after every render. If any element changed, the cached
value is discarded and the factory function re-runs.
const total = useMemo(() => {
return items.reduce((sum, item) => sum + item.price, 0)
}, [items]) // re-compute only when `items` reference changes
// Common mistakes:
// [] — never re-computes (stale data if items changes)
// no array — re-computes every render (defeats memoisation)
// [items.length] — misses mutations that don't change length
React's ESLint plugin (eslint-plugin-react-hooks) enforces the
exhaustive-deps rule, which flags any value used inside the
factory that is missing from the dependency array. This prevents
stale closure bugs at the cost of occasionally breaking
memoisation when a dependency changes too often.
Rule of thumb: Always list every reactive value read inside the
factory; rely on eslint-plugin-react-hooks to catch omissions
automatically.
Referential stability means an object or function returns the
same memory reference across renders. In JavaScript, two objects
with identical contents are still different values under Object.is,
which is what React uses for prop comparison and dependency checks.
function Parent() {
// NEW object reference every render — breaks child memo
const style = { color: 'red' }
// STABLE reference — child memo works
const style = useMemo(() => ({ color: 'red' }), [])
return <MemoizedChild style={style} />
}
const MemoizedChild = React.memo(function Child({ style }) {
console.log('render') // fires every render without useMemo above
return <div style={style}>hello</div>
})
Without referential stability, React.memo always sees "new" props,
useEffect dependency checks always see "new" dependencies, and
downstream useMemo/useCallback hooks always invalidate — making
all memoisation downstream pointless.
Rule of thumb: Stabilise object and array props with useMemo
and function props with useCallback before passing them into
React.memo components or useEffect dependencies.
Memoisation has a cost: React must store the previous value, run the dependency comparison on every render, and keep a closure alive in memory. For cheap operations, this overhead exceeds the saving.
// Bad — wrapping a trivial computation
const doubled = useMemo(() => count * 2, [count])
// Good — just compute it inline
const doubled = count * 2
// Bad — memoizing a callback that is never passed to a memo'd child
const handleClick = useCallback(() => setOpen(true), [])
// Good — inline is fine if the parent re-renders anyway
const handleClick = () => setOpen(true)
Avoid useMemo/useCallback when:
- The computation is fast (arithmetic, simple string ops)
- The dependency changes on every render anyway
- The child receiving the value is not wrapped in
React.memo - You're in early development — premature optimisation obscures intent
Rule of thumb: Profile first with React DevTools Profiler; add memoisation only where you can measure a render-time win, not preemptively everywhere.
There is no fixed rule, but the common benchmark is >1 ms in the
profiler during normal usage. React's own docs suggest a rough test:
wrap the call in console.time / console.timeEnd and see if it
consistently exceeds 1 ms on mid-range hardware.
// Quick check in development
console.time('filter')
const result = hugeList.filter(expensivePredicate)
console.timeEnd('filter') // e.g. "filter: 3.4 ms" → worth memoizing
// Production pattern
const filtered = useMemo(
() => hugeList.filter(expensivePredicate),
[hugeList, expensivePredicate]
)
Common genuinely expensive operations: filtering/sorting large
arrays (10 000+ items), heavy string processing, recursive tree
traversals, and cryptographic hashes. Cheap operations that look
expensive but aren't: .map over <500 items, simple arithmetic,
and string concatenation.
Rule of thumb: If console.time shows <1 ms consistently, skip
useMemo — the hook overhead may actually be slower than recomputing.
React.memo skips re-rendering a child when all props are
shallowly equal. For that to work, object and function props must
be referentially stable — which is exactly what useMemo and
useCallback provide in the parent.
const Chart = React.memo(function Chart({ data, onHover }) {
console.log('Chart render')
return <canvas /> // expensive canvas draw
})
function Dashboard({ rawData }) {
// Without these, Chart re-renders on every Dashboard render
const data = useMemo(
() => transformForChart(rawData), // stable reference
[rawData]
)
const onHover = useCallback(
(point) => showTooltip(point),
[] // no deps — stable forever
)
return <Chart data={data} onHover={onHover} />
}
The three work as a unit: React.memo is the gate; useMemo and
useCallback are what keep the keys from changing unnecessarily.
Without stable props, React.memo is effectively a no-op.
Rule of thumb: Whenever you wrap a component in React.memo,
audit its parent for inline objects/functions and stabilise them
with useMemo/useCallback.
A stale closure occurs when a useCallback (or useMemo)
captures a value from an earlier render because the dependency array
was incomplete. The function "closes over" the old value and continues
using it even after the real value has changed.
function Counter() {
const [count, setCount] = useState(0)
// BUG: count is captured at 0 and never updated
const logCount = useCallback(() => {
console.log(count) // always logs 0
}, []) // ← missing `count` dependency
// FIX 1: add count to deps (new function ref when count changes)
const logCount = useCallback(() => {
console.log(count)
}, [count])
// FIX 2: use functional updater when only setting state
const increment = useCallback(() => {
setCount(prev => prev + 1) // no stale closure — reads latest
}, [])
return <button onClick={logCount}>Log {count}</button>
}
The ESLint exhaustive-deps rule catches most stale closures at
lint time. For callbacks that need to always read the latest value
without changing reference, consider the ref-callback pattern:
store the latest function in a ref and call it from a stable wrapper.
Rule of thumb: Never omit a dependency to "keep the function
stable" — either add it honestly or use useReducer / functional
updates to remove the dependency legitimately.
Three situations call for object memoisation:
- Derived data — transform props into a new object for rendering
- Config objects — pass stable option bags to child components
- Context values — prevent all consumers from re-rendering
function UserCard({ user, theme }) {
// 1. Derived data
const displayName = useMemo(
() => `${user.firstName} ${user.lastName}`.trim(),
[user.firstName, user.lastName]
)
// 2. Stable config object for a memoized child
const chartOptions = useMemo(
() => ({ color: theme.primary, legend: false }),
[theme.primary]
)
// 3. Context value — prevents all consumers re-rendering
const ctx = useMemo(
() => ({ user, displayName }),
[user, displayName]
)
return (
<UserContext.Provider value={ctx}>
<MemoizedChart options={chartOptions} />
</UserContext.Provider>
)
}
Avoid memoizing objects whose dependencies change on every render — you get the cost of comparison with none of the saving.
Rule of thumb: Memoize objects when you control neither the
re-render frequency of the parent nor the comparison logic of the
consumer — context providers and React.memo children are the
canonical cases.
The answer depends entirely on whether the callback is passed to a
memoized child or used as a useEffect dependency.
function Form() {
const [value, setValue] = useState('')
// INLINE — fine: onChange is not passed to a memo'd child
// and causes no extra effects
return (
<input
onChange={e => setValue(e.target.value)} // new ref each render
value={value}
/>
)
}
function SearchPanel({ onSearch }) {
// EXTRACTED — necessary: ResultList is React.memo'd
const handleSearch = useCallback(() => {
onSearch(query)
}, [onSearch, query])
return <MemoizedResultList onSearch={handleSearch} />
}
Inline lambdas are idiomatic and readable for event handlers on
DOM elements. Extracting with useCallback adds noise and overhead
when the consumer doesn't benefit from a stable reference.
Rule of thumb: Default to inline; extract with useCallback
only when the callback flows into a React.memo boundary or a
useEffect / useMemo dependency list.
A custom hook's returned functions become dependencies for any
useEffect or useCallback in the consuming component. If the
hook returns a new function reference on every call, the consumer
is forced to either omit it from deps (stale closure risk) or accept
spurious effect re-runs.
// Bad — new reference every render
function useSearch(query) {
const search = () => fetchResults(query) // new fn each time
return { search }
}
// Good — stable reference, updates only when query changes
function useSearch(query) {
const search = useCallback(
() => fetchResults(query),
[query]
)
return { search }
}
// Consumer can now safely list `search` as a dep
function SearchBox({ query }) {
const { search } = useSearch(query)
useEffect(() => {
search() // no stale closure, no spurious re-runs
}, [search])
}
This is why popular hook libraries (React Query, SWR, Zustand) memoize every returned function — it makes composition predictable without requiring callers to know the hook's internals.
Rule of thumb: Treat functions returned from custom hooks the
same as public API — wrap them in useCallback so consumers can
safely include them in dependency arrays.
Every time a Context.Provider re-renders, all consumers
re-render unless the context value is referentially the same
object. Without useMemo, a provider's value object is recreated
on every parent render, making the context update cascade
unavoidable.
// Bad — new object on every Parent render
function AuthProvider({ children }) {
const [user, setUser] = useState(null)
return (
<AuthContext.Provider value={{ user, setUser }}>
{children}
</AuthContext.Provider>
)
}
// Good — stable object; consumers only re-render when user changes
function AuthProvider({ children }) {
const [user, setUser] = useState(null)
const value = useMemo(
() => ({ user, setUser }),
[user] // setUser is stable from useState, safe to omit
)
return (
<AuthContext.Provider value={value}>
{children}
</AuthContext.Provider>
)
}
For large apps, splitting context into a read context and a write
context (each with its own useMemo value) further reduces
re-renders: write-only consumers don't re-render when the user data
changes.
Rule of thumb: Always wrap a context value in useMemo in
providers that live high in the tree — the blast radius of a
needless update is proportional to how many consumers are below it.
Memoisation can introduce subtle bugs when the dependency array is wrong or when the memoized value is mutated instead of replaced.
// PITFALL 1: mutating the cached value
const items = useMemo(() => [], [])
items.push(newItem) // ← mutates; useMemo won't notice, UI goes stale
// Fix: always return a new array
const items = useMemo(() => [...baseItems, newItem], [baseItems, newItem])
// PITFALL 2: object identity as a dep (always "new")
const options = { limit: 10 }
const result = useMemo(() => query(options), [options]) // re-runs every render
// Fix: depend on primitives, not the object
const result = useMemo(() => query({ limit: 10 }), []) // or [limit]
// PITFALL 3: async factory (useMemo is synchronous only)
const data = useMemo(async () => fetchData(), []) // returns a Promise, not data
// Fix: use useEffect + useState for async work
These pitfalls share a root cause: treating useMemo as smarter
than it is. It only tracks the dependency array — it cannot detect
mutation or understand async semantics.
Rule of thumb: Treat memoized values as immutable; never mutate them in place, depend only on primitives or already-stable references, and keep factory functions synchronous.
The ref-callback pattern (sometimes called "latest ref") keeps
a useCallback reference stable while still reading the latest
closure values. It solves the tension between "stable function" and
"always current data".
function useLatestCallback(fn) {
const ref = useRef(fn)
// keep the ref up to date on every render (safe: layout effect)
useLayoutEffect(() => { ref.current = fn })
// return a stable wrapper that delegates to the latest fn
return useCallback((...args) => ref.current(...args), [])
}
function SearchBox({ onSearch }) {
// onSearch may change often, but handleSearch is always stable
const handleSearch = useLatestCallback(onSearch)
useEffect(() => {
const id = setInterval(handleSearch, 5000)
return () => clearInterval(id)
}, [handleSearch]) // no stale closure, no interval reset on each render
}
This pattern is useful for event handlers and interval callbacks
that must be stable but need to capture the very latest state or
props. React's upcoming useEffectEvent hook formalises this
pattern in the framework itself.
Rule of thumb: Reach for the ref-callback pattern when
useCallback forces an awkward choice between adding a fast-moving
dep (breaking stability) and omitting it (creating a stale closure).
No. Native DOM event handlers (on <button>, <input>, etc.) do
not benefit from useCallback. React attaches DOM events at
the root via event delegation — the function reference you pass is
never directly stored on the DOM node, and changing it between
renders has zero cost.
function Counter() {
const [count, setCount] = useState(0)
// Unnecessary — the <button> doesn't care about reference stability
const handleClick = useCallback(
() => setCount(c => c + 1),
[]
)
// Equivalent and simpler
const handleClick = () => setCount(c => c + 1)
// useCallback IS needed when handleClick flows into a memo'd child
return (
<>
<button onClick={handleClick}>{count}</button>
<MemoizedExpensiveChild onAction={handleClick} />
</>
)
}
The second <MemoizedExpensiveChild> case justifies useCallback
because React.memo's prop comparison is what creates the
stability requirement — the DOM element does not.
Rule of thumb: If the only consumer of a callback is a plain
DOM element (not a React.memo component), skip useCallback —
it adds complexity with no measurable benefit.
More Rendering and Performance interview questions
More ways to practice
The self-quiz is live. Get notified when mock interviews and new question packs drop.