Context

9 min read
Rapid overview

React Context - Complete Guide

Master React Context API for efficient state sharing and avoiding prop drilling.

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)

Common Mistakes

Missing Provider

// ❌ Bad - no provider in tree
function App() {
  return <UserProfile />; // Will throw error or get undefined
}

// ✅ Good - provider wraps consumers
function App() {
  return (
    <UserProvider>
      <UserProfile />
    </UserProvider>
  );
}

Not Memoizing Value

// ❌ Bad - causes all consumers to re-render
function Provider({ children }) {
  const [count, setCount] = useState(0);
  return (
    <Context.Provider value={{ count, setCount }}>
      {children}
    </Context.Provider>
  );
}

// ✅ Good - memoized value
function Provider({ children }) {
  const [count, setCount] = useState(0);
  const value = useMemo(() => ({ count, setCount }), [count]);
  return (
    <Context.Provider value={value}>
      {children}
    </Context.Provider>
  );
}

Overusing Context

// ❌ Bad - context for local state
function Form() {
  return (
    <FormContext.Provider value={formState}>
      <FormField name="email" />
      <FormField name="password" />
    </FormContext.Provider>
  );
}

// ✅ Good - props for local state
function Form() {
  const [formState, setFormState] = useState({});
  return (
    <>
      <FormField name="email" value={formState.email} onChange={...} />
      <FormField name="password" value={formState.password} onChange={...} />
    </>
  );
}

Questions & Answers

Q: What is React Context and when should I use it?

A: Context provides a way to share values between components without prop drilling. Use it for cross-cutting concerns like themes, auth, or locale that many components need access to.

Q: How do I prevent unnecessary re-renders with Context?

A: Memoize the context value with useMemo, split contexts by update frequency, use separate state and dispatch contexts, or consider libraries like Zustand for high-frequency updates.

Q: What's the difference between Context and Redux?

A: Context is built-in and simpler but lacks devtools, middleware, and optimized selectors. Redux has more features for complex state management but requires more setup. Use Context for simple sharing, Redux for complex app state.

Q: Can I have multiple contexts?

A: Yes, you can nest multiple providers. Split contexts by domain (auth, theme, locale) or update frequency to optimize re-renders.

Q: How do I type Context with TypeScript?

A: Use createContext<T | undefined>(undefined) and create a custom hook that throws if context is undefined. This ensures type safety and catches missing providers.

Q: Should I use Context for form state?

A: Generally no. Form state is local and props/controlled components work well. Use Context only if form state needs to be accessed by deeply nested components outside the form.

Q: What's the compound component pattern?

A: A pattern where a parent component shares state with children via Context. Children access shared state without prop drilling. Used for tabs, accordions, menus, etc.

Q: How do I access Context outside of React components?

A: You can't use useContext outside components. Store the value in a ref, use a state management library, or restructure to keep the logic inside components.