ReactThe Context API

The Context API

Lifting state up solves the problem of sharing state between siblings, but as an application grows the state often needs to be accessed many levels deep. Passing props through every intermediate component — prop drilling — becomes painful. The Context API solves this by letting a parent make a value available to any descendant, no matter how deep, without threading props manually.

Three Steps to Using Context
  • Create the context object with React.createContext(defaultValue).

  • Provide a value to the tree by wrapping components in <MyContext.Provider value={...}>.

  • Consume the value anywhere in the tree with useContext(MyContext).

Step 1 — createContext

React.createContext accepts an optional default value. This default is only used when a component reads the context but there is no matching Provider above it in the tree. In practice you almost always have a Provider, so the default mainly serves as a fallback for testing or isolated component usage:

JSX
import { createContext } from 'react'

// Create the context. The argument is the default value used when there
// is no Provider above the consumer in the tree.
export const ThemeContext = createContext('light')

// It is conventional to put contexts in their own file and export them.
// Other files import ThemeContext to provide or consume it.
Step 2 — The Provider

Wrap the part of the tree that needs the value in <ThemeContext.Provider value={...}>. Every component inside the Provider (at any depth) can read this value. Components outside the Provider still see the default:

JSX
import { ThemeContext } from './ThemeContext'

function App() {
  return (
    // All children — at any depth — can read 'dark'
    <ThemeContext.Provider value="dark">
      <Layout />
    </ThemeContext.Provider>
  )
}

// Layout does NOT need to pass theme as a prop to its children.
// They can read it directly from the context.
function Layout() {
  return (
    <main>
      <Sidebar />
      <Content />
    </main>
  )
}
Step 3 — useContext

Call useContext(ThemeContext) inside any component inside the Provider tree. React walks up the component tree to find the nearest matching Provider and returns its current value:

JSX
import { useContext } from 'react'
import { ThemeContext } from './ThemeContext'

function Sidebar() {
  // No theme prop needed — read it directly from context
  const theme = useContext(ThemeContext)

  return (
    <aside className={`sidebar sidebar--${theme}`}>
      <nav>...</nav>
    </aside>
  )
}

function DeepButton() {
  const theme = useContext(ThemeContext)

  return (
    <button className={`btn btn--${theme}`}>
      Click me
    </button>
  )
}
Note
useContext reads the value from the NEAREST Provider ancestor. If you nest two Providers of the same context, the inner one wins for all components inside it.
A Complete ThemeContext Example

Here is a self-contained example that wires together all three steps. The theme can be toggled by the user, which demonstrates pairing context with useState to make it dynamic:

JSX
import { createContext, useContext, useState } from 'react'

// 1. Create the context — export it so consumers can import it
const ThemeContext = createContext({
  theme: 'light',
  toggleTheme: () => {},   // placeholder — real function provided by Provider
})

// 2. Build a custom Provider component that owns the state
function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light')

  function toggleTheme() {
    setTheme(prev => (prev === 'light' ? 'dark' : 'light'))
  }

  return (
    // Pass both the value and the updater so consumers can change the theme
    <ThemeContext.Provider value={{ theme, toggleTheme }}>
      {children}
    </ThemeContext.Provider>
  )
}

// 3. Wrap the app
function App() {
  return (
    <ThemeProvider>
      <Page />
    </ThemeProvider>
  )
}

// Deeply nested — no prop threading needed
function Page() {
  return (
    <main>
      <Header />
      <Article />
    </main>
  )
}

function Header() {
  const { theme, toggleTheme } = useContext(ThemeContext)

  return (
    <header style={{ background: theme === 'dark' ? '#1a1a1a' : '#ffffff' }}>
      <h1>My App</h1>
      <button onClick={toggleTheme}>
        Switch to {theme === 'light' ? 'dark' : 'light'} mode
      </button>
    </header>
  )
}

function Article() {
  const { theme } = useContext(ThemeContext)

  return (
    <article style={{ color: theme === 'dark' ? '#f0f0f0' : '#111111' }}>
      <p>This article adapts to the current theme.</p>
    </article>
  )
}
Dynamic Context via useState

The key to dynamic context is pairing createContext with useState (or useReducer) in the Provider component. The Provider holds the mutable state and passes both the current value and a function to change it into the context value object. Consumers can then both read and update the shared state:

JSX
// Pattern: always expose [value, setter] or {value, updater}
// so consumers can read AND write.

const CounterContext = createContext(null)

function CounterProvider({ children }) {
  const [count, setCount] = useState(0)

  return (
    <CounterContext.Provider value={{ count, setCount }}>
      {children}
    </CounterContext.Provider>
  )
}

// Consumer — reads and writes
function Counter() {
  const { count, setCount } = useContext(CounterContext)
  return <button onClick={() => setCount(c => c + 1)}>Count: {count}</button>
}

// Another consumer — reads only
function CountDisplay() {
  const { count } = useContext(CounterContext)
  return <p>The current count is {count}</p>
}
Multiple Contexts

An application typically has several independent contexts — one for authentication, one for the theme, one for the locale, and so on. Compose Providers by nesting them:

JSX
function App() {
  return (
    <AuthProvider>
      <ThemeProvider>
        <LocaleProvider>
          <Router />
        </LocaleProvider>
      </ThemeProvider>
    </AuthProvider>
  )
}

// Any descendant can read from any of the three contexts:
function ProfilePage() {
  const { user }   = useContext(AuthContext)
  const { theme }  = useContext(ThemeContext)
  const { locale } = useContext(LocaleContext)

  return <p>{locale === 'de' ? `Hallo, ${user.name}` : `Hello, ${user.name}`}</p>
}
Tip
You can write a small utility to collapse nested Providers into a flat list if the nesting becomes visually deep. This is a cosmetic refactor — it does not change behaviour.
When Context Re-renders Consumers

Every time the Provider's value prop changes (by reference using Object.is), all consumers re-render, regardless of whether the specific field they use has changed. This is an important performance consideration:

JSX
// ✗ Problem: new object created on every render → all consumers re-render
function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light')

  // This object is recreated on every render of ThemeProvider.
  // All useContext(ThemeContext) consumers will re-render, even if
  // only an unrelated piece of parent state changed.
  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      {children}
    </ThemeContext.Provider>
  )
}

// ✓ Fix: memoize the context value with useMemo
import { useMemo } from 'react'

function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light')

  const value = useMemo(() => ({ theme, setTheme }), [theme])

  return (
    <ThemeContext.Provider value={value}>
      {children}
    </ThemeContext.Provider>
  )
}
Warning
Passing an object literal directly into value={{ ... }} creates a new object on every render of the Provider. All consumers will re-render even when the actual data has not changed. Always memoize the value object with useMemo, or split the context into separate contexts to minimise re-renders.
Context vs Props — When to Use Each
  • Use props when passing data 1–2 levels down. They are explicit and easy to follow.

  • Use context when data is needed by many components at different nesting levels.

  • Use context for global, low-frequency data: current user, theme, locale, feature flags.

  • Avoid context for frequently changing data (e.g. form values) — it will cause excessive re-renders across the whole tree.

  • Context is not a state manager — it is a dependency injection mechanism. For complex state logic, pair it with useReducer or reach for a dedicated library.