Skip to content

Fix: React.memo Not Preventing Re-renders

FixDevs · (Updated: )

Part of:  React & Frontend Errors

Quick Answer

How to fix React.memo not working — components still re-rendering despite being wrapped in memo, caused by new object/function references, missing useCallback, and incorrect comparison functions.

When React.memo Quietly Does Nothing

Personally, I think React.memo is one of the most misunderstood APIs in React. The wrapping looks like protection, but it only works if every single prop maintains reference equality across renders. One inline object, one inline function, and the memo silently does nothing. I learned to open the Profiler before adding memo and confirm WHY a component re-renders. You wrap a component in React.memo() to prevent unnecessary re-renders, but the component still re-renders every time the parent renders. There is no error; the optimization simply does not work.

Common symptoms:

  • A component wrapped in React.memo re-renders on every parent render.
  • Adding React.memo has no measurable performance improvement.
  • The React DevTools Profiler shows the memoized component highlighted on every render.
  • A component re-renders even when its props appear unchanged.

Quick Reference Before You Dive In

If you arrived here from Google with a memo that does not work, the five facts that resolve roughly 90 percent of cases:

  1. React.memo does SHALLOW reference equality (Object.is) on props. {a: 1} !== {a: 1} because the references differ even if the values look identical. The React.memo documentation and the useMemo reference are the canonical sources.
  2. Inline objects and arrays as props ALWAYS break memo. <Child config={{theme: "dark"}} /> creates a new object every render. Wrap in useMemo in the parent.
  3. Inline functions as props ALWAYS break memo. onClick={() => doSomething()} is a new function every render. Wrap in useCallback.
  4. React.memo does NOT block context updates. A consumer using useContext re-renders when context changes regardless of memo. Split the context or pass values as props.
  5. React DevTools Profiler shows WHY a component rendered. “Props changed”, “Context changed”, “Hooks changed”, or “Parent rendered”. Use it before adding any memoization.

The rest of this article walks through each cause in detail, plus the failure modes most other guides skip.

Why React.memo Quietly Fails

React.memo performs a shallow comparison of props between renders. It skips re-rendering only if all props pass Object.is() equality. The optimization fails when:

  • Props include new object or array literals on every render: {} and [] create a new reference each time, so shallow comparison always finds them different.
  • Props include inline functions: () => {} creates a new function reference on every render.
  • A parent’s context or state change causes a re-render that produces genuinely new prop values.
  • The component uses useContext: context changes bypass React.memo entirely.
  • The comparison function is wrong: a custom comparator passed to React.memo has a bug.
  • Children prop is passed: JSX elements (<div>...</div>) are objects and always create new references.

A more fundamental point that often gets missed: React.memo is a hint, not a guarantee. React reserves the right to re-render a memoized component anyway (for example, after a Concurrent rendering interruption, or when React’s internal heuristics decide it is faster to re-render than to remember the previous result). This means relying on React.memo for correctness (e.g., to avoid running a side effect) is a bug. It is only an optimization. The actual contract is “skip rendering if props are shallow-equal, when it is convenient.”

The shallow comparison itself is also stricter than most developers expect. Object.is({a: 1}, {a: 1}) is false because the two object literals occupy different memory locations. Even returning the same data from useMemo with a different dependency array gives a new reference. Every render in a function component is a fresh execution of the function body, and every literal in that body is a new allocation.

Platform and Environment Differences

React.memo behaves differently depending on which React renderer and integration you are running. Knowing your environment narrows the root cause significantly.

React 18+ Concurrent Renderer. In Concurrent mode, React can pause, abort, and restart renders. A memoized component may render more than once for what looks like a single update because React threw away the first attempt. The DevTools Profiler labels these “render aborted”; they are not bugs in your memo logic. Do not chase these.

Strict Mode double-invoke (development only). <React.StrictMode> intentionally invokes function components twice in development to surface side effects. This makes React.memo look broken because every render shows up doubled in DevTools. In production builds, the doubling stops. Always benchmark in a production build (vite build, next build, etc.) before concluding React.memo is failing; see React Strict Mode double render.

Next.js App Router (Server Components). In the App Router (app/ directory), Server Components do not re-render on the client. Wrapping a Server Component in React.memo is meaningless; the component is rendered once on the server and streamed as serialized output. React.memo only applies to Client Components (files marked with "use client"). If you put React.memo in a server file, the bundler will either silently strip it or throw a build error.

