useContext Hook
As your component tree grows, passing data through props from a top-level component down to a deeply nested child becomes tedious and error-prone. Every component in the middle must accept and forward the prop even if it has no use for it. This problem is called prop drilling.
The Context API combined with the useContext hook solves this by letting you make data available to any component in the tree — without passing it through every intermediate level. Think of it as a way to "teleport" a value directly to the component that needs it.
The Prop Drilling Problem
// Theme must travel through every layer — even though Sidebar doesn't use it
function App() {
const [theme, setTheme] = useState('dark')
return <Layout theme={theme} setTheme={setTheme} />
}
function Layout({ theme, setTheme }) {
return <Sidebar theme={theme} setTheme={setTheme} />
}
function Sidebar({ theme, setTheme }) {
return <ThemeToggle theme={theme} setTheme={setTheme} />
}
function ThemeToggle({ theme, setTheme }) {
// This is the only component that actually needs the props
return (
<button onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}>
Switch to {theme === 'dark' ? 'light' : 'dark'} mode
</button>
)
}Step 1 — Create a Context
Start by calling createContext() with a default value. The default is used when a component reads the context but has no matching Provider above it in the tree.
import { createContext } from 'react'
// The default value is used only when there is no Provider above in the tree
export const ThemeContext = createContext('light')
// With TypeScript, specify the shape:
// type ThemeContextType = { theme: string; setTheme: (t: string) => void }
// export const ThemeContext = createContext<ThemeContextType | null>(null)Step 2 — Provide the Context
Wrap the part of your tree that needs access to the value with Context.Provider. Any component inside the Provider — no matter how deeply nested — can read the value.
import { useState } from 'react'
import { ThemeContext } from './ThemeContext'
export function App() {
const [theme, setTheme] = useState('dark')
// Wrap children in the Provider; pass the current value
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
<Layout />
</ThemeContext.Provider>
)
}
// Layout and Sidebar no longer need any theme-related props!
function Layout() {
return <Sidebar />
}
function Sidebar() {
return <ThemeToggle />
}Step 3 — Consume with useContext
Any component that needs the value calls useContext(ThemeContext). React will find the nearest Provider above it in the tree and return its current value.
import { useContext } from 'react'
import { ThemeContext } from './ThemeContext'
function ThemeToggle() {
const { theme, setTheme } = useContext(ThemeContext)
return (
<button
onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
style={{
background: theme === 'dark' ? '#333' : '#eee',
color: theme === 'dark' ? '#fff' : '#000',
padding: '8px 16px',
borderRadius: 4,
border: 'none',
cursor: 'pointer',
}}
>
Switch to {theme === 'dark' ? 'light' : 'dark'} mode
</button>
)
}Complete Theme-Switching Example
Here is a complete, self-contained example showing context creation, provision, and consumption in one file:
import { createContext, useContext, useState } from 'react'
// 1. Create
const ThemeContext = createContext({ theme: 'light', toggleTheme: () => {} })
// 2. Custom hook for convenience (optional but recommended)
function useTheme() {
return useContext(ThemeContext)
}
// 3. Provider component
function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light')
const toggleTheme = () => setTheme(t => (t === 'light' ? 'dark' : 'light'))
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
)
}
// 4. Deeply nested consumer — no prop drilling needed
function Page() {
const { theme } = useTheme()
return (
<div
style={{
background: theme === 'dark' ? '#1a1a1a' : '#fff',
color: theme === 'dark' ? '#fff' : '#000',
minHeight: '100vh',
padding: 24,
}}
>
<Header />
<main>Content goes here</main>
</div>
)
}
function Header() {
const { theme, toggleTheme } = useTheme()
return (
<header style={{ display: 'flex', justifyContent: 'space-between' }}>
<h1>My App</h1>
<button onClick={toggleTheme}>
{theme === 'light' ? '🌙 Dark' : '☀️ Light'}
</button>
</header>
)
}
// 5. Root — wrap the app with the provider
export default function App() {
return (
<ThemeProvider>
<Page />
</ThemeProvider>
)
}Context Value Can Be Any JavaScript Value
Context is not limited to strings or objects. You can pass numbers, booleans, arrays, functions, or any combination thereof. Common patterns:
Current user —
{ user, logout }passed from an auth providerLocale / language — an ISO string like
"en"or"de"Cart state — items array +
addItem/removeItemactionsRouter state — current pathname, push/replace navigation helpers
Updating Context with State
Context on its own is static. To make it dynamic, pair it with useState (or useReducer) in the Provider component. When the state changes, React re-renders all consumers that read that context:
const UserContext = createContext(null)
function UserProvider({ children }) {
const [user, setUser] = useState(null)
function login(credentials) {
// In a real app, call your API here
setUser({ name: 'Alice', role: 'admin' })
}
function logout() {
setUser(null)
}
return (
<UserContext.Provider value={{ user, login, logout }}>
{children}
</UserContext.Provider>
)
}
// Any component can now read and mutate the user
function NavBar() {
const { user, logout } = useContext(UserContext)
if (!user) return <a href="/login">Sign in</a>
return (
<div>
<span>Hello, {user.name}</span>
<button onClick={logout}>Sign out</button>
</div>
)
}Multiple Contexts
There is no limit on how many contexts you can have. It is common to have separate contexts for separate concerns so consumers only subscribe to what they need:
export default function App() {
return (
<ThemeProvider>
<UserProvider>
<CartProvider>
<Router />
</CartProvider>
</UserProvider>
</ThemeProvider>
)
}When Context Is NOT the Answer
Situation | Better approach |
|---|---|
Data needed by only one or two siblings | Lift state up to their common parent |
Data used by a handful of props levels deep | Component composition (pass children or render props) |
Frequently updating data (e.g., cursor position) | Local state + event handlers, or a dedicated state library |
Server-fetched data that needs caching | TanStack Query, SWR, or similar |
Global app state with complex mutations | Zustand, Redux Toolkit, or Jotai |
A key performance concern: every consumer re-renders whenever the Provider value changes. If you put a large object in context and any field in it changes, every consumer re-renders — even components that only use a field that did not change. Split contexts by update frequency and consider useMemo on the value object to avoid unnecessary re-renders.
// Safer custom hook with runtime validation
export function useTheme() {
const ctx = useContext(ThemeContext)
if (!ctx) {
throw new Error('useTheme must be used inside a ThemeProvider')
}
return ctx
}