Difficulty: Advanced
Unnecessary re-renders are the most common performance problem in React applications. A re-render happens when a component's state changes, when its parent re-renders, or when a consumed context value changes. While React is fast at re-rendering, doing it thousands of times on every interaction creates noticeable lag, especially on mobile devices.
The first and most impactful technique is state colocation - moving state down to the component that actually needs it. If only a search input needs to track its value, that state should live in the search input component, not in a parent that also renders a heavy list. When state lives too high in the tree, every state change re-renders the entire subtree, including components that don't care about that state.
The children-as-props pattern is a powerful technique that is often overlooked. When a parent component has state that causes frequent re-renders, any JSX passed as the children prop is NOT re-created - it was created by the grandparent and passed down as a stable reference. This means wrapping expensive children in a stateful parent does not re-render the children on every state change.
Composition over wrapping is another pattern. Instead of one large component that manages multiple concerns, break it into smaller components where each manages only the state it needs. The expensive rendering work happens in components that don't have rapidly-changing state, so they stay stable.
Use the React DevTools Profiler to identify which components re-render and why. The 'Highlight updates' feature visually flashes components that re-render. The Profiler tab records render timings and shows you the commit with the longest duration. Always measure before optimizing - intuition about performance is often wrong.
// BAD: State in parent re-renders the expensive list
function PageBad() {
const [search, setSearch] = React.useState('');
return (
<div>
<input value={search} onChange={e => setSearch(e.target.value)} />
<ExpensiveList /> {/* Re-renders on every keystroke! */}
</div>
);
}
// GOOD: State colocated in its own component
function PageGood() {
return (
<div>
<SearchInput /> {/* Has its own state */}
<ExpensiveList /> {/* Never re-renders from search */}
</div>
);
}
function SearchInput() {
const [search, setSearch] = React.useState('');
return <input value={search} onChange={e => setSearch(e.target.value)} />;
}
Moving the search state into SearchInput means only SearchInput re-renders on keystrokes. ExpensiveList is a sibling with no state dependency, so it stays stable.
// The children prop is created by the caller and stays stable
function ScrollTracker({ children }) {
const [scrollY, setScrollY] = React.useState(0);
React.useEffect(() => {
const handler = () => setScrollY(window.scrollY);
window.addEventListener('scroll', handler);
return () => window.removeEventListener('scroll', handler);
}, []);
return (
<div>
<p>Scroll: {scrollY}px</p>
{children} {/* This does NOT re-render on scroll */}
</div>
);
}
// Usage
function Page() {
return (
<ScrollTracker>
<ExpensiveContent /> {/* Created by Page, not ScrollTracker */}
</ScrollTracker>
);
}
ExpensiveContent is created by Page (the parent of ScrollTracker) and passed as children. When ScrollTracker re-renders due to scrollY state change, children is the same reference - it was not recreated.
function useRenderCount(componentName) {
const renderCount = React.useRef(0);
renderCount.current += 1;
console.log(`${componentName} rendered: ${renderCount.current} times`);
}
function Parent() {
const [count, setCount] = React.useState(0);
useRenderCount('Parent');
return (
<div>
<button onClick={() => setCount(c => c + 1)}>+1</button>
<Child name="Alice" />
</div>
);
}
function Child({ name }) {
useRenderCount('Child');
return <p>Hello {name}</p>;
}
// Clicking button logs:
// Parent rendered: 2 times
// Child rendered: 2 times (unnecessary!)
Child re-renders even though its props haven't changed, because React re-renders all children when a parent's state changes. Use React.memo on Child to prevent this.
re-render causes, state colocation, children as props, composition pattern, React DevTools profiler