Skip to main content
Back to module
React Fundamentals · Effects and Data

Context and lifting state

Reading40mLesson 3 of 3 in this module

Share state between components by lifting it to a common parent, and deliver it deep in the tree with context.

Why it matters

Sooner or later two components that are far apart in the tree need the same data: the header shows the signed-in user and so does the comment box; a theme switch in the settings changes colours everywhere. React gives you two tools. Lifting state up moves state to the closest common parent. Context delivers a value to any component below a provider without passing it through every level.

Lifting state up

Imagine two panels where only one may be open at a time. If each panel keeps its own isOpen state, they cannot coordinate. The fix is to move the state to their parent and pass it down:

function Panel({ title, isOpen, onOpen, children }) {
  return (
    <section>
      <h3>{title}</h3>
      {isOpen ? <p>{children}</p> : <button onClick={onOpen}>Show</button>}
    </section>
  );
}

function Accordion() {
  const [openIndex, setOpenIndex] = React.useState(0);
  return (
    <>
      <Panel title="About" isOpen={openIndex === 0} onOpen={() => setOpenIndex(0)}>
        We teach coding for free.
      </Panel>
      <Panel title="Contact" isOpen={openIndex === 1} onOpen={() => setOpenIndex(1)}>
        Email us any time.
      </Panel>
    </>
  );
}

Panel is now controlled by its parent: it receives isOpen and asks for changes through onOpen. There is one source of truth for which panel is open, so the two can never disagree.

The recipe:

  1. Find the components that need the data.
  2. Find their closest common parent.
  3. Move the state there and pass the value down as props, with functions to change it.

Lifting state is the first tool to reach for. It keeps data flow explicit and easy to trace.

When props get tiresome: prop drilling

If the common parent is high up, you may pass a prop through many layers that do not use it:

<App theme={theme}>
  <Layout theme={theme}>
    <Sidebar theme={theme}>
      <ThemeButton theme={theme} />

That is prop drilling. A couple of levels is fine. Ten levels, or the same value needed in dozens of places (theme, current user, language), is where context helps.

Context in three steps

// 1. Create a context (the argument is the default value)
const ThemeContext = React.createContext(null);

// 2. Provide a value high in the tree
function ThemeProvider({ children }) {
  const [theme, setTheme] = React.useState("light");
  const toggleTheme = () => setTheme((t) => (t === "light" ? "dark" : "light"));
  return (
    <ThemeContext.Provider value={{ theme, toggleTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}

// 3. Read it anywhere below
function ThemeButton() {
  const { theme, toggleTheme } = React.useContext(ThemeContext);
  return <button onClick={toggleTheme}>Switch to {theme === "light" ? "dark" : "light"}</button>;
}

useContext returns the value of the nearest provider above the component. When that value changes, every component reading it re-renders. Note that the provider still uses ordinary state: context does not store anything by itself. It is a delivery mechanism for state you lifted up.

(In React 19 you can render <ThemeContext value={...}> directly; <ThemeContext.Provider> works in both 18 and 19.)

A custom hook for the context

Wrapping useContext in a custom hook gives callers a nicer API and a clear error when the provider is missing:

function useTheme() {
  const value = React.useContext(ThemeContext);
  if (value === null) {
    throw new Error("useTheme must be used inside a ThemeProvider");
  }
  return value;
}

Because the default value is null, a component rendered outside the provider fails loudly instead of silently using a fake theme.

Multiple providers

Each provider creates its own value, and the nearest one wins. Two ThemeProviders side by side keep separate themes, and a provider inside another overrides it for its subtree. That is useful for, say, a dark preview pane inside a light page.

Good uses for context

  • Theme, language and formatting preferences.
  • The signed-in user and permissions.
  • A shared service such as a toast or notification API.
  • State that one large feature shares across its own components (a wizard, an editor).

When not to use it

  • Data only two or three nearby components need. Lift state up and pass props.
  • Fast-changing values used by many components, such as a mouse position. Every consumer re-renders on each change. Split contexts so unrelated values do not update together, or use a dedicated state library.
  • Server data (API results with caching) is often better in a data-fetching library.

If the provider's value is a new object every render, every consumer re-renders whenever the provider does. For heavier apps, wrap it: const value = React.useMemo(() => ({ theme, toggleTheme }), [theme]); (and make toggleTheme stable with useCallback).

Common mistakes

  • Calling useContext in a component that is not inside the provider, and getting the default value.
  • Putting every piece of app state in one giant context.
  • Forgetting that context still needs state in the provider to change over time.

Recap

  • Lift shared state to the closest common parent; pass values down and change functions as props.
  • Context (createContext, a provider, useContext) skips the intermediate layers.
  • Wrap context in a useX hook that throws when the provider is missing.
  • Keep context for widely shared, slowly changing values.

Try it

Open the Do tab to build a ThemeProvider and a useTheme hook so a deeply nested button can switch a panel between light and dark.

Also in this lesson: