Skip to content

Fix: React.memo Not Preventing Re-renders

FixDevs ·

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.

The Error

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.

Why This Happens

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.

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.

Why this matters: JavaScript creates a new object or array literal every time a function executes. { theme: "dark" } in a render creates a new object on every render — even though the content is identical, Object.is(obj1, obj2) returns false because they are different references. React.memo compares references, not deep values.

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

Common Mistake: Wrapping every function in useCallback “just in case.” useCallback itself has overhead — only use it when the function is passed as a prop to a memoized component, used as a dependency in another useEffect or useMemo, or otherwise needs a stable reference.

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.

Real-world scenario: A dashboard with 50+ chart components all re-rendering when a single counter updates is a good candidate for React.memo. A simple navigation bar that re-renders when page state changes is probably not worth memoizing — it renders in microseconds regardless.

Still Not Working?

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.

For related performance issues, see Fix: React Too Many Re-renders and 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