React Context API Explained: Share Data Without Passing Props

React.js Full Course: Beginner to Advanced · Hooks and Data · 9 min read · Updated

Context skips the middle

  1. A provider near the top of the app holds the value, such as the theme.
  2. The components in the middle do not receive or pass any props for it.
  3. A deep component calls useContext and gets the value directly from the provider.
  4. When the provider state changes, every component that reads the context draws again.
  5. Use context for data many components share, and wrap it in a custom hook with a helpful error.

In the lifting state lesson we met prop drilling: passing data through many components that do not use it. Context is React's built-in way to make some data available to any component below a certain point, without passing props through every level. Think of it as a shared board in a building that every room can read.

When is context a good choice?

Use context for data that many components at different levels need, and that does not change very often. Typical examples:

  • The current user who is signed in.
  • The theme: light or dark.
  • The chosen language.
  • A shopping cart used by the header, the product pages and the checkout.

Do not use context for everything. If only a parent and child need a value, props are simpler and clearer.

The three steps

  1. Create the context with createContext(defaultValue).
  2. Provide a value with <Context.Provider value={...}> around the part of the page that needs it.
  3. Use the value in any component below with useContext(Context).

Example: a theme

First we create the context, in its own file:

// ThemeContext.jsx
import { createContext, useContext, useState } from 'react';

const ThemeContext = createContext('light');

