ReactuseContext Hook

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

JSX
// 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.

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

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

JSX
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>
  )
}
Note
useContext always returns the value from the nearest Provider above in the component tree, not across trees. If there is no Provider, it returns the default value you passed to createContext().
Complete Theme-Switching Example

Here is a complete, self-contained example showing context creation, provision, and consumption in one file:

JSX
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 provider

  • Locale / language — an ISO string like "en" or "de"

  • Cart state — items array + addItem / removeItem actions

  • Router 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:

JSX
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:

JSX
export default function App() {
  return (
    <ThemeProvider>
      <UserProvider>
        <CartProvider>
          <Router />
        </CartProvider>
      </UserProvider>
    </ThemeProvider>
  )
}
When Context Is NOT the Answer
Warning
Context is not a replacement for all state management. Misusing it leads to performance problems and hard-to-follow data flows.

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.

Tip
The cleanest pattern is to export a custom hook — e.g. `useTheme()` — rather than exposing the context object directly. The hook can validate that the consumer is inside the Provider and throw a helpful error if not.

JSX
// 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
}