Himanshu SharmaSoftware engineer
Let’s talk

React · Lesson 28 of 38

Sharing data with context

Pass data deep into the tree without prop drilling: createContext, a provider, useContext, a theme switcher, context with useReducer, and when not to use it.

20 minSharing state and performanceUpdated 24 October 2026

By the end of this lessonCreate a context, provide a value, read it in nested components, and decide when context is the right tool.

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:

jsx
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;
result
A button

Page 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:

jsx
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:

jsx
<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:

jsx
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;
result
A dark button

Page 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:

jsx
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;
result
Content inside a themed panel.
Switch to light

After 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}. App stays small and just wraps the tree.
  • Consumers call useContext and 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:

jsx
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;
result
Add task
Task 1 x
Task 2 x

AddButton 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:

  1. A new object on every render re-renders every consumer. In ThemeProvider the value object 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.
  2. 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 useContext in 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

  1. Add a language context ("en" or "hi") and a Greeting component deep in the tree that shows "Hello" or "Namaste".
  2. Make a UserContext holding { name } or null, and show "Sign in" or the user's name in a navigation bar and in a footer without props.
  3. Wrap the task example's contexts in custom hooks useTasks and useTasksDispatch, and use them in the components.
  4. Nest a second ThemeContext provider 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.

Check yourself

1. What problem does context solve?
2. Which hook reads the nearest value of a context?
3. What happens when the value passed to a provider changes?
4. When should you avoid context?