React Context provides component-tree-wide state sharing. Here's how to use it effectively.
Creating Context#
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 };
38
39 const value = { user, login, logout, isLoading };
40
41 return (
42 <AuthContext.Provider value={value}>
43 {children}
44 </AuthContext.Provider>
45 );
46}
47
48// Custom hook for consuming
49function useAuth() {
50 const context = useContext(AuthContext);
51 if (context === undefined) {
52 throw new Error('useAuth must be used within an AuthProvider');
53 }
54 return context;
55}
56
57export { AuthProvider, useAuth };Splitting State and Dispatch#
1// Separate contexts for better performance
2const StateContext = createContext<State | undefined>(undefined);
3const DispatchContext = createContext<Dispatch | undefined>(undefined);
4
5interface State {
6 count: number;
7 items: string[];
8}
9
10type Action =
11 | { type: 'INCREMENT' }
12 | { type: 'DECREMENT' }
13 | { type: 'ADD_ITEM'; payload: string };
14
15function reducer(state: State, action: Action): State {
16 switch (action.type) {
17 case 'INCREMENT':
18 return { ...state, count: state.count + 1 };
19 case 'DECREMENT':
20 return { ...state, count: state.count - 1 };
21 case 'ADD_ITEM':
22 return { ...state, items: [...state.items, action.payload] };
23 default:
24 return state;
25 }
26}
27
28function AppProvider({ children }: { children: ReactNode }) {
29 const [state, dispatch] = useReducer(reducer, { count: 0, items: [] });
30
31 return (
32 <StateContext.Provider value={state}>
33 <DispatchContext.Provider value={dispatch}>
34 {children}
35 </DispatchContext.Provider>
36 </StateContext.Provider>
37 );
38}
39
40// Separate hooks
41function useAppState() {
42 const context = useContext(StateContext);
43 if (context === undefined) {
44 throw new Error('useAppState must be used within AppProvider');
45 }
46 return context;
47}
48
49function useAppDispatch() {
50 const context = useContext(DispatchContext);
51 if (context === undefined) {
52 throw new Error('useAppDispatch must be used within AppProvider');
53 }
54 return context;
55}
56
57// Components can subscribe to only what they need
58function Counter() {
59 const { count } = useAppState();
60 return <span>{count}</span>;
61}
62
63function IncrementButton() {
64 const dispatch = useAppDispatch();
65 return (
66 <button onClick={() => dispatch({ type: 'INCREMENT' })}>
67 Increment
68 </button>
69 );
70}Memoizing Context Value#
1function AppProvider({ children }: { children: ReactNode }) {
2 const [user, setUser] = useState<User | null>(null);
3 const [theme, setTheme] = useState<'light' | 'dark'>('light');
4
5 // Memoize callbacks
6 const login = useCallback(async (credentials: Credentials) => {
7 const user = await api.login(credentials);
8 setUser(user);
9 }, []);
10
11 const logout = useCallback(() => {
12 setUser(null);
13 }, []);
14
15 const toggleTheme = useCallback(() => {
16 setTheme(t => t === 'light' ? 'dark' : 'light');
17 }, []);
18
19 // Memoize context value
20 const value = useMemo(
21 () => ({
22 user,
23 theme,
24 login,
25 logout,
26 toggleTheme,
27 }),
28 [user, theme, login, logout, toggleTheme]
29 );
30
31 return (
32 <AppContext.Provider value={value}>
33 {children}
34 </AppContext.Provider>
35 );
36}Composing Multiple Providers#
1// Provider composition
2function AppProviders({ children }: { children: ReactNode }) {
3 return (
4 <ErrorBoundary>
5 <AuthProvider>
6 <ThemeProvider>
7 <NotificationProvider>
8 <DataProvider>
9 {children}
10 </DataProvider>
11 </NotificationProvider>
12 </ThemeProvider>
13 </AuthProvider>
14 </ErrorBoundary>
15 );
16}
17
18// Compose function helper
19function composeProviders(...providers: React.ComponentType<{ children: ReactNode }>[]) {
20 return ({ children }: { children: ReactNode }) =>
21 providers.reduceRight(
22 (child, Provider) => <Provider>{child}</Provider>,
23 children
24 );
25}
26
27const AppProviders = composeProviders(
28 ErrorBoundary,
29 AuthProvider,
30 ThemeProvider,
31 NotificationProvider,
32 DataProvider
33);Context with Initialization#
1// Context that requires initialization
2interface Config {
3 apiUrl: string;
4 features: string[];
5}
6
7const ConfigContext = createContext<Config | null>(null);
8
9function ConfigProvider({
10 config,
11 children,
12}: {
13 config: Config;
14 children: ReactNode;
15}) {
16 return (
17 <ConfigContext.Provider value={config}>
18 {children}
19 </ConfigContext.Provider>
20 );
21}
22
23function useConfig() {
24 const config = useContext(ConfigContext);
25 if (config === null) {
26 throw new Error('useConfig must be used within ConfigProvider');
27 }
28 return config;
29}
30
31// Usage
32function App() {
33 const [config, setConfig] = useState<Config | null>(null);
34
35 useEffect(() => {
36 loadConfig().then(setConfig);
37 }, []);
38
39 if (!config) {
40 return <Loading />;
41 }
42
43 return (
44 <ConfigProvider config={config}>
45 <MainApp />
46 </ConfigProvider>
47 );
48}Context with Selectors#
1// For fine-grained subscriptions
2import { useSyncExternalStore } from 'react';
3
4function createStore<T>(initialState: T) {
5 let state = initialState;
6 const listeners = new Set<() => void>();
7
8 return {
9 getState: () => state,
10 setState: (newState: T | ((prev: T) => T)) => {
11 state = typeof newState === 'function'
12 ? (newState as (prev: T) => T)(state)
13 : newState;
14 listeners.forEach(listener => listener());
15 },
16 subscribe: (listener: () => void) => {
17 listeners.add(listener);
18 return () => listeners.delete(listener);
19 },
20 };
21}
22
23type Store<T> = ReturnType<typeof createStore<T>>;
24
25const StoreContext = createContext<Store<AppState> | null>(null);
26
27function useSelector<T>(selector: (state: AppState) => T): T {
28 const store = useContext(StoreContext);
29 if (!store) throw new Error('Store not found');
30
31 return useSyncExternalStore(
32 store.subscribe,
33 () => selector(store.getState())
34 );
35}
36
37// Only re-renders when selected value changes
38function UserName() {
39 const name = useSelector(state => state.user?.name);
40 return <span>{name}</span>;
41}Testing Context#
1// Test wrapper
2function createTestWrapper(initialState?: Partial<AppState>) {
3 return function TestWrapper({ children }: { children: ReactNode }) {
4 return (
5 <AppProvider initialState={initialState}>
6 {children}
7 </AppProvider>
8 );
9 };
10}
11
12// Test with custom render
13import { render, screen } from '@testing-library/react';
14
15function renderWithProviders(
16 ui: React.ReactElement,
17 options?: { initialState?: Partial<AppState> }
18) {
19 const Wrapper = createTestWrapper(options?.initialState);
20 return render(ui, { wrapper: Wrapper });
21}
22
23// Tests
24describe('UserProfile', () => {
25 it('displays user name', () => {
26 renderWithProviders(<UserProfile />, {
27 initialState: { user: { name: 'Alice' } },
28 });
29
30 expect(screen.getByText('Alice')).toBeInTheDocument();
31 });
32});
33
34// Mock context for unit tests
35const mockAuthContext: AuthContextType = {
36 user: { id: '1', name: 'Test User', email: 'test@example.com' },
37 login: jest.fn(),
38 logout: jest.fn(),
39 isLoading: false,
40};
41
42function MockAuthProvider({ children }: { children: ReactNode }) {
43 return (
44 <AuthContext.Provider value={mockAuthContext}>
45 {children}
46 </AuthContext.Provider>
47 );
48}Context with Persistence#
1// Persist context to localStorage
2function createPersistedContext<T>(key: string, defaultValue: T) {
3 const Context = createContext<{
4 value: T;
5 setValue: (value: T) => void;
6 } | undefined>(undefined);
7
8 function Provider({ children }: { children: ReactNode }) {
9 const [value, setValue] = useState<T>(() => {
10 try {
11 const stored = localStorage.getItem(key);
12 return stored ? JSON.parse(stored) : defaultValue;
13 } catch {
14 return defaultValue;
15 }
16 });
17
18 useEffect(() => {
19 localStorage.setItem(key, JSON.stringify(value));
20 }, [value]);
21
22 return (
23 <Context.Provider value={{ value, setValue }}>
24 {children}
25 </Context.Provider>
26 );
27 }
28
29 function usePersistedContext() {
30 const context = useContext(Context);
31 if (context === undefined) {
32 throw new Error('usePersistedContext must be within Provider');
33 }
34 return context;
35 }
36
37 return { Provider, usePersistedContext };
38}
39
40// Usage
41const { Provider: ThemeProvider, usePersistedContext: useTheme } =
42 createPersistedContext('theme', 'light');Common Pitfalls#
1// BAD: New object on every render
2function BadProvider({ children }) {
3 const [user, setUser] = useState(null);
4
5 return (
6 <Context.Provider value={{ user, setUser }}> {/* New object each render */}
7 {children}
8 </Context.Provider>
9 );
10}
11
12// GOOD: Memoize the value
13function GoodProvider({ children }) {
14 const [user, setUser] = useState(null);
15 const value = useMemo(() => ({ user, setUser }), [user]);
16
17 return (
18 <Context.Provider value={value}>
19 {children}
20 </Context.Provider>
21 );
22}
23
24// BAD: Using context for frequently changing values
25function BadFrequentUpdates() {
26 const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 });
27 // Every mouse move re-renders all consumers!
28}
29
30// GOOD: Use refs or external store for frequent updatesBest Practices#
Design:
✓ Create custom hooks for consuming context
✓ Split state and dispatch contexts
✓ Memoize context values
✓ Use TypeScript for type safety
Performance:
✓ Avoid frequent context updates
✓ Split contexts by update frequency
✓ Memoize callbacks in providers
✓ Consider selector patterns
Testing:
✓ Create test wrappers
✓ Allow initial state injection
✓ Mock contexts for unit tests
✓ Test provider and consumer separately
Avoid:
✗ Prop drilling alternatives only
✗ Context for high-frequency updates
✗ Too many nested providers
✗ Missing error boundaries
Conclusion#
React Context is powerful for sharing state across components. Split contexts by update frequency, memoize values and callbacks, and always create custom hooks for consuming context. For high-frequency updates or complex state, consider dedicated state management libraries.