useContext - Global State Without Prop Drilling
Documentation for useContext - Global State Without Prop Drilling.
useContext - Global State Without Prop Drilling
What is useContext?
useContext provides a way to share values between components without prop drilling.
Definition: Context provides a way to pass data through the component tree without manually passing props at every level. The useContext hook lets functional components consume context values. It's ideal for global data like themes, authentication, locale, or any data needed by many components at different nesting levels.
Why Use Context?
| Problem | Context Solution |
|---|---|
| Prop Drilling | Access data without passing through |
| Global State | Theme, auth, locale, preferences |
| Cross-cutting Concerns | Data needed at multiple nest levels |
| Component Coupling | Less explicit data threading |
Prop Drilling Problem
┌─────────────────────────────────────────────────────────┐
│ Without Context │
├─────────────────────────────────────────────────────────┤
│ │
│ App (user state) │
│ ↓ passes user prop │
│ Dashboard │
│ ↓ passes user prop │
│ Sidebar │
│ ↓ passes user prop │
│ UserWidget (actually uses user) │
│ │
│ Problem: Dashboard & Sidebar don't need user! │
│ │
├─────────────────────────────────────────────────────────┤
│ With Context │
├─────────────────────────────────────────────────────────┤
│ │
│ App (provides user context) │
│ │ │
│ Dashboard │
│ │ │
│ Sidebar │
│ │ │
│ UserWidget (consumes user context directly) │
│ │
│ Solution: Only components that need data access it! │
│ │
└─────────────────────────────────────────────────────────┘Context API Basics
1. Create Context
// contexts/ThemeContext.js
import { createContext } from "react";
// Create with default value
export const ThemeContext = createContext("light");2. Provide Context
// App.jsx
import { ThemeContext } from "./contexts/ThemeContext";
function App() {
const [theme, setTheme] = useState("dark");
return (
<ThemeContext.Provider value={theme}>
<Header />
<Main />
<Footer />
</ThemeContext.Provider>
);
}3. Consume Context
// Any nested component
import { useContext } from "react";
import { ThemeContext } from "./contexts/ThemeContext";
function Button() {
const theme = useContext(ThemeContext);
return <button className={`btn btn-${theme}`}>Click me</button>;
}Complete Example: Theme Context
Create Context with Custom Hook
// contexts/ThemeContext.jsx
import { createContext, useContext, useState } from "react";
// Create context
const ThemeContext = createContext(null);
// Provider component
export function ThemeProvider({ children }) {
const [theme, setTheme] = useState("light");
const toggleTheme = () => {
setTheme((prev) => (prev === "light" ? "dark" : "light"));
};
const value = {
theme,
toggleTheme,
isDark: theme === "dark",
};
return (
<ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>
);
}
// Custom hook for consuming context
export function useTheme() {
const context = useContext(ThemeContext);
if (context === null) {
throw new Error("useTheme must be used within a ThemeProvider");
}
return context;
}Use in App
// App.jsx
import { ThemeProvider } from "./contexts/ThemeContext";
function App() {
return (
<ThemeProvider>
<Header />
<Main />
</ThemeProvider>
);
}Consume in Component
// Header.jsx
import { useTheme } from "./contexts/ThemeContext";
function Header() {
const { theme, toggleTheme, isDark } = useTheme();
return (
<header className={`header-${theme}`}>
<h1>My App</h1>
<button onClick={toggleTheme}>
Switch to {isDark ? "Light" : "Dark"} Mode
</button>
</header>
);
}Authentication Context Example
// contexts/AuthContext.jsx
import { createContext, useContext, useState, useEffect } from "react";
const AuthContext = createContext(null);
export function AuthProvider({ children }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
// Check for stored auth on mount
const storedUser = localStorage.getItem("user");
if (storedUser) {
setUser(JSON.parse(storedUser));
}
setLoading(false);
}, []);
const login = async (email, password) => {
const response = await fetch("/api/login", {
method: "POST",
body: JSON.stringify({ email, password }),
});
const userData = await response.json();
setUser(userData);
localStorage.setItem("user", JSON.stringify(userData));
};
const logout = () => {
setUser(null);
localStorage.removeItem("user");
};
const value = {
user,
loading,
isAuthenticated: !!user,
login,
logout,
};
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}
export function useAuth() {
const context = useContext(AuthContext);
if (!context) {
throw new Error("useAuth must be used within AuthProvider");
}
return context;
}Protected Component
function Dashboard() {
const { user, isAuthenticated, logout, loading } = useAuth();
if (loading) {
return <p>Loading...</p>;
}
if (!isAuthenticated) {
return <Navigate to="/login" />;
}
return (
<div>
<h1>Welcome, {user.name}!</h1>
<button onClick={logout}>Logout</button>
</div>
);
}Multiple Contexts
// App.jsx
function App() {
return (
<AuthProvider>
<ThemeProvider>
<LanguageProvider>
<Router>
<Main />
</Router>
</LanguageProvider>
</ThemeProvider>
</AuthProvider>
);
}
// Using multiple contexts
function Header() {
const { user } = useAuth();
const { theme, toggleTheme } = useTheme();
const { language, setLanguage } = useLanguage();
return (
<header className={theme}>
<span>{user?.name}</span>
<button onClick={toggleTheme}>Toggle Theme</button>
<select value={language} onChange={(e) => setLanguage(e.target.value)}>
<option value="en">English</option>
<option value="es">Español</option>
</select>
</header>
);
}Context with useReducer
For complex state logic, combine context with useReducer.
// contexts/CartContext.jsx
import { createContext, useContext, useReducer } from "react";
const CartContext = createContext(null);
const cartReducer = (state, action) => {
switch (action.type) {
case "ADD_ITEM":
const existing = state.items.find((i) => i.id === action.payload.id);
if (existing) {
return {
...state,
items: state.items.map((item) =>
item.id === action.payload.id
? { ...item, quantity: item.quantity + 1 }
: item,
),
};
}
return {
...state,
items: [...state.items, { ...action.payload, quantity: 1 }],
};
case "REMOVE_ITEM":
return {
...state,
items: state.items.filter((i) => i.id !== action.payload),
};
case "CLEAR_CART":
return { items: [] };
default:
return state;
}
};
export function CartProvider({ children }) {
const [state, dispatch] = useReducer(cartReducer, { items: [] });
const addItem = (item) => dispatch({ type: "ADD_ITEM", payload: item });
const removeItem = (id) => dispatch({ type: "REMOVE_ITEM", payload: id });
const clearCart = () => dispatch({ type: "CLEAR_CART" });
const totalItems = state.items.reduce((sum, i) => sum + i.quantity, 0);
const totalPrice = state.items.reduce(
(sum, i) => sum + i.price * i.quantity,
0,
);
return (
<CartContext.Provider
value={{
items: state.items,
addItem,
removeItem,
clearCart,
totalItems,
totalPrice,
}}
>
{children}
</CartContext.Provider>
);
}
export const useCart = () => {
const context = useContext(CartContext);
if (!context) throw new Error("useCart must be within CartProvider");
return context;
};Performance Optimization
Split Context for Performance
// ❌ One context = all consumers re-render
const AppContext = createContext({ user, theme, cart });
// ✅ Separate contexts = targeted updates
const UserContext = createContext(user);
const ThemeContext = createContext(theme);
const CartContext = createContext(cart);Memoize Context Value
function ThemeProvider({ children }) {
const [theme, setTheme] = useState("light");
// ❌ New object every render = unnecessary re-renders
// return <ThemeContext.Provider value={{ theme, setTheme }}>
// ✅ Memoized value
const value = useMemo(
() => ({
theme,
setTheme,
}),
[theme],
);
return (
<ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>
);
}Common Mistakes & Exceptions
1. Using Outside Provider
// ❌ No provider wrapped around component
function App() {
return <UserProfile />; // useTheme() will fail or use default
}
function UserProfile() {
const theme = useContext(ThemeContext); // Gets default or undefined
}
// ✅ Wrap with provider
function App() {
return (
<ThemeProvider>
<UserProfile />
</ThemeProvider>
);
}2. Not Memoizing Object Values
// ❌ Creates new object every render
function Provider({ children }) {
const [state, setState] = useState({});
return (
<Context.Provider value={{ state, setState }}>{children}</Context.Provider>
);
}
// ✅ Memoize the value
function Provider({ children }) {
const [state, setState] = useState({});
const value = useMemo(() => ({ state, setState }), [state]);
return <Context.Provider value={value}>{children}</Context.Provider>;
}3. Over-using Context
// ❌ Context for every piece of state
<FormContext.Provider value={formData}>
<ErrorContext.Provider value={errors}>
<SubmitContext.Provider value={handleSubmit}>
...
</SubmitContext.Provider>
</ErrorContext.Provider>
</FormContext.Provider>
// ✅ Just pass props for closely related components
<Form data={formData} errors={errors} onSubmit={handleSubmit} />
// Context is best for truly global dataWhen to Use Context
| ✅ Good Use Cases | ❌ Avoid Context For |
|---|---|
| Theme (dark/light mode) | Frequently changing data |
| Current user / auth | Local component state |
| Language / locale | Data only used by few components |
| Global UI state (modals, toast) | Form data between few components |
| Cart in e-commerce | Performance-critical data |
Interview Questions & Answers
Q1: What is React Context and when should you use it?
React Context is a way to share data across components without passing props through every level (prop drilling). Create context with createContext, provide values with Provider, consume with useContext. Use for: global data needed by many components (themes, auth, localization), or when prop drilling becomes cumbersome (3+ levels deep). Don't overuse - props are fine for closely related components, and context re-renders all consumers on value change.
Q2: What is prop drilling and how does Context solve it?
Prop drilling is passing props through intermediate components that don't need them, just to reach deeply nested components. Example: App → Dashboard → Sidebar → UserInfo passing user prop, but only UserInfo uses it. Context solves this by making data available to any component in the tree that asks for it. The intermediate components don't need to know about or forward the data. This reduces coupling and makes code cleaner.
Q3: How do you create and use a Context?
Three steps: (1) Create: const MyContext = createContext(defaultValue). (2) Provide: wrap components with <MyContext.Provider value={...}>. (3) Consume: use useContext(MyContext) in any nested component. Best practice: create a custom hook like useMyContext() that includes error handling for missing provider. Export both the provider component and the custom hook, keeping the context itself private.
Q4: What is the default value in createContext for?
The default value is used when a component calls useContext but isn't wrapped by a Provider. It serves as a fallback. Common patterns: use null or undefined and throw an error in your custom hook if context is missing (catches bugs). Use a meaningful default for testing or optional contexts. The default bypasses the provider, so it's typically only useful for testing components in isolation.
Q5: How does Context affect performance?
Every component consuming a context re-renders when the context value changes - React uses reference equality. Performance issues arise with: large context values, frequently changing values, many consumers. Solutions: split into multiple contexts (separate themes from user from cart), memoize context values with useMemo, compose providers carefully. Consider state management libraries for high-frequency updates or many consumers.
Q6: How do you optimize Context to prevent unnecessary re-renders?
Key strategies: (1) Split contexts - separate concerns so changing one doesn't affect unrelated consumers. (2) Memoize value - use useMemo for object values to maintain reference stability. (3) Separate state from dispatch - consumers that only call functions don't need to re-render on state change. (4) Use memo on consumers - React.memo won't help with context changes though. Consider colocating state closer to usage when possible.
Q7: What's the difference between Context and Redux?
Context is React's built-in mechanism for sharing state, simple for basic needs, no middleware, and re-renders all consumers. Redux is a separate library with: centralized store, actions/reducers pattern, middleware for async, DevTools for debugging, and optimized re-renders (connect/useSelector). Use Context for: small apps, simple global state. Use Redux for: complex state logic, debugging needs, middleware requirements, or performance-sensitive apps.
Q8: How do you combine useContext with useReducer?
Create context, use useReducer in the provider for complex state management, expose state and dispatch (or wrapped action functions) via context. Consumers get centralized state logic without Redux. Pattern: const [state, dispatch] = useReducer(reducer, initialState) in provider, pass { state, dispatch } or action functions as context value. This gives you Redux-like patterns with just React hooks.
Q9: Why should you create a custom hook for your context?
Custom hooks like useAuth() or useTheme() provide: (1) Error handling - throw descriptive errors if used outside provider. (2) Encapsulation - hide context implementation, consumers don't import context directly. (3) Convenience - better DX than importing context + useContext everywhere. (4) Type safety - easier TypeScript integration. (5) Flexibility - can add logic, derived values, or change implementation without affecting consumers.
Q10: When should you NOT use Context?
Avoid context for: Frequently updating data - causes many re-renders. Local state - just use useState in the component. Few levels deep - props are fine for 1-2 levels. Form data - managed better with form libraries. Performance-critical data - consider state management with selectors. Context isn't a replacement for all state management - it's specifically for avoiding prop drilling of truly global, relatively static data.