Props are the normal way to give data to a component, and they are explicit: you can see where each value comes from. But sometimes a value is needed by components far down the tree: the colour theme, the logged-in user, the chosen language. Passing it as a prop through every level in between is tedious, and the middle components are cluttered with props they never use. This is called prop drilling. Context is React's built-in way to avoid it.
The problem: prop drilling
Imagine the theme ("light" or "dark") is chosen at the top, but only the Button at the bottom of the tree needs it:
function Button({ theme }) {
return <button style={{ background: theme === "dark" ? "#222" : "#eee", color: theme === "dark" ? "#fff" : "#000" }}>A button</button>;
}
function Toolbar({ theme }) { // does not use theme, just forwards it
return <div><Button theme={theme} /></div>;
}
function Page({ theme }) { // also just forwards it
return <section><Toolbar theme={theme} /></section>;
}
function App() {
return <Page theme="dark" />;
}
export default App;A buttonPage and Toolbar have a theme prop only to hand it down. Add two more layers, or two more values (language, user), and the plumbing grows. Context removes it.
Step 1: create a context
createContext makes a context object. Its argument is the default value, used by components that have no provider above them:
import { createContext } from "react";
export const ThemeContext = createContext("light");In a real project, put the context in its own file so any component can import it.
Step 2: provide a value
A component wraps part of the tree in the context and gives it a value. Everything inside can read it. In React 19 you render the context itself as the provider; in earlier versions you wrote <ThemeContext.Provider value={...}>, which still works:
<ThemeContext value="dark">
<Page />
</ThemeContext>Step 3: read it where it is needed
Any component inside can call useContext to read the value of the nearest provider above it. No props are needed on the way down:
import { createContext, useContext } from "react";
const ThemeContext = createContext("light");
function Button() {
const theme = useContext(ThemeContext);
return (
<button style={{ background: theme === "dark" ? "#222" : "#eee", color: theme === "dark" ? "#fff" : "#000" }}>
A {theme} button
</button>
);
}
function Toolbar() {
return <div><Button /></div>;
}
function Page() {
return <section><Toolbar /></section>;
}
function App() {
return (
<ThemeContext value="dark">
<Page />
</ThemeContext>
);
}
export default App;A dark buttonPage and Toolbar no longer mention the theme at all. Button asked for it directly. The lookup goes up the tree to the closest ThemeContext provider; if you nest providers, the inner one wins for the components inside it. A component rendered without any provider gets the default value ("light"). React 19 also offers use(ThemeContext), which reads context in the same way but may be called inside conditions and loops.
A context that changes: theme switcher
The value can be state, so that a change anywhere re-renders all the consumers. Pass both the value and a way to change it:
import { createContext, useContext, useState } from "react";
const ThemeContext = createContext({ theme: "light", toggle: () => {} });
function ThemeProvider({ children }) {
const [theme, setTheme] = useState("light");
const value = { theme, toggle: () => setTheme(t => (t === "light" ? "dark" : "light")) };
return <ThemeContext value={value}>{children}</ThemeContext>;
}
function Panel({ children }) {
const { theme } = useContext(ThemeContext);
return (
<div style={{ padding: 12, background: theme === "dark" ? "#222" : "#f4f4f4", color: theme === "dark" ? "#fff" : "#000" }}>
{children}
</div>
);
}
function SwitchButton() {
const { theme, toggle } = useContext(ThemeContext);
return <button className="switch" onClick={toggle}>Switch to {theme === "light" ? "dark" : "light"}</button>;
}
function App() {
return (
<ThemeProvider>
<Panel>
<p>Content inside a themed panel.</p>
<SwitchButton />
</Panel>
</ThemeProvider>
);
}
export default App;Content inside a themed panel.
Switch to lightAfter the test click the button reads "Switch to light", so the theme is now dark. This arrangement is the standard pattern:
- A provider component (
ThemeProvider) owns the state and renders the context with{children}.Appstays small and just wraps the tree. - Consumers call
useContextand need no props. - The default value matches the shape of the real value, which helps editors and lets components work in tests without a provider.
Context with useReducer
Combine the last two lessons for larger shared state: the reducer holds the logic, and context shares both the state and dispatch with the whole tree. Provide them in two contexts so components that only dispatch do not render when the state changes:
import { createContext, useContext, useReducer } from "react";
const TasksContext = createContext([]);
const TasksDispatchContext = createContext(() => {});
let nextId = 1;
function tasksReducer(tasks, action) {
switch (action.type) {
case "added":
return [...tasks, { id: action.id, text: action.text }];
case "removed":
return tasks.filter(task => task.id !== action.id);
default:
throw new Error("Unknown action: " + action.type);
}
}
function TasksProvider({ children }) {
const [tasks, dispatch] = useReducer(tasksReducer, []);
return (
<TasksContext value={tasks}>
<TasksDispatchContext value={dispatch}>{children}</TasksDispatchContext>
</TasksContext>
);
}
function AddButton() {
const dispatch = useContext(TasksDispatchContext);
return <button className="add" onClick={() => dispatch({ type: "added", id: nextId, text: "Task " + nextId++ })}>Add task</button>;
}
function TaskList() {
const tasks = useContext(TasksContext);
const dispatch = useContext(TasksDispatchContext);
return (
<ul>
{tasks.map(task => (
<li key={task.id}>
{task.text} <button onClick={() => dispatch({ type: "removed", id: task.id })}>x</button>
</li>
))}
</ul>
);
}
function App() {
return (
<TasksProvider>
<AddButton />
<TaskList />
</TasksProvider>
);
}
export default App;Add task
Task 1 x
Task 2 xAddButton and TaskList are siblings that never receive props, yet they share the list. The function dispatch never changes identity, so AddButton (which reads only the dispatch context) does not need to re-render when tasks change. In large apps you would also wrap the access in small custom hooks: useTasks() and useTasksDispatch(), so that components do not touch the context objects directly.
How context renders
Remember the rule: when a provider's value changes (compared with Object.is), every component that reads that context renders again, wherever it is. Two practical consequences:
- A new object on every render re-renders every consumer. In
ThemeProviderthevalueobject is created on each render of the provider, so every consumer renders whenever the provider does. This is usually fine, because the provider renders rarely (only when its own state changes). For frequently re-rendering providers, memoise the value (see the next lesson) or split the context. - Do not put everything in one big context. A single "app state" context re-renders all consumers when any field changes. Create several focused contexts: theme, user, tasks, language.
Context changes also pass through memo (introduced in the next lesson): a memoised component that reads a context still renders when that context's value changes.
What context is good for
Context shines for data that is global-ish, read by many components at different depths, and changes infrequently:
- the theme or colour scheme,
- the current user and permissions,
- the language or locale,
- the router state (React Router uses it),
- a shopping cart or the toast notifications queue,
- a form library's current form.
It is not a replacement for props. Prefer plain props when the value goes through one or two levels, because props make the flow of data explicit and easy to follow. Prefer composition (passing components as children) when you only want to avoid threading a component through the layers. And for rapidly changing, complex or server-derived data, a state library or a data-fetching library is better suited than context.
Common mistakes
- Using context for everything, hiding where data comes from.
- Providing a new object or function every render without realising that all consumers re-render.
- Putting unrelated values in one huge context.
- Forgetting the provider, and being confused when the default value appears.
- Mutating the value instead of using state in the provider.
- Using
useContextin a component outside the provider's subtree. - Forgetting that the context file should export the context (and hooks), not be redefined in several places, which creates separate contexts that do not match.
Exercise
- Add a
languagecontext ("en"or"hi") and aGreetingcomponent deep in the tree that shows "Hello" or "Namaste". - Make a
UserContextholding{ name }ornull, and show "Sign in" or the user's name in a navigation bar and in a footer without props. - Wrap the task example's contexts in custom hooks
useTasksanduseTasksDispatch, and use them in the components. - Nest a second
ThemeContextprovider with a different value inside the first, and check which value each button sees.
Key points
- Context lets a component make a value available to every component below it, without passing props through each level.
- Create it with createContext(default), provide it with <MyContext value={...}> (React 19) and read it with useContext(MyContext) or use(MyContext).
- When the provided value changes, every component that reads that context renders again.
- Use context for data that many components need, such as theme, current user or language; for most other data, props and lifting state are simpler.