Context · How it works
1 min readRapid overview
- How it works
- 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
How it works
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)