Skip to the chapter
←ReactGGroundwork⚛React
React/Intermediate/I2
13 / 57 written chapters
/

The cover
  1. B1Setup & JSX
  2. B2Thinking in React
  3. B3Components
  4. B4Props
  5. B5useState
  6. B6Events & conditionals
  7. B7Lists & keys
  8. B8Forms
  9. B9useEffect
  10. B10Lifting state & styling
  11. B11Guided project
  12. ★Cheat page
  1. I1useRef
  2. I2Context
  3. I3useReducer
  4. I4Custom hooks
  5. I5Effects in depth
  6. I6Routing
  7. I7State architecture
  8. I8Memoisation
  9. I9Boundaries & portals
  10. I10Data fetching
  11. I11Forms at scale
  12. I12State libraries
  13. I13Auth
  14. I14TanStack Query
  15. I15Testing
  16. I16TypeScript
  17. I17Animation
  18. I18E2E testing
  19. I19i18n
  20. I20File uploads
  21. I21Real-time UI
  22. I22shadcn/ui
  23. ★Cheat page
  1. A1Fiber
  2. A2Concurrent
  3. A3Suspense
  4. A4Server Components
  5. A5Server Actions
  6. A6use()
  7. A7Rendering strategies
  8. A8Effect timing
  9. A9External stores
  10. A10Refs in depth
  11. A11Component patterns
  12. A12Performance
  13. A13Accessibility
  14. A14Design systems
  15. A15Advanced TypeScript
  16. A16Architecture at scale
  17. A17Custom renderers
  18. A18Migrating & upgrading
  19. A19Production debugging
  20. A20Review & judgement
  21. A21State machines
  22. A22Feature flags
  23. A23Interview bank
  24. ★Cheat page

⚛I2 · Part 2 · Intermediate

Context API

A way to skip the middle layers — not a state manager, and not free.

3 min read8 sections5 exercises
13 of 57

In this chapter

  • What it solves
  • The default is not a fallback
  • The performance trap
  • Split by change frequency
  • What context is not

and 3 more

about 3 min
08On this page
  1. What it solves
  2. The default is not a fallback
  3. The performance trap
  4. Split by change frequency
  5. What context is not
  6. A provider that is worth writing
  7. Nesting and overriding
  8. When to use it

What it solves

Some values are genuinely ambient: the theme, the signed-in user, the locale, a toast dispatcher. Threading them through six components that do not care is noise. Context lets a provider put a value into the tree and any descendant read it directly.

const ThemeContext = createContext("light");        // default if no provider

function App() {
  const [theme, setTheme] = useState("light");
  return (
    <ThemeContext value={theme}>      // React 19: no .Provider needed
      <Page />
    </ThemeContext>
  );
}

function DeepButton() {
  const theme = useContext(ThemeContext);           // no props threaded
  return <button className={theme}>Save</button>;
}

Before React 19 the provider was <ThemeContext.Provider value={...}>. Both work in 19; the old form is what you will see in existing code.

The default is not a fallback

createContext(defaultValue) is used only when a component reads the context with no provider above it. That is almost always a mistake rather than a feature, so make it loud:

const AuthContext = createContext(null);

export function useAuth() {
  const ctx = useContext(AuthContext);
  if (!ctx) throw new Error("useAuth must be used inside <AuthProvider>");
  return ctx;
}

Exporting a hook rather than the context is worth doing anyway: it gives you one place to add the check, keeps the context object private, and means consumers never import useContext at all.

The performance trap

Every consumer re-renders when the value changes

Context does not do partial subscriptions. When the provider's value changes by Object.is, every component calling useContext for it re-renders — even if it only reads a field that did not change.

<AuthContext value={{ user, login, logout }}>   // ✗ new object every render

That object literal is a fresh reference on every render of the provider, so every consumer re-renders every time — including when the value is identical. Memoise it:

const value = useMemo(() => ({ user, login, logout }), [user, login, logout]);
<AuthContext value={value}>

And keep login/logout stable too, with useCallback or by defining them outside — otherwise the memo is recomputed anyway.

Split by change frequency

The most effective context optimisation is not memoisation, it is splitting. Values that change often and values that never change should not share a provider.

const StateContext = createContext(null);      // changes on every keystroke
const DispatchContext = createContext(null);   // never changes

<StateContext value={state}>
  <DispatchContext value={dispatch}>{children}</DispatchContext>
</StateContext>

Now a component that only dispatches — a button, a form's submit — reads DispatchContext and never re-renders when the state moves. dispatch from useReducer is guaranteed stable, which makes this pairing particularly clean.

What context is not

BeliefReality
"Context is a state manager"It is a transport. It moves a value down the tree; you still need useState or useReducer to hold it.
"Context replaces Redux"Only for values that rarely change. A real store gives selectors, middleware and devtools, and re-renders only what actually reads the changed slice.
"Context avoids prop drilling"So does composition, with no re-render cost. Try passing the element as children first.
"One context for the app"The fastest way to re-render everything on every change. Several small contexts beat one large one.

A provider that is worth writing

function ThemeProvider({ children }) {
  const [theme, setTheme] = useState(() => localStorage.getItem("theme") ?? "light");

  useEffect(() => {
    document.documentElement.dataset.theme = theme;
    localStorage.setItem("theme", theme);
  }, [theme]);

  const value = useMemo(
    () => ({ theme, toggle: () => setTheme((t) => (t === "light" ? "dark" : "light")) }),
    [theme]
  );

  return <ThemeContext value={value}>{children}</ThemeContext>;
}

The provider owns the state, the side effect and the API. Everything below gets useTheme() and knows nothing about storage or the DOM attribute — which is the point.

Nesting and overriding

A consumer reads the nearest provider above it, so contexts can be overridden for a subtree. That is how a dark panel inside a light page works, and it is genuinely useful:

<ThemeContext value="light">
  <Page>
    <ThemeContext value="dark">
      <Sidebar />      // reads "dark"
    </ThemeContext>
  </Page>
</ThemeContext>

When to use it

  • Yes: theme, locale, the current user, a design-system config, a dispatch function, anything read by many components and written rarely.
  • No: form values, a list being filtered, anything changing per keystroke — unless you have split the contexts carefully.
  • Not yet: a prop travelling two layers. That is not drilling, that is just props.
Interview answer, one sentence

"Context is a transport for values that many components need and few components change. Every consumer re-renders when the provider value changes by identity, so the object goes in a useMemo and contexts get split by how often they change — state in one, dispatch in another."

05Practice this chapter

Opens in the editor: write it, run it, and check it against real tests.

  • 9 testsA selector for a context valueintermediate
  • 10 testsDid the part this consumer reads change?intermediate
  • 9 testsA theme context that doesn't re-render everyoneintermediate
  • 6 testsA default that only applies without a providerintermediate
  • 5 testsA nested provider overrides its parent, locallyintermediate
All 66 for this level →
I2

Finished reading?

0 of 57 chapters read in this series

→Next stop

  1. I1Previous · I1useRef
  2. I2you are here
  3. Next · I3useReducerWhen the next state depends on the current state and on which thing happened.5 min read

01On this page

8 sections · 0%

  1. 01What it solves
  2. 02The default is not a fallback
  3. 03The performance trap
  4. 04Split by change frequency
  5. 05What context is not
  6. 06A provider that is worth writing
  7. 07Nesting and overriding
  8. 08When to use it

[] chapters · t top