React.createContext creates a context object that consists of two parts: a Provider component to supply the value and a Consumer to read it. The optional default value you pass to createContext() is used only when a component reads the context without any matching Provider anywhere above it in the tree. Context solves the prop drilling problem — any descendant component, no matter how deeply nested, can access the context value directly without passing it through every intermediate layer. Export the context object so both the Provider and consumers can import and use it.
import { createContext } from 'react';
const ThemeContext = createContext('light'); // default value
export default ThemeContext;
The default value is only used when a component reads context without a matching Provider above it in the tree. It is useful for testing or fallback scenarios.
Why it matters: Context lets you share values like the current user or theme across deeply nested components without threading props down every level.
Real applications: Authentication state, theme settings, language/locale preferences, user permissions.
Common mistakes: Creating a context without a meaningful default value, causing components outside a Provider to silently use undefined.
useContext is a hook that reads the current value from the nearest Provider above the calling component in the tree. It subscribes the component to context changes — whenever the Provider's value changes, every component calling useContext with that context will re-render. The hook takes the Context object directly (not the Provider or Consumer) as its argument. If there is no matching Provider, it falls back to the default value passed to createContext().
import { useContext } from 'react';
import ThemeContext from './ThemeContext';
function ThemedButton() {
const theme = useContext(ThemeContext);
return <button className={theme}>Click me</button>;
}
The component will re-render whenever the context value changes. useContext replaces the older Context.Consumer render prop pattern.
Why it matters: useContext makes reading shared data from any component simple — no props needed, no render props wrapping.
Real applications: Reading the logged-in user in a header component, getting the theme in a button, accessing the cart in a product page.
Common mistakes: Using useContext outside of the Provider — it will return the default value instead of the actual data.
value prop to all descendants that read that context. Any change to the value prop triggers a re-render of all consuming components in that subtree. You can place the Provider at the application root for truly global data (theme, auth) or higher in the tree than only the components that need it. Nesting multiple Providers of the same context is valid — the nearest one wins.
import ThemeContext from './ThemeContext';
function App() {
const [theme, setTheme] = useState('dark');
return (
<ThemeContext.Provider value={theme}>
<Toolbar />
<button onClick={() => setTheme('light')}>Light</button>
</ThemeContext.Provider>
);
}
All consumers nested inside the Provider re-render when the value prop changes.
Why it matters: The Provider is the source of truth — it controls what value all nested consumers receive.
Real applications: Wrapping your app in an AuthProvider, ThemeProvider, or CartProvider at the top level to share data everywhere.
Common mistakes: Placing the Provider too low in the tree so some components that need the context sit above it and don't receive the value.
useContext hook — the modern, concise API that reads the value and subscribes in one line. Class components have two older options: static contextType = MyContext exposes the value as this.context, and the <Context.Consumer> render-prop pattern renders JSX inside a function that receives the value. Since hooks are not available in class components, static contextType or the Consumer pattern are class-only solutions. For any new code, use useContext in functional components.
// Functional
function Display() {
const theme = useContext(ThemeContext);
return <div className={theme}>Themed</div>;
}
// Class
class Display extends React.Component {
static contextType = ThemeContext;
render() { return <div className={this.context}>Themed</div>; }
}
The useContext hook is the modern and preferred approach for consuming context values.
Why it matters: Knowing both approaches lets you work with older class-based codebases and modern functional components.
Real applications: Accessing theme, auth, or locale context in both legacy class components and newer functional components in the same app.
Common mistakes: Using Context.Consumer in new code when useContext is simpler and more readable.
// Without Context — prop drilling through 4 levels
<App user={user}> → <Layout user> → <Nav user> → <Avatar user />
// With Context — direct access
<UserContext.Provider value={user}>
<App /> // Avatar can useContext(UserContext) directly
</UserContext.Provider>
Context is ideal for global or semi-global data. For data used by only one or two levels, regular props are simpler and more explicit.
Why it matters: Prop drilling through many levels makes code hard to maintain. Context solves this but adds indirection, so use it only when needed.
Real applications: Current user (auth), app theme, language setting, shopping cart — all needed by many unrelated components at different depths.
Common mistakes: Putting all app state in one big context, causing every component that reads it to re-render on any state change.
value prop. A common pattern is to wrap the state and setter in an object: value={{ theme, setTheme }}. Consumer components can then call the setter to trigger a state update in the Provider's owner, which propagates the new value down. To prevent the value object from being recreated on every render (which would cause all consumers to re-render), wrap it in useMemo.
const ThemeContext = createContext();
function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
);
}
function ToggleButton() {
const { theme, setTheme } = useContext(ThemeContext);
return <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>Toggle</button>;
}
Why it matters: Including the setter function in context lets any child modify the shared value without prop drilling a callback down every level.
Real applications: Theme toggle buttons, logout buttons, language switchers — any child that needs to change shared state.
Common mistakes: Passing only the value and not the setter in context, forcing you to use callbacks passed as props instead.
useContext separately for each context you need in a component. Each context is completely independent — updating one does not affect consumers of another. A common pattern is separate contexts for Auth, Theme, and Language, each with its own Provider. Compose the Providers at the app root either by nesting them directly or by building a single AppProviders wrapper component that combines all of them.
function App() {
return (
<ThemeContext.Provider value="dark">
<AuthContext.Provider value={{ user: "Alice" }}>
<Dashboard />
</AuthContext.Provider>
</ThemeContext.Provider>
);
}
function Dashboard() {
const theme = useContext(ThemeContext);
const { user } = useContext(AuthContext);
return <div className={theme}>Welcome, {user}</div>;
}
Splitting concerns into separate contexts prevents unnecessary re-renders and keeps each context focused.
Why it matters: A component that uses two contexts only re-renders when the context it reads actually changes — not when any other context changes.
Real applications: Using AuthContext for current user and CartContext for cart items in the same checkout component.
Common mistakes: Combining unrelated data into one context — an update to any value causes all consumers of that context to re-render.
value prop changes, regardless of whether the specific data they use changed. The most common cause is providing an inline object literal as the value — value={{ user, theme }} creates a new object reference on every parent render, so all consumers see a change even when user and theme are identical. Wrap the value in useMemo to ensure the reference only changes when actual contents change, preventing unnecessary consumer re-renders.
// Problem: new object on every render triggers all consumers
<MyContext.Provider value={{ user, theme }}>
// Fix: memoize the value
const value = useMemo(() => ({ user, theme }), [user, theme]);
<MyContext.Provider value={value}>
Split large contexts into smaller ones so consumers only subscribe to the data they need. Memoize value objects to avoid unnecessary reference changes.
Why it matters: Every consumer re-renders when context value changes. A poorly structured context can cause the entire app to re-render on small updates.
Real applications: High-frequency updates like mouse position or scroll position should not go in context — use local state or a specialized store instead.
Common mistakes: Creating the context value as a new object literal directly in the Provider without useMemo, causing a re-render on every parent render.
useReducer with Context creates a scalable state management pattern similar to Redux without external dependencies. The reducer manages state transitions through explicit actions, and Context makes both the state and the dispatch function available to any component in the tree. Splitting context into two — one for the state and one for dispatch — optimizes rendering because components that only dispatch actions don't re-render when state changes. This pattern is ideal for complex shared state with many possible transitions.
const StateContext = createContext();
const DispatchContext = createContext();
function AppProvider({ children }) {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<StateContext.Provider value={state}>
<DispatchContext.Provider value={dispatch}>
{children}
</DispatchContext.Provider>
</StateContext.Provider>
);
}
Separating state and dispatch into different contexts prevents components that only dispatch actions from re-rendering when state changes.
Why it matters: useReducer handles complex state transitions cleanly. Combining it with Context gives you a lightweight alternative to Redux.
Real applications: Shopping cart management, multi-step form state, undo/redo functionality, any state with multiple action types.
Common mistakes: Putting state and dispatch in the same context — components that only dispatch will re-render every time state changes.
useReducer paired with context. For truly high-frequency global state across large apps, dedicated libraries like Zustand, Jotai, or Recoil offer fine-grained subscriptions that update only the components that use the specific changed slice.
// Good for Context: theme, auth, locale
<ThemeContext.Provider value={theme}>
// Consider Redux/Zustand for:
// - Frequently changing data (e.g., real-time updates)
// - Complex state with many reducers
// - DevTools and middleware requirements
// - Large applications with many state consumers
Context lacks built-in performance optimizations like selectors. Libraries such as Redux, Zustand, or Jotai offer selective subscriptions and better tooling for complex scenarios.
Why it matters: Context is simple but scales poorly for large, frequently-updated global state. Picking the right tool avoids performance problems later.
Real applications: Context for auth/theme; Redux or Zustand for e-commerce carts, complex filters, or real-time collaborative apps.
Common mistakes: Starting with Context for everything and only switching to a state library when performance problems are already hurting users.
createContext, the Provider component, and a custom hook wrapper inside a single module. The custom hook calls useContext internally and throws a descriptive error if the consumer tries to use it outside its Provider. This gives consumers a clean, safe API — they import and call useAuth() instead of importing the raw context object. This encapsulation also means you can change the internals (switch from useState to useReducer) without affecting consumer code.
const AuthContext = createContext(null);
export function AuthProvider({ children }) {
const [user, setUser] = useState(null);
const login = (u) => setUser(u);
const logout = () => setUser(null);
return (
<AuthContext.Provider value={{ user, login, logout }}>
{children}
</AuthContext.Provider>
);
}
export function useAuth() {
const ctx = useContext(AuthContext);
if (!ctx) throw new Error('useAuth must be used within AuthProvider');
return ctx;
}
Throwing in the custom hook gives a clear error message if a component accidentally reads context outside the Provider.
Why it matters: A custom hook wrapping useContext gives you a single place to add validation, default values, and error messages for misuse.
Real applications: useAuth(), useCart(), useTheme() — any context that's used in many places benefits from a dedicated hook.
Common mistakes: Calling useContext(MyContext) directly in every component instead of a reusable hook — harder to rename or refactor later.
value={{ user, dispatch }} creates a brand-new object every render. Wrap the value in useMemo with the actual dependencies so the reference only changes when the underlying data changes, preventing all consumers from re-rendering on every unrelated parent update.
// ❌ New object on every render — all consumers re-render
<MyContext.Provider value={{ user, theme }}>
// ✅ Memoize the value — consumers only re-render when user or theme change
const value = useMemo(() => ({ user, theme }), [user, theme]);
<MyContext.Provider value={value}>
// Also split fast-changing and slow-changing values into separate contexts
<UserContext.Provider value={user}>
<ThemeContext.Provider value={theme}>
{children}
</ThemeContext.Provider>
</UserContext.Provider>
Split contexts by update frequency so each consumer only re-renders when its specific data changes.
Why it matters: Without memoization, every parent render creates a new context value object — triggering a re-render in every consumer even if nothing changed.
Real applications: High-traffic apps where many components read context — e-commerce sites, social feeds, dashboards with live data.
Common mistakes: Writing value={{ user, setUser }} inline in the Provider — this creates a new object on every render and causes all consumers to re-render.
createContext(defaultValue) is only used when a consumer has no matching Provider anywhere above it in the tree. It is a safety net for standalone usage, testing, or components rendered outside the normal app tree. It is not the initial value for the Provider — if a Provider renders with an undefined value, consumers see undefined, not the default. Set a meaningful default that makes the component work in isolation, or use null and check for it with a guard in your custom hook.
const ThemeContext = createContext('light'); // default: 'light'
// ComponentA is outside any Provider
function ComponentA() {
const theme = useContext(ThemeContext); // gets 'light' (default)
return <div>{theme}</div>;
}
// ComponentB is inside a Provider
function App() {
return (
<ThemeContext.Provider value="dark">
<ComponentB /> {/* gets 'dark' from Provider */}
</ThemeContext.Provider>
);
}
The default value is useful for testing components in isolation without wrapping them in a Provider.
Why it matters: Understanding the difference prevents subtle bugs where a component gets the wrong value because the Provider is missing.
Real applications: Unit testing individual components that use context by providing a mock value via the Provider wrapper in tests.
Common mistakes: Assuming the default value in createContext() will be used in production — it's only used when no matching Provider is found above the component.
const ThemeContext = createContext();
function ThemeProvider({ children }) {
const [dark, setDark] = useState(false);
return (
<ThemeContext.Provider value={{ dark, toggle: () => setDark(d => !d) }}>
<div className={dark ? 'theme-dark' : 'theme-light'}>
{children}
</div>
</ThemeContext.Provider>
);
}
function ThemeToggle() {
const { dark, toggle } = useContext(ThemeContext);
return <button onClick={toggle}>{dark ? '☀ Light' : '🌙 Dark'}</button>;
}
Wrap the entire app in ThemeProvider so every component can read and toggle the theme without prop drilling.
Why it matters: A theme switcher needs to reach every styled component in the app — context is the cleanest way to do this.
Real applications: Dark/light mode toggle, branded themes for different user roles, accessibility color schemes.
Common mistakes: Storing the theme in local state inside a component deep in the tree — only that component's children can access it.