Context · How it works

1 min read
Mid-level3 min read
Rapid overview

How it works

Table of Contents


What is Context

Context provides a way to pass data through the component tree without manually passing props at every level (prop drilling).

When to Use Context

  • Theme data: Dark/light mode, color schemes
  • User data: Current authenticated user
  • Locale/language: i18n preferences
  • Feature flags: A/B testing, feature toggles
  • UI state: Modal open/close, sidebar collapsed
// ❌ Prop drilling - passing through every component
function App() {
  const [user, setUser] = useState(null);
  return <Layout user={user} setUser={setUser} />;
}

function Layout({ user, setUser }) {
  return <Sidebar user={user} setUser={setUser} />;
}

function Sidebar({ user, setUser }) {
  return <UserProfile user={user} setUser={setUser} />;
}

// ✅ Context - components access what they need
function App() {
  return (
    <UserProvider>
      <Layout />
    </UserProvider>
  );
}

function UserProfile() {
  const { user, setUser } = useUser(); // Direct access
  return <div>{user.name}</div>;
}

Creating and Using Context

Basic Pattern

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

// 1. Create context with default value
const ThemeContext = createContext('light');

// 2. Create provider component
function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');

  const toggleTheme = () => {
    setTheme(prev => prev === 'light' ? 'dark' : 'light');
  };

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

// 3. Create custom hook for consuming
function useTheme() {
  const context = useContext(ThemeContext);
  if (context === undefined) {
    throw new Error('useTheme must be used within a ThemeProvider');
  }
  return context;
}

// 4. Use in components
function ThemedButton() {
  const { theme, toggleTheme } = useTheme();

  return (
    <button
      onClick={toggleTheme}
      className={theme === 'dark' ? 'btn-dark' : 'btn-light'}
    >
      Current: {theme}
    </button>
  );
}

// 5. Wrap app with provider
function App() {
  return (
    <ThemeProvider>
      <ThemedButton />
    </ThemeProvider>
  );
}

Multiple Contexts

function App() {
  return (
    <AuthProvider>
      <ThemeProvider>
        <LocaleProvider>
          <MainApp />
        </LocaleProvider>
      </ThemeProvider>
    </AuthProvider>
  );
}

// Compose multiple contexts
function Providers({ children }) {
  return (
    <AuthProvider>
      <ThemeProvider>
        <LocaleProvider>
          {children}
        </LocaleProvider>
      </ThemeProvider>
    </AuthProvider>
  );
}

function App() {
  return (
    <Providers>
      <MainApp />
    </Providers>
  );
}

Context with Reducer

const TodoContext = createContext(null);

const initialState = {
  todos: [],
  filter: 'all'
};

function todoReducer(state, action) {
  switch (action.type) {
    case 'ADD_TODO':
      return {
        ...state,
        todos: [...state.todos, {
          id: Date.now(),
          text: action.payload,
          completed: false
        }]
      };
    case 'TOGGLE_TODO':
      return {
        ...state,
        todos: state.todos.map(todo =>
          todo.id === action.payload
            ? { ...todo, completed: !todo.completed }
            : todo
        )
      };
    case 'SET_FILTER':
      return { ...state, filter: action.payload };
    default:
      return state;
  }
}

function TodoProvider({ children }) {
  const [state, dispatch] = useReducer(todoReducer, initialState);

  const value = useMemo(() => ({ state, dispatch }), [state]);

  return (
    <TodoContext.Provider value={value}>
      {children}
    </TodoContext.Provider>
  );
}

function useTodos() {
  const context = useContext(TodoContext);
  if (!context) {
    throw new Error('useTodos must be used within TodoProvider');
  }
  return context;
}

Context with TypeScript

Typed Context

interface User {
  id: string;
  name: string;
  email: string;
}

interface AuthContextType {
  user: User | null;
  login: (credentials: { email: string; password: string }) => Promise<void>;
  logout: () => void;
  isLoading: boolean;
}

const AuthContext = createContext<AuthContextType | undefined>(undefined);

function AuthProvider({ children }: { children: React.ReactNode }) {
  const [user, setUser] = useState<User | null>(null);
  const [isLoading, setIsLoading] = useState(false);

  const login = async (credentials: { email: string; password: string }) => {
    setIsLoading(true);
    try {
      const user = await api.login(credentials);
      setUser(user);
    } finally {
      setIsLoading(false);
    }
  };

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

  const value = useMemo(
    () => ({ user, login, logout, isLoading }),
    [user, isLoading]
  );

  return (
    <AuthContext.Provider value={value}>
      {children}
    </AuthContext.Provider>
  );
}

function useAuth(): AuthContextType {
  const context = useContext(AuthContext);
  if (context === undefined) {
    throw new Error('useAuth must be used within an AuthProvider');
  }
  return context;
}

Generic Context Factory

function createSafeContext<T>(displayName: string) {
  const Context = createContext<T | undefined>(undefined);
  Context.displayName = displayName;

  function useContextSafe(): T {
    const context = useContext(Context);
    if (context === undefined) {
      throw new Error(
        `use${displayName} must be used within a ${displayName}Provider`
      );
    }
    return context;
  }

  return [Context.Provider, useContextSafe] as const;
}

// Usage
interface CounterContextType {
  count: number;
  increment: () => void;
  decrement: () => void;
}

const [CounterProvider, useCounter] = createSafeContext<CounterContextType>('Counter');

function CounterContextProvider({ children }: { children: React.ReactNode }) {
  const [count, setCount] = useState(0);

  const value = useMemo(() => ({
    count,
    increment: () => setCount(c => c + 1),
    decrement: () => setCount(c => c - 1)
  }), [count]);

  return <CounterProvider value={value}>{children}</CounterProvider>;
}

