useCallback and useMemo: Stop Reaching for Them by Default

Technical PM + Software Engineer
If you ask most React teams when to use useMemo and useCallback, you often hear a rule of thumb like “use them for performance.”
That sounds reasonable, but it is usually too vague to be useful in production.
In real codebases, broad memoization habits create a familiar mess:
- handlers wrapped everywhere “just in case,”
- dependency arrays that are hard to reason about,
- stale-closure bugs hidden behind optimization intent,
- and almost no measured improvement in user-perceived performance.
This guide gives you an evidence-first framework so memoization becomes a precision tool instead of a reflex.
The core shift: from API knowledge to decision quality
Knowing the API signatures for useMemo and useCallback is not the hard part.
The hard part is making the right call under real constraints:
- shared components,
- changing product requirements,
- partial profiling data,
- and team pressure to “optimize” before a clear bottleneck exists.
A better default is simple:
Only add memoization when you can name the exact work it avoids and show that work matters.
What these hooks really do (and do not do)
useMemo
useMemo memoizes the result of a computation until dependencies change.
It is useful for expensive derived values, not for every small transform.
useCallback
useCallback memoizes a function identity until dependencies change.
It is useful when referential stability is semantically important (for memoized children, effect subscriptions, or wrapper utilities like debounce/throttle).
What neither hook guarantees
Neither hook automatically improves performance.
Both add overhead:
- dependency tracking burden,
- extra indirection in code reading,
- and correctness risk when dependencies are wrong.
Memoization is a cache. Caches are tradeoffs, not free wins.
The highest-value question to ask in PR review
Before approving a memoization change, ask one sentence:
“What concrete work are we avoiding, and where is the evidence this matters?”
If the answer is fuzzy, the hook should not merge yet.
When useMemo is genuinely justified
Use useMemo when all of these are true:
- The computation is materially expensive.
- The computation runs frequently under realistic interaction.
- Inputs are stable often enough to make caching effective.
- There is profiler or user-observable evidence of impact.
const sortedVisibleRows = React.useMemo(() => {
return heavyFilterSort(rows, query, sort, featureFlags);
}, [rows, query, sort, featureFlags]);
If heavyFilterSort is cheap, or dependencies churn on nearly every render, this cache is likely noise.
When useCallback is genuinely justified
Use useCallback when function identity stability prevents expensive or incorrect downstream behavior.
Common valid cases:
- callback passed into
React.memochildren where identity changes trigger unnecessary re-renders, - callback used in effect attach/detach paths (sockets, DOM listeners, SDK subscriptions),
- callback wrapped by debounce/throttle utilities that depend on stable references.
const onMessage = React.useCallback((event: MessageEvent) => {
// parse + dispatch
}, []);
React.useEffect(() => {
socket.addEventListener("message", onMessage);
return () => socket.removeEventListener("message", onMessage);
}, [socket, onMessage]);
If the callback is only used directly on native DOM props (onClick, onChange) and there is no proven identity-sensitive consumer, useCallback is often unnecessary.
Expensive first moves that are usually better than memoization
Teams frequently reach for hooks before fixing architecture. In many apps, these changes yield bigger gains:
- Split oversized components by state ownership.
- Move expensive work to server or loader boundaries where appropriate.
- Virtualize long lists.
- Prevent unnecessary parent renders at source.
- Reduce render-frequency churn from broad context updates.
Memoization can help after this, not instead of this.
Dependency arrays are correctness boundaries
Memoization bugs are often correctness bugs in disguise.
If a dependency is missing, you may keep stale data or stale behavior alive.
If dependencies are over-broad, caches invalidate constantly and provide little benefit.
Practical rules:
- include all reactive values,
- avoid mutating dependency objects in place,
- prefer explicit values over implicit closure assumptions,
- do not suppress lint warnings without a written rationale.
If dependency management becomes painful, that is usually a design signal, not a lint problem.
The stale-closure trap (and how it appears in production)
A common anti-pattern:
- memoized callback captures outdated state,
- UI appears fine in light testing,
- bug surfaces only under asynchronous timing or high interaction frequency.
This is why “it renders faster” is not enough. Memoization must preserve correctness first.
Profiling workflow that teams can actually repeat
Use a short, deterministic loop with the React DevTools Profiler:
- Reproduce a specific laggy interaction.
- Capture a baseline profile.
- Identify real hot paths (not guessed ones).
- Apply one targeted change.
- Re-profile and compare.
No profiler signal means no optimization commit.
Practical examples: good and bad
Bad: memoizing trivial work
const label = React.useMemo(() => `${first} ${last}`, [first, last]);
String concatenation here is trivial. This likely harms readability more than it helps runtime.
Better: plain computation
const label = `${first} ${last}`;
Good: expensive derivation used repeatedly
const groupedMetrics = React.useMemo(() => {
return aggregateMetrics(rawEvents, windowSize, segmentFilters);
}, [rawEvents, windowSize, segmentFilters]);
If profiling shows this computation dominates commit time, memoization is justified.
Good: identity-sensitive callback into memoized child
const onSelect = React.useCallback((id: string) => {
setSelectedId(id);
}, []);
return <MemoizedGrid onSelect={onSelect} data={data} />;
If child re-render churn was measured and drops after this change, it is a valid use.
Team-level policy that prevents drift
If you want consistency, adopt a lightweight internal policy:
- memoization changes require brief evidence in PR description,
- include “why this hook exists” comment only when non-obvious,
- remove obsolete hooks during refactors,
- reject “pre-optimization” changes with no measured bottleneck.
This keeps performance work intentional instead of ornamental.
How this applies in mixed React + Next.js codebases
In Next.js projects, some expensive derivations can move server-side or into cache-aware data layers, which reduces client memoization pressure.
When deciding where optimization belongs, prefer:
- server/data boundary improvements first,
- client memoization second,
- broad hook sprinkling last.
This aligns better with maintainability and bundle health over time.
A simple decision matrix you can pin in your repo
Use this quick matrix in reviews:
- Expensive computation with stable dependencies and measured cost? →
useMemocandidate. - Function identity affects memoized consumers/subscriptions? →
useCallbackcandidate. - No measured bottleneck and no identity contract need? → do not memoize.
If the benefit cannot be explained clearly in one line, the optimization is probably premature.
Common misconceptions worth correcting
“useCallback prevents all re-renders”
It only stabilizes function identity. Re-renders still happen when props/state/context changes elsewhere.
“useMemo is always faster”
Not true. Cache bookkeeping and invalidation cost can exceed recomputation cost for trivial operations.
“Lint warnings mean add memoization”
Lint guidance is not a performance oracle. It helps dependency correctness, not optimization strategy.
What success looks like after applying this approach
In teams that adopt evidence-first memoization:
- code becomes easier to read,
- fewer stale-closure bugs appear during QA,
- optimization discussions become measurable,
- and performance work focuses on high-impact paths.
That is a much better outcome than “we wrapped everything.”
Closing
useMemo and useCallback are excellent tools when used deliberately.
Use them where measured cost or identity stability demands them. Skip them where they only increase cognitive load.
Readable, correct React code with targeted optimization will outperform blanket memoization strategies over the lifetime of a codebase.
Reference docs used inline: React useMemo, React useCallback, and React Profiler guidance (accessed March 12, 2026).