Pitfalls of React context
The Short Answer
React context is powerful for avoiding prop drilling, but it comes with pitfalls that can hurt performance and maintainability. The biggest issues are: unnecessary re-renders of all consumers when any part of the context value changes, difficulty testing components that depend on context, overuse as a global state manager when simpler solutions exist, and tight coupling between components and their context shape. Understanding these pitfalls helps you use context effectively without falling into traps.
Pitfall: All Consumers Re-render on Any Change
This is the most impactful pitfall. When the context value changes (new reference), every component that calls useContext() for that context re-renders — even if the specific data it reads hasn't changed. There's no built-in selector mechanism. A context with { user, theme, notifications } re-renders theme-only consumers when notifications update.
// ❌ One context with multiple concerns
const AppContext = createContext<{
user: User | null;
theme: string;
notifications: Notification[];
unreadCount: number;
}>(/* ... */);
// This re-renders when notifications change, even though it only reads theme
function ThemeSwitcher() {
const { theme } = useContext(AppContext); // Re-renders on ANY context change
return <button>{theme}</button>;
}
// ✅ Fix: Split into separate contexts
const ThemeContext = createContext<string>('light');
const NotificationContext = createContext<Notification[]>([]);
function ThemeSwitcher() {
const theme = useContext(ThemeContext); // Only re-renders when theme changes
return <button>{theme}</button>;
}
The fix is straightforward: split contexts by update frequency and consumer groups. Data that changes together and is consumed together belongs in one context. Data that changes independently belongs in separate contexts.
Pitfall: Creating New Object References Every Render
If the provider component re-renders (for any reason), and the value prop is an inline object, a new reference is created every time — triggering all consumers to re-render even when the actual data hasn't changed. This is subtle because the data looks the same, but the object identity is different.
// ❌ New object every render — consumers always re-render
function AuthProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState<User | null>(null);
// This creates a new object on every render of AuthProvider
return (
<AuthContext.Provider value={{ user, login, logout }}>
{children}
</AuthContext.Provider>
);
}
// ✅ Fix: Memoize the value
function AuthProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const login = useCallback(async (creds: Credentials) => { /* ... */ }, []);
const logout = useCallback(async () => { /* ... */ }, []);
const value = useMemo(
() => ({ user, login, logout }),
[user, login, logout]
);
return (
<AuthContext.Provider value={value}>
{children}
</AuthContext.Provider>
);
}
Pitfall: Using Context for Frequently-Updating State
Context is designed for data that changes infrequently — theme, locale, auth status, feature flags. Using it for rapidly-changing state (form inputs, animation values, mouse position, real-time data) causes performance problems because every update re-renders all consumers. There's no way to bail out of a context re-render.
Good for context (infrequent updates)
- ✅Authentication state (login/logout)
- ✅Theme (light/dark, changes rarely)
- ✅Locale/language preference
- ✅Feature flags
- ✅Route-level data that changes on navigation
Bad for context (frequent updates)
- ❌Form input values (every keystroke)
- ❌Animation/transition state (every frame)
- ❌Mouse/scroll position (continuous)
- ❌Real-time data (WebSocket messages)
- ❌Any state that updates more than a few times per second
Pitfall: Deep Provider Nesting
When you split context aggressively (which is good for performance), you can end up with deeply nested providers — sometimes called 'provider hell'. This makes the component tree hard to read and the app structure difficult to understand at a glance.
// ❌ Provider hell — hard to read and maintain
function App() {
return (
<AuthProvider>
<ThemeProvider>
<LocaleProvider>
<NotificationProvider>
<FeatureFlagProvider>
<RouterProvider>
<Layout />
</RouterProvider>
</FeatureFlagProvider>
</NotificationProvider>
</LocaleProvider>
</ThemeProvider>
</AuthProvider>
);
}
// ✅ Fix: Compose providers into a single wrapper
function AppProviders({ children }: { children: React.ReactNode }) {
return (
<AuthProvider>
<ThemeProvider>
<LocaleProvider>
<NotificationProvider>
<FeatureFlagProvider>
{children}
</FeatureFlagProvider>
</NotificationProvider>
</LocaleProvider>
</ThemeProvider>
</AuthProvider>
);
}
function App() {
return (
<AppProviders>
<RouterProvider>
<Layout />
</RouterProvider>
</AppProviders>
);
}
Composing providers into a single AppProviders component keeps the main App clean. You can also use a composeProviders utility that reduces the nesting visually, though the runtime behavior is identical.
Pitfall: Missing Default Values and Error Handling
Using useContext outside its provider returns the default value (or undefined if none was set). This fails silently — your component renders with wrong data instead of throwing a clear error. A custom hook that throws when the provider is missing makes bugs obvious immediately.
// ❌ Silent failure — returns undefined, component breaks mysteriously
const AuthContext = createContext<AuthState | undefined>(undefined);
function useAuth() {
return useContext(AuthContext); // Could be undefined!
}
// ✅ Throw immediately if used outside provider
function useAuth(): AuthState {
const context = useContext(AuthContext);
if (context === undefined) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
}
The custom hook pattern with a throw gives you an immediate, descriptive error instead of a cryptic 'cannot read property of undefined' somewhere deep in a child component. This is a best practice for every context you create.
Why Interviewers Ask This
This question tests whether you've used context in production and hit its limitations. Interviewers want to see that you understand the re-render behavior (all consumers, no selectors), know when context is the wrong tool (frequent updates), can articulate the fixes (split contexts, memoize values, custom hooks with guards), and understand the tradeoffs between context and external state managers. It reveals practical experience versus tutorial-level knowledge.
Quick Revision Cheat Sheet
Re-render problem: All consumers re-render on any value change — no selector support
Object reference: Inline objects create new refs every render — useMemo the value
Update frequency: Context is for infrequent updates — not forms, animations, or real-time data
Provider nesting: Compose into AppProviders component to reduce visual nesting
Missing provider: Custom hook should throw if context is undefined — fail fast
When to use alternatives: Many consumers + frequent updates + need selectors → Zustand/Jotai