Back to Blog
ReactContextState ManagementPatterns

React Context Patterns for State Management

Master React Context for state management. From basic usage to performance optimization to avoiding common pitfalls.

B
Bootspring Team
Engineering
April 1, 2022
8 min read

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.

Share this article

Help spread the word about Bootspring