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:
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?
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
- Understanding React’s Re-rendering Behavior
- Solutions to Prevent Unnecessary Re-renders
- Advanced Memoization Techniques
- Debugging React.memo Issues
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.
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:
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.
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:
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.
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:
// 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.
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:
// 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:
- State changes: When a component’s state changes, it and all its descendants re-render
- 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:
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:
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:
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:
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:
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:
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:
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:
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:
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:
// 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:
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:
- Function props being recreated on every parent render (solve with
useCallback) - Object/array props with new references (solve with
useMemo) - Context changes affecting the component (solve by isolating context values)
- Children prop issues (solve by memoizing children or passing as props)
- 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
- React Documentation - memo
- Why React.Memo() keeps rendering my component - Stack Overflow
- Using React.memo to Avoid Unnecessary Rerenders - Kyle Shevlin
- Fixing memoization-breaking re-renders in React - Sentry Blog
- React memo keeps rendering when props have not changed - Stack Overflow
- Avoiding React component re-renders with React.memo - Ahead Creative