NeuroAgent

React.memo Not Preventing Re-renders: Complete Guide

Learn why React.memo fails to prevent re-renders when props haven't changed. Discover solutions like useCallback, useMemo, and custom comparison functions to optimize React performance.

Why does my React component re-render even when props haven’t changed?

I’m working on a React app where a child component keeps re-rendering even when the props passed to it remain unchanged. I’m using React.memo, but the re-render still happens whenever the parent updates. Here’s my sample code:

javascript
const Child = React.memo(({ name }) => {
  console.log("Rendered");
  return <div>{name}</div>;
});

function Parent() {
  const [count, setCount] = useState(0);
  return (
    <>
      <Child name="Yuvraj" />
      <button onClick={() => setCount(count + 1)}>Click {count}</button>
    </>
  );
}

What could be causing this behavior, and how can I prevent unnecessary re-renders in my React application?

NeuroAgent

React.memo works by memoizing a component’s rendered output, but there are several common scenarios where it might not prevent re-renders even when props appear unchanged. The main reasons your Child component is still re-rendering include:

Contents


Common Causes of React.memo Not Working

1. Function Props are Re-created on Parent Re-render

The most frequent issue is when memoized components receive function props that get recreated on every parent render. Even if the function logic doesn’t change, a new reference is created.

javascript
function Parent() {
  const [count, setCount] = useState(0);
  
  // This function is recreated on every render
  const handleClick = () => {
    console.log("Button clicked");
  };

  return (
    <>
      <Child name="Yuvraj" onClick={handleClick} />
      <button onClick={() => setCount(count + 1)}>Click {count}</button>
    </>
  );
}

Solution: Use useCallback to memoize functions:

javascript
const handleClick = useCallback(() => {
  console.log("Button clicked");
}, []); // Empty dependency array means this function never changes

2. Object/Array Props with New References

React.memo performs shallow comparison by default. If you pass objects or arrays as props, they maintain the same value but get new references on each parent render.

javascript
function Parent() {
  const [count, setCount] = useState(0);
  
  // This object is recreated on every render
  const user = { name: "Yuvraj", age: 25 };

  return (
    <>
      <Child user={user} />
      <button onClick={() => setCount(count + 1)}>Click {count}</button>
    </>
  );
}

Solution: Use useMemo to memoize objects/arrays:

javascript
const user = useMemo(() => ({ name: "Yuvraj", age: 25 }), []);

3. Context Changes

If your memoized component uses React Context, any change in context value will cause a re-render regardless of memoization.

javascript
const ThemeContext = React.createContext();

function Parent() {
  const [count, setCount] = useState(0);
  const [theme, setTheme] = useState('light');

  return (
    <ThemeContext.Provider value={theme}>
      <Child name="Yuvraj" />
      <button onClick={() => setCount(count + 1)}>Click {count}</button>
    </ThemeContext.Provider>
  );
}

Solution: Pass only the context values the component needs, or use context selectors:

javascript
// Pass specific context value as prop
<Child 
  name="Yuvraj" 
  theme={theme} 
/>

4. Children Prop Issues

The children prop is treated like any other prop. If you pass JSX elements directly as children, they get recreated on each render.

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

  return (
    <>
      <div>
        <Child>
          <span>Content</span>
        </Child>
      </div>
      <button onClick={() => setCount(count + 1)}>Click {count}</button>
    </>
  );
}

Solution: Memoize children or pass them as props:

javascript
// Option 1: Memoize children
const memoizedChildren = useMemo(() => <span>Content</span>, []);

// Option 2: Pass children as props
<Child content={<span>Content</span>} />

Understanding React’s Re-rendering Behavior

React re-renders components for two main reasons:

  1. State changes: When a component’s state changes, it and all its descendants re-render
  2. Prop changes: When a component receives new props, it re-renders

React.memo only addresses the second issue - it prevents re-rendering when props haven’t changed. However, it doesn’t prevent the initial re-render that happens when the parent component updates state.

As Kyle Shevlin explains: “If a component updates, then it rerenders everything in that component. This is necessary. The new state of the component may affect anything rendered below it.”


Solutions to Prevent Unnecessary Re-renders

1. Custom Comparison Function

For complex props, you can provide a custom comparison function to React.memo:

javascript
const Child = React.memo(({ name, user }) => {
  console.log("Rendered");
  return <div>{name} - {user.name}</div>;
}, (prevProps, nextProps) => {
  // Custom comparison logic
  return prevProps.name === nextProps.name && 
         prevProps.user.name === nextProps.user.name;
});

2. Use useCallback for Event Handlers

Always memoize functions passed to child components:

