Updated June 2026. Tested on React 19 and modern JavaScript. Part of the Techalyst React series.

Prop drilling is the slow pain of passing a value down through layers of components that do not care about it, just to reach the one at the bottom that does. React Context solves it by letting you put a value high in the tree and read it from anywhere below, with no forwarding in between. It is the right tool for a specific kind of state, and the wrong one for another, so it helps to know both.

The three pieces

Context has three parts: you create it, you provide a value, and you read it.

import { createContext, useContext } from 'react';

const ThemeContext = createContext('light');

function App() {
  return (
    <ThemeContext value="dark">
      <Page />
    </ThemeContext>
  );
}

function Button() {
  const theme = useContext(ThemeContext); // 'dark', no props passed
  return <button className={theme}>Save</button>;
}

createContext makes the context with a default. You wrap part of the tree to provide a value, and any component inside reads it with useContext, however deep it sits. In React 19 you can use the context itself as the provider, as above, instead of the older ThemeContext.Provider.

The custom hook wrapper

A small pattern makes context much nicer to use. Instead of importing the context everywhere and calling useContext directly, wrap it in a custom hook that also guards against being used outside the provider:

function useTheme() {
  const value = useContext(ThemeContext);
  if (value === undefined) {
    throw new Error('useTheme must be used inside a ThemeProvider');
  }
  return value;
}

Now components just call useTheme(), and a misuse fails loudly instead of silently handing back the default. This is the same idea as the custom hooks you write for any reusable logic.

The re-render caveat

Here is the catch to know before you put everything in context. When a context's value changes, every component that reads it re-renders, all of them. If you stuff unrelated things into one big context, a change to any one of them re-renders every consumer of the whole thing.

Two habits keep this in check. Split context by concern, a theme context, an auth context, rather than one mega-context. And when the value is an object, memoise it with useMemo so it does not become a new reference on every render and re-render all consumers for nothing.

What context is good for, and what it is not

Context shines for app-wide values that change rarely: the theme, the signed-in user, the locale, a feature flag. Provide them once near the root and read them anywhere.

It is a poor fit for state that changes often or is large and structured, because of that re-render-all behaviour. For a busy global store, a dedicated state library is a better tool, which we compare in the state-management post. A common middle ground is context plus useReducer, where the context carries the state and a dispatch function for a small, self-contained store.

Wrapping up

React Context removes prop drilling by letting you provide a value high in the tree and read it anywhere below with useContext. Wrap each context in a custom hook that throws when used outside its provider, split contexts by concern, and memoise object values so you do not re-render every consumer needlessly. Use it for app-wide, slow-changing data like theme and auth, and reach for a real store when the state is busy or large. Used in its lane, context is one of the cleanest tools React gives you.