You’ve mastered memo, useCallback, and useMemo. Your components are optimized. Life is good.
Then you add Context… and everything re-renders again. Welcome to the Context trap.
How Context Works
Every component using useContext re-renders when the context value changes. No exceptions.
React.memo won’t save you. Your optimizations? Worthless.
// ❌ This memo does NOTHING
const UserProfile = memo(function UserProfile() {
const user = useContext(UserContext);
return <div>{user.name}</div>;
});
When state in a context provider updates, React forces all context consumers to re-render, completely bypassing any memoization.
Real-World Disaster
// ❌ The nightmare scenario
const AppContext = createContext();
function App() {
const [user, setUser] = useState({ name: "Alice" });
const [theme, setTheme] = useState("dark");
const value = { user, setUser, theme, setTheme };
return (
<AppContext.Provider value={value}>
<Header />
<Sidebar />
<UserProfile />
</AppContext.Provider>
);
}
Change the theme? Everything re-renders. Update a notification? Everything re-renders. Type in a search bar? You get the idea.
Fix #1: Split Your Contexts
Different data = different contexts.
// âś… Surgical precision
const UserContext = createContext();
const ThemeContext = createContext();
function App() {
const [user, setUser] = useState({ name: "Alice" });
const [theme, setTheme] = useState("dark");
return (
<UserContext.Provider value={user}>
<ThemeContext.Provider value={theme}>
<Header /> {/* Only re-renders for theme */}
<UserProfile /> {/* Only re-renders for user */}
</ThemeContext.Provider>
</UserContext.Provider>
);
}
Fix #2: Separate Data from Setters
Splitting dispatch and state into separate contexts prevents unnecessary re-renders for components that only need to update state.
// âś… Setters never change
const UserContext = createContext();
const UserActionsContext = createContext();
function UserProvider({ children }) {
const [user, setUser] = useState({ name: "Alice" });
const actions = useMemo(() => ({ setUser }), []);
return (
<UserActionsContext.Provider value={actions}>
<UserContext.Provider value={user}>{children}</UserContext.Provider>
</UserActionsContext.Provider>
);
}
// Form doesn't re-render when user changes
const UserForm = memo(function UserForm() {
const { setUser } = useContext(UserActionsContext);
return <button onClick={() => setUser({ name: "Bob" })}>Update</button>;
});
Fix #3: Memoize Context Values
// ❌ New object every render
function App() {
const [user, setUser] = useState({ name: "Alice" });
const value = { user, setUser }; // New reference!
return <AppContext.Provider value={value}>...</AppContext.Provider>;
}
// âś… Stable reference
function App() {
const [user, setUser] = useState({ name: "Alice" });
const value = useMemo(() => ({ user, setUser }), [user]);
return <AppContext.Provider value={value}>...</AppContext.Provider>;
}
When to Ditch Context
Context isn’t a state management tool. When dealing with frequent updates or complex state, libraries like Jotai, Zustand, or Redux provide better performance through selective subscriptions.
Use Context for:
- Theme/auth state
- Infrequent updates
- Simple data passing
Alternatives
Reach for Jotai, Zustand, or Redux when you need:
- Frequent updates (forms, animations)
- Complex state logic
- Selective subscriptions
The Rules
- Split contexts by update frequency
- Separate data from setters
- Always memoize context values
- Consider alternatives for complex/frequent updates
Context is great for the right use cases. Just don’t use it like a state manager.
Further Reading
- Josh Comeau’s “Why React Re-Renders” explains the fundamental rendering behavior that makes context consumers re-render
- Mark Erikson’s guide clarifies why Context isn’t state management and compares performance with Redux
Your optimizations matter again 🚀