WritinguseMemo and useCallback Mistakes That Silently Hurt React Performance — LinkIT
5 min readreact, usememo, usecallback, performance, memoization

useMemo and useCallback Mistakes That Silently Hurt React Performance

Review useMemo and useCallback for unnecessary overhead, unstable references, missing dependencies, and context updates.

Memoization can avoid expensive work when dependencies stay unchanged. It also adds dependency tracking and cached values, so measure whether a particular use improves the interaction you are optimizing.

Why These Hooks Have a Cost

Both hooks compare dependencies between renders. useMemo caches a calculation result; useCallback caches a function reference without invoking it. Inline function creation still happens. See the useCallback reference.

Profile the slow interaction before adding memoization.

Mistake 1: Memoizing Trivial Computations

A cheap string concatenation or boolean check rarely needs caching solely to reduce computation:

// No measurable benefit — the comparison costs as much as the computation
const label = useMemo(() => `${firstName} ${lastName}`, [firstName, lastName]);

Consider useMemo for a measured expensive calculation or when a stable value reference lets another optimization work. A large list is a reason to profile, not proof that caching will help.

How to tell if a computation is expensive

Compare representative interactions with and without caching. Include dependency changes, target devices, and the cost of any child renders affected by reference identity.

Mistake 2: Using useCallback on Every Handler

A stable callback can help a memoized child skip rendering or keep another hook’s dependencies stable. Check how the receiving code uses the reference.

// useCallback here does nothing unless Button is React.memo'd
const handleClick = useCallback(() => {
  doSomething();
}, []);

A stable callback alone does not stop a child from rendering when its parent renders. It supplies a reusable reference that other memoization can use.

Mistake 3: Breaking Memoization with Inline Objects or Arrays

Inline object and array props can defeat a memoized child’s default prop comparison, even when its callback props are stable.

// This object is created fresh on every render — React.memo sees a new reference
return <Chart options={{ color: "blue", size: 12 }} />;

A newly created object prop compares unequal to its previous value under the default comparison. Consider passing simpler props or stabilizing a reference when profiling shows a benefit.

Mistake 4: Missing or Wrong Dependency Arrays

An empty dependency array means there are no reactive dependencies to invalidate the cache. Include all reactive values read by the calculation or callback; memoization is an optimization, not a guarantee that React retains a value forever.

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.

If dependencies change too often, restructure the calculation or effect instead of omitting values it reads.

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

A changed provider value updates consumers even when they use memo. Stabilizing an object value can avoid context-driven updates when its contents have not changed; it does not suppress unrelated parent-driven renders. See useContext.

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.memo children: When a parent renders frequently and a child is demonstrably slow.
  • Dependency arrays for other hooks: Passing a stable callback or value into a useEffect dependency 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:

  1. Is the computation genuinely expensive, or is it trivial?
  2. Is the child receiving this callback or value actually wrapped in React.memo?
  3. Did I measure a render slowdown before adding this?

Remove hooks that have no measured benefit and do not provide a needed stable reference. Check the interaction again after simplifying.

Discuss React performance with LinkIT.