Context

Published:

Props carry data from a component to its immediate children:

function App() {
  return <Toolbar theme="dark" />;
}

That path is explicit. But the component that needs the value may be several layers below App:

App -> Toolbar -> Actions -> SaveButton

Toolbar and Actions arrange the interface. Only SaveButton uses the theme. With props alone, every component on the path must still receive and forward it.

That leaves the next hole:

how can a parent make a shared value available deep in its tree without turning every intermediate component into a courier?

Trying a distant prop #

Perhaps passing theme to the top of the branch is enough for a deeper child to receive it:

function SaveButton({ theme }) {
  return (
    <button data-theme={theme}>
      Received theme: {String(theme)}
    </button>
  );
}

function Actions() {
  return <SaveButton />;
}

function Toolbar({ theme }) {
  return (
    <nav aria-label="Document actions">
      <Actions />
    </nav>
  );
}

function App() {
  return <Toolbar theme="dark" />;
}

root.render(<App />);

setTimeout(() => {
  console.log(mountNode.querySelector('button').textContent);
}, 20);

Waiting to run

Not run yet.

Toolbar receives dark, but SaveButton receives undefined. A prop crosses exactly the component boundary where JSX passes it. React does not search ancestors for props, and rendering a child does not automatically relay the parent's input.

The failed attempt exposes the first constraint:

with props, every boundary on the path must pass the value deliberately

Repairing every boundary #

Use the mechanism we already have at each step:

function SaveButton({ theme }) {
  return (
    <button data-theme={theme}>
      Save using {theme} theme
    </button>
  );
}

function Actions({ theme }) {
  return <SaveButton theme={theme} />;
}

function Toolbar({ theme }) {
  return (
    <nav aria-label="Document actions">
      <Actions theme={theme} />
    </nav>
  );
}

function App() {
  return <Toolbar theme="dark" />;
}

root.render(<App />);

setTimeout(() => {
  console.log(mountNode.querySelector('button').textContent);
}, 20);

Waiting to run

Not run yet.

The button receives dark, but inspect the component boundaries:

Component Uses theme Forwards theme
App chooses it yes
Toolbar no yes
Actions no yes
SaveButton yes no

The code works, but every path from owner to consumer must preserve a prop its intermediate components do not otherwise understand. More consumers create more paths. Moving a component can require rewiring those paths, and one missing handoff produces the undefined value from the first experiment.

This is usually called prop drilling. It is not inherently wrong: props make local data flow visible, and a short prop path is often the clearest design.[1] The hole appears when one value belongs to a whole subtree but the intermediate APIs exist only to transport it.

We need three distinct roles:

create a channel -> provide a value above -> read the value below

Creating a shared channel #

createContext creates the channel:[2]

import { createContext } from 'react';

const ThemeContext = createContext('light');

Call it outside components. The returned context object identifies one kind of shared value. It does not contain the current theme itself; providers and consumers use the same object to identify which value they mean.[2]

The argument fills a smaller hole:

what should a consumer read when no matching provider exists above it?

It reads the context's default value. Test the channel before providing anything:

const { createContext, useContext } = React;

const ThemeContext = createContext('light');

function SaveButton() {
  const theme = useContext(ThemeContext);

  return (
    <button data-theme={theme}>
      Save using {theme} theme
    </button>
  );
}

root.render(<SaveButton />);

setTimeout(() => {
  console.log(mountNode.querySelector('button').textContent);
}, 20);

Waiting to run

Not run yet.

useContext(ThemeContext) reads light because no ThemeContext provider is above SaveButton.[3]

The default is a static last-resort value, not a changing global variable. Calling createContext('light') again would create a different channel, not update this one. If there is no sensible fallback, null can make the missing-provider case explicit.

We can now name and read the shared value, but App still cannot supply its dark theme.

That exposes the provider hole:

how does one rendered subtree select the value its consumers will read?

Providing a value to a subtree #

In React 19, render the context object around the subtree and pass its current value through value:[a][2]

