ESC

Type to search the knowledge base.

Context API Basics

React context for dependency injection — createContext, Provider, useContext, default values, and when context is the wrong tool.

beginner5 min read
  • react
  • context
  • useContext
  • state

Context passes data through the tree without threading props at every level. It is dependency injection for React: a Provider publishes a value; any descendant calls useContext to read the nearest Provider above it.

Context is not a global state management library. It solves prop drilling for shared ambient data (theme, locale, auth session, dial tone). For high-frequency updates across large trees, you still need structure — split contexts, memoize values, or use a real store.

Performance follow-up: Context Performance Pitfalls.

The problem: prop drilling

function App({ user }) {
  return <Page user={user} />;
}
function Page({ user }) {
  return <Dashboard user={user} />;
}
function Dashboard({ user }) {
  return <Avatar user={user} />;
}

Page and Dashboard don’t care about user — they only forward it. Context lets Avatar subscribe without the middle props.

Model

const UserContext = createContext(null); // default if no Provider

function App({ user }) {
  return (
    <UserContext.Provider value={user}>
      <Page />
    </UserContext.Provider>
  );
}

function Avatar() {
  const user = useContext(UserContext);
  if (!user) return null;
  return <img src={user.avatarUrl} alt={user.name} />;
}

Rules of the model:

  1. Nearest Provider wins — nested providers override outer ones for their subtree.
  2. Default value applies only when no Provider exists above.
  3. Any change to the Provider’s value re-renders all consumers that read that context (in the classic API).
  4. Context is read during render — it participates in the rules of hooks when using useContext.

Full pattern: create, provide, consume

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

const ThemeContext = createContext(null);

export function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');
  const value = useMemo(
    () => ({
      theme,
      toggle: () => setTheme((t) => (t === 'light' ? 'dark' : 'light')),
    }),
    [theme]
  );

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

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

Custom hooks around context:

  • Encode the “must be under Provider” check once
  • Hide the context object as a module private
  • Give a clear API (useTheme() vs raw useContext)

This is the usual custom hooks design for app infrastructure.

Default values: useful vs footgun

const LocaleContext = createContext('en'); // soft default

function Label() {
  const locale = useContext(LocaleContext);
  return <span>{locale}</span>;
}

Defaults help tests and Storybook without a Provider. For required services (auth, data clients), prefer createContext(null) + throw in the hook so missing providers fail loud.

What belongs in context

Good fit Poor fit
Theme / density Per-keystroke input text for a large tree
Current user / auth flag Ephemeral hover state
i18n dictionary / locale High-frequency mouse coordinates
Feature flags (read-mostly) Unrelated mixed bag of all app state
DI for services (logger, tracker) Avoiding learning props entirely

If only two components deep need a value, props are clearer. Context earns its place when many distant consumers share the same ambient dependency.

Composition often beats context

// Instead of drilling or context for layout chrome:
function Shell({ sidebar, main }) {
  return (
    <div className="shell">
      <aside>{sidebar}</aside>
      <main>{main}</main>
    </div>
  );
}

<Shell sidebar={<Nav />} main={<Page />} />

Pass elements as props (children, sidebar) so parents don’t re-render middle layers just to forward data. See Avoid Prop Drilling with Composition.

Value identity matters

// New object every render → all consumers re-render every time
<UserContext.Provider value={{ user, logout }}>

Stabilize with useMemo / useCallback or split state so the object only changes when data changes. A Provider that re-renders because its parent re-rendered will still re-render consumers if value is a new reference — even if the fields are equal.

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

Multiple contexts vs one mega-value

One context with { user, theme, cart, flags } forces every consumer of any field to re-render when any field changes. Split by update rate and concern:

<AuthProvider>
  <ThemeProvider>
    <CartProvider>{children}</CartProvider>
  </ThemeProvider>
</AuthProvider>

Read only what you need in each component.

Context + state updates

function AuthProvider({ children }) {
  const [user, setUser] = useState(null);
  const value = useMemo(() => ({ user, setUser }), [user]);
  return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}

Consumers that only need setUser still re-render when user changes if both live in one value. Advanced pattern: split UserContext and UserDispatchContext (state / dispatch) so dispatch consumers stay quiet. Same idea as React’s own docs for reducers + context.

Footguns

  1. Using context as a default app store without considering re-renders.
  2. Inline object value={{…}} every render.
  3. Missing Provider + silent default that masks wiring bugs.
  4. Deeply nesting 15 providers — consider composition or a store for truly global high-churn state.
  5. Reading context in a memoized child — context change still re-renders that child; memo does not block context.
  6. Putting unstable functions in value without memo — breaks child memo even when data is stable.

Interview angle

Prompt: “What is React context and when would you use it?”

Strong answer: “Context lets me publish a value to a subtree so consumers can read it without prop drilling. I use it for ambient dependencies like theme, locale, and auth. Provider value identity controls consumer re-renders, so I memoize values and split contexts by concern. It’s not a replacement for local state or for specialized stores when updates are frequent and trees are large.”

Follow-ups: nearest provider wins; memo vs context; composition as alternative.

Further reading

Related guides