React Context provides a way to share state without prop drilling. Here's how to use it effectively.
Basic Context Setup#
1import { createContext, useContext, useState, ReactNode } from 'react';
2
3// Define types
4interface User {
5 id: string;
6 name: string;
7 email: string;
8}
9
10interface AuthContextType {
11 user: User | null;
12 login: (email: string, password: string) => Promise<void>;
13 logout: () => void;
14 isLoading: boolean;
15}
16
17// Create context with default value
18const AuthContext = createContext<AuthContextType | undefined>(undefined);
19
20// Provider component
21function AuthProvider({ children }: { children: ReactNode }) {
22 const [user, setUser] = useState<User | null>(null);
23 const [isLoading, setIsLoading] = useState(false);
24
25 const login = async (email: string, password: string) => {
26 setIsLoading(true);
27 try {
28 const response = await api.login(email, password);
29 setUser(response.user);
30 } finally {
31 setIsLoading(false);
32 }
33 };
34
35 const logout = () => {
36 setUser(null);
37 api.logout();
38 };
39
40 return (
41 <AuthContext.Provider value={{ user, login, logout, isLoading }}>
42 {children}
43 </AuthContext.Provider>
44 );
45}
46
47// Custom hook for consuming context
48function useAuth() {
49 const context = useContext(AuthContext);
50
51 if (context === undefined) {
52 throw new Error('useAuth must be used within an AuthProvider');
53 }
54
55 return context;
56}
57
58// Usage in components
59function UserProfile() {
60 const { user, logout } = useAuth();
61
62 if (!user) return <LoginPrompt />;
63
64 return (
65 <div>
66 <h1>Welcome, {user.name}</h1>
67 <button onClick={logout}>Logout</button>
68 </div>
69 );
70}Separating State and Actions#
1// Separate contexts for better performance
2import { createContext, useContext, useReducer, useMemo, ReactNode } from 'react';
3
4// State context
5interface TodoState {
6 todos: Todo[];
7 filter: 'all' | 'active' | 'completed';
8}
9
10const TodoStateContext = createContext<TodoState | undefined>(undefined);
11
12// Actions context
13interface TodoActions {
14 addTodo: (text: string) => void;
15 toggleTodo: (id: string) => void;
16 deleteTodo: (id: string) => void;
17 setFilter: (filter: TodoState['filter']) => void;
18}
19
20const TodoActionsContext = createContext<TodoActions | undefined>(undefined);
21
22// Reducer
23type TodoAction =
24 | { type: 'ADD_TODO'; payload: string }
25 | { type: 'TOGGLE_TODO'; payload: string }
26 | { type: 'DELETE_TODO'; payload: string }
27 | { type: 'SET_FILTER'; payload: TodoState['filter'] };
28
29function todoReducer(state: TodoState, action: TodoAction): TodoState {
30 switch (action.type) {
31 case 'ADD_TODO':
32 return {
33 ...state,
34 todos: [
35 ...state.todos,
36 { id: Date.now().toString(), text: action.payload, completed: false },
37 ],
38 };
39 case 'TOGGLE_TODO':
40 return {
41 ...state,
42 todos: state.todos.map((todo) =>
43 todo.id === action.payload
44 ? { ...todo, completed: !todo.completed }
45 : todo
46 ),
47 };
48 case 'DELETE_TODO':
49 return {
50 ...state,
51 todos: state.todos.filter((todo) => todo.id !== action.payload),
52 };
53 case 'SET_FILTER':
54 return {
55 ...state,
56 filter: action.payload,
57 };
58 default:
59 return state;
60 }
61}
62
63// Provider
64function TodoProvider({ children }: { children: ReactNode }) {
65 const [state, dispatch] = useReducer(todoReducer, {
66 todos: [],
67 filter: 'all',
68 });
69
70 // Memoize actions to prevent unnecessary re-renders
71 const actions = useMemo<TodoActions>(
72 () => ({
73 addTodo: (text) => dispatch({ type: 'ADD_TODO', payload: text }),
74 toggleTodo: (id) => dispatch({ type: 'TOGGLE_TODO', payload: id }),
75 deleteTodo: (id) => dispatch({ type: 'DELETE_TODO', payload: id }),
76 setFilter: (filter) => dispatch({ type: 'SET_FILTER', payload: filter }),
77 }),
78 []
79 );
80
81 return (
82 <TodoStateContext.Provider value={state}>
83 <TodoActionsContext.Provider value={actions}>
84 {children}
85 </TodoActionsContext.Provider>
86 </TodoStateContext.Provider>
87 );
88}
89
90// Hooks
91function useTodoState() {
92 const context = useContext(TodoStateContext);
93 if (!context) throw new Error('useTodoState must be used within TodoProvider');
94 return context;
95}
96
97function useTodoActions() {
98 const context = useContext(TodoActionsContext);
99 if (!context) throw new Error('useTodoActions must be used within TodoProvider');
100 return context;
101}
102
103// Components only re-render when their specific context changes
104function TodoList() {
105 const { todos, filter } = useTodoState();
106 const { toggleTodo, deleteTodo } = useTodoActions();
107
108 const filteredTodos = todos.filter((todo) => {
109 if (filter === 'active') return !todo.completed;
110 if (filter === 'completed') return todo.completed;
111 return true;
112 });
113
114 return (
115 <ul>
116 {filteredTodos.map((todo) => (
117 <TodoItem
118 key={todo.id}
119 todo={todo}
120 onToggle={toggleTodo}
121 onDelete={deleteTodo}
122 />
123 ))}
124 </ul>
125 );
126}
127
128// This component doesn't need state, won't re-render on state changes
129function AddTodoButton() {
130 const { addTodo } = useTodoActions();
131
132 return <button onClick={() => addTodo('New todo')}>Add Todo</button>;
133}Context with Selectors#
1// Use selectors to prevent unnecessary re-renders
2import { createContext, useContext, useReducer, ReactNode } from 'react';
3import { useSyncExternalStore } from 'react';
4
5// Store pattern
6function createStore<T>(initialState: T) {
7 let state = initialState;
8 const listeners = new Set<() => void>();
9
10 return {
11 getState: () => state,
12 setState: (newState: T | ((prev: T) => T)) => {
13 state = typeof newState === 'function'
14 ? (newState as (prev: T) => T)(state)
15 : newState;
16 listeners.forEach((listener) => listener());
17 },
18 subscribe: (listener: () => void) => {
19 listeners.add(listener);
20 return () => listeners.delete(listener);
21 },
22 };
23}
24
25// Create store with context
26interface AppState {
27 user: User | null;
28 theme: 'light' | 'dark';
29 notifications: Notification[];
30}
31
32const store = createStore<AppState>({
33 user: null,
34 theme: 'light',
35 notifications: [],
36});
37
38const StoreContext = createContext(store);
39
40// Selector hook - only re-renders when selected value changes
41function useStore<T>(selector: (state: AppState) => T): T {
42 const store = useContext(StoreContext);
43
44 return useSyncExternalStore(
45 store.subscribe,
46 () => selector(store.getState()),
47 () => selector(store.getState())
48 );
49}
50
51// Usage - component only re-renders when theme changes
52function ThemeToggle() {
53 const theme = useStore((state) => state.theme);
54 const store = useContext(StoreContext);
55
56 const toggleTheme = () => {
57 store.setState((prev) => ({
58 ...prev,
59 theme: prev.theme === 'light' ? 'dark' : 'light',
60 }));
61 };
62
63 return (
64 <button onClick={toggleTheme}>
65 Current: {theme}
66 </button>
67 );
68}
69
70// This component only re-renders when notifications change
71function NotificationBadge() {
72 const count = useStore((state) => state.notifications.length);
73
74 return <span className="badge">{count}</span>;
75}Compound Components Pattern#
1// Context for component composition
2import { createContext, useContext, useState, ReactNode } from 'react';
3
4interface TabsContextType {
5 activeTab: string;
6 setActiveTab: (id: string) => void;
7}
8
9const TabsContext = createContext<TabsContextType | undefined>(undefined);
10
11function useTabs() {
12 const context = useContext(TabsContext);
13 if (!context) throw new Error('Tab components must be used within Tabs');
14 return context;
15}
16
17// Main component
18interface TabsProps {
19 defaultTab: string;
20 children: ReactNode;
21}
22
23function Tabs({ defaultTab, children }: TabsProps) {
24 const [activeTab, setActiveTab] = useState(defaultTab);
25
26 return (
27 <TabsContext.Provider value={{ activeTab, setActiveTab }}>
28 <div className="tabs">{children}</div>
29 </TabsContext.Provider>
30 );
31}
32
33// Tab list
34function TabList({ children }: { children: ReactNode }) {
35 return <div className="tab-list" role="tablist">{children}</div>;
36}
37
38// Individual tab
39function Tab({ id, children }: { id: string; children: ReactNode }) {
40 const { activeTab, setActiveTab } = useTabs();
41
42 return (
43 <button
44 role="tab"
45 aria-selected={activeTab === id}
46 className={activeTab === id ? 'active' : ''}
47 onClick={() => setActiveTab(id)}
48 >
49 {children}
50 </button>
51 );
52}
53
54// Tab panels container
55function TabPanels({ children }: { children: ReactNode }) {
56 return <div className="tab-panels">{children}</div>;
57}
58
59// Individual panel
60function TabPanel({ id, children }: { id: string; children: ReactNode }) {
61 const { activeTab } = useTabs();
62
63 if (activeTab !== id) return null;
64
65 return (
66 <div role="tabpanel" className="tab-panel">
67 {children}
68 </div>
69 );
70}
71
72// Attach sub-components
73Tabs.List = TabList;
74Tabs.Tab = Tab;
75Tabs.Panels = TabPanels;
76Tabs.Panel = TabPanel;
77
78// Usage
79function App() {
80 return (
81 <Tabs defaultTab="overview">
82 <Tabs.List>
83 <Tabs.Tab id="overview">Overview</Tabs.Tab>
84 <Tabs.Tab id="features">Features</Tabs.Tab>
85 <Tabs.Tab id="pricing">Pricing</Tabs.Tab>
86 </Tabs.List>
87
88 <Tabs.Panels>
89 <Tabs.Panel id="overview">
90 <h2>Overview Content</h2>
91 </Tabs.Panel>
92 <Tabs.Panel id="features">
93 <h2>Features Content</h2>
94 </Tabs.Panel>
95 <Tabs.Panel id="pricing">
96 <h2>Pricing Content</h2>
97 </Tabs.Panel>
98 </Tabs.Panels>
99 </Tabs>
100 );
101}Multiple Contexts Composition#
1// Compose multiple contexts
2function AppProviders({ children }: { children: ReactNode }) {
3 return (
4 <AuthProvider>
5 <ThemeProvider>
6 <NotificationProvider>
7 <CartProvider>
8 {children}
9 </CartProvider>
10 </NotificationProvider>
11 </ThemeProvider>
12 </AuthProvider>
13 );
14}
15
16// Or use a compose utility
17function composeProviders(...providers: React.FC<{ children: ReactNode }>[]) {
18 return ({ children }: { children: ReactNode }) =>
19 providers.reduceRight(
20 (acc, Provider) => <Provider>{acc}</Provider>,
21 children
22 );
23}
24
25const AppProviders = composeProviders(
26 AuthProvider,
27 ThemeProvider,
28 NotificationProvider,
29 CartProvider
30);Context with Persistence#
1// Persist context to localStorage
2import { createContext, useContext, useState, useEffect, ReactNode } from 'react';
3
4interface Settings {
5 theme: 'light' | 'dark';
6 language: string;
7 notifications: boolean;
8}
9
10const defaultSettings: Settings = {
11 theme: 'light',
12 language: 'en',
13 notifications: true,
14};
15
16interface SettingsContextType {
17 settings: Settings;
18 updateSettings: (updates: Partial<Settings>) => void;
19 resetSettings: () => void;
20}
21
22const SettingsContext = createContext<SettingsContextType | undefined>(undefined);
23
24function SettingsProvider({ children }: { children: ReactNode }) {
25 const [settings, setSettings] = useState<Settings>(() => {
26 if (typeof window === 'undefined') return defaultSettings;
27
28 const stored = localStorage.getItem('settings');
29 return stored ? JSON.parse(stored) : defaultSettings;
30 });
31
32 // Persist to localStorage
33 useEffect(() => {
34 localStorage.setItem('settings', JSON.stringify(settings));
35 }, [settings]);
36
37 const updateSettings = (updates: Partial<Settings>) => {
38 setSettings((prev) => ({ ...prev, ...updates }));
39 };
40
41 const resetSettings = () => {
42 setSettings(defaultSettings);
43 };
44
45 return (
46 <SettingsContext.Provider
47 value={{ settings, updateSettings, resetSettings }}
48 >
49 {children}
50 </SettingsContext.Provider>
51 );
52}
53
54function useSettings() {
55 const context = useContext(SettingsContext);
56 if (!context) throw new Error('useSettings must be used within SettingsProvider');
57 return context;
58}Best Practices#
Performance:
✓ Split state and actions into separate contexts
✓ Memoize context values
✓ Use selectors for fine-grained subscriptions
✓ Avoid putting everything in one context
Organization:
✓ Create custom hooks for context consumption
✓ Throw helpful errors when used outside provider
✓ Co-locate context with related components
✓ Use TypeScript for type safety
When to Use Context:
✓ Theme/UI preferences
✓ User authentication state
✓ Locale/i18n settings
✓ Feature flags
When NOT to Use:
✗ Frequently changing data
✗ Large state objects
✗ Server state (use React Query)
✗ Complex state logic (use Zustand/Redux)
Conclusion#
React Context is powerful for sharing state across components. Split contexts for performance, use custom hooks for clean consumption, and consider compound component patterns for flexible APIs. For complex state or frequent updates, consider dedicated state management libraries.