JSTAcademy
0 XP
Dashboard
Crash Courses
Context & Prop Drilling
11 min
Masters+175 XP
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 it

Every 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.

0%