javascript
function Parent() {
  const [count, setCount] = useState(0);
  
  const handleClick = useCallback(() => {
    console.log("Button clicked");
  }, []);

  return (
    <>
      <Child name="Yuvraj" onClick={handleClick} />
      <button onClick={() => setCount(count + 1)}>Click {count}</button>
    </>
  );
}

3. Use useMemo for Complex Values

Memoize objects, arrays, and computed values:

javascript
function Parent() {
  const [count, setCount] = useState(0);
  
  const userInfo = useMemo(() => ({
    name: "Yuvraj",
    details: { age: 25, location: "India" }
  }), []);

  return (
    <>
      <Child userInfo={userInfo} />
      <button onClick={() => setCount(count + 1)}>Click {count}</button>
    </>
  );
}

4. Separate Components for Different Concerns

Split components to isolate frequently changing state:

javascript
function Parent() {
  const [count, setCount] = useState(0);
  
  // This component only re-renders when count changes
  const Counter = () => (
    <button onClick={() => setCount(count + 1)}>Click {count}</button>
  );

  return (
    <>
      <Child name="Yuvraj" />
      <Counter />
    </>
  );
}

Advanced Memoization Techniques

1. Context with Custom Selectors

For context-heavy applications, create custom selectors:

javascript
const useThemeSelector = () => {
  const theme = useContext(ThemeContext);
  return useMemo(() => ({
    isDark: theme === 'dark',
    isLight: theme === 'light'
  }), [theme]);
};

const Child = React.memo(({ name }) => {
  const { isDark } = useThemeSelector();
  return <div style={{ color: isDark ? 'white' : 'black' }}>{name}</div>;
});

2. React DevTools Profiler

Use React DevTools to identify re-rendering bottlenecks:

javascript
import { Profiler } from 'react';

function Parent() {
  return (
    <Profiler id="Parent" onRender={onRenderCallback}>
      <Child name="Yuvraj" />
      <button onClick={() => setCount(count + 1)}>Click {count}</button>
    </Profiler>
  );
}

3. useEffect Cleanup for Subscription Management

If your component uses subscriptions, ensure proper cleanup:

javascript
const Child = React.memo(({ userId }) => {
  const [data, setData] = useState(null);
  
  useEffect(() => {
    const subscription = fetchData(userId).subscribe(setData);
    return () => subscription.unsubscribe();
  }, [userId]);

  return <div>{data ? data.name : 'Loading...'}</div>;
});

Debugging React.memo Issues

1. Add Render Logging

Add console.log statements to identify when and why components render:

javascript
const Child = React.memo(({ name }) => {
  console.log("Child rendered with name:", name);
  return <div>{name}</div>;
});

2. Check Prop References

Use React DevTools to inspect prop values and references:

javascript
const Child = React.memo(({ name, onClick }) => {
  console.log("Props:", { name, onClick });
  console.log("onClick reference changed:", onClick !== prevProps.onClick);
  return <div onClick={onClick}>{name}</div>;
});

3. Test with Simple Props

Test with primitive props first to isolate the issue:

javascript
// Test with primitive props first
const Child = React.memo(({ name, count }) => {
  console.log("Rendered with primitive props");
  return <div>{name} - {count}</div>;
});

// Then gradually add complex props

4. Use React.memo with Empty Dependencies

For truly static components, use React.memo with no dependencies:

javascript
const StaticChild = React.memo(() => {
  console.log("Static component rendered");
  return <div>Static Content</div>;
});

Conclusion

React.memo is a powerful optimization tool, but it has limitations. The main reasons your component might still re-render despite unchanged props include:

  1. Function props being recreated on every parent render (solve with useCallback)
  2. Object/array props with new references (solve with useMemo)
  3. Context changes affecting the component (solve by isolating context values)
  4. Children prop issues (solve by memoizing children or passing as props)
  5. Custom comparison needs (solve with custom comparison function)

The key to effective memoization is understanding React’s shallow comparison behavior and carefully managing prop references. Start with simple fixes like useCallback and useMemo, then move to more advanced techniques like custom comparison functions when needed.

Remember that memoization comes with a small performance overhead, so use it strategically only for components that are actually expensive to render or re-render frequently.

Sources

  1. React Documentation - memo
  2. Why React.Memo() keeps rendering my component - Stack Overflow
  3. Using React.memo to Avoid Unnecessary Rerenders - Kyle Shevlin
  4. Fixing memoization-breaking re-renders in React - Sentry Blog
  5. React memo keeps rendering when props have not changed - Stack Overflow
  6. Avoiding React component re-renders with React.memo - Ahead Creative