Chapter 15: Context API & State Management
Today we’ll learn how to use React’s built-in Context API to share state deeply in the component tree without passing props everywhere.
We’ll go very slowly and clearly, like I’m sitting next to you in Mumbai with our laptops open, chai on the table, and I’m explaining everything live with complete, copy-paste-ready examples.
1. When to Use Context (and When NOT to)
Use Context when:
- You have global or shared data that many components (at different levels) need to access
- Examples:
- Current theme (light/dark)
- Authenticated user info
- Language/locale settings
- Cart contents in an e-commerce app
- App-wide settings (font size, accessibility preferences)
Do NOT use Context when:
- The data is only needed by one or two close components → use props or lift state up
- You’re dealing with very frequent updates (like mouse position) → Context can cause unnecessary re-renders
- You need very complex state logic → better to use Zustand, Jotai, or Redux Toolkit (we’ll cover later)
Golden Rule (2026 best practice): Context is perfect for low-frequency, global data that needs to be accessed by many nested components.
2. Creating & Providing Context (Step by Step)
Let’s build a real-world Theme Switcher that works across the entire app.
Step 1: Create the Context
Create src/context/ThemeContext.tsx
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
import { createContext, useState, ReactNode, useContext } from 'react'; // 1. Define the shape of the context value interface ThemeContextType { theme: 'light' | 'dark'; toggleTheme: () => void; } // 2. Create the Context (with undefined as default so we can force usage inside provider) const ThemeContext = createContext<ThemeContextType | undefined>(undefined); // 3. Provider Component (this is what you wrap your app with) export function ThemeProvider({ children }: { children: ReactNode }) { const [theme, setTheme] = useState<'light' | 'dark'>('light'); const toggleTheme = () => { setTheme(prev => prev === 'light' ? 'dark' : 'light'); }; // Provide the value to all descendants return ( <ThemeContext.Provider value={{ theme, toggleTheme }}> {children} </ThemeContext.Provider> ); } // 4. Custom hook to consume context safely (best practice) export function useTheme() { const context = useContext(ThemeContext); if (!context) { throw new Error('useTheme must be used within a ThemeProvider'); } return context; } |
Step 2: Wrap Your App with the Provider
In src/main.tsx:
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
import React from 'react' import ReactDOM from 'react-dom/client' import { BrowserRouter } from 'react-router-dom' import { ThemeProvider } from './context/ThemeContext.tsx' import App from './App.tsx' import './index.css' ReactDOM.createRoot(document.getElementById('root')!).render( <React.StrictMode> <ThemeProvider> <BrowserRouter> <App /> </BrowserRouter> </ThemeProvider> </React.StrictMode>, ) |
Now every component in your app can access the theme!
Step 3: Consume Context with useContext (or our custom hook)
Create a component deep in the tree: src/components/ThemedButton.tsx
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
import { useTheme } from '../context/ThemeContext'; function ThemedButton() { const { theme, toggleTheme } = useTheme(); // ← magic! No props needed return ( <button onClick={toggleTheme} style={{ padding: '14px 28px', fontSize: '18px', backgroundColor: theme === 'dark' ? '#333' : '#646cff', color: 'white', border: 'none', borderRadius: '8px', cursor: 'pointer', transition: 'all 0.3s' }} > Toggle Theme (Current: {theme.toUpperCase()}) </button> ); } export default ThemedButton; |
Now you can drop <ThemedButton /> anywhere in your app — even 10 levels deep — and it will work!
3. Full Example: Complete Theme + User Auth Context
Let’s make it more realistic — combine theme and user authentication.
Create src/context/AppContext.tsx
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 |
import { createContext, useState, ReactNode, useContext } from 'react'; interface User { name: string; email: string; isAuthenticated: boolean; } interface AppContextType { theme: 'light' | 'dark'; toggleTheme: () => void; user: User; login: (name: string, email: string) => void; logout: () => void; } const AppContext = createContext<AppContextType | undefined>(undefined); export function AppProvider({ children }: { children: ReactNode }) { const [theme, setTheme] = useState<'light' | 'dark'>('light'); const [user, setUser] = useState<User>({ name: '', email: '', isAuthenticated: false }); const toggleTheme = () => setTheme(prev => prev === 'light' ? 'dark' : 'light'); const login = (name: string, email: string) => { setUser({ name, email, isAuthenticated: true }); }; const logout = () => { setUser({ name: '', email: '', isAuthenticated: false }); }; return ( <AppContext.Provider value={{ theme, toggleTheme, user, login, logout }}> {children} </AppContext.Provider> ); } export function useAppContext() { const context = useContext(AppContext); if (!context) throw new Error('useAppContext must be used within AppProvider'); return context; } |
Wrap in main.tsx:
|
0 1 2 3 4 5 6 7 8 9 10 |
<AppProvider> <BrowserRouter> <App /> </BrowserRouter> </AppProvider> |
Now use it anywhere:
src/components/ProfileCard.tsx
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
import { useAppContext } from '../context/AppContext'; function ProfileCard() { const { user, login, logout, theme } = useAppContext(); return ( <div style={{ padding: '30px', background: theme === 'dark' ? '#333' : '#f8f9ff', color: theme === 'dark' ? 'white' : 'black', borderRadius: '16px', boxShadow: '0 8px 30px rgba(0,0,0,0.1)', maxWidth: '500px', margin: '40px auto', textAlign: 'center' }}> {user.isAuthenticated ? ( <> <h2>Welcome, {user.name}!</h2> <p>Email: {user.email}</p> <button onClick={logout} style={{ padding: '12px 24px', background: '#ff6b6b', color: 'white', border: 'none', borderRadius: '8px' }} > Logout </button> </> ) : ( <> <h2>Please Login</h2> <button onClick={() => login('Webliance', 'webliance@example.com')} style={{ padding: '12px 24px', background: '#646cff', color: 'white', border: 'none', borderRadius: '8px' }} > Login as Webliance </button> </> )} </div> ); } |
4. Avoiding Prop Drilling – Before & After
Before Context (prop drilling hell):
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
// App → Layout → Sidebar → UserMenu → Profile → Avatar // You pass user prop through EVERY level! <Layout user={user}> <Sidebar user={user}> <UserMenu user={user}> <Profile user={user}> <Avatar user={user} /> </Profile> </UserMenu> </Sidebar> </Layout> |
After Context:
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
<AppProvider> <Layout> {/* no props */} <Sidebar> {/* no props */} <UserMenu> {/* no props */} <Profile> {/* no props */} <Avatar /> {/* magically gets user from context */} </Profile> </UserMenu> </Sidebar> </Layout> </AppProvider> |
Summary – Chapter 15 Key Takeaways
- Context = share state deeply without prop drilling
- Create context with createContext
- Provide value with <Provider>
- Consume with useContext (or custom hook like useTheme)
- Use custom provider + hook pattern → clean & safe
- Perfect for global, low-frequency data (theme, auth, settings)
- For very complex/global state → consider Zustand or Jotai later
Mini Homework
- Create an AuthContext with login/logout + current user
- Make a Navbar that shows “Login” or “Welcome, Webliance!” + Logout button
- Make a ProtectedRoute that redirects to login if not authenticated
