React Context
Share state across components without prop drilling using Context and useContext
Context lets a parent provide data to any descendant in the tree without passing props through every level — eliminating prop drilling.
The Problem: Prop Drilling
Without context, deeply nested components require threading props through intermediaries that don't need them.
Two or three levels of props is fine. Context adds complexity — use it when drilling becomes painful (5+ levels) or when many unrelated components need the same data.
Creating and Providing Context
The standard pattern: create context, define a provider component, export a custom hook.
"use client"
import { createContext, useContext, useState, type ReactNode } from "react"
// 1. Define the type and create context
type Theme = "light" | "dark"
type ThemeContext = { theme: Theme; toggleTheme: () => void }
const ThemeContext = createContext<ThemeContext | null>(null)
// 2. Provider component
export function ThemeProvider({ children }: { children: ReactNode }) {
const [theme, setTheme] = useState<Theme>("light")
const toggleTheme = () => {
setTheme((prev) => (prev === "light" ? "dark" : "light"))
}
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
)
}
// 3. Custom hook for consuming
export function useTheme() {
const ctx = useContext(ThemeContext)
if (!ctx) throw new Error("useTheme must be used within a ThemeProvider")
return ctx
}
Consuming Context
Any component inside the provider reads context with the custom hook.
// Consumer component
function ThemedCard() {
const { theme, toggleTheme } = useTheme()
return (
<div
className={
theme === "dark" ? "bg-gray-900 text-white" : "bg-white text-black"
}
>
<p>Current theme: {theme}</p>
<button onClick={toggleTheme}>Toggle Theme</button>
</div>
)
}
Performance: Context Splitting
Every context value update re-renders all consumers, even if only part of the value changed.
// ❌ Bad: single context for unrelated data
const AppContext = createContext({
user: null,
theme: "light",
notifications: [],
} as AppState)
// ✅ Good: split by update frequency
const UserContext = createContext<User | null>(null) // updates rarely
const ThemeContext = createContext<Theme>("light") // updates on toggle
const NotificationsContext = createContext<Notification[]>([]) // updates frequently
When context value changes, every consumer re-renders. Split contexts by how
often the value changes — stable data in one, frequently changing data in
another. Memoize the value prop with useMemo to avoid unnecessary
re-renders from the provider itself.
Auth Context Example
A realistic auth context with login/logout.
"use client"
import {
createContext,
useContext,
useState,
useCallback,
type ReactNode,
} from "react"
type User = { id: string; name: string; email: string }
type AuthContext = {
user: User | null
login: (email: string, password: string) => Promise<void>
logout: () => void
isLoading: boolean
}
const AuthContext = createContext<AuthContext | null>(null)
export function AuthProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<User | null>(null)
const [isLoading, setIsLoading] = useState(false)
const login = useCallback(async (email: string, _password: string) => {
setIsLoading(true)
try {
const res = await fetch("/api/login", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email }),
})
const data = await res.json()
setUser(data.user)
} finally {
setIsLoading(false)
}
}, [])
const logout = useCallback(() => setUser(null), [])
return (
<AuthContext.Provider value={{ user, login, logout, isLoading }}>
{children}
</AuthContext.Provider>
)
}
export function useAuth() {
const ctx = useContext(AuthContext)
if (!ctx) throw new Error("useAuth must be used within AuthProvider")
return ctx
}
See It In Action
Build a theme toggle. Create a ThemeContext with "light" | "dark" state, a provider, and a useTheme hook. Components should read and toggle the theme.
1"use client"2 3import { createContext, useContext, useState, type ReactNode } from "react"4 5type Theme = "light" | "dark"6type ThemeContextType = {7theme: Theme8toggleTheme: () => void9}10 11const ThemeContext = createContext<ThemeContextType | null>(null)12 13function ThemeProvider({ children }: { children: ReactNode }) {14const [theme, setTheme] = useState<Theme>("light")15const toggleTheme = () => {16setTheme((prev) => (prev === "light" ? "dark" : "light"))17}18return (19 20<ThemeContext.Provider value={{ theme, toggleTheme }}>21{children}22</ThemeContext.Provider>23)24}25 26function useTheme() {27const ctx = useContext(ThemeContext)28if (!ctx) throw new Error("useTheme must be used within ThemeProvider")29return ctx30}31 32function CurrentTheme() {33const { theme } = useTheme()34return <p>Theme: {theme}</p>35}36 37function ThemeToggle() {38const { toggleTheme } = useTheme()39return <button onClick={toggleTheme}>Toggle Theme</button>40}41 42export default function App() {43return (44 <ThemeProvider>45 <div>46 <CurrentTheme />47 <ThemeToggle />48 </div>49 </ThemeProvider>50)51}This example defines a ThemeContext with a ThemeProvider that manages light/dark state. The useTheme hook lets any child component read the current theme and toggle it. CurrentTheme displays the theme, ThemeToggle switches it, and App wraps everything in the provider.