useContext Hook
Read and subscribe to context from any component without prop drilling.
Search across all documentation pages
Read and subscribe to context from any component without prop drilling.
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
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.
"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:
createContext and ProvideruseContext in a deeply nested componentuseContext finds the nearest Provider above the calling component in the treevalue changes, every component consuming that context re-renderscreateContext is usedObject.is) to detect changes - a new object reference triggers re-renders even if the contents are identical| 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 |
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);// 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
}Re-render avalanche - Passing a new object literal as value on every render causes all consumers to re-render. Fix: Memoize the value with useMemo or split into separate contexts.
Missing provider - Forgetting the Provider silently returns the default value, which may be undefined. Fix: Use a custom hook that throws if context is null.
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.
| 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.
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) uses null as 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.useCallback on showToast and dismissToast keeps these function references stable across renders. Without it, any component consuming useToast() would re-render on every toast state change because the context value object would contain new function references.setToasts((prev) => ...)) are used instead of reading toasts directly. This avoids adding toasts to the useCallback dependency array, which would break the stable reference.setTimeout for auto-dismiss captures the id in its closure and uses a functional updater to filter. This avoids stale closure bugs where the toasts array has changed by the time the timeout fires.useToast() narrows the type from ToastContextValue | null to ToastContextValue, so consumers never need to handle null. The throw ensures a clear error message during development if the provider is missing.createContext(defaultValue).undefined or null, your component may silently fail.null to catch this during development.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
}Object.is to detect changes. Passing a new object literal { theme, toggle } as the provider value creates a new reference every render.useMemo:const value = useMemo(() => ({ theme, toggle }), [theme, toggle]);
return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;useSyncExternalStore, Zustand, or Jotai instead.createContext(default) is used only when there is no Provider above the consumer.value prop is the actual runtime value passed to consumers.function AppProviders({ children }: { children: ReactNode }) {
return (
<AuthProvider>
<ThemeProvider>
<LocaleProvider>
{children}
</LocaleProvider>
</ThemeProvider>
</AuthProvider>
);
}composeProviders utility that reduces an array of providers.use(Context) is new in React 19 and can be called inside conditions and loops, unlike useContext.useContext is the standard approach if you need React 18 compatibility.useCallback keeps their references stable across renders.// 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;
}use() can read context conditionallyuseContext in a custom hook for type safetyReviewed by Chris St. John·Last updated Jul 16, 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