LEARN · DEBUGGING GUIDE

React.memo Fails to Prevent Re-render: Deep Props, Functions, and Traps

React.memo should prevent pointless renders, but it often doesn't. If your component still updates, you need to nail down exactly why—this guide shows you how.

IntermediateReact bugs5 min read

What this usually means

React.memo only does a shallow prop comparison. If your memoized component receives new references to objects, arrays, or inline functions—even if their contents are identical—React treats them as new and re-renders. Unstable references (like inline functions or freshly constructed objects in JSX), as well as context value changes or missed custom comparison logic, are the usual suspects. Sometimes, rerenders are mistakenly blamed on React.memo, when context or hooks are the real culprits.

( 01 )Fast diagnosis

The first ten minutes — establish facts before touching code.

  • 1Enable React DevTools Profiler and record renders of the memoized component; verify if props have actually changed.
  • 2Console.log the props passed to the memoized component each render; inspect for identity changes on objects/arrays/functions.
  • 3Replace inline functions/objects in parent JSX with stable useCallback/useMemo hooks and retest.
  • 4Temporarily swap React.memo with React.PureComponent and compare render frequency.
  • 5Check if the component consumes any context—context changes will always trigger re-renders.
( 02 )Where to look

The specific files, logs, configs, and dashboards that usually own this bug.

  • searchParent component render method or function body (look for new objects or functions passed as props)
  • searchThe props signature in the memoized component (spot props that can change identity despite value equality)
  • searchReact DevTools Profiler (focus on why did this render)
  • searchAny hooks in parent: useCallback, useMemo (make sure dependencies are correct)
  • searchThe custom comparison function (second argument to React.memo, if present)
  • searchContext providers/consumers used in, or above, the memoized component
( 03 )Common root causes

Practical causes, not theory. These are the things you will actually find.

  • warningPassing inline arrow functions as props (e.g., onClick={() => ...})
  • warningPassing new arrays/objects directly in JSX rather than memoizing with useMemo
  • warningForgetting that context changes always break memoization
  • warningUnstable dependencies in useCallback/useMemo in parent components
  • warningMissing or incorrect custom comparison function in React.memo
  • warningDeriving props from state in parent in a way that creates new references each render
( 04 )Fix patterns

Concrete fix directions. Pick the one that matches your root cause.

  • buildWrap callback props with useCallback in parent components (e.g., const cb = useCallback(() => ..., []))
  • buildMemoize derived objects/arrays with useMemo before passing as props
  • buildAvoid creating new inline objects/functions in JSX or render
  • buildIf props are complex (objects, arrays), provide a custom areEqual function to React.memo
  • buildMove context consumers away from the memoized component if possible to isolate context-triggered updates
  • buildAudit dependency arrays for useCallback/useMemo to ensure stable references across renders
( 05 )How to verify

A fix you cannot prove is a guess. Close the loop.

  • verifiedUse React DevTools Profiler to confirm the memoized component no longer re-renders unnecessarily
  • verifiedLog props and verify their references are stable across renders (e.g., === comparison holds)
  • verifiedRun a performance test and observe reduced render counts for the component
  • verifiedTemporarily add a console.log in the component body and verify it only logs when actual prop changes occur
  • verifiedConfirm via visual regression (UI doesn't reset unexpectedly on unrelated parent updates)
( 06 )Mistakes to avoid

Things that make this bug worse or harder to find.

  • warningAssuming React.memo will do deep equality checks—it doesn't
  • warningForgetting that context changes always ignore memoization
  • warningUsing unstable inline functions or objects in props repeatedly
  • warningBlaming React.memo before checking actual prop reference changes
  • warningAdding useCallback/useMemo everywhere without understanding dependencies, leading to stale closures
( 07 )War story

Unexpected Re-renders with React.memo-wrapped ListItem

Frontend EngineerReact 18.2, TypeScript, Chrome React DevTools, Redux Toolkit

Timeline

  1. 09:15Triage slow list re-renders after user feedback
  2. 09:18Notice ListItem component wrapped in React.memo but still re-rendering
  3. 09:21Use React DevTools Profiler; see ListItem re-rendering on every parent List update
  4. 09:23Console log props in ListItem; see onClick prop is a new function each render
  5. 09:25Wrap onClick handler in useCallback in List; rerun, see stable references
  6. 09:29Profiler now shows re-renders only when actual item props change
  7. 09:33Close incident after confirming resolution with reduced render count

I got a bug report about our list UI lagging when updating filter selections. The profiler showed our supposedly memoized ListItem was constantly re-rendering. My first guess was prop changes, so I sprinkled console.logs into ListItem and saw the onClick prop kept changing identity, even though its logic didn't vary.

Turns out, the parent List component created a new inline arrow function for every item on every render—classic rookie mistake, but easy to overlook when moving fast. Wrapping the onClick handler in useCallback fixed the problem instantly. Now, ListItem only re-renders when its actual data changes.

The tricky part: the code review that added React.memo never checked how props were constructed. Memoization isn't magic. You have to guarantee prop reference stability, or it's a no-op.

Root cause

Inline arrow functions passed from parent List to ListItem caused new references, so React.memo couldn't block renders.

The fix

Wrapped List's onClick factory in useCallback with correct dependencies, ensuring ListItem got a stable handler prop.

The lesson

React.memo only optimizes render if you also keep props stable—watch for invisible footguns like new functions or objects.

( 08 )How React.memo Actually Compares Props

React.memo uses Object.is for a shallow comparison of each prop key. It treats any new reference for objects, arrays, or functions as a change. If your prop is {a: 1}, creating it inline every render produces a fresh reference and forces a rerender—even if the contents are identical.

If you need deep equality (rare, expensive), you must provide a custom areEqual function as the second argument. For most apps, the real fix is to stabilize references before passing them.

( 09 )Detecting Problematic Prop References

To find unstable props, log them with console.log(props) in the memoized component, then compare reference equality between renders. In DevTools, using the 'why did this render' plugin can pinpoint which prop changed (by reference or value).

If you see functions or objects that aren't === their previous version, they're likely breaking memoization. The culprit usually lives in the parent component.

( 10 )Functions, useCallback, and Stale Closures

Inline handlers (e.g., onClick={() => ...}) break memoization every time. useCallback solves this, but only if its dependencies are set correctly.

If your callback needs state from the parent, be careful: setting dependencies wrongly can recreate the function every render—the same original problem. Use useCallback(fn, [relevantDeps]), no more, no less.

( 11 )Context, useContext, and When Memoization Fails

Any context change triggers all descendent consumers to rerender, regardless of React.memo. This is a known architectural limit. If you wrap a context-consuming component in React.memo, it does nothing for context-induced updates.

If memoization is critical, move context consumption to a higher level and pass results as stable props to children where possible.

Frequently asked questions

Does React.memo do deep comparison of props?

No. React.memo only does shallow comparison using Object.is. Deeply nested objects or arrays will trigger rerenders if their reference changes, even if their contents are equal.

Why does my memoized component rerender when props look the same?

The most common reason is that props are objects, arrays, or functions created inline, so their reference changes every render. Use useMemo/useCallback to stabilize them.

Can React.memo prevent rerenders caused by context changes?

No. Any context value change will force all context consumer components to rerender, regardless of React.memo.

When should I write a custom areEqual function for React.memo?

Only when shallow comparison isn't enough: if you pass complex props and want to skip renders based on deep equality. Otherwise, stabilize references with useMemo/useCallback.

Is React.memo always worth using?

No. For most components, the extra cost of memoization outweighs its benefits unless you have real render bottlenecks. Profile first, optimize later.