Back to Blog
ReactHooksuseContextState Management

React useContext Guide

Master React useContext hook for sharing state across components without prop drilling.

B
Bootspring Team
Engineering
September 5, 2018
7 min read

useContext provides a way to share values between components without passing props through every level of the tree.

Basic Usage#

1import { createContext, useContext } from 'react'; 2 3// Create context with default value 4const ThemeContext = createContext('light'); 5 6// Provider component 7function App() { 8 return ( 9 <ThemeContext.Provider value="dark"> 10 <Toolbar /> 11 </ThemeContext.Provider> 12 ); 13} 14 15// Consuming context 16function Toolbar() { 17 return <ThemedButton />; 18} 19 20function ThemedButton() { 21 const theme = useContext(ThemeContext); 22 return <button className={theme}>Click me</button>; 23}

Creating Context with Types#

1// Define context type 2interface User { 3 id: number; 4 name: string; 5 email: string; 6} 7 8interface UserContextType { 9 user: User | null; 10 login: (user: User) => void; 11 logout: () => void; 12} 13 14// Create with undefined default (requires null check) 15const UserContext = createContext<UserContextType | undefined>(undefined); 16 17// Custom hook with null check 18function useUser() { 19 const context = useContext(UserContext); 20 if (context === undefined) { 21 throw new Error('useUser must be used within UserProvider'); 22 } 23 return context; 24}

Provider Pattern#

