React Context Provider
History
The Context API became a stable public API in React 16.3 (March 2018). Before that, an experimental context mechanism existed, but it was undocumented and strongly discouraged because it could break between minor versions.
The useContext hook arrived in React 16.8 (February 2019) with the rest of Hooks. Before Hooks, context
consumption relied on either <Context.Consumer> (render-prop) or static contextType in class components, both of
which were more verbose and harder to compose.
The pattern used in this document (createContext + useContext + custom hook) has been the recommended approach since
React 16.8 and remains valid through React 19.
What is a Provider?
A React Context Provider is a component that makes a value available to every component in its subtree, without passing props through intermediate layers. This avoids prop drilling1.
The mechanism has three parts:
- Context — the container created with
createContext(). It defines the shape of the shared value. - Provider — a component that wraps part of the tree and supplies the actual value.
- Consumer — any component inside the tree that reads that value, typically via
useContext.
<ValueProvider> ← supplies { value, setValue }
<ParentContent /> ← reads it via useValue()
<ChildComponent /> ← reads it via useValue() too
Both ParentContent and ChildComponent read the same state. When either calls setValue, both re-render with the
updated value.
How to Create a Provider
1. Define the context shape
Create a context with values that will be passed by the provider.
// ValueContext.tsx
import { createContext, useContext, useState } from 'react';
interface ValueContextType {
value: string;
setValue: (v: string) => void;
}
export const ValueContext = createContext<ValueContextType | null>(null);
Initialize with null instead of a default object. This allows the custom hook guard to fail fast when a component is
rendered outside the provider.
2. Write the Provider component
The provider owns the state and exposes it through context:
export function ValueProvider({ children }: { children: React.ReactNode }) {
const [value, setValue] = useState('');
return (
<ValueContext.Provider value={{ value, setValue }}>
{children}
</ValueContext.Provider>
);
}
3. Export a custom hook
Wrap useContext in a named hook. This keeps imports clean and gives one place for safety checks:
export function useValue(): ValueContextType {
const ctx = useContext(ValueContext);
if (!ctx) throw new Error('useValue must be used within a ValueProvider');
return ctx;
}
This guard turns a silent undefined bug into an immediate, explicit error.
How to Use It
Wrap the subtree with the Provider
Only wrap the part of the tree that needs shared state. The provider does not need to live at the app root.
// page.tsx
import { ValueProvider } from './ValueContext';
import { ParentContent } from './ParentContent';
export default function ContextDemoPage() {
return (
<ValueProvider>
<Component />
</ValueProvider>
);
}
You can also wrap providers around providers.
Consume in a component
Any component under <ValueProvider> can call useValue(), no matter how deep it is:
// Component.tsx
import { useValue } from './ValueContext';
function Component() {
const { value, setValue } = useValue();
return (
<div>
<p>Current value: {value}</p>
<input
value={value}
onChange={(e) => setValue(e.target.value)}
placeholder="Update shared value from parent..."
/>
</div>
);
}
When the component’s input changes, setValue updates the state inside ValueProvider.
React then re-renders all subscribers, so all components reading from the context stay in sync.
Testing
Testing context-connected components is straightforward: render them with the real provider.
No mocking is required.
This validates initialization, updates, and re-renders through the exact production path. Examples below use
@testing-library/react and @testing-library/user-event.
Testing a component in isolation
Wrap the component in ValueProvider directly in the test. The provider starts with an empty string.
// tests/context-demo/Component.test.tsx
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import Component from '@/app/context-demo/Component';
import { ValueProvider } from '@/app/context-demo/ValueContext';
describe('Component', () => {
it('updates the shared context value when the input changes', async () => {
const user = userEvent.setup();
render(
<ValueProvider>
<Component />
</ValueProvider>
);
const input = screen.getByTestId('child-input');
await user.type(input, 'hello');
expect(input).toHaveValue('hello');
expect(screen.getByTestId('child-value-display')).toHaveTextContent('hello');
});
});
Mocking context is possible, but it replaces the real useValue implementation with a stub.
We would rather avoid it since using the real ValueProvider means:
- Tests execute the same behavior as production.
- Provider bugs (bad initial state, missed updates) are detectable.
- Tests stay stable across internal refactors (no need to update the provider’s mock)
React Context vs. Redux
Both approaches solve shared state, but they differ in scope, structure, and tooling.
We could wonder if it means Redux which is close to a state database (one global tree with explicit updates through actions and reducers) is becoming obsolete.
Quick comparison
| Feature | Context | Redux |
|---|---|---|
| Setup | Lightweight, no extra dependency. | Additional setup (store, reducers, actions, selectors). |
| DevTools | No built-in time travel or action log. | Mature debugging and action history. |
| Performance model | All consumers re-render when the provided value changes. | Selectors support fine-grained subscriptions. |
| Async workflows | Manual orchestration (useEffect, custom hooks). | Middleware patterns. |
| Typical scope | Feature-local or subtree-local state. | Application-wide shared domain state. |
| Best fit | UI state (theme, local feature state, simple auth wiring). | Complex domain state and larger teams needing strong conventions. |
There are also some libraries in between the React context and Redux, for example:
- Zustand which is a lightweight store: define state and actions once, then use a hook to read and update from any component.
- Jotai which uses atoms: define small independent pieces of state and combine them into complex structures.
-
Prop drilling is the process of passing props down through multiple layers of components. ↩