You wrote a simple component. A list, a filter, a form. Nothing fancy. But when you open React DevTools and hit the profiler, your innocent little component re-renders forty-seven times on a single interaction. Where did things go wrong?
Re-renders are React’s mechanism for keeping the DOM in sync with state. In theory, this is elegant. In practice, it’s a performance swamp that sneaks up on you. Most developers underestimate how often their components re-render because the visual output doesn’t change — React’s diffing saves you from unnecessary DOM writes, but the JavaScript execution still happens. Every. Single. Time.

The Three Sources of Re-renders
Every re-render in React traces back to one of three triggers:
- State changes —
setStateoruseStatesetter is called. - Parent re-renders — A parent component re-renders and passes new props (or the same props, but React doesn’t know that yet).
- Context changes — A consumed context value updates.
State changes are usually intentional. You expect a re-render when you call setCount. The real problems are the other two.
Parent Re-renders Cascade Like a Bad Cold
Here’s the fundamental rule most developers learn the hard way: when a parent re-renders, every child re-renders too. Regardless of props. Regardless of whether anything actually changed.
function Parent() {
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount(count + 1)}>{count}</button>
<ExpensiveChild />
</div>
);
}
Every click on that button re-renders ExpensiveChild. It receives no props at all, yet React calls its function again because it has no way to know the output will be identical. This is the single most common source of wasted renders in production React code.
The fix: React.memo
Wrap the child in React.memo:
const ExpensiveChild = React.memo(function ExpensiveChild() {
return <div>I only re-render when my props change</div>;
});
Now React skips this component when the parent re-renders and the props haven’t changed. Since ExpensiveChild receives no props, it will never re-render from a parent update. Use React.memo liberally on components that are expensive to render or appear deep in the tree. Don’t bother memoing tiny leaf components — the comparison cost can exceed the render cost.
Inline Objects and Functions: Silent Prop Killers
You added React.memo but the component still re-renders every time. Why? Because you’re passing a new object or function as a prop on every render:
<UserCard
user={user}
style={{ margin: 16 }}
onClick={() => navigate('/profile')}
/>

Every render creates a new object for style and a new function for onClick. React.memo does a shallow comparison, and {} !== {}. Your memo is useless.
The fix: useMemo and useCallback
const cardStyle = useMemo(() => ({ margin: 16 }), []);
const handleClick = useCallback(() => navigate('/profile'), []);
return (
<UserCard user={user} style={cardStyle} onClick={handleClick} />
);
Yes, these hooks have their own cost. Don’t wrap everything — only values that get passed as props to memoized components or are used as dependencies in other hooks. See the official React docs on useMemo for the specific guidance on when it helps.
Context: The Sneaky Re-render Bomb
Context feels like a clean solution for shared state until you realize that every consumer re-renders when any part of the context value changes.
const AuthContext = createContext();
function AuthProvider({ children }) {
const [user, setUser] = useState(null);
const [theme, setTheme] = useState('dark');
return (
<AuthContext.Provider value={{ user, theme, setUser, setTheme }}>
{children}
</AuthContext.Provider>
);
}
Every time theme changes, every component consuming AuthContext re-renders — even if it only reads user. This is a well-documented problem. The context value object is recreated on every render, so even consumers that don’t care about the change will see a “new” value.
The fix: Split contexts or memo the value
Option one: split into separate contexts for separate concerns:
const UserContext = createContext();
const ThemeContext = createContext();
Option two: stabilize the context value object:
const value = useMemo(
() => ({ user, theme, setUser, setTheme }),
[user, theme]
);

Splitting contexts is the more scalable solution. A single monolithic context guaranteed to re-render your entire app on any state change will haunt you as your application grows.
State Updates in Render: The Infinite Loop Trap
Sometimes re-renders aren’t just wasteful — they’re infinite. This happens when you trigger a state update during the render phase:
function BadComponent({ items }) {
const [count, setCount] = useState(0);
// Don't do this
setCount(items.length);
return <div>{count}</div>;
}
React renders the component, sees setCount, updates state, re-renders, sees setCount again — and now you’re in an infinite loop. The correct approach is to compute derived state without storing it:
function GoodComponent({ items }) {
const count = items.length;
return <div>{count}</div>;
}
Or, if you genuinely need to sync state with props occasionally, use useEffect:
useEffect(() => {
setCount(items.length);
}, [items.length]);
This fires after render, breaking the cycle.
How to Actually Find Wasted Renders
Stop guessing. Open React DevTools, go to the Profiler tab, click the gear icon, and enable “Highlight updates when components render”. Now interact with your app. Components that flash are re-rendering. If something far from the interaction flashes, you have a cascade problem.
For deeper analysis, the React DevTools profiler documentation explains how to record a profile and read the flamegraph. Look for wide, flat sections — those represent components rendering many children at once. Look for repeated colors at the same depth — those are components re-rendering without cause.
You can also add a quick console log to suspect components:
function SuspectComponent(props) {
console.trace('SuspectComponent rendered');
return <div>...</div>;
}
The stack trace tells you exactly what triggered the render. Remove these logs before shipping — they’ll tank performance in production.
A Practical Checklist
Before you start memoizing everything in sight, work through this list:
- Profile first. Don’t optimize what isn’t slow. Use the profiler to identify actual bottlenecks.
- Lift state down. If only part of a component needs state, extract that part into its own component so the parent doesn’t re-render.
- Memo expensive children. Wrap heavy components in
React.memowhen their parents re-render often. - Stabilize props. Use
useCallbackanduseMemofor props passed to memoized components. - Split contexts. Don’t put unrelated data in the same context.
- Avoid derived state. Compute values directly instead of syncing them to state.
Re-renders aren’t evil. React is designed to re-render frequently. The problem is unnecessary re-renders that create perceptible lag. Fix those, leave the rest alone, and your app will run well without turning your codebase into a memoization museum.
FAQ
Does React.memo shallow-compare all props, including functions?
Yes. React.memo does a shallow comparison on every prop. For functions, fn === fn is only true if it’s the same function reference. That’s why inline arrow functions always break memoization — they create a new reference on every render. Use useCallback to stabilize function references passed as props to memoized components.
Should I wrap every component in React.memo?
No. React.memo adds comparison overhead. For small, fast-rendering components, the cost of comparing props exceeds the cost of just re-rendering. Reserve memoization for components that are computationally expensive, render large subtrees, or receive stable props from frequently-updating parents. Profile first, memo second.
Why does useEffect cause extra renders?
useEffect runs after the render cycle completes. If a useEffect callback calls a state setter, it triggers another render. This pattern — render, effect, setState, re-render — is common for data fetching and synchronization. It’s not inherently wrong, but it means your component renders twice on mount. If this becomes a performance issue, consider using useSyncExternalStore or restructuring your data flow so the state is available on the first render.