useContext reads a value that a parent component provides, so a component deep in the tree can get it without every component in between passing it as a prop. You create a context with createContext, wrap part of the tree in a provider with a value, and call useContext(TheContext) wherever you need that value.
Toolbar never mentions the theme, yet both buttons change when you tick the box. That is the whole point of context: the components in the middle do not carry a prop they do not use, which is the prop drilling problem context solves.
The three steps
// 1. Create it once, outside any component, usually in its own file
export const ThemeContext = createContext('light');
// 2. Provide a value to a part of the tree
<ThemeContext value={theme}>
<Page />
</ThemeContext>
// 3. Read it in any component inside that part
const theme = useContext(ThemeContext);
In React 19 the context object itself is the provider. Older code writes <ThemeContext.Provider value={theme}>, which still works, so you will see both.
useContext looks up the tree for the nearest provider of that context. A component reads the closest one, so you can nest providers to override a value for one section.
The default value
The argument to createContext is what useContext returns when no provider sits above the component. It does not change when state changes; it is a fallback.
The first Greeting has no provider above it and gets the default 'en'. The second reads 'es', and the third reads the nearer 'ja'. Change the outer value="es" to value="ja" and only the second line changes.
Updating context from a child
Context only passes a value down. To let a deep child change it, keep the value in state where the provider is, and put the setter into the context next to it.
LoginButton calls setUser, the state in App changes, the provider gets a new value, and Header updates too. Neither component received a prop.
Every consumer re-renders when the value changes
When a provider's value changes, React renders every component that calls useContext for it, even if that component sits inside a memo and its props stayed the same. React compares the old and new value with Object.is. So value={{ user, setUser }} is a new object on every render of App, and every consumer renders again even when user did not change.
Both readers log once on the first render. Click the button: only PlainReader logs again, because its context got a new object. useMemo hands MemoContext the same object until name changes, so MemoReader stays put. This only matters once rendering gets slow; for a small app, a new object per render is fine.
Splitting contexts
A value that changes often and a value that never changes should not share a context. If the setter lives with the state, a component that only needs to call setUser still renders every time user changes. Put them in two contexts and each consumer subscribes to only what it reads:
const UserContext = createContext(null);
const SetUserContext = createContext(() => {});
function UserProvider({ children }) {
const [user, setUser] = useState(null);
return (
<SetUserContext value={setUser}>
<UserContext value={user}>{children}</UserContext>
</SetUserContext>
);
}
setUser from useState keeps the same identity forever, so components that read only SetUserContext never render because of a login. The same idea applies to unrelated data: a theme and a shopping cart belong in separate contexts, not one big AppContext.
A common finishing touch is a custom hook such as useUser() that calls useContext(UserContext) and throws a clear error when the provider is missing, so callers never import the context object directly.
Context, props or a state library
- Props are the default. Passing a value two or three levels down is clear and easy to trace.
- Context fits values many components need at many depths and that change rarely: theme, language, the signed-in user, a feature flag object.
- A state library (Redux Toolkit, Zustand, Jotai and others) fits large state that updates often, where you want a component to subscribe to one slice and skip renders caused by the rest. Context has no such selector: a consumer renders on any change to the value.
Before reaching for either context or a library, check whether passing components as children removes the drilling. It often does, with no new API at all.
Frequently Asked Questions
What does useContext do in React?
It returns the current value of a context, taken from the nearest provider above the component in the tree. When that provider's value changes, every component that reads it renders again.
What is the default value in createContext for?
It is what useContext returns when there is no provider above the component. It is useful for tests and for components rendered on their own, and it never changes.
Do I still need Context.Provider in React 19?
No. In React 19 you can render the context itself as the provider: <ThemeContext value="dark">. <ThemeContext.Provider value="dark"> still works, so older code keeps running.
How do I update a context value from a child component?
Keep the value in state in the component that renders the provider, and put the setter in the context next to the value: value={{ theme, setTheme }}. The child reads setTheme with useContext and calls it.
Is React context a replacement for Redux?
Context passes a value down the tree; it does not manage state on its own. Combined with useState or useReducer it covers many apps. A state library adds things context lacks, such as subscribing to one slice so unrelated updates do not re-render a component, plus devtools and middleware.