Context
9 min read- React Context - Complete Guide
- Table of Contents
- What is Context
- When to Use Context
- Creating and Using Context
- Basic Pattern
- Multiple Contexts
- Context with Reducer
- Context with TypeScript
- Typed Context
- Generic Context Factory
- Context Patterns
- Compound Components with Context
- Split State and Dispatch Contexts
- Selector Pattern
- Performance Optimization
- Memoize Context Value
- Split Contexts by Update Frequency
- Colocate Providers
- Context vs Other State Solutions
- When to Use Context
- When NOT to Use Context
- Common Mistakes
- Missing Provider
- Not Memoizing Value
- Overusing Context
- Questions & Answers
React Context - Complete Guide
Master React Context API for efficient state sharing and avoiding prop drilling.
Table of Contents
- What is Context
- Creating and Using Context
- Context with TypeScript
- Context Patterns
- Performance Optimization
- Context vs Other State Solutions
- Common Mistakes
- Questions & Answers
What is Context
Context provides a way to pass data through the component tree without manually passing props at every level (prop drilling).
When to Use Context
- Theme data: Dark/light mode, color schemes
- User data: Current authenticated user
- Locale/language: i18n preferences
- Feature flags: A/B testing, feature toggles
- UI state: Modal open/close, sidebar collapsed
// ❌ Prop drilling - passing through every component
function App() {
const [user, setUser] = useState(null);
return <Layout user={user} setUser={setUser} />;
}
function Layout({ user, setUser }) {
return <Sidebar user={user} setUser={setUser} />;
}
function Sidebar({ user, setUser }) {
return <UserProfile user={user} setUser={setUser} />;
}
// ✅ Context - components access what they need
function App() {
return (
<UserProvider>
<Layout />
</UserProvider>
);
}
function UserProfile() {
const { user, setUser } = useUser(); // Direct access
return <div>{user.name}</div>;
}
Creating and Using Context
Basic Pattern
import { createContext, useContext, useState } from 'react';
// 1. Create context with default value
const ThemeContext = createContext('light');
// 2. Create provider component
function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
const toggleTheme = () => {
setTheme(prev => prev === 'light' ? 'dark' : 'light');
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}
// 3. Create custom hook for consuming
function useTheme() {
const context = useContext(ThemeContext);
if (context === undefined) {
throw new Error('useTheme must be used within a ThemeProvider');
}
return context;
}
// 4. Use in components
function ThemedButton() {
const { theme, toggleTheme } = useTheme();
return (
<button
onClick={toggleTheme}
className={theme === 'dark' ? 'btn-dark' : 'btn-light'}
>
Current: {theme}
</button>
);
}
// 5. Wrap app with provider
function App() {
return (
<ThemeProvider>
<ThemedButton />
</ThemeProvider>
);
}
Multiple Contexts
function App() {
return (
<AuthProvider>
<ThemeProvider>
<LocaleProvider>
<MainApp />
</LocaleProvider>
</ThemeProvider>
</AuthProvider>
);
}
// Compose multiple contexts
function Providers({ children }) {
return (
<AuthProvider>
<ThemeProvider>
<LocaleProvider>
{children}
</LocaleProvider>
</ThemeProvider>
</AuthProvider>
);
}
function App() {
return (
<Providers>
<MainApp />
</Providers>
);
}
Context with Reducer
const TodoContext = createContext(null);
const initialState = {
todos: [],
filter: 'all'
};
function todoReducer(state, action) {
switch (action.type) {
case 'ADD_TODO':
return {
...state,
todos: [...state.todos, {
id: Date.now(),
text: action.payload,
completed: false
}]
};
case 'TOGGLE_TODO':
return {
...state,
todos: state.todos.map(todo =>
todo.id === action.payload
? { ...todo, completed: !todo.completed }
: todo
)
};
case 'SET_FILTER':
return { ...state, filter: action.payload };
default:
return state;
}
}
function TodoProvider({ children }) {
const [state, dispatch] = useReducer(todoReducer, initialState);
const value = useMemo(() => ({ state, dispatch }), [state]);
return (
<TodoContext.Provider value={value}>
{children}
</TodoContext.Provider>
);
}
function useTodos() {
const context = useContext(TodoContext);
if (!context) {
throw new Error('useTodos must be used within TodoProvider');
}
return context;
}
Context with TypeScript
Typed Context
interface User {
id: string;
name: string;
email: string;
}
interface AuthContextType {
user: User | null;
login: (credentials: { email: string; password: string }) => Promise<void>;
logout: () => void;
isLoading: boolean;
}
const AuthContext = createContext<AuthContextType | undefined>(undefined);
function AuthProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const [isLoading, setIsLoading] = useState(false);
const login = async (credentials: { email: string; password: string }) => {
setIsLoading(true);
try {
const user = await api.login(credentials);
setUser(user);
} finally {
setIsLoading(false);
}
};
const logout = () => {
setUser(null);
};
const value = useMemo(
() => ({ user, login, logout, isLoading }),
[user, isLoading]
);
return (
<AuthContext.Provider value={value}>
{children}
</AuthContext.Provider>
);
}
function useAuth(): AuthContextType {
const context = useContext(AuthContext);
if (context === undefined) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
}
Generic Context Factory
function createSafeContext<T>(displayName: string) {
const Context = createContext<T | undefined>(undefined);
Context.displayName = displayName;
function useContextSafe(): T {
const context = useContext(Context);
if (context === undefined) {
throw new Error(
`use${displayName} must be used within a ${displayName}Provider`
);
}
return context;
}
return [Context.Provider, useContextSafe] as const;
}
// Usage
interface CounterContextType {
count: number;
increment: () => void;
decrement: () => void;
}
const [CounterProvider, useCounter] = createSafeContext<CounterContextType>('Counter');
function CounterContextProvider({ children }: { children: React.ReactNode }) {
const [count, setCount] = useState(0);
const value = useMemo(() => ({
count,
increment: () => setCount(c => c + 1),
decrement: () => setCount(c => c - 1)
}), [count]);
return <CounterProvider value={value}>{children}</CounterProvider>;
}
Context Patterns
Compound Components with Context
const AccordionContext = createContext(null);
function Accordion({ children, allowMultiple = false }) {
const [openItems, setOpenItems] = useState(new Set());
const toggle = (id) => {
setOpenItems(prev => {
const next = new Set(prev);
if (next.has(id)) {
next.delete(id);
} else {
if (!allowMultiple) next.clear();
next.add(id);
}
return next;
});
};
const isOpen = (id) => openItems.has(id);
return (
<AccordionContext.Provider value={{ toggle, isOpen }}>
<div className="accordion">{children}</div>
</AccordionContext.Provider>
);
}
function AccordionItem({ id, children }) {
return <div className="accordion-item">{children}</div>;
}
function AccordionHeader({ id, children }) {
const { toggle, isOpen } = useContext(AccordionContext);
return (
<button
className="accordion-header"
onClick={() => toggle(id)}
aria-expanded={isOpen(id)}
>
{children}
<span>{isOpen(id) ? '−' : '+'}</span>
</button>
);
}
function AccordionPanel({ id, children }) {
const { isOpen } = useContext(AccordionContext);
if (!isOpen(id)) return null;
return <div className="accordion-panel">{children}</div>;
}
// Attach subcomponents
Accordion.Item = AccordionItem;
Accordion.Header = AccordionHeader;
Accordion.Panel = AccordionPanel;
// Usage
<Accordion allowMultiple>
<Accordion.Item id="1">
<Accordion.Header id="1">Section 1</Accordion.Header>
<Accordion.Panel id="1">Content 1</Accordion.Panel>
</Accordion.Item>
<Accordion.Item id="2">
<Accordion.Header id="2">Section 2</Accordion.Header>
<Accordion.Panel id="2">Content 2</Accordion.Panel>
</Accordion.Item>
</Accordion>
Split State and Dispatch Contexts
const StateContext = createContext(null);
const DispatchContext = createContext(null);
function AppProvider({ children }) {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<StateContext.Provider value={state}>
<DispatchContext.Provider value={dispatch}>
{children}
</DispatchContext.Provider>
</StateContext.Provider>
);
}
function useAppState() {
const context = useContext(StateContext);
if (!context) throw new Error('useAppState must be within AppProvider');
return context;
}
function useAppDispatch() {
const context = useContext(DispatchContext);
if (!context) throw new Error('useAppDispatch must be within AppProvider');
return context;
}
// Components that only dispatch don't re-render when state changes
function AddButton() {
const dispatch = useAppDispatch();
return <button onClick={() => dispatch({ type: 'ADD' })}>Add</button>;
}
// Components that read state re-render when state changes
function ItemCount() {
const state = useAppState();
return <span>{state.items.length} items</span>;
}
Selector Pattern
const StoreContext = createContext(null);
function useSelector(selector) {
const store = useContext(StoreContext);
return selector(store);
}
function UserName() {
// Only re-renders when user.name changes
const name = useSelector(state => state.user.name);
return <span>{name}</span>;
}
function ItemCount() {
// Only re-renders when items.length changes
const count = useSelector(state => state.items.length);
return <span>{count} items</span>;
}
Performance Optimization
Memoize Context Value
// ❌ Bad - new object every render
function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
// This object is recreated every render
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
);
}
// ✅ Good - memoized value
function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
const value = useMemo(
() => ({ theme, setTheme }),
[theme]
);
return (
<ThemeContext.Provider value={value}>
{children}
</ThemeContext.Provider>
);
}
Split Contexts by Update Frequency
// ❌ Bad - all consumers re-render on any change
const AppContext = createContext(null);
function AppProvider({ children }) {
const [user, setUser] = useState(null); // Rarely changes
const [theme, setTheme] = useState('light'); // Rarely changes
const [notifications, setNotifications] = useState([]); // Frequently changes
return (
<AppContext.Provider value={{ user, theme, notifications }}>
{children}
</AppContext.Provider>
);
}
// ✅ Good - separate contexts by update frequency
const UserContext = createContext(null);
const ThemeContext = createContext(null);
const NotificationContext = createContext(null);
function AppProvider({ children }) {
return (
<UserProvider>
<ThemeProvider>
<NotificationProvider>
{children}
</NotificationProvider>
</ThemeProvider>
</UserProvider>
);
}
Colocate Providers
// ✅ Provider only wraps components that need it
function Dashboard() {
return (
<DashboardDataProvider>
<DashboardHeader />
<DashboardContent />
<DashboardFooter />
</DashboardDataProvider>
);
}
// Other parts of app don't re-render when dashboard data changes
function App() {
return (
<GlobalProviders>
<Header />
<Dashboard /> {/* Has its own provider */}
<Sidebar />
</GlobalProviders>
);
}
Context vs Other State Solutions
| Feature | Context | Redux | Zustand | Jotai |
|---|---|---|---|---|
| Bundle Size | 0kb (built-in) | ~10kb | ~2kb | ~3kb |
| Boilerplate | Low | Medium-High | Very Low | Low |
| DevTools | No | Yes | Yes | Yes |
| Middleware | No | Yes | Yes | Limited |
| Selectors | Manual | Built-in | Built-in | Built-in |
| Best For | Low-frequency updates | Complex app state | Simple global state | Atomic state |
When to Use Context
- Theme/UI preferences
- Auth/user data
- Locale settings
- Compound component internal state
- Avoiding prop drilling for rarely-changing data
When NOT to Use Context
- High-frequency updates (cursor position, animations)
- Complex state with many consumers
- Need for devtools/debugging
- Need for middleware (logging, persistence)
Common Mistakes
Missing Provider
// ❌ Bad - no provider in tree
function App() {
return <UserProfile />; // Will throw error or get undefined
}
// ✅ Good - provider wraps consumers
function App() {
return (
<UserProvider>
<UserProfile />
</UserProvider>
);
}
Not Memoizing Value
// ❌ Bad - causes all consumers to re-render
function Provider({ children }) {
const [count, setCount] = useState(0);
return (
<Context.Provider value={{ count, setCount }}>
{children}
</Context.Provider>
);
}
// ✅ Good - memoized value
function Provider({ children }) {
const [count, setCount] = useState(0);
const value = useMemo(() => ({ count, setCount }), [count]);
return (
<Context.Provider value={value}>
{children}
</Context.Provider>
);
}
Overusing Context
// ❌ Bad - context for local state
function Form() {
return (
<FormContext.Provider value={formState}>
<FormField name="email" />
<FormField name="password" />
</FormContext.Provider>
);
}
// ✅ Good - props for local state
function Form() {
const [formState, setFormState] = useState({});
return (
<>
<FormField name="email" value={formState.email} onChange={...} />
<FormField name="password" value={formState.password} onChange={...} />
</>
);
}
Questions & Answers
A: Context provides a way to share values between components without prop drilling. Use it for cross-cutting concerns like themes, auth, or locale that many components need access to.
A: Memoize the context value with useMemo, split contexts by update frequency, use separate state and dispatch contexts, or consider libraries like Zustand for high-frequency updates.
A: Context is built-in and simpler but lacks devtools, middleware, and optimized selectors. Redux has more features for complex state management but requires more setup. Use Context for simple sharing, Redux for complex app state.
A: Yes, you can nest multiple providers. Split contexts by domain (auth, theme, locale) or update frequency to optimize re-renders.
A: Use createContext<T | undefined>(undefined) and create a custom hook that throws if context is undefined. This ensures type safety and catches missing providers.
A: Generally no. Form state is local and props/controlled components work well. Use Context only if form state needs to be accessed by deeply nested components outside the form.
A: A pattern where a parent component shares state with children via Context. Children access shared state without prop drilling. Used for tabs, accordions, menus, etc.
A: You can't use useContext outside components. Store the value in a ref, use a state management library, or restructure to keep the logic inside components.