1import { createContext, useContext, useState, useMemo } from 'react'; 2 3const AuthContext = createContext(null); 4 5function AuthProvider({ children }) { 6 const [user, setUser] = useState(null); 7 const [loading, setLoading] = useState(true); 8 9 const login = async (credentials) => { 10 setLoading(true); 11 const user = await authService.login(credentials); 12 setUser(user); 13 setLoading(false); 14 }; 15 16 const logout = async () => { 17 await authService.logout(); 18 setUser(null); 19 }; 20 21 // Memoize value to prevent unnecessary re-renders 22 const value = useMemo(() => ({ 23 user, 24 loading, 25 login, 26 logout, 27 isAuthenticated: !!user 28 }), [user, loading]); 29 30 return ( 31 <AuthContext.Provider value={value}> 32 {children} 33 </AuthContext.Provider> 34 ); 35} 36 37// Custom hook 38function useAuth() { 39 const context = useContext(AuthContext); 40 if (!context) { 41 throw new Error('useAuth must be used within AuthProvider'); 42 } 43 return context; 44} 45 46// Usage 47function App() { 48 return ( 49 <AuthProvider> 50 <Navigation /> 51 <MainContent /> 52 </AuthProvider> 53 ); 54} 55 56function Navigation() { 57 const { user, logout } = useAuth(); 58 59 return ( 60 <nav> 61 {user ? ( 62 <> 63 <span>Hello, {user.name}</span> 64 <button onClick={logout}>Logout</button> 65 </> 66 ) : ( 67 <LoginButton /> 68 )} 69 </nav> 70 ); 71}

Multiple Contexts#

1// Theme context 2const ThemeContext = createContext({ theme: 'light', toggleTheme: () => {} }); 3 4// Language context 5const LanguageContext = createContext({ language: 'en', setLanguage: () => {} }); 6 7// Combined providers 8function AppProviders({ children }) { 9 return ( 10 <ThemeProvider> 11 <LanguageProvider> 12 <AuthProvider> 13 {children} 14 </AuthProvider> 15 </LanguageProvider> 16 </ThemeProvider> 17 ); 18} 19 20// Consume multiple contexts 21function Settings() { 22 const { theme, toggleTheme } = useContext(ThemeContext); 23 const { language, setLanguage } = useContext(LanguageContext); 24 25 return ( 26 <div> 27 <button onClick={toggleTheme}> 28 Theme: {theme} 29 </button> 30 <select value={language} onChange={e => setLanguage(e.target.value)}> 31 <option value="en">English</option> 32 <option value="es">Spanish</option> 33 </select> 34 </div> 35 ); 36}

Context with Reducer#

1import { createContext, useContext, useReducer } from 'react'; 2 3// Define actions 4const ACTIONS = { 5 ADD_TODO: 'ADD_TODO', 6 TOGGLE_TODO: 'TOGGLE_TODO', 7 DELETE_TODO: 'DELETE_TODO' 8}; 9 10// Reducer 11function todoReducer(state, action) { 12 switch (action.type) { 13 case ACTIONS.ADD_TODO: 14 return [...state, { 15 id: Date.now(), 16 text: action.payload, 17 completed: false 18 }]; 19 case ACTIONS.TOGGLE_TODO: 20 return state.map(todo => 21 todo.id === action.payload 22 ? { ...todo, completed: !todo.completed } 23 : todo 24 ); 25 case ACTIONS.DELETE_TODO: 26 return state.filter(todo => todo.id !== action.payload); 27 default: 28 return state; 29 } 30} 31 32// Separate contexts for state and dispatch 33const TodoStateContext = createContext(); 34const TodoDispatchContext = createContext(); 35 36function TodoProvider({ children }) { 37 const [todos, dispatch] = useReducer(todoReducer, []); 38 39 return ( 40 <TodoStateContext.Provider value={todos}> 41 <TodoDispatchContext.Provider value={dispatch}> 42 {children} 43 </TodoDispatchContext.Provider> 44 </TodoStateContext.Provider> 45 ); 46} 47 48// Custom hooks 49function useTodos() { 50 const context = useContext(TodoStateContext); 51 if (context === undefined) { 52 throw new Error('useTodos must be within TodoProvider'); 53 } 54 return context; 55} 56 57function useTodoDispatch() { 58 const context = useContext(TodoDispatchContext); 59 if (context === undefined) { 60 throw new Error('useTodoDispatch must be within TodoProvider'); 61 } 62 return context; 63} 64 65// Usage 66function TodoList() { 67 const todos = useTodos(); 68 const dispatch = useTodoDispatch(); 69 70 return ( 71 <ul> 72 {todos.map(todo => ( 73 <li key={todo.id}> 74 <span 75 style={{ textDecoration: todo.completed ? 'line-through' : 'none' }} 76 onClick={() => dispatch({ 77 type: ACTIONS.TOGGLE_TODO, 78 payload: todo.id 79 })} 80 > 81 {todo.text} 82 </span> 83 <button onClick={() => dispatch({ 84 type: ACTIONS.DELETE_TODO, 85 payload: todo.id 86 })}> 87 Delete 88 </button> 89 </li> 90 ))} 91 </ul> 92 ); 93}

Performance Optimization#

1import { createContext, useContext, useState, useMemo, useCallback, memo } from 'react'; 2 3// Split context to avoid unnecessary re-renders 4const CountContext = createContext(); 5const CountActionsContext = createContext(); 6 7function CountProvider({ children }) { 8 const [count, setCount] = useState(0); 9 10 // Memoize actions 11 const actions = useMemo(() => ({ 12 increment: () => setCount(c => c + 1), 13 decrement: () => setCount(c => c - 1), 14 reset: () => setCount(0) 15 }), []); 16 17 return ( 18 <CountContext.Provider value={count}> 19 <CountActionsContext.Provider value={actions}> 20 {children} 21 </CountActionsContext.Provider> 22 </CountContext.Provider> 23 ); 24} 25 26// Component that only reads count 27const CountDisplay = memo(function CountDisplay() { 28 const count = useContext(CountContext); 29 console.log('CountDisplay rendered'); 30 return <div>Count: {count}</div>; 31}); 32 33// Component that only uses actions 34const CountButtons = memo(function CountButtons() { 35 const { increment, decrement, reset } = useContext(CountActionsContext); 36 console.log('CountButtons rendered'); 37 38 return ( 39 <div> 40 <button onClick={decrement}>-</button> 41 <button onClick={increment}>+</button> 42 <button onClick={reset}>Reset</button> 43 </div> 44 ); 45});

Default Values#

1// Context with meaningful default 2const ThemeContext = createContext({ 3 theme: 'light', 4 toggleTheme: () => { 5 console.warn('toggleTheme called outside provider'); 6 } 7}); 8 9// Can use without provider (fallback to default) 10function Button() { 11 const { theme } = useContext(ThemeContext); 12 return <button className={`btn-${theme}`}>Click</button>; 13} 14 15// Provider overrides default 16function App() { 17 const [theme, setTheme] = useState('light'); 18 19 const value = useMemo(() => ({ 20 theme, 21 toggleTheme: () => setTheme(t => t === 'light' ? 'dark' : 'light') 22 }), [theme]); 23 24 return ( 25 <ThemeContext.Provider value={value}> 26 <Button /> 27 </ThemeContext.Provider> 28 ); 29}

Nested Providers#

1// Providers can be nested and override 2function App() { 3 return ( 4 <ThemeContext.Provider value="light"> 5 <Header /> 6 <ThemeContext.Provider value="dark"> 7 <Sidebar /> 8 </ThemeContext.Provider> 9 <Footer /> 10 </ThemeContext.Provider> 11 ); 12} 13 14// Header and Footer get "light" 15// Sidebar gets "dark"

Context Selectors (Custom Pattern)#

1// Without selectors: all consumers re-render on any change 2const BigContext = createContext(); 3 4function useBigContext() { 5 return useContext(BigContext); 6} 7 8// Every component re-renders when anything changes 9function UserName() { 10 const { user } = useBigContext(); // Re-renders on any context change 11 return <span>{user.name}</span>; 12} 13 14// With selector pattern (using useSyncExternalStore or libraries) 15import { useSyncExternalStore } from 'react'; 16 17function createStore(initialState) { 18 let state = initialState; 19 const listeners = new Set(); 20 21 return { 22 getState: () => state, 23 setState: (newState) => { 24 state = { ...state, ...newState }; 25 listeners.forEach(l => l()); 26 }, 27 subscribe: (listener) => { 28 listeners.add(listener); 29 return () => listeners.delete(listener); 30 } 31 }; 32} 33 34function useSelector(store, selector) { 35 return useSyncExternalStore( 36 store.subscribe, 37 () => selector(store.getState()) 38 ); 39} 40 41// Only re-renders when selected value changes 42function UserName({ store }) { 43 const name = useSelector(store, state => state.user.name); 44 return <span>{name}</span>; 45}

Testing with Context#

1// Component using context 2function Greeting() { 3 const { user } = useAuth(); 4 return <h1>Hello, {user?.name || 'Guest'}</h1>; 5} 6 7// Test wrapper 8function renderWithAuth(ui, { user = null } = {}) { 9 const mockAuth = { 10 user, 11 login: jest.fn(), 12 logout: jest.fn() 13 }; 14 15 return render( 16 <AuthContext.Provider value={mockAuth}> 17 {ui} 18 </AuthContext.Provider> 19 ); 20} 21 22// Tests 23test('shows guest when not logged in', () => { 24 renderWithAuth(<Greeting />); 25 expect(screen.getByText('Hello, Guest')).toBeInTheDocument(); 26}); 27 28test('shows user name when logged in', () => { 29 renderWithAuth(<Greeting />, { user: { name: 'John' } }); 30 expect(screen.getByText('Hello, John')).toBeInTheDocument(); 31});

Common Patterns#

1// Feature flag context 2const FeatureFlagContext = createContext({}); 3 4function useFeatureFlag(flag) { 5 const flags = useContext(FeatureFlagContext); 6 return flags[flag] ?? false; 7} 8 9function NewFeature() { 10 const enabled = useFeatureFlag('newDashboard'); 11 12 if (!enabled) return null; 13 return <NewDashboard />; 14} 15 16// Modal context 17const ModalContext = createContext(); 18 19function ModalProvider({ children }) { 20 const [modal, setModal] = useState(null); 21 22 const openModal = useCallback((content) => setModal(content), []); 23 const closeModal = useCallback(() => setModal(null), []); 24 25 return ( 26 <ModalContext.Provider value={{ openModal, closeModal }}> 27 {children} 28 {modal && ( 29 <div className="modal-overlay" onClick={closeModal}> 30 <div className="modal" onClick={e => e.stopPropagation()}> 31 {modal} 32 </div> 33 </div> 34 )} 35 </ModalContext.Provider> 36 ); 37}

Best Practices#

Context Design: ✓ Split state and dispatch contexts ✓ Memoize context values ✓ Create custom hooks ✓ Use meaningful defaults Performance: ✓ Keep context values stable ✓ Split contexts by update frequency ✓ Use memo for consumers ✓ Consider external state libraries for complex needs Organization: ✓ One context per concern ✓ Colocate provider with related components ✓ Export custom hooks, not raw context ✓ Document context requirements Avoid: ✗ Putting everything in one context ✗ Frequent context value updates ✗ Deeply nested providers ✗ Using context for local state

Conclusion#

useContext eliminates prop drilling and enables clean state sharing across component trees. Create custom hooks to encapsulate context access, split contexts by update frequency for performance, and memoize values to prevent unnecessary re-renders. For complex state management, consider combining context with useReducer or external libraries.

Share this article

Help spread the word about Bootspring