useMemo and useCallback look like they solve the same problem. The first memoizes a computed value, the second memoizes a function reference. That difference matters far more than most tutorials admit, and conflating the two is where a large share of bad memoization code begins. One caches the result of work; the other caches the identity of a function so that identity stays stable across renders. Everything below flows from that distinction.

Top Mistakes Developers Make With useMemo and useCallback

Myth 1: Memoization Always Improves Performance

The most persistent myth is that adding useMemo or useCallback is a safe default — that wrapping a value or function can only help. In practice, both hooks add bookkeeping. React has to store the previous value, compare dependency arrays on every render, and decide whether to reuse or recompute. For a cheap calculation like a + b or a string concatenation, that overhead frequently exceeds the cost of just recomputing.

The reality is that memoization is a targeted fix for a measured problem, not a general-purpose optimization. Reach for it when:

  • A computation is expensive (sorting or filtering a large list, heavy transforms).
  • A value or function identity is a dependency in another hook’s dependency array.
  • A child component is wrapped in React.memo and depends on a stable prop reference to skip re-renders.

If none of those applies, the memo wrapper is likely dead weight.

Myth 2: useCallback Caches the Function’s Result

This is the most common conceptual error. useCallback(fn, deps) does not cache fn’s return value. It caches the function itself, returning the same reference across renders as long as deps are unchanged. The function still runs every time it’s called — the hook only prevents the reference from changing.

// useCallback caches the function reference, NOT the computed result
const expensiveResult = useCallback(() => {
  return items.sort((a, b) => a.rank - b.rank);
}, [items]);

// Every call re-sorts the array. The reference is stable; the work is not.

If the intent is to avoid a heavy recomputation, useMemo is the correct tool:

// useMemo caches the RESULT, recomputing only when deps change
const sortedItems = useMemo(() => {
  return [...items].sort((a, b) => a.rank - b.rank);
}, [items]);

Note the spread in the useMemo version. Sorting mutates the source array, and memoizing over a mutated array defeats the purpose of a pure dependency comparison. Return a new array or object from a memoized computation so the value on subsequent renders is deterministic.

Myth 3: Passing an Inline Function to useCallback Fixes It

A frequent pattern looks like this:

const handleClick = useCallback(() => {
  doSomething(id);
}, []); // Missing `id` in deps — stale closure

The dependency array is empty, but the callback closes over id. When id changes, the function still references the original value — a stale closure. This is a correctness bug, not a performance bug, and it typically shows up as a handler acting on the wrong item.

The fix is to list every reactive value the callback reads:

const handleClick = useCallback(() => {
  doSomething(id);
}, [id]);

There is a subtler trap here, though: adding id to the dependency array means the callback reference changes whenever id changes, which negates the reference stability that useCallback was supposed to provide. If id changes frequently and the callback is passed to a React.memo child, the memoization benefit disappears. In that scenario, the real fix is usually architectural — move the state down, or use a reducer that dispatches actions rather than closing over changing values.

Myth 4: useMemo Can Be Used for Side Effects

useMemo runs during render. Side effects — fetching data, mutating the DOM, calling console.log for debugging — do not belong there. React may invoke a useMemo factory more than once (for example, in Strict Mode during development), and it may discard the result without committing it. Anything with an observable side effect will run an unpredictable number of times.

The reality is that useMemo is for deriving values, not for triggering behavior. Use useEffect for effects, and a plain function call inside an event handler for user-triggered work.

// Wrong: side effect inside useMemo
const user = useMemo(() => {
  fetch('/api/user'); // Runs during render — do not do this
  return cachedUser;
}, []);

// Right: derive a value, keep side effects in useEffect
const user = useMemo(() => deriveUser(rawData), [rawData]);

useEffect(() => {
  fetch('/api/user');
}, []);

Myth 5: Every Function in a Component Needs useCallback

Blanket-wrapping every function in useCallback is a recognizable anti-pattern. It adds a dependency array to review, forces readers to reason about closure freshness, and provides no benefit when the function is passed to a plain DOM element or used only inside the component body.

The rule of thumb: wrap a function in useCallback only when its identity matters to something downstream. Those downstream consumers are typically:

  • A React.memo child that receives the function as a prop.
  • A useEffect that lists the function in its dependency array.
  • Another hook that depends on a stable function reference.

If none of those is true, a plain function declaration is clearer and cheaper.

Myth 6: useMemo Fixes Values That Change Every Render Anyway

A dependency array full of values that are recreated on every render — inline object literals, inline arrays, inline functions in the parent — makes useMemo a no-op. The comparison always returns “changed,” so the memoized value is recomputed on every render, and the only thing gained is the overhead of the comparison itself.

// The options object is new every render, so useMemo never helps
const filtered = useMemo(
  () => items.filter(applyOptions(options)),
  [items, options] // `options` is a fresh object each render
);

The fix is to stabilize options upstream — via useMemo, or by moving it out of the render path entirely — so that the dependency itself is stable before relying on useMemo to stabilize the derived value.

A Practical Checklist Before Adding Either Hook

Run through these questions before wrapping anything:

  1. Is the computation or function creation expensive enough to warrant caching? Measured, not assumed.
  2. Does anything downstream depend on the value’s or function’s identity?
  3. Are all dependencies listed correctly, including reactive values captured by closures?
  4. Will each dependency remain stable across renders when nothing relevant changes?
  5. Could the problem be solved by moving state, splitting components, or restructuring props instead?

If the answer to (1) or (2) is no, skip the hook. If the answer to (3) or (4) is no, the hook will not work as intended until that is fixed.

When Not to Reach for These Hooks

There are situations where memoization is the wrong answer, regardless of how the hooks are applied:

  • Unstable parent props. If a parent passes a new object or function on every render, memoizing the child has limited effect unless the parent is fixed first. Address the source.
  • Cheap computations. String formatting, small arithmetic, and simple property access rarely justify the comparison cost.
  • Overuse masking a design problem. If a component needs five useMemo calls and three useCallback calls to keep working, the deeper issue is usually component structure, not performance tuning.

Verifying Whether It Helped

Tools matter here more than intuition. React DevTools Profiler shows which components re-render and how often, which is the only reliable way to know whether a memo wrapper is doing anything. The “Highlight updates when components render” option gives a quick visual sense of render churn during interaction. React’s why-did-you-render library can flag shallow-equality failures that defeat memoization silently.

A useful pattern is to profile before adding the hook, add one hook at a time, and profile again. If the render count or commit time doesn’t move, remove the hook. Keeping unverified memo wrappers in the codebase adds maintenance cost with no measured benefit and makes future refactors harder to reason about.

Scenario useMemo useCallback
Expensive computation Apply Not applicable
Stable function reference for React.memo child Not applicable Apply
Cheap computation Skip Not applicable
Function used only inside component body Not applicable Skip
Value/function identity is a dependency of another hook Apply Apply
Dependencies change every render Fix deps first Fix deps first

The hooks are powerful and worth understanding well, but they are neither free nor universally beneficial. Treat them as precise tools: reach for them when a specific identity or computation cost is causing a problem you can point to in a profiler, and leave them out when the problem is hypothetical. Codebases that follow this discipline tend to have fewer memo calls and better performance than ones that scatter these hooks as a reflex.

Working through a specific memoization problem in your own React codebase? Describe the component structure and which hook you’re considering, and I can help you think through whether it’s the right tool.

Related guides: React Memoization: When and How to Use memo, useMemo, and useCallback, Optimizing Custom Hooks for Performance: A Beginner vs. Advanced Comparison, and The Real Performance Impact of Inline Functions and Objects in JSX.