When state changes trigger too many component re-renders, it can significantly impact performance. Here’s how to optimize your components to avoid unnecessary re-renders:
Common Causes
- State changes that don’t affect the UI
- Props changing unnecessarily
- Complex components re-rendering when only part of their state changes
- Parent components re-rendering and forcing child re-renders
Optimization Techniques
1. React.memo (for Functional Components)
const MyComponent = React.memo(function MyComponent(props) {
/* render using props */
});
2. shouldComponentUpdate (for Class Components)
class MyComponent extends React.Component {
shouldComponentUpdate(nextProps, nextState) {
// Only re-render if these specific props/state change
return nextProps.value !== this.props.value;
}
}
3. Use State Selectors
Instead of storing derived data in state, compute it during render:
// Instead of:
const [fullName, setFullName] = useState('');
useEffect(() => {
setFullName(`${firstName} ${lastName}`);
}, [firstName, lastName]);
// Do:
const fullName = `${firstName} ${lastName}`;
4. Colocate State
Keep state as close to where it’s used as possible to minimize re-render scope:
// Instead of lifting state up unnecessarily
function Parent() {
const [value, setValue] = useState('');
return (
<>
<ExpensiveComponent />
<Input value={value} onChange={setValue} />
</>
);
}
// Move state down when possible
function Parent() {
return (
<>
<ExpensiveComponent />
<InputWithState />
</>
);
}
5. Use useMemo for Expensive Calculations
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
6. Use useCallback for Stable Function References
const handleClick = useCallback(() => {
doSomething(a, b);
}, [a, b]);
7. Split Components
Break large components into smaller ones with their own state:
// Instead of one big component:
function UserProfile({ user }) {
return (
<div>
<h1>{user.name}</h1>
<p>{user.bio}</p>
{/* 20 more fields */}
</div>
);
}
// Create smaller components:
function UserProfile({ user }) {
return (
<div>
<UserName name={user.name} />
<UserBio bio={user.bio} />
{/* etc */}
</div>
);
}
8. Use Context Selectively
When using Context, avoid putting frequently changing values at the top:
// Instead of:
<AppContext.Provider value={{ user, setUser, theme, setTheme }}>
<App />
</AppContext.Provider>
// Split into multiple providers:
<UserContext.Provider value={{ user, setUser }}>
<ThemeContext.Provider value={{ theme, setTheme }}>
<App />
</ThemeContext.Provider>
</UserContext.Provider>
9. Use State Management Libraries Wisely
With Redux, select only what you need:
// Instead of:
const { user, posts, comments } = useSelector(state => state);
// Select precisely:
const userId = useSelector(state => state.user.id);
10. Debugging Re-renders
To identify unnecessary re-renders:
// Add this to components you want to monitor
useEffect(() => {
console.log('Component re-rendered');
});
Or use React DevTools Profiler to record and analyze renders.
Framework-Specific Notes
React 18+ Automatic Batching
React 18 batches more state updates automatically, reducing unnecessary renders.
Concurrent Features
Consider using startTransition
for non-urgent state updates:
import { startTransition } from 'react';
// Urgent: Show what was typed
setInputValue(input);
// Mark any non-urgent state updates inside as transitions
startTransition(() => {
setSearchQuery(input);
});
By applying these techniques, you can significantly reduce unnecessary re-renders and improve your application’s performance.