React Context

Pass data through the component tree without prop drilling:

  import { createContext, useContext, useState } from 'react';

const ThemeContext = createContext(null);

export function ThemeProvider({ children }) {
    const [theme, setTheme] = useState('light');

    const toggleTheme = () => setTheme(t => t === 'light' ? 'dark' : 'light');

    return (
        <ThemeContext.Provider value={{ theme, toggleTheme }}>
            {children}
        </ThemeContext.Provider>
    );
}

export function useTheme() {
    const context = useContext(ThemeContext);
    if (!context) throw new Error('useTheme must be used within ThemeProvider');
    return context;
}
  

Usage:

  // App.jsx
function App() {
    return (
        <ThemeProvider>
            <Header />
            <Main />
        </ThemeProvider>
    );
}

// Any nested component
function Header() {
    const { theme, toggleTheme } = useTheme();
    return (
        <header className={theme}>
            <button onClick={toggleTheme}>Toggle {theme}</button>
        </header>
    );
}
  

Auth Context Example

  const AuthContext = createContext(null);

export function AuthProvider({ children }) {
    const [user, setUser] = useState(null);
    const [loading, setLoading] = useState(true);

    useEffect(() => {
        checkAuth().then(setUser).finally(() => setLoading(false));
    }, []);

    const login = async (credentials) => {
        const user = await api.login(credentials);
        setUser(user);
    };

    const logout = () => setUser(null);

    return (
        <AuthContext.Provider value={{ user, loading, login, logout }}>
            {children}
        </AuthContext.Provider>
    );
}

export function useAuth() {
    return useContext(AuthContext);
}
  

useReducer for Complex State

  const todoReducer = (state, action) => {
    switch (action.type) {
        case 'ADD':
            return [...state, { id: Date.now(), text: action.text, done: false }];
        case 'TOGGLE':
            return state.map(todo =>
                todo.id === action.id ? { ...todo, done: !todo.done } : todo
            );
        case 'DELETE':
            return state.filter(todo => todo.id !== action.id);
        default:
            return state;
    }
};

function TodoApp() {
    const [todos, dispatch] = useReducer(todoReducer, []);

    return (
        <div>
            <button onClick={() => dispatch({ type: 'ADD', text: 'New task' })}>
                Add
            </button>
            {todos.map(todo => (
                <div key={todo.id}>
                    <span onClick={() => dispatch({ type: 'TOGGLE', id: todo.id })}>
                        {todo.done ? '✓' : '○'} {todo.text}
                    </span>
                    <button onClick={() => dispatch({ type: 'DELETE', id: todo.id })}>×</button>
                </div>
            ))}
        </div>
    );
}
  

Context + useReducer Pattern

Combine for global state management (similar to Redux):

  const AppContext = createContext(null);

const appReducer = (state, action) => {
    switch (action.type) {
        case 'SET_USER': return { ...state, user: action.payload };
        case 'SET_THEME': return { ...state, theme: action.payload };
        default: return state;
    }
};

export function AppProvider({ children }) {
    const [state, dispatch] = useReducer(appReducer, { user: null, theme: 'light' });
    return (
        <AppContext.Provider value={{ state, dispatch }}>
            {children}
        </AppContext.Provider>
    );
}
  

When to Use What

Solution Use when
useState Local component state
Lifting state Sibling communication
Context Theme, auth, locale — moderate global state
useReducer Complex state transitions
Zustand/Redux Large apps, many state slices

Context is great for infrequently changing global data — avoid putting fast-changing state in Context (causes wide re-renders).