Next.js Pages Router. The Pages Router renders everything as a Client Component after hydration. React.memo works there the same way as a plain React app. However, the SSR-then-hydrate cycle means the first render is always “Props changed” because the server rendered different references than the client recreates on hydration. Profile after hydration, not during.

React Server Components in general. Memoization on the server is handled by React’s request-scoped cache (cache() from react), not React.memo. Putting memo() around a server component will work in the sense that it does not error, but it does not deduplicate server renders.

React Native. RN uses the same reconciler but renders to native views via the JS-to-native bridge. A re-render in a memoized component still costs a bridge message. The cost calculus is different: even cheap-looking re-renders (a <View> with one text child) can hurt scroll performance because each crosses the bridge. Memoize aggressively in RN list cells (FlatList renderItem).

React Compiler (experimental, RC May 2025). The React Compiler automatically inserts memoization for components and values. When the compiler is enabled, manual React.memo and useCallback often become redundant; the compiler analyzes your component and inserts equivalent memoization automatically. If you have the compiler enabled and a memoized component is still re-rendering, the compiler may have intentionally chosen not to memoize that prop because its analysis decided the comparison cost outweighed the saving. Read the compiler’s output annotations in the Babel plugin’s log.

Preact compat. If you are using preact/compat, memo is implemented but uses Preact’s reconciler, which has slightly different scheduling. Behavior is mostly identical but profiling tools may behave differently.

Hot Module Replacement (HMR). Vite, Next.js, and Create React App with Fast Refresh sometimes invalidate memo caches when a component file changes. After HMR, the first render is always a fresh render; this is not a bug in your code.

When to Use Which Fix

The next seven sections cover the fixes in detail. The table below maps your situation to the recommended fix.

Your situationRecommended fixWhy
Inline object / array propFix 1: useMemo for the prop valueStable reference between renders
Inline function propFix 2: useCallback for the functionStable function reference
Component re-renders on context changeFix 3: split context, or pass prop insteadMemo does not block context
JSX passed as childrenFix 4: useMemo the childrenEach render creates new elements
Complex prop needs deep compareFix 5: custom comparator (carefully)Custom equality logic
Not sure WHY component re-rendersFix 6: React DevTools Profiler”Props / Context / Hooks / Parent”
Considering memo for a trivial componentFix 7: skip it; memo has overheadProfile first

If multiple rows apply, pick the topmost one.

Fix 1: Memoize Object and Array Props with useMemo

If you pass an object or array as a prop, create it with useMemo so its reference only changes when the data changes:

Broken: new object on every render:

function Parent() {
  const [count, setCount] = useState(0);

  return (
    <MemoizedChild
      config={{ theme: "dark", size: "large" }} // New object every render
      items={[1, 2, 3]}                          // New array every render
    />
  );
}

const MemoizedChild = React.memo(function Child({ config, items }) {
  console.log("Child rendered"); // Runs every time Parent renders
  return <div>{config.theme}</div>;
});

Fixed: stable references with useMemo:

function Parent() {
  const [count, setCount] = useState(0);

  const config = useMemo(() => ({ theme: "dark", size: "large" }), []);
  const items = useMemo(() => [1, 2, 3], []);

  return (
    <MemoizedChild config={config} items={items} />
  );
}

Now config and items have the same reference between renders (as long as their dependencies don’t change), so React.memo’s shallow comparison finds them equal and skips re-rendering.

The reason this matters: every time a function component renders, every literal in its body is a fresh allocation. { theme: "dark" } written inline in JSX creates a NEW object reference on every render even though the content is identical. Object.is compares references, not deep values; the comparison fails. useMemo produces a stable reference that survives across renders as long as its dependencies do not change.

Fix 2: Memoize Function Props with useCallback

Functions passed as props also create new references on every render:

Broken: inline function as prop:

function Parent() {
  const [count, setCount] = useState(0);

  return (
    <MemoizedButton
      onClick={() => console.log("clicked")} // New function every render
    />
  );
}

Fixed: stable function with useCallback:

function Parent() {
  const [count, setCount] = useState(0);

  const handleClick = useCallback(() => {
    console.log("clicked");
  }, []); // Empty deps; function never changes

  return <MemoizedButton onClick={handleClick} />;
}

When the callback depends on state or props, include them in the dependency array:

const handleSubmit = useCallback(() => {
  submitForm(formData); // formData changes when user types
}, [formData]); // Re-create only when formData changes

A specific mistake I have seen in production code: wrapping EVERY function in useCallback “just in case.” The wrap itself has overhead and adds dependency-array maintenance to your code. Reach for useCallback only when the function is passed as a prop to a memoized component, used as a dependency in useEffect / useMemo, or otherwise needs a stable reference for an identifiable reason.

