useCallback and useMemo: Stop Reaching for Them by Default

Technical PM + Software Engineer
useCallback and useMemo are both powerful tools in React's toolbox. They let you memoize function identities and computed values to avoid unnecessary work. But they are also one of the most common premature optimizations in React codebases. Developers add them reflexively to 'fix' perceived performance problems without measuring, adding complexity and subtle bugs (stale closures, incorrect dependencies) in the process. This article argues a contrarian position: stop reaching for useCallback and useMemo by default. Instead, understand the specific problems they solve, measure impact, apply patterns safely, and prefer simpler alternatives when possible.
1) Why useCallback and useMemo are so tempting
At a glance, useCallback and useMemo are low-friction ways to signal intent: 'I want this function or value to be stable across renders.' They promise to prevent unnecessary re-renders, reduce repeated work, and avoid expensive calculations. That promise fits an intuitive mental model: avoid repeating work by caching. When performance issues are visible (jank, frequent re-renders of large trees), reaching for memoization seems logical.
They also become a shorthand for two different problems: controlling referential equality (important when passing props to Pure/React.memo children) and avoiding repeated heavy computation. Because both problems can cause wasted renders or computations, developers conflate them and reflexively wrap functions and values in useCallback/useMemo.
- useCallback(fn, deps) memoizes function identity.
- useMemo(() => value, deps) memoizes a computed value.
- Both depend on correctly listing dependencies; mistakes introduce bugs.
2) The cost and trade-offs: memoization isn’t free
Memoization and identity-stability come with concrete costs. Each hook call has overhead: constructing closures, storing cached values, comparing dependency arrays, and keeping references in memory. For trivial functions or cheap values, that overhead can exceed the cost of recalculating or re-rendering. That’s the core reason these hooks are premature optimizations: their bookkeeping can be more expensive than the work they avoid.
Beyond performance cost, they increase cognitive load. useCallback/useMemo create extra contract: dependency arrays must be maintained. Missing dependencies lead to stale closures and subtle bugs. Overusing them spreads these risks throughout a codebase, making refactors riskier and components harder to reason about.
- Runtime overhead: creation, compare deps, and memory for cached values.
- Maintenance overhead: developers must keep dependency arrays accurate.
- Bug risk: stale closures cause hard-to-debug behavior.
- False positives: perceived fixes that don't improve user-visible performance.
3) When they actually help — clear, measurable cases
There are concrete scenarios where useMemo/useCallback produce real benefits. You should reach for them only when you have evidence or a strong theoretical reason to believe they'd help. The most common valid cases:
1) Expensive computations executed on every render. If a calculation takes substantial CPU time (e.g., heavy data transformations, sorting of large arrays), wrapping it in useMemo and measuring the time saved is appropriate. 2) Stable function identity matters for pure children. When you pass a callback to a child wrapped by React.memo (or a custom shouldComponentUpdate), a new function identity every render will force the child to re-render even if props otherwise equal. 3) Referential equality required by dependencies. If an effect or memo depends on an object/array created inline and you need the effect to run only when contents change, useMemo can stabilize identity and reduce unnecessary effect runs.
In each case, the benefit is measurable: a meaningful reduction in render count or CPU time attributable to the memoization.
- Valid use: expensive compute that would otherwise run frequently.
- Valid use: callbacks passed to React.memo children to avoid re-renders.
- Valid use: stabilizing objects/arrays used as effect dependencies.
4) Practical examples and patterns
Example A — unnecessary useCallback: a parent creates an inline handler that only updates local state. The child receives that function but is not memoized, so the function identity doesn’t affect anything. useCallback is noise here.
Bad:
const Parent = () => { const [count, setCount] = useState(0); const inc = useCallback(() => setCount(c => c + 1), []); return <Child onClick={inc} /> } // Child is not memoized — no benefit
Good alternative: remove useCallback (simpler) or memoize the child if repeated re-renders are expensive and the handler identity matters.
Example B — valid useMemo for expensive work:
const filtered = useMemo(() => heavyFilter(items, query), [items, query]); // heavyFilter is expensive
Profile the render time before/after. If heavyFilter takes 10–20ms and runs many times per second, memoization typically helps.
Example C — stable callback for React.memo child:
const handleSelect = useCallback((id) => setSelection(id), [setSelection]); return <MemoizedRow onSelect={handleSelect} />; // MemoizedRow won't re-render if props equal
- Don't use useCallback to silence linter warnings—fix the underlying structure.
- Prefer optimizing child components (React.memo) or lifting state if suitable.
- Always measure: React DevTools Profiler, console.time, or performance.mark.
5) Common mistakes and how to avoid bugs
Stale closures are probably the most insidious problem. If you use useCallback with an incomplete deps array, the callback may close over old variables, causing behavior that's correct only until state changes. Likewise, omitting dependencies from useMemo causes similar stale-value issues.
Another frequent mistake is assuming useMemo preserves deep equality. useMemo only preserves the reference; you still need to compare contents yourself if necessary. Also, wrapping everything in memoization hides the real problem: overly large component trees or poor component boundaries.
How to avoid mistakes: prefer explicit and minimal dependency arrays, use ESLint rules (exhaustive-deps) as a guide rather than a reflex, and run unit tests/behavioral tests around edge cases. If you need to lock a value and you intentionally want to ignore a dependency, document it and use a ref with a clear reason comment.
- Rule: never silence missing-deps warnings without a reason and a comment.
- Rule: useMemo does not deep-compare values; it preserves reference only.
- Rule: measure before and after applying memoization.
- Tooling: React DevTools Profiler, performance.now(), Lighthouse.
6) A short decision checklist you can use in code reviews
When you see useCallback/useMemo added in a PR, run this quick checklist instead of approving reflexively:
1) Is there a demonstrated performance problem? Profile or reproduce perceivable slowness. 2) Is the memoized computation actually expensive or is it allocating tiny values? 3) Is the identity stability required (child is pure/memoized or an effect depends on it)? 4) Are dependencies correct and documented? 5) Can a simpler structural change (lift state, split component, memoize child) fix it without adding hooks?
If the answer to 1–3 is no, remove the hook and prefer simplicity. If yes, add tests or profiling artifacts that show improvement and document the reason in code comments.
- Checklist for PR reviews: Measure → Confirm necessity → Keep deps accurate → Prefer simple refactors
- If unsure, leave a TODO with benchmarking instructions rather than leaving widespread memoization in place
Conclusion
useCallback and useMemo are valuable but specialized tools. The common mistake is treating them as default reflexes rather than measured interventions. The right approach: prefer simplicity, measure before optimizing, limit memoization to cases with measurable benefit, and be disciplined about dependency management to avoid stale closures and subtle bugs. Code reviews should enforce that memoization is justified, not decorative.
Action Checklist
- Instrument a suspicious component with React DevTools Profiler and measure render times and component render counts before and after memoization.
- Run the checklist on three recent PRs in your codebase: identify any unnecessary uses of useCallback/useMemo and simplify them.
- If you must memoize, add a short comment explaining why and include a linked profiling snapshot or benchmark.
- Familiarize your team with alternatives: React.memo for pure children, component splitting, lifting state, and useRef to hold persistent mutable values.
- Adopt a code-review rule: require one of (profiling evidence, an explained theoretical reason, or a structural refactor attempt) before accepting memoization.