export function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');

  function toggleTheme() {
    setTheme(theme === 'light' ? 'dark' : 'light');
  }

  return (
    <ThemeContext.Provider value={{ theme, toggleTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}

export function useTheme() {
  return useContext(ThemeContext);
}

ThemeProvider is a normal component that keeps the theme in state and shares it. children is whatever you put inside it. We also export a small helper, useTheme, so other components do not need to know about useContext or the context object. It is a custom hook, which lesson 17 explains.

Next we wrap the app once, near the top:

// main.jsx
createRoot(document.getElementById('root')).render(
  <ThemeProvider>
    <App />
  </ThemeProvider>
);

Now any component, at any depth, can read the theme:

function ThemeButton() {
  const { theme, toggleTheme } = useTheme();
  return (
    <button onClick={toggleTheme}>
      Switch to {theme === 'light' ? 'dark' : 'light'} mode
    </button>
  );
}

function Page() {
  const { theme } = useTheme();
  return <main className={theme}>Hello!</main>;
}

No props travel through the layers between. When toggleTheme runs, the provider's state changes, and every component that uses the context draws again with the new value.

Example: the signed-in user

const AuthContext = createContext(null);

export function AuthProvider({ children }) {
  const [user, setUser] = useState(null);

  function login(name) { setUser({ name }); }
  function logout() { setUser(null); }

  return (
    <AuthContext.Provider value={{ user, login, logout }}>
      {children}
    </AuthContext.Provider>
  );
}

export function useAuth() {
  const value = useContext(AuthContext);
  if (value === null) {
    throw new Error('useAuth must be used inside an AuthProvider');
  }
  return value;
}

The check in useAuth is a helpful habit. If you forget to wrap your app in the provider, you get a clear error message instead of a confusing undefined.

function Header() {
  const { user, logout } = useAuth();
  return user ? (
    <p>Hello {user.name} <button onClick={logout}>Log out</button></p>
  ) : (
    <p>Please sign in.</p>
  );
}

How context finds its value

useContext looks up the tree for the nearest provider of that context. You can even put a provider inside another one to change the value for one part of the page only. If there is no provider above, you get the default value from createContext(defaultValue).

A performance warning

When the value in a provider changes, every component that reads that context draws again. In the examples above we wrote value={{ theme, toggleTheme }}, which makes a new object on every draw of the provider, so all readers redraw even when nothing really changed. For small apps this is fine. In bigger apps you can keep the value steady with useMemo, which lesson 21 covers:

const value = useMemo(() => ({ user, login, logout }), [user]);
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
  • Split big contexts: one for the user, one for the theme, one for the cart. A change in one does not redraw the readers of the others.
  • Keep rarely changing and often changing data in different contexts.
  • Put the provider as low in the tree as it can be, only around the parts that need it.

Context and useReducer together

For bigger shared state, combine them. The reducer holds the rules, and the context shares the state and dispatch:

const CartContext = createContext(null);

export function CartProvider({ children }) {
  const [cart, dispatch] = useReducer(cartReducer, []);
  return (
    <CartContext.Provider value={{ cart, dispatch }}>
      {children}
    </CartContext.Provider>
  );
}

This is a small, complete state manager with no extra library. For very large apps or special needs you can reach for Redux Toolkit or Zustand, which lesson 24 introduces.

Context is not a database

Context only shares state with components. The state itself still lives in useState or useReducer inside the provider. If you need data from a server, load it (lesson 12) and then put the result in the provider, or use a data library (lesson 25).

Common mistakes

  • Forgetting the provider, so the component gets the default value (often null).
  • Putting everything in one giant context, so every change redraws everything.
  • Using context for data that only one parent and child share, where props are simpler.
  • Making the value a new object every draw without thinking about performance in a larger app.
  • Trying to change a context value directly. You must call the function the provider shares, which changes the provider's state.

Following the value through the tree

It is useful to picture what happens when someone clicks the theme button. The button lives deep inside the page, and the provider is at the very top.

  1. The button calls toggleTheme, a function that came from the context.
  2. That function belongs to the provider, so it calls setTheme in the provider's own state.
  3. The provider's state changes, so the provider renders again, and puts a new value into the context.
  4. React finds every component that called useContext for this context, and draws them again with the new value.
  5. Components that do not read the context are left alone, unless their parent redrew.

This shows an important point: the state still lives in an ordinary useState. The context is only the delivery system that brings the value, and the functions to change it, to every place that needs them. You can think of the provider as a small power station, and useContext as the plug in each room.

A rule of thumb for choosing

  • If you pass a prop through one or two levels, keep passing props. It is clear and simple.
  • If you pass the same prop through three or more levels that do not use it, try composition first.
  • If many unrelated components need the same value, such as the user or the theme, use context.

Questions beginners often ask

Can I have more than one provider? Yes, as many as you need. You can nest them, for example a theme provider inside an auth provider. Each context is separate.

Where does the provider go? Around the parts of the app that need the data. Often that is main.jsx, wrapped around App. You can also place a provider lower in the tree for one section.

Can I change context from a child? Yes, but through a function that the provider shares, such as toggleTheme. The provider owns the state, so changes go through it.

Is context only for global data? It is for data that many components share. It does not have to be for the whole app. A form with many parts can have a small context just for itself.

Your task

  1. Build a ThemeProvider with a toggle button, and show the theme name in two different components.
  2. Build an AuthProvider with login and logout, and show different content in a header for each case.
  3. Make useTheme throw a helpful error when used outside a provider.
  4. Create a CartProvider with useReducer and show the number of items in the header and in a cart page.

How to check it worked

  • Both components change when you press the toggle.
  • Using the hook outside a provider shows your clear error message.
  • The cart count in the header always matches the cart page.

Summary

  • Context shares data with any component below a provider, with no prop drilling.
  • Steps: createContext, wrap with Provider value={...}, read with useContext.
  • Wrap the context in a custom hook, and check that a provider exists.
  • A changed value redraws every reader. Split contexts and keep values steady in big apps.

Frequently asked questions

What is the React Context API used for?

To share data such as the user, theme or language with many components at different levels, without passing props through every level.

Does context replace Redux?

For small and medium apps, context with useReducer is often enough. Redux and similar libraries add tools and optimisations that help in large apps.

Why do all my components redraw when context changes?

Every component that reads a context draws again when its value changes. Split big contexts into smaller ones, and keep the value steady.

Where should I put the provider?

Around the smallest part of the tree that needs the data. Often that is the top of the app, for things like the user and theme.