Skip to content
Topic 8 of 13
Theory

useContext: data for the whole app

The problem: props through ten levels

Data in React flows top down through props. Usually that's good: you can see where everything came from. But some data is needed by almost all components: the current user, the theme, the language, the cart. Passing it through props has to go through every intermediate component — even those that don't need it:

function App() {
  const [user, setUser] = useState({ name: "Ann" });
  return <Layout user={user} />;
}
function Layout({ user }) {
  return <Header user={user} />;        // Layout doesn't use user itself
}
function Header({ user }) {
  return <Menu user={user} />;          // and neither does Header
}
function Menu({ user }) {
  return <p>Hello, {user.name}</p>;     // it's only needed here
}

This is called prop drilling. The code becomes fragile: to add a new value, you have to change all the intermediate components.

Context lets a component at the top "declare" a value, and any component below read it directly, without passing it through the intermediate levels.

Three steps: create, provide, read

const { createContext, useContext } = React;

// 1. Create the context (usually in a separate file). The argument is the default value
const UserContext = createContext(null);

// 2. Provide the value: wrap part of the tree in a Provider
function App() {
  const [user, setUser] = useState({ name: "Ann" });
  return (
    <UserContext.Provider value={user}>
      <Layout />
    </UserContext.Provider>
  );
}

function Layout() {
  return <Header />;          // no props
}
function Header() {
  return <Menu />;
}

// 3. Read it in any component inside the Provider
function Menu() {
  const user = useContext(UserContext);
  return <p>Hello, {user.name}</p>;
}
  • createContext(defaultValue) creates a context object.
  • <Ctx.Provider value={...}> makes the value available to all components inside, at any depth.
  • useContext(Ctx) returns the value of the nearest Provider above in the tree. If there's no Provider — the default value.

When the value changes

When a Provider's value changes, React re-renders all the components that read this context via useContext — even if their parents didn't re-render. That's how context stays up to date throughout the application.

Providers can be nested: the inner one "overrides" the outer one for its part of the tree.

<ThemeContext.Provider value="dark">
  <Page />                          {/* the theme is dark here */}
  <ThemeContext.Provider value="light">
    <Footer />                      {/* and light here */}
  </ThemeContext.Provider>
</ThemeContext.Provider>

Context + state

Context by itself stores nothing — it only passes a value down. To make the data changeable, context is combined with state, and both the value and the functions to change it are passed down:

const AuthContext = createContext(null);

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

  const login = (name) => setUser({ name });
  const logout = () => setUser(null);

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

function LoginButton() {
  const { user, login, logout } = useContext(AuthContext);
  return user
    ? <button onClick={logout}>Log out ({user.name})</button>
    : <button onClick={() => login("Ann")}>Log in</button>;
}

function App() {
  return (
    <AuthProvider>
      <Header />
      <LoginButton />
    </AuthProvider>
  );
}

This is the standard pattern: a "provider" component with state inside and children outside. Through children the whole application ends up inside the Provider.

A custom hook for the context

Projects almost always make a small wrapper hook. It's shorter, and it reports an error right away if a component accidentally ends up outside the provider:

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

function Profile() {
  const { user } = useAuth();      // instead of useContext(AuthContext)
  return <p>{user ? user.name : "Guest"}</p>;
}

A hook is an ordinary function whose name starts with use and inside which other hooks are called. More on custom hooks in the data loading topic.

When you don't need context

Context is handy, but you shouldn't put everything in it:

  • if the data is needed one or two levels down — pass props, it's simpler and clearer;
  • composition often helps: instead of passing data through Layout, pass a ready-made element through children — <Layout><Menu user={user} /></Layout>;
  • every change of value re-renders all readers. For frequently changing data (the mouse position, the text in a field) context is a poor choice.

Good candidates for context: the current user, the theme, the language, the cart, settings.

Common mistakes

  1. A component outside the Provider. useContext silently returns the default value (null), and somewhere later you get Cannot read properties of null. A hook with a check gives a clear error right away.
  2. Passing the wrong object to useContext. You need useContext(ThemeContext), not useContext(ThemeContext.Provider) or a string.
  3. Forgetting value. <Ctx.Provider> without value passes undefined.
  4. Expecting that changing an object inside value updates the screen. Same as with state: you need a new object via set….

Summary

  • Context passes data deep into the tree without props: createContext → <Ctx.Provider value> → useContext(Ctx).
  • useContext takes the value of the nearest Provider above, otherwise the default value.
  • When value changes, all readers of the context re-render.
  • Context + useState in a provider component = shared changeable application state.
  • A custom useX() hook with a null check is handy and safe.
  • For data one or two levels down — ordinary props.

A button in the theme colour

Easy+10 XP

The ThemeContext context has already been created ("light" by default). Write a component ThemeButton with children: it draws a <button> with that text, and takes the button's class (className) from the context.

<ThemeContext.Provider value="dark">
  <ThemeButton>OK</ThemeButton>       → <button class="dark">OK</button>
</ThemeContext.Provider>
<ThemeButton>OK</ThemeButton>         → <button class="light">OK</button>   (no Provider)

Connect the connector to solve tasks

Task code runs on your own computer — faster and more reliable than in the browser. This needs the free CodeWithMe connector app. You only connect it once.

Looking for the connector on this computer…

  1. 1
    Download the connector for your system (Windows, macOS or Linux).Download the connectorOn the download page pick the file with your system in its name, e.g. windows-amd64.
  2. 2Run it and choose a folder for your solutions. It will install Python and Node.js there by itself when needed.
  3. 3
    Enter below the 6-digit code shown in the connector window (“Code for CodeWithMe”). If the browser asks about local network access — allow it.

The connector is open source (on GitHub), works only on your computer and runs code only after you enter the pairing code.