useContext Hook
Read and subscribe to context from any component without prop drilling.
Recipe
Quick-reference recipe card — copy-paste ready.
// 1. Create context
const ThemeContext = createContext<Theme>("light");
// 2. Provide it
<ThemeContext.Provider value={theme}>
<App />
</ThemeContext.Provider>
// 3. Consume it
const theme = useContext(ThemeContext);When to reach for this: You need to share values (theme, auth, locale) across many components without passing props at every level.
Working Example
"use client";
import { createContext, useContext, useState, type ReactNode } from "react";
type Theme = "light" | "dark";
const ThemeContext = createContext<Theme>("light");
const ThemeToggleContext = createContext<() => void>(() => {});
export function ThemeProvider({ children }: { children: ReactNode }) {
const [theme, setTheme] = useState<Theme>("light");
const toggle = () => setTheme((t) => (t === "light" ? "dark" : "light"));
return (
<ThemeContext.Provider value={theme}>
<ThemeToggleContext.Provider value={toggle}>
{children}
</ThemeToggleContext.Provider>
</ThemeContext.Provider>
);
}
export function ThemeDisplay() {
const theme = useContext(ThemeContext);
const toggle = useContext(ThemeToggleContext);
return (
<div className={theme === "dark" ? "bg-gray-900 text-white p-4" : "bg-white text-black p-4"}>
<p>Current theme: {theme}</p>
<button onClick={toggle} className="mt-2 px-3 py-1 border rounded">
Toggle Theme
</button>
</div>
);
}What this demonstrates:
- Creating and providing context with
createContextandProvider - Splitting value and updater into separate contexts to avoid unnecessary re-renders
- Consuming context with
useContextin a deeply nested component - Type-safe context with explicit generics
Deep Dive
How It Works
useContextfinds the nearestProviderabove the calling component in the tree- When the provider's
valuechanges, every component consuming that context re-renders - If no provider is found, the default value passed to
createContextis used - Context uses reference equality (
Object.is) to detect changes — a new object reference triggers re-renders even if the contents are identical
Parameters & Return Values
| Parameter | Type | Description |
|---|---|---|
context | Context<T> | The context object created by createContext |
| Return | Type | Description |
|---|---|---|
value | T | The current context value from the nearest provider |
Variations
Context with a custom hook (recommended pattern):
const AuthContext = createContext<AuthState | null>(null);
export function useAuth() {
const ctx = useContext(AuthContext);
if (!ctx) throw new Error("useAuth must be used within an AuthProvider");
return ctx;
}Multiple contexts composed:
export function AppProviders({ children }: { children: ReactNode }) {
return (
<AuthProvider>
<ThemeProvider>
<LocaleProvider>
{children}
</LocaleProvider>
</ThemeProvider>
</AuthProvider>
);
}Context with reducer for complex state:
const DispatchContext = createContext<Dispatch<Action>>(() => {});
const StateContext = createContext<AppState>(initialState);TypeScript Notes
// Always type your context — avoid `any`
const UserContext = createContext<User | null>(null);
// Use a custom hook to narrow the type
export function useUser(): User {
const user = useContext(UserContext);
if (!user) throw new Error("useUser requires a UserProvider");
return user; // narrowed to User, never null
}Gotchas
-
Re-render avalanche — Passing a new object literal as
valueon every render causes all consumers to re-render. Fix: Memoize the value withuseMemoor split into separate contexts. -
Missing provider — Forgetting the
Providersilently returns the default value, which may beundefined. Fix: Use a custom hook that throws if context isnull. -
Overusing context for high-frequency updates — Context is not optimized for values that change on every keystroke or animation frame. Fix: Use
useSyncExternalStore, Zustand, or Jotai for high-frequency shared state. -
Default value confusion — The default in
createContext(defaultValue)is only used when there is no provider, not as an initial state for the provider. Fix: Always wrap consuming components in the appropriate provider.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
| Prop drilling | Only 1–2 levels deep and few consumers | Many levels or many consumers |
| Zustand / Jotai | High-frequency updates or complex shared state | Simple, infrequent values like theme or locale |
| Component composition | Children can be passed as props to avoid intermediate components needing the data | Data is needed at many arbitrary levels |
use(Context) (React 19) | You want to read context conditionally or in loops | You need to support React 18 or earlier |
Why not always use Zustand? Context is built-in, requires no dependency, and is the right tool for low-frequency, tree-scoped values like theme, auth, or locale.
Real-World Example
From a production Next.js 15 / React 19 SaaS application (SystemsArchitect.io).
// Production example: ToastProvider with createContext and guard hook
// File: src/components/toast-provider.tsx
'use client';
import { createContext, useContext, useState, useCallback, type ReactNode } from 'react';
interface Toast {
id: string;
message: string;
type: 'success' | 'error' | 'info';
}
interface ToastContextValue {
toasts: Toast[];
showToast: (message: string, type?: Toast['type']) => void;
dismissToast: (id: string) => void;
}
const ToastContext = createContext<ToastContextValue | null>(null);
export function ToastProvider({ children }: { children: ReactNode }) {
const [toasts, setToasts] = useState<Toast[]>([]);
const showToast = useCallback((message: string, type: Toast['type'] = 'info') => {
const id = crypto.randomUUID();
setToasts((prev) => [...prev, { id, message, type }]);
setTimeout(() => {
setToasts((prev) => prev.filter((t) => t.id !== id));
}, 5000);
}, []);
const dismissToast = useCallback((id: string) => {
setToasts((prev) => prev.filter((t) => t.id !== id));
}, []);
return (
<ToastContext.Provider value={{ toasts, showToast, dismissToast }}>
{children}
<div className="fixed bottom-4 right-4 space-y-2 z-50">
{toasts.map((toast) => (
<div key={toast.id} className="rounded bg-gray-900 text-white px-4 py-2 shadow">
{toast.message}
<button onClick={() => dismissToast(toast.id)} className="ml-2">x</button>
</div>
))}
</div>
</ToastContext.Provider>
);
}
// Guard hook: throws if used outside the provider
export function useToast(): ToastContextValue {
const ctx = useContext(ToastContext);
if (!ctx) {
throw new Error('useToast must be used within a ToastProvider');
}
return ctx;
}What this demonstrates in production:
createContext<ToastContextValue | null>(null)usesnullas the default so the guard hook can detect when it is used outside a provider. Using a real default value would silently fail instead of throwing a helpful error.useCallbackonshowToastanddismissToastkeeps these function references stable across renders. Without it, any component consuminguseToast()would re-render on every toast state change because the context value object would contain new function references.- Functional state updates (
setToasts((prev) => ...)) are used instead of readingtoastsdirectly. This avoids addingtoaststo theuseCallbackdependency array, which would break the stable reference. - The
setTimeoutfor auto-dismiss captures theidin its closure and uses a functional updater to filter. This avoids stale closure bugs where thetoastsarray has changed by the time the timeout fires. - The guard hook
useToast()narrows the type fromToastContextValue | nulltoToastContextValue, so consumers never need to handlenull. Thethrowensures a clear error message during development if the provider is missing.
FAQs
Why should I split value and updater into separate contexts?
- When a single context holds both the state value and its updater function, every consumer re-renders whenever the value changes.
- Components that only call the updater (like a button) don't need the value, yet they re-render anyway.
- Splitting into two contexts lets updater-only consumers avoid unnecessary re-renders.
What happens if I use useContext without a matching Provider?
- React returns the default value passed to
createContext(defaultValue). - If the default is
undefinedornull, your component may silently fail. - Use a guard hook that throws an error when the context is
nullto catch this during development.
How do I create a type-safe guard hook for context in TypeScript?
const MyContext = createContext<MyType | null>(null);
export function useMyContext(): MyType {
const ctx = useContext(MyContext);
if (!ctx) throw new Error("useMyContext must be used within a Provider");
return ctx; // narrowed to MyType, never null
}Gotcha: Why do all my context consumers re-render even when the value hasn't really changed?
- Context uses
Object.isto detect changes. Passing a new object literal{ theme, toggle }as the provider value creates a new reference every render. - Fix by memoizing the value with
useMemo:
const value = useMemo(() => ({ theme, toggle }), [theme, toggle]);
return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;Can I use context for high-frequency updates like typing or animations?
- Context is not optimized for high-frequency updates because every consumer re-renders on every change.
- For high-frequency shared state, use
useSyncExternalStore, Zustand, or Jotai instead. - Context works well for low-frequency values like theme, auth, or locale.
What is the difference between the default value in createContext and the Provider's value prop?
- The default value in
createContext(default)is used only when there is no Provider above the consumer. - The Provider's
valueprop is the actual runtime value passed to consumers. - The default is not an "initial state" for the Provider.
How do I compose multiple Providers without deeply nesting JSX?
function AppProviders({ children }: { children: ReactNode }) {
return (
<AuthProvider>
<ThemeProvider>
<LocaleProvider>
{children}
</LocaleProvider>
</ThemeProvider>
</AuthProvider>
);
}- Alternatively, create a
composeProvidersutility that reduces an array of providers.
Should I use useContext or the React 19 use(Context) API?
use(Context)is new in React 19 and can be called inside conditions and loops, unlikeuseContext.useContextis the standard approach if you need React 18 compatibility.- Both read from the nearest Provider; the difference is call-site flexibility.
Gotcha: Why does wrapping useCallback around context updater functions matter?
- If the updater function is recreated every render, the context value object changes reference.
- That triggers re-renders in all consumers, even if the state itself hasn't changed.
- Wrapping updaters in
useCallbackkeeps their references stable across renders.
How do I type createContext with TypeScript when the context may not have a Provider?
// Use null default + guard hook pattern
const UserContext = createContext<User | null>(null);
// The guard hook narrows the type
export function useUser(): User {
const user = useContext(UserContext);
if (!user) throw new Error("useUser requires UserProvider");
return user;
}Related
- useReducer — pair with context for complex state management
- useMemo — memoize context values to prevent unnecessary re-renders
- use — React 19's
use()can read context conditionally - Custom Hooks — wrap
useContextin a custom hook for type safety