Robin van der Vleuten

How to use React Context

React Context, introduced in React v16.3.0, lets components read values from a shared context instead of receiving everything through props. It is useful when several components need the same value and prop drilling starts to get noisy.

This assumes you already know the basics of React: JSX, components, state, and props. If those are still new, start with the React docs.

What is React Context?

React Context lets you share data across multiple components. The common use case is state that several child components need. Start with a prop-based version:

jsx
import React from 'react'
const ParentComponent = () => {
const [counter, setCounter] = React.useState(0)
return (
<ChildComponent
counter={counter}
onIncrement={() => setCounter(counter + 1)}
/>
)
}
const ChildComponent = ({ counter, onIncrement }) => {
return (
<div>
{counter}
<button onClick={onIncrement}>increment</button>
</div>
)
}

Here, <ParentComponent> owns the counter state. It passes the current value and an increment callback to <ChildComponent />.

That is fine for a small example. It gets noisy when more components need the same state, because every component in the middle has to keep passing props along.

Instead of passing counter into ChildComponent, share it through context:

jsx
import React from 'react'
const CounterContext = React.createContext(0)
const ParentComponent = () => {
const [counter, setCounter] = React.useState(0)
const increment = () => setCounter(counter + 1)
return (
<CounterContext.Provider value={{ counter, increment }}>
<ChildComponent />
</CounterContext.Provider>
)
}
const ChildComponent = () => {
const { counter, increment } = React.useContext(CounterContext)
return (
<div>
{counter}
<button onClick={() => increment()}>increment</button>
</div>
)
}

ChildComponent now reads the counter state and increment action from context instead of props. It no longer needs ParentComponent to pass them directly.

How is this different from useState()?

The main difference is scope. useState() stores state inside one component. Context makes a value available to multiple components below a provider.

Context can also share more than state. It can share functions, configuration, a current user object, or anything else React can pass as a value.

How does the context API work?

The Context object has two main pieces: Provider and Consumer. In modern function components, you will often use the provider plus the useContext() hook instead of rendering a consumer component directly.

A single provider can serve multiple components:

jsx
import React from 'react'
const CounterContext = React.createContext(0)
const ParentComponent = () => {
const [counter, setCounter] = React.useState(0)
const increment = () => setCounter(counter + 1)
return (
<CounterContext.Provider value={{ counter, increment }}>
<DisplayCounterComponent />
<IncrementCounterComponent />
</CounterContext.Provider>
)
}
const DisplayCounterComponent = () => {
const { counter } = React.useContext(CounterContext)
return <div>{counter}</div>
}
const IncrementCounterComponent = () => {
const { increment } = React.useContext(CounterContext)
return (
<div>
<button onClick={() => increment()}>increment</button>
</div>
)
}

Here, DisplayCounterComponent reads the counter value, while IncrementCounterComponent reads the increment function. Both come from the same provider.

Create a custom context provider

The previous examples used CounterContext.Provider directly. You can wrap that in a CounterProvider component to make the context easier to reuse.

jsx
import React from 'react'
const CounterContext = React.createContext(0)
const CounterProvider = ({ children, initialCount = 0 }) => {
const [counter, setCounter] = React.useState(initialCount)
const increment = () => setCounter(counter + 1)
return (
<CounterContext.Provider value={{ counter, increment }}>
{children}
</CounterContext.Provider>
)
}

CounterProvider receives an initial count from the parent and uses it as the initial state. It also renders children, so anything inside the provider can read the context.

Use it like this:

jsx
import React from 'react'
const ParentComponent = () => {
return (
<CounterProvider initialCount={0}>
<ChildComponent />
</CounterProvider>
)
}
const ChildComponent = () => {
const { counter, increment } = React.useContext(CounterContext)
return (
<div>
{counter}
<button onClick={() => increment()}>increment</button>
</div>
)
}

The child still uses useContext to read the counter context. If you use the same context in several places, a tiny custom hook keeps that call in one place:

jsx
const useCounterContext = () => React.useContext(CounterContext)

Then components can use useCounterContext() directly:

jsx
const ChildComponent = () => {
const { counter, increment } = useCounterContext()
return (
<div>
{counter}
<button onClick={() => increment()}>increment</button>
</div>
)
}

Closing thoughts

Context is useful when several components need the same value and passing props through every layer starts to get in the way. Keep it small, name the provider clearly, and avoid turning it into a dumping ground for unrelated state.

The React docs cover the API in more detail.