Fix 3: React.memo Does Not Block Context Changes

React.memo only checks props. If a component consumes a context with useContext, it re-renders whenever the context value changes, regardless of React.memo:

const ThemeContext = createContext("light");

const MemoizedChild = React.memo(function Child() {
  const theme = useContext(ThemeContext); // Context bypasses memo
  return <div className={theme}>Content</div>;
});

function Parent() {
  const [theme, setTheme] = useState("light");

  return (
    <ThemeContext.Provider value={theme}>
      <MemoizedChild />  {/* Re-renders on every theme change; expected */}
    </ThemeContext.Provider>
  );
}

This is expected behavior. Solutions:

Split context into stable and frequently-changing parts (see Fix: React Context not updating for the splitting pattern).

Move context consumption up and pass the value as a prop. React.memo can then prevent re-renders when the context-derived prop does not change.

Fix 4: Memoize the children Prop

If you pass JSX as children, each render creates new React element objects:

Broken: children bypasses memo:

<MemoizedLayout>
  <HeavyComponent />  {/* New React element object every render */}
</MemoizedLayout>

Fixed: memoize children explicitly:

const children = useMemo(() => <HeavyComponent />, []);

<MemoizedLayout>{children}</MemoizedLayout>

Or restructure so the memoized component does not need to accept children; pass data props instead and let it render its own children.

Fix 5: Use a Custom Comparison Function

For deep comparison of complex props, pass a custom comparator as the second argument to React.memo:

function areEqual(prevProps, nextProps) {
  // Return true to skip re-render (props are equal)
  // Return false to re-render (props are different)
  return (
    prevProps.user.id === nextProps.user.id &&
    prevProps.user.name === nextProps.user.name &&
    prevProps.onUpdate === nextProps.onUpdate
  );
}

const UserCard = React.memo(function UserCard({ user, onUpdate }) {
  return (
    <div>
      <p>{user.name}</p>
      <button onClick={onUpdate}>Update</button>
    </div>
  );
}, areEqual);

Warning: Deep comparison has its own cost. For very large objects, the comparison itself may be slower than just re-rendering. Profile before using custom comparators.

Do not use JSON.stringify for comparison: it is slow, does not handle circular references, and ignores functions:

// Avoid this: slow and incorrect for functions / undefined
const areEqual = (prev, next) =>
  JSON.stringify(prev) === JSON.stringify(next);

Fix 6: Verify with React DevTools Profiler

Before optimizing, confirm which component is re-rendering and why:

  1. Install React Developer Tools.
  2. Open DevTools → Profiler tab.
  3. Click Record, interact with your app, click Stop.
  4. Click on any component bar in the flame chart.
  5. The panel shows why the component rendered: “Props changed”, “Context changed”, “Hooks changed”, or “Parent rendered”.

If the Profiler shows “Props changed”, click into it to see which specific prop changed. This points directly to which prop needs memoization.

Enable “Highlight updates” in React DevTools:

Settings → General → “Highlight updates when components render.” Components flash blue when they re-render. A component that flashes constantly despite being memoized has a reference stability problem.

Fix 7: When NOT to Use React.memo

React.memo adds complexity and has overhead. Skip it when:

  • The component renders quickly: memoization overhead may exceed the rendering cost.
  • Props always change: if every render produces genuinely new prop values (e.g., a timer updating every second), memoization never helps.
  • The component rarely re-renders anyway: only leaf components that render frequently are worth memoizing.
  • The component is small: a simple <Button> with minimal DOM output costs almost nothing to re-render.

Focus optimization effort on:

  • Components that render large lists (use React.memo + useCallback for list item components).
  • Components with expensive computations (use useMemo for the computation itself).
  • Components deep in the tree that are re-rendering due to unrelated parent state changes.

A specific scenario where React.memo genuinely earned its keep for me: a dashboard with 50+ chart components, each of which ran a non-trivial reduce / sum on data. A single counter update at the top would trigger every chart to recompute. Wrapping each chart in memo with stable prop references dropped CPU usage from “scroll-laggy” to “instant.” That kind of measurable win is what memo is for; a navigation bar that renders in microseconds is not.

Stranger Causes I Have Tracked Down

Check if the component is defined inside another component. A component defined inside a render function gets a new reference on every render, breaking memoization:

// Broken: new component type every render
function Parent() {
  const Child = React.memo(() => <div>Child</div>); // New type each render!
  return <Child />;
}

