Crash Courses · Masters
Context & Prop Drilling
Share state across deep component trees without prop drilling using React Context.
11 min read+175 XP on completionCert: React Crash Course
Tap any word in the text below to start reading from there.
Context & Prop Drilling
The Problem: Prop Drilling
Imagine a NavBar component deep in your tree that needs the current user's name. That data lives at the top of the tree in App. Without context, you pass it down through every layer:
<App user={user}> // has the data
<Layout user={user}> // does not need it — just passes it down
<Sidebar user={user}> // same
<NavBar user={user}> // finally uses itEvery intermediate component is polluted with a prop it does not use. Add another 3 levels and this becomes unmaintainable.
Context solves this by making a value available to any descendant without threading it through props.
Creating Context
import { createContext, useContext, useState } from 'react'
interface ThemeContextValue {
theme: 'light' | 'dark'
toggleTheme: () => void
}
// createContext creates the context with a default value (used when no Provider wraps the consumer)
const ThemeContext = createContext<ThemeContextValue | null>(null)
// Provider component — wraps the tree that needs access
export function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = useState<'light' | 'dark'>('light')
const toggleTheme = () => setTheme(prev => prev === 'light' ? 'dark' : 'light')
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
)
}
// Custom hook — safer than raw useContext, throws a helpful error
export function useTheme() {
const ctx = useContext(ThemeContext)
if (!ctx) throw new Error('useTheme must be used inside ThemeProvider')
return ctx
}Consuming Context
Any component inside ThemeProvider can call useTheme() — no matter how deeply nested:
// Deep in the tree — no prop drilling
function ThemeToggle() {
const { theme, toggleTheme } = useTheme()
return (
<button onClick={toggleTheme}>
{theme === 'light' ? 'Switch to Dark' : 'Switch to Light'}
</button>
)
}
// In App.tsx
export default function App() {
return (
<ThemeProvider>
<Layout>
{/* ThemeToggle can be anywhere in the tree */}
<NavBar />
</Layout>
</ThemeProvider>
)
}Course Progress Context — Real Example
interface ProgressContextValue {
completed: Set<string>
markComplete: (courseId: string) => void
isComplete: (courseId: string) => boolean
}
const ProgressContext = createContext<ProgressContextValue | null>(null)
export function ProgressProvider({ children }: { children: React.ReactNode }) {
const [completed, setCompleted] = useState<Set<string>>(new Set())
const markComplete = (id: string) =>
setCompleted(prev => new Set([...prev, id]))
const isComplete = (id: string) => completed.has(id)
return (
<ProgressContext.Provider value={{ completed, markComplete, isComplete }}>
{children}
</ProgressContext.Provider>
)
}
export function useProgress() {
const ctx = useContext(ProgressContext)
if (!ctx) throw new Error('useProgress must be inside ProgressProvider')
return ctx
}When to Use Context vs Props
Use context for:
- Theme (light/dark mode)
- Current authenticated user
- Language/locale settings
- Global notifications/toasts
- Progress tracking shared across pages
Use props for:
- Component-specific data
- Data used only 1-2 levels deep
- Data that varies between instances of the same component
Context is not a replacement for all prop passing — it makes every consumer re-render when the context value changes. Over-using it is a performance anti-pattern.
Reading progress
0% read
In this module
Context
createContext
Provider
useContext
0%