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.