// Fixed: define outside
const Child = React.memo(() => <div>Child</div>);

function Parent() {
  return <Child />;
}

Check for key prop changes. If the key prop on a memoized component changes, React unmounts and remounts the component entirely; React.memo is irrelevant. A changing key is intentional when you want to reset component state.

Check React.StrictMode double renders. In development with <React.StrictMode>, React intentionally renders components twice to detect side effects. This double render appears in DevTools but does not happen in production. Memoization works correctly in production even if DevTools shows extra renders in development.

Check whether the React Compiler is double-memoizing. If you have the React Compiler enabled, it may already insert memoization automatically. Adding manual React.memo on top is harmless but can confuse the compiler’s heuristics. Disable manual memoization temporarily and check the compiler’s Babel output to confirm the component is being memoized automatically.

Check for the destructured-default-prop trap. If you destructure with a default value like function Child({ items = [] }), the default [] is created on every render inside the child after props pass shallow comparison. But React.memo compares incoming props, so the default does not affect memo behavior. However, if the parent passes items={items || []} instead, every render creates a new array reference. Move the fallback to a useMemo in the parent, or accept undefined and handle it inside the child.

Check for parent-level state thrashing. A common pattern is a parent that calls setState inside an effect that runs every render, causing the parent to re-render in a loop and forcing memoized children to be evaluated for shallow equality on every tick. If memoized children skip rendering but the parent never settles, the perceived “memo not working” is actually too many re-renders at the parent level.

What Other Tutorials Get Wrong About React.memo

Most React tutorials list the same fixes but frame them in ways that produce subtle bugs.

They show React.memo examples WITHOUT useMemo / useCallback for the props. That is the most common false-positive in the React performance literature: the wrapped component still re-renders, the article never explains why. Inline objects and inline functions are the universal cause.

They omit the context-bypass rule. React.memo does NOT block context updates. Articles that show useContext inside a memoized component without flagging this leave readers wondering why memo is “broken.”

They miss the children problem. JSX passed as children is a new element object every render; memo cannot stop the re-render. Articles that show <MemoLayout><Heavy/></MemoLayout> examples produce the bug while pretending to fix it.

They recommend useCallback everywhere. It has overhead and adds dependency-array maintenance. Use it ONLY when the function is passed to a memoized child, used as a dep in useEffect / useMemo, or otherwise needs reference stability for a reason you can name.

They miss Strict Mode double-render. Development renders run twice under <React.StrictMode> to surface side effects. Articles that benchmark in dev conclude memo is broken when production-only timing shows it works correctly. Always profile in a production build.

They confuse the React Compiler’s auto-memoization. When the Compiler is enabled, manual React.memo and useCallback often become redundant. Articles written pre-Compiler can recommend patterns the Compiler now handles automatically.

Frequently Asked Questions

Why does React.memo not work when I pass {theme: "dark"} inline?

Every render creates a NEW object literal. Object.is({}, {}) is false even when the contents look identical. React.memo performs shallow reference equality on props, finds the new object different from the previous one, and re-renders. Wrap the object in useMemo in the parent.

Does React.memo work with useContext inside the component?

No. Context changes bypass React.memo entirely. A consumer using useContext re-renders when the context value changes regardless of React.memo. To prevent unnecessary re-renders, split the context into stable and changing parts, or read the context in a parent and pass the value as a prop.

Should I wrap every component in React.memo?

No. React.memo has overhead (the comparison itself takes time) and only helps when a component re-renders frequently with stable props. For components that always receive new props, or that are cheap to render, memo costs more than it saves. Profile first.

What is the difference between React.memo, useMemo, and useCallback?

React.memo wraps a component, skipping re-render when props are shallow-equal. useMemo memoizes a VALUE inside a component, recomputing only when dependencies change. useCallback memoizes a FUNCTION reference. The three work together: memo skips re-renders, useMemo / useCallback ensure the props you pass to memo’d components have stable references.

Why does my memoized component still re-render after key prop changes?

A changing key causes React to unmount and REMOUNT the component, which is a fresh creation, not a re-render. React.memo has nothing to do with this case. If you do not want remount behavior, make the key stable.

Will the React Compiler make React.memo obsolete?

For most code, yes. The compiler analyzes components and inserts memoization automatically. Manual React.memo and useCallback will become redundant for code the compiler processes. The transition is gradual; manual memoization remains valuable until the compiler is universally adopted.

For related performance issues, see Fix: React useEffect Infinite Loop.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles