React Context
Context lets a React component make a value available to its entire subtree without passing it through every intermediate component as props. It's the built-in answer to prop drilling: threading theme, locale, or currentUser through five layers of components that don't use it themselves.
Context is a dependency injection mechanism, not a state management library. It's excellent for low-frequency, widely needed values and for scoping state to a feature. It becomes a performance problem when you put frequently changing, broadly consumed state into one big context, because every consumer re-renders on every change.
TL;DR
createContext(default)creates a context; a Provider supplies a value;useContext(or React 19'suse) reads it.- Components read the value from the nearest provider above them; without one, they get the default.
- Every consumer re-renders when the provider's value changes (compared with
Object.is). - Memoize provider values, and split contexts by concern and update frequency.
- Great for theme, locale, auth user, feature flags, and dependency injection (API clients, services).
- For frequently changing global state with many consumers, prefer a store with selectors (Zustand, Redux, Jotai) or a server-state library.
Quick Example
An auth context with a typed hook and memoized value:
In React 18 and earlier, render <AuthContext.Provider value={value}> instead of <AuthContext value={value}>.
Core Concepts
Creating, Providing, Consuming
createContext(defaultValue)returns a context object. The default is used only when no provider exists above the consumer. Usingnullplus a guarded custom hook catches missing providers early.- Providing: wrap a subtree with the provider and a
value. Providers can be nested; the nearest one wins, which lets you override values for part of the tree (a dark sidebar in a light app). - Consuming:
useContext(Ctx)in function components. React 19'suse(Ctx)does the same and can be called conditionally.
Re-render Behavior
When a provider's value changes, React re-renders every component that reads that context, even if it uses only one field that didn't change. Components between the provider and consumers that don't read the context aren't forced to re-render by context itself.
Two consequences:
- A provider value created inline (
value={{ user, setUser }}) is a new object on every render of the provider's parent, so all consumers re-render every time. Memoize it. - Combining unrelated data in one context couples their update frequencies.
Splitting Contexts
A common refinement is to split state and dispatch into separate contexts, so components that only trigger updates (buttons calling dispatch) don't re-render when state changes. dispatch from useReducer is stable.
Context + useReducer
For feature-scoped state such as a multi-step wizard, a document editor, or a checkout flow, context with useReducer gives a small, dependency-free store:
Context vs Other Options
See state management and Redux for store-based approaches.
Best Practices
Try Composition Before Context
Often prop drilling can be avoided by passing components as children or as props, so the intermediate layers never need the data. Reserve context for values that many components at different depths need.
Wrap Context in a Custom Hook
Export useAuth() rather than the raw context. You get one place to validate that a provider exists, a clearer API, and freedom to change the implementation later.
Memoize Values and Keep Them Focused
Memoize provider values with useMemo, keep each context small and cohesive, and separate fast-changing from slow-changing data. The React Compiler can memoize values automatically, but splitting by update frequency still matters.
Place Providers as Low as Possible
A provider only needed by the checkout pages should wrap the checkout routes, not the whole app. Scoping keeps re-renders local and makes the dependency obvious.
Common Mistakes
Inline Provider Values
A Single Global "AppContext"
Putting everything into one context makes every consumer re-render on any change and turns the context into an untyped grab bag. Split by domain.
Using Context for High-Frequency Updates
Mouse positions, animation frames, or text input values in a context consumed across the app cause app-wide re-renders on every tick. Keep such state local, or use a store with selectors.
FAQ
Is Context a replacement for Redux?
For many apps, yes. Context plus useReducer handles modest shared state, and server state belongs in a data-fetching library anyway. Redux (or Zustand) still wins when you have lots of frequently changing global state, need selector-based subscriptions to avoid re-renders, or want devtools, middleware, and established patterns for large teams.
Why do all my consumers re-render when one value changes?
Context has no built-in selectors: any change to the provider's value re-renders all consumers. Split the context, separate state from dispatch, memoize the value, or switch to a store that supports selectors.
What's the default value used for?
It's returned when a component reads the context without a matching provider above it. That's useful for tests and optional contexts. Many teams set the default to null and throw in a custom hook, to make a missing provider an explicit error.
Can I use Context in Server Components?
No. Context is a client-side feature. In the Next.js App Router, providers must be Client Components ("use client"), though they can wrap Server Component children. See React Server Components.
Related Topics
- React — The library overview
- React Hooks — useContext, useReducer, and use
- State Management — Choosing where state lives
- Redux — A store-based alternative for global state
- React Performance — Avoiding context-driven re-renders
- React Server Components — Where providers can live