Why React.memo Doesn't Always Help (And When It Does)

Wrapping a component in React.memo and finding it still re-renders on every parent render is one of the most common React performance frustrations. The fix is almost always the same — and it has nothing to do with memo itself.
Here's a situation most React developers encounter once and remember forever.
A component is re-rendering too often. You wrap it in React.memo. It still re-renders on every parent render. You stare at it. You add console.log to confirm. It's definitely still re-rendering. React.memo appears to be doing nothing.
This is not a bug in React.memo. It's a reference stability problem — and once you understand it, you'll never be confused by it again.
What React.memo actually compares
React.memo wraps a component and performs a shallow comparison of props between renders. If the props haven't changed, the component skips the render.
Shallow comparison means: for each prop, check if prevProp === nextProp. For primitive values — strings, numbers, booleans — this works perfectly. "hello" === "hello" is true. 42 === 42 is true.
For objects, arrays, and functions, === compares references — memory addresses. Two objects with identical contents are not equal if they're different instances:
This is why React.memo appears to do nothing when your parent component creates objects, arrays, or functions inline — they get new references on every render, and memo sees them as changed props.
The three unstable prop types
Inline objects
Every time Parent renders, { color: 'red', fontWeight: 'bold' } is a new object — different reference, same values. memo's shallow comparison sees a changed prop and re-renders Child.
Fix: move the object outside the component or stabilise it with useMemo.
For truly static values — styles, config objects, constants — moving them outside the component is always cleaner than useMemo. useMemo is for values that depend on props or state.
Inline arrays
Same logic. The array values are identical but the reference is new on every render. Move constants outside the component.
Inline functions
The function is recreated on every render. memo sees a new reference. Fix: useCallback.
Now MemoizedChild only re-renders when userId changes — not when count changes.
Measuring before optimising
Before adding memo, useCallback, and useMemo to a component, confirm there's a real problem.
The React DevTools Profiler is the right tool. Record a session, interact with the component you're concerned about, and examine the flame graph.
If the component renders in 0.3ms, it doesn't matter how often it re-renders. The optimisation overhead — the comparison in memo, the memoization in useCallback — might actually be more expensive than the render you're trying to prevent.
The rule: profile first, optimise second. memo is not a free operation.
When React.memo genuinely helps
After fixing reference instability, memo earns its place in these scenarios:
Large lists with expensive items
A list that renders 200 rows. Each row renders a component that does moderate work — formatting, conditional styling, nested children. When the parent's state changes (a filter, a sort, a different selected item), all 200 rows re-render even if only one changed.
With memo and a stable onSelect (via useCallback), clicking a row to select it only re-renders the previously-selected row (to clear its highlight) and the newly-selected row (to add the highlight). The other 198 rows skip their renders entirely.
Without memo: 200 renders. With memo: 2 renders. That's where the performance win is real and measurable.
Components that receive primitive props
Components whose props are only strings, numbers, and booleans benefit from memo without needing useCallback or useMemo as companions — primitives are compared by value, not reference.
status is a string, count is a number. Both compare by value. If the parent re-renders but neither value changed, StatusBadge skips its render without any additional setup.
Components below a high-frequency state owner
A parent component whose state updates frequently — a text input, a scroll handler, a clock — causes all its children to re-render on every update. If one of those children is expensive and its props don't change with every update, memo prevents unnecessary re-renders.
The custom comparator
React.memo accepts a second argument — a comparison function. Return true to skip the render (props are equal), false to allow it (props changed).
This is useful when a prop object has fields that update frequently but don't affect the component's output. The custom comparator ignores the irrelevant fields.
Use this sparingly. A custom comparator that's wrong will silently prevent renders that should happen — a worse bug than unnecessary renders.
The three-tool mental model
React.memo, useCallback, and useMemo form a system. They're designed to work together.
React.memo — decides whether to re-render based on props comparison useCallback — stabilises function props so memo's comparison works useMemo — stabilises object/array props so memo's comparison works
Using memo without stabilising the props it compares accomplishes nothing. Stabilising props with useCallback and useMemo without memo on the component also accomplishes nothing — the child still re-renders when the parent does.
All three, applied together where a real performance problem exists, in a component with stable primitive or stabilised reference props — that's when the combination genuinely helps.
When all props are either primitives or stabilised references, memo's shallow comparison can actually do its job.
Up next: The render props pattern is not dead — here's where it still wins — the cases where render props outperform hooks and compound components.
Related: Fine-grained reactivity in React — minimising re-renders at scale — the architecture-level complement to component-level memoisation.
Alex Chen is a senior frontend engineer who writes about React patterns, JavaScript internals, and the decisions that separate maintainable codebases from ones that fight back. Opinionated by design.
Photo by Shane Aldendorff on Unsplash