<ThemeContext value="dark">
  <Toolbar />
</ThemeContext>

Now remove theme from the intermediate component APIs:

const { createContext, useContext } = React;

const ThemeContext = createContext('light');

function SaveButton() {
  const theme = useContext(ThemeContext);

  return (
    <button data-theme={theme}>
      Save using {theme} theme
    </button>
  );
}

function Actions() {
  return <SaveButton />;
}

function Toolbar() {
  return (
    <nav aria-label="Document actions">
      <Actions />
    </nav>
  );
}

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

root.render(<App />);

setTimeout(() => {
  console.log(mountNode.querySelector('button').textContent);
}, 20);

Waiting to run

Not run yet.

Toolbar and Actions no longer mention the theme. They only preserve the ordinary parent-child tree:

ThemeContext value="dark"
└── Toolbar
    └── Actions
        └── SaveButton -> useContext(ThemeContext) -> "dark"

The provider does not send a value to a named component or skip rendering layers. It makes a value available to consumers of that particular context anywhere inside its React subtree.[1]

This fills the transport hole:

a provider publishes a value by tree position; a consumer reads it by context identity

The value still flows downward. Context removes explicit forwarding, not the direction of React's data flow.

Testing two values of the same context #

A single application may need different theme values in different regions. A global variable would have only one current value.

That leaves a scope hole:

if several matching providers exist, which value belongs to a consumer?

Nest a second provider:

const { createContext, useContext } = React;

const ThemeContext = createContext('light');

function Label({ children }) {
  const theme = useContext(ThemeContext);
  return <p>{children}: {theme}</p>;
}

function App() {
  return (
    <ThemeContext value="dark">
      <Label>Toolbar</Label>

      <ThemeContext value="contrast">
        <section aria-label="Preview">
          <Label>Preview</Label>
        </section>
      </ThemeContext>

      <Label>Status bar</Label>
    </ThemeContext>
  );
}

root.render(<App />);

setTimeout(() => {
  console.log(
    [...mountNode.querySelectorAll('p')].map((node) => node.textContent),
  );
}, 20);

Waiting to run

Not run yet.

The three consumers read dark, contrast, then dark. For each useContext call, React searches upward through the React tree and uses the value from the nearest matching provider.[3]

The inner provider shadows the outer value only for its own descendants:

provider "dark"
├── consumer -> "dark"
├── provider "contrast"
│   └── consumer -> "contrast"
└── consumer -> "dark"

The search follows the React tree, not DOM containment.

A provider returned by a component cannot affect a useContext call made earlier in that same component. The provider must be above the consumer in the returned React tree, so put the read in a descendant when it needs the newly provided value.[3]

This fills the scope hole:

context is lexically identified by its context object and dynamically scoped by the nearest provider above the consumer

Changing the provided value #

So far every provider value has been a string literal. Real shared values change: a user selects a theme, authentication completes, or a locale switches.

Perhaps context owns that change. But a context object has no setter, and its default never changes. Context distributes a value; it does not store an evolving one.[2]

That leaves the update hole:

where does a changing context value live, and how do consumers receive its next value?

The answer reuses state. The parent owns the value in state and provides the current render's value through context:

const { createContext, useContext, useState } = React;

const ThemeContext = createContext('light');

function SaveButton() {
  const theme = useContext(ThemeContext);
  console.log('SaveButton rendered with:', theme);

  return (
    <button data-theme={theme}>
      Save using {theme} theme
    </button>
  );
}

function Toolbar() {
  return <SaveButton />;
}

function App() {
  const [theme, setTheme] = useState('light');

  return (
    <section>
      <button onClick={() => setTheme(
        theme === 'light' ? 'dark' : 'light'
      )}>
        Toggle theme
      </button>

      <ThemeContext value={theme}>
        <Toolbar />
      </ThemeContext>
    </section>
  );
}

root.render(<App />);

setTimeout(() => {
  mountNode.querySelector('button').click();
}, 20);

