useMemo and useCallback Mistakes That Silently Hurt React Performance
The most common useMemo and useCallback mistakes in React that add overhead without improving performance — and how to fix each one correctly.
Most React codebases use useMemo and useCallback in good faith, but a significant portion of those usages make no measurable difference — and some actively slow the app down. The hooks themselves are not the problem. Applying them without understanding their cost model is.
This post covers the most common useMemo and useCallback mistakes and how to reason through them correctly before reaching for either hook.
Why These Hooks Have a Cost
Both useMemo and useCallback do two things on every render: they compare the current dependency array to the previous one, and they either return the cached value or run the function and cache the new result. That comparison and memory allocation are not free. For cheap computations, this overhead can exceed the cost of simply running the calculation inline.
The rule is simple but routinely ignored: profile first, memoize second.
Mistake 1: Memoizing Trivial Computations
Wrapping a string concatenation, a boolean check, or a two-element array creation in useMemo is the most common mistake in the codebase.
// No measurable benefit — the comparison costs as much as the computation
const label = useMemo(() => `${firstName} ${lastName}`, [firstName, lastName]);useMemo is justified when the wrapped computation is genuinely expensive: filtering or sorting a list of thousands of items, running a complex transformation, or calling a function that does significant work. For everything else, remove it.
How to tell if a computation is expensive
If you cannot measure a render slowdown without it using the Profiler, the computation is not expensive enough to memoize. That is the only test that matters.
Mistake 2: Using useCallback on Every Handler
A function passed as a prop does not automatically benefit from useCallback. The callback is only worth stabilizing if the child component is wrapped in React.memo and re-rendering that child is actually slow.
// useCallback here does nothing unless Button is React.memo'd
const handleClick = useCallback(() => {
doSomething();
}, []);Without React.memo on the child, the child re-renders every time the parent renders regardless of whether the callback reference is stable. useCallback prevents a new function from being created, but it does not prevent the child from re-rendering.
Mistake 3: Breaking Memoization with Inline Objects or Arrays
This is one of the most confusing failure modes. A developer adds React.memo to a component and useCallback to its handlers, but the component still re-renders on every parent render. The cause is almost always an inline object or array in the props.
// This object is created fresh on every render — React.memo sees a new reference
return <Chart options={{ color: "blue", size: 12 }} />;Every primitive in the dependency array must be stable, and every non-primitive must itself be memoized. One unstable reference anywhere in the props breaks React.memo on that component entirely.
Mistake 4: Missing or Wrong Dependency Arrays
An empty dependency array ([]) in useCallback or useMemo means the value is computed once and never updated. That is correct only when the computation genuinely depends on nothing from the component's scope.
A stale closure — where the memoized function captures an old value and never sees updates — is a correctness bug, not just a performance issue. Linters with eslint-plugin-react-hooks catch most of these, but they only help if the warnings are not suppressed.
The opposite mistake is listing every variable as a dependency when only some of them should trigger recomputation. Think carefully about which inputs actually change the output.
Mistake 5: Memoizing Inside Loops or Conditionally
Hooks must be called in the same order on every render. Calling useMemo inside a loop or after an early return violates that contract and will throw in development.
// Invalid — hooks cannot be inside loops
items.map(item => useMemo(() => transform(item), [item]));If you need per-item memoization, push the transformation into a child component and let React's component lifecycle handle it.
Mistake 6: Expecting useMemo and useCallback to Fix Slow Context
Context re-renders every consumer when the context value changes. Memoizing the value object with useMemo helps only if the consumers are also wrapped in React.memo or useMemo. Without that, stabilizing the context value has no observable effect.
The better fix for expensive context is usually to split the context into two: one for values that change often (like the current user's cursor position) and one for values that change rarely (like authentication state). Consumers subscribe only to what they need.
When useMemo and useCallback Are the Right Call
There are real cases where memoization pays off:
- Expensive derived data: Sorting or filtering a list with hundreds of rows on every keystroke.
- Stable references for
React.memochildren: When a parent renders frequently and a child is demonstrably slow. - Dependency arrays for other hooks: Passing a stable callback or value into a
useEffectdependency array to avoid re-running effects unintentionally.
Outside these cases, the default should be: write the straightforward code, measure, and only add memoization when the Profiler points to a specific, measured problem.
A Practical Audit Approach
Run a search across your codebase for useMemo and useCallback. For each usage, ask:
- Is the computation genuinely expensive, or is it trivial?
- Is the child receiving this callback or value actually wrapped in
React.memo? - Did I measure a render slowdown before adding this?
You will likely find that removing a third or more of those hooks has no visible effect. The benefit: a simpler codebase with less noise for the next engineer reading it.
If your team is building or auditing a React application and performance issues have surfaced in production, Clixo can help. We do hands-on product engineering, not generic advice.