React Context: When to Use It (and When Not To)
React Context passes values through the component tree without prop drilling. How createContext, providers and useContext work, a typed auth context example, why every consumer re-renders when the value changes, splitting contexts, and when to use Zustand or TanStack Query instead.
When many components need the same value — the logged-in user, the theme, the current language — passing it down as props through every layer ("prop drilling") gets tedious. Context lets a value skip the layers in between. (React props explained)
The three pieces
import { createContext, useContext, useState, type ReactNode } from 'react'
// 1. create
const ThemeContext = createContext<{ theme: 'light' | 'dark'; toggle: () => void } | null>(null)
// 2. provide
export function ThemeProvider({ children }: { children: ReactNode }) {
const [theme, setTheme] = useState<'light' | 'dark'>('light')
const toggle = () => setTheme(t => (t === 'light' ? 'dark' : 'light'))
return <ThemeContext value={{ theme, toggle }}>{children}</ThemeContext>
}
// 3. consume
export function useTheme() {
const ctx = useContext(ThemeContext)
if (!ctx) throw new Error('useTheme must be used inside ThemeProvider')
return ctx
}
Wrap the app (or part of it) in the provider, and any component inside can call useTheme(). (In React 19 you can render <ThemeContext> directly as the provider; older code uses <ThemeContext.Provider>.)
The custom hook with a null check is a good habit: it gives a clear error instead of a confusing undefined if someone forgets the provider. (React custom hooks)
Good uses for Context
Values that are read by many components and change rarely:
- current user / session
- theme and dark mode (How to add dark mode)
- language and locale
- feature flags (Feature flags)
- a service object (API client, analytics)
The performance catch
When a provider's value changes, every component that uses that context re-renders — even if it only uses a part that didn't change.
And a common mistake makes it worse:
<UserContext value={{ user, setUser }}> // new object every render
Every time the provider's parent re-renders, a new object is created, so all consumers re-render too. Stabilise it:
const value = useMemo(() => ({ user, setUser }), [user])
<UserContext value={value}>
Split contexts by how often they change
Instead of one giant AppContext with everything, use several small ones. A context that changes on every keystroke shouldn't share a provider with the user object.
Splitting state from actions also helps: components that only call toggle() don't need to re-render when theme changes.
When Context is the wrong tool
| Need | Better choice |
|---|---|
| Server data (lists, records) with caching and refetching | TanStack Query (TanStack Query vs useEffect) |
| Frequently changing global state (editor, cart with many updates) | Zustand, Jotai or Redux (React state management) |
| State used by one or two components | Plain useState, lifted to the nearest parent |
| URL-shareable state (filters, tabs) | Query parameters (Query parameters explained) |
Context isn't a state manager; it's a way to pass a value down. Combined with useState or useReducer it handles simple global state well, but it has no selectors to subscribe to just part of the value.
Context in Next.js App Router
Providers use state, so they must be client components. Create a providers.tsx with 'use client' and wrap children in your root layout. Server components can't read context — pass data to them via props or fetch it on the server. (React Server Components explained)
Testing
Components that use context need the provider in tests. A small renderWithProviders helper keeps tests tidy. (Vitest vs Jest)
EasySpawn runs your React app on its own server with Claude Code alongside, which can trace a re-render problem through your providers and fix it. See how it works or join the waitlist.
Related: React State Management · React Custom Hooks · useMemo vs useCallback · React Props Explained
Keep reading
The Vercel AI SDK: Streaming Chat and Tool Calls in TypeScript
The AI SDK is a TypeScript toolkit for calling language models from any provider with one API, streaming responses to React with useChat, calling tools, and generating structured data. A working Next.js chat with Claude, tool calling with stopWhen, structured output, provider choice, and production concerns.
useMemo vs useCallback (and React.memo): When Memoization Helps
useMemo caches a computed value; useCallback caches a function; React.memo skips re-rendering a component whose props didn't change. When each actually helps, when it's wasted effort, the dependency array rules, and how the React Compiler now does most of this for you.