Waiting to run

Not run yet.

The click updates App's state. Its next render supplies dark to the provider. React then rerenders consumers below that read ThemeContext, and SaveButton receives the current value.[3]

The responsibilities stay separate:

Mechanism Responsibility
state owns a value that changes over time
context transports the current value through a subtree
useContext reads and subscribes to that transported value

React compares the previous and next provided values with Object.is. A different value notifies consumers; the same value does not.[3]

This matters for objects and functions. A freshly created object is different on every render:

<SessionContext value=>
  <Page />
</SessionContext>

That may be exactly what the render requires. If unrelated parent renders become a measured performance problem, stabilize the function with useCallback and the provided object with useMemo. Do not add that complexity merely because the value is an object; first preserve correct dependencies and measure the actual work.

This fills the update hole:

keep changing data in state, provide its current value, and let context subscriptions carry later renders to consumers

Choosing the boundary #

Context makes distant access easy, which creates a design hole of its own:

which values should become implicit inputs to an entire subtree?

Start with props. They show a component's inputs at its call site and keep components easy to reuse in another tree. If intermediate components only wrap content, passing JSX through children may move the data-using component closer to its owner without introducing context.[1]

Context earns its boundary when the value is genuinely ambient to that region and distant consumers need the same kind of information. Common examples include:

  • visual theme
  • current account or session
  • locale
  • routing information
  • shared state paired with a reducer

Context does not make data globally available. A consumer outside the provider's subtree receives another nearer value or the default. That locality is useful: tests, previews, and nested application regions can provide their own values.

Prefer contexts with one coherent meaning over one large bag of unrelated application data. A component can read several independent contexts, and changing one then targets consumers of that channel rather than coupling every ambient concern to one value.

Filling the hole #

The smallest complete context path has three parts:

import { createContext, useContext } from 'react';

const ThemeContext = createContext('light');

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

function Toolbar() {
  return <SaveButton />;
}

function SaveButton() {
  const theme = useContext(ThemeContext);
  return <button data-theme={theme}>Save</button>;
}

Each part owns one responsibility:

  1. createContext(defaultValue) creates the shared channel and its provider-free fallback
  2. <ThemeContext value={value}> scopes the current value to a subtree
  3. useContext(ThemeContext) reads the closest value and subscribes the component to changes

Intermediate components remain ordinary tree structure. They do not need to accept or forward a prop merely to connect the owner with the consumer.

Final definition #

Context is React's tree-scoped mechanism for making one kind of value available to descendant components without explicitly threading that value through every intervening component. A context object identifies the channel, a provider establishes its value for a subtree, and useContext reads the nearest value while subscribing the component to updates.

Context transports data; it does not own mutable state and it does not erase component boundaries. Its scope is the React tree below a provider.

Summary #

Context fills the shared-value transport hole:

  • props remain the clearest mechanism for direct parent-child data flow
  • prop drilling appears when intermediate components forward values they do not use
  • createContext(defaultValue) creates a distinct context object outside components
  • the default value is a static fallback used only when no matching provider exists above
  • a React 19 provider is written as <SomeContext value={value}>
  • React 18 and earlier use <SomeContext.Provider value={value}>
  • useContext(SomeContext) reads and subscribes to that context
  • the nearest matching provider above the consumer determines the value
  • nested providers can override one context for one region of the tree
  • context follows React-tree ancestry, not DOM position
  • state owns changing data; context distributes the current state value
  • consumers rerender when their provided context value changes
  • props and children composition should remain the first tools for local data flow
  • context fits values that are genuinely shared by distant consumers in a subtree

Notes

  1. The live demos in this article use the React 19.2.5 and react-dom 19.2.5 development modules. React 19 allows a context object itself to render as its provider. React 18 and earlier use `` instead. · Back

References

  1. React: Passing Data Deeply with Context (opens in a new tab) · Back
  2. React `createContext` API reference (opens in a new tab) · Back
  3. React `useContext` API reference (opens in a new tab)