Context Patterns

Compound Components with Context

const AccordionContext = createContext(null);

function Accordion({ children, allowMultiple = false }) {
  const [openItems, setOpenItems] = useState(new Set());

  const toggle = (id) => {
    setOpenItems(prev => {
      const next = new Set(prev);
      if (next.has(id)) {
        next.delete(id);
      } else {
        if (!allowMultiple) next.clear();
        next.add(id);
      }
      return next;
    });
  };

  const isOpen = (id) => openItems.has(id);

  return (
    <AccordionContext.Provider value={{ toggle, isOpen }}>
      <div className="accordion">{children}</div>
    </AccordionContext.Provider>
  );
}

function AccordionItem({ id, children }) {
  return <div className="accordion-item">{children}</div>;
}

function AccordionHeader({ id, children }) {
  const { toggle, isOpen } = useContext(AccordionContext);

  return (
    <button
      className="accordion-header"
      onClick={() => toggle(id)}
      aria-expanded={isOpen(id)}
    >
      {children}
      <span>{isOpen(id) ? '−' : '+'}</span>
    </button>
  );
}

function AccordionPanel({ id, children }) {
  const { isOpen } = useContext(AccordionContext);

  if (!isOpen(id)) return null;

  return <div className="accordion-panel">{children}</div>;
}

// Attach subcomponents
Accordion.Item = AccordionItem;
Accordion.Header = AccordionHeader;
Accordion.Panel = AccordionPanel;

// Usage
<Accordion allowMultiple>
  <Accordion.Item id="1">
    <Accordion.Header id="1">Section 1</Accordion.Header>
    <Accordion.Panel id="1">Content 1</Accordion.Panel>
  </Accordion.Item>
  <Accordion.Item id="2">
    <Accordion.Header id="2">Section 2</Accordion.Header>
    <Accordion.Panel id="2">Content 2</Accordion.Panel>
  </Accordion.Item>
</Accordion>

Split State and Dispatch Contexts

const StateContext = createContext(null);
const DispatchContext = createContext(null);

function AppProvider({ children }) {
  const [state, dispatch] = useReducer(reducer, initialState);

  return (
    <StateContext.Provider value={state}>
      <DispatchContext.Provider value={dispatch}>
        {children}
      </DispatchContext.Provider>
    </StateContext.Provider>
  );
}

function useAppState() {
  const context = useContext(StateContext);
  if (!context) throw new Error('useAppState must be within AppProvider');
  return context;
}

function useAppDispatch() {
  const context = useContext(DispatchContext);
  if (!context) throw new Error('useAppDispatch must be within AppProvider');
  return context;
}

// Components that only dispatch don't re-render when state changes
function AddButton() {
  const dispatch = useAppDispatch();
  return <button onClick={() => dispatch({ type: 'ADD' })}>Add</button>;
}

// Components that read state re-render when state changes
function ItemCount() {
  const state = useAppState();
  return <span>{state.items.length} items</span>;
}

Selector Pattern

const StoreContext = createContext(null);

function useSelector(selector) {
  const store = useContext(StoreContext);
  return selector(store);
}

function UserName() {
  // Only re-renders when user.name changes
  const name = useSelector(state => state.user.name);
  return <span>{name}</span>;
}

function ItemCount() {
  // Only re-renders when items.length changes
  const count = useSelector(state => state.items.length);
  return <span>{count} items</span>;
}

Performance Optimization

Memoize Context Value

// ❌ Bad - new object every render
function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');

  // This object is recreated every render
  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}

// ✅ Good - memoized value
function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');

  const value = useMemo(
    () => ({ theme, setTheme }),
    [theme]
  );

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

Split Contexts by Update Frequency

// ❌ Bad - all consumers re-render on any change
const AppContext = createContext(null);

function AppProvider({ children }) {
  const [user, setUser] = useState(null);      // Rarely changes
  const [theme, setTheme] = useState('light'); // Rarely changes
  const [notifications, setNotifications] = useState([]); // Frequently changes

  return (
    <AppContext.Provider value={{ user, theme, notifications }}>
      {children}
    </AppContext.Provider>
  );
}

// ✅ Good - separate contexts by update frequency
const UserContext = createContext(null);
const ThemeContext = createContext(null);
const NotificationContext = createContext(null);

function AppProvider({ children }) {
  return (
    <UserProvider>
      <ThemeProvider>
        <NotificationProvider>
          {children}
        </NotificationProvider>
      </ThemeProvider>
    </UserProvider>
  );
}

Colocate Providers

// ✅ Provider only wraps components that need it
function Dashboard() {
  return (
    <DashboardDataProvider>
      <DashboardHeader />
      <DashboardContent />
      <DashboardFooter />
    </DashboardDataProvider>
  );
}

// Other parts of app don't re-render when dashboard data changes
function App() {
  return (
    <GlobalProviders>
      <Header />
      <Dashboard /> {/* Has its own provider */}
      <Sidebar />
    </GlobalProviders>
  );
}

Context vs Other State Solutions

FeatureContextReduxZustandJotai
Bundle Size0kb (built-in)~10kb~2kb~3kb
BoilerplateLowMedium-HighVery LowLow
DevToolsNoYesYesYes
MiddlewareNoYesYesLimited
SelectorsManualBuilt-inBuilt-inBuilt-in
Best ForLow-frequency updatesComplex app stateSimple global stateAtomic state

When to Use Context

  • Theme/UI preferences
  • Auth/user data
  • Locale settings
  • Compound component internal state
  • Avoiding prop drilling for rarely-changing data

When NOT to Use Context

  • High-frequency updates (cursor position, animations)
  • Complex state with many consumers
  • Need for devtools/debugging
  • Need for middleware (logging, persistence)

See also