Chapter 15 of 20 · React for Beginners — Complete Free Tutorial

Managing Global State with Context

You can now move shared state to a common parent in the React for Beginners series — but what about state that every component needs? The current user, the colour theme, the selected locale. Passing those through fifteen layers of props is called prop drilling, and it makes unrelated components depend on each other. React's built-in answer is Context: a way to publish a value that any component can read without explicit prop chains.

The prop drilling problem

Suppose the theme lives in App but a deeply nested footer needs it. Without context, every component in between must forward the prop, whether it uses it or not:

<App theme={theme}>
  <Page theme={theme}>
    <Section theme={theme}>
      <Footer theme={theme} />   {/* every layer must cooperate */}
    </Section>
  </Page>
</App>

Expected output: a working footer — but Page and Section now both depend on a prop they never use, and inserting a new layer breaks them all.

createContext and Provider

Context has three moving parts: create it, provide a value, consume it. First, create the context once at module scope:

import { createContext, useContext, useState } from 'react';

const ThemeContext = createContext(null);   // default value: null

export function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');
  const toggle = () => setTheme((t) => (t === 'light' ? 'dark' : 'light'));

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

The provider wraps your tree (usually near the root) and owns the state. Everything below it can read the current value without receiving it as a prop.

useContext: reading from anywhere

Any descendant calls the useContext hook with the context object and gets the provider's current value:

function ThemeToggle() {
  const { theme, toggle } = useContext(ThemeContext);
  return (
    <button onClick={toggle}>
      Theme is {theme} — click to switch
    </button>
  );
}

Expected output: clicking the button flips every consumer — toggle, page, footer — in one update, because they all read the same state. No layer in between heard anything about it.

The full picture

PieceRoleLives where
createContext()Creates the context objectModule scope, next to its provider
<Context.Provider value>Publishes the current value downwardHigh in the tree
useContext(ctx)Reads the nearest provider's valueAny component that needs it
Default argumentValue used when no provider is aboveKeep it null and guard — it hides bugs

What belongs in context (and what does not)

Context solves low-frequency, widely-shared data. Use it well by choosing carefully:

  • Good fits — theme, current user, locale, feature flags, router state.
  • Bad fits — a search box value used by two siblings (lift state instead), fast-changing counters, large lists that re-render everything on each keystroke.
  • Not a state manager — context publishes values; it does not memoise, cache or batch for you. Heavy apps layer libraries (Redux, Zustand, TanStack Query) on top, often still using context for the last mile.

Because every consumer re-renders when the provider's value changes, keep the value stable — build it with useMemo when it is an object or function (see chapter 18), and split independent values into separate contexts so a theme change does not re-render user-dependent UI.

Custom consumer hooks

A tidy convention wraps the lookup in a named hook, centralising the null check and simplifying call sites:

export function useTheme() {
  const ctx = useContext(ThemeContext);
  if (!ctx) throw new Error('useTheme must be used inside ThemeProvider');
  return ctx;
}

// anywhere below the provider:
const { theme, toggle } = useTheme();

Expected output: a clear error — "useTheme must be used inside ThemeProvider" — instead of a silent null crash when a component is misplaced. This hook also previews chapter 19: wrapping hook logic into named, reusable functions.

Provider composition

Providers are just components, so nesting them reads like a configuration list — auth inside theme inside router, or the reverse, depending on who needs whom:

<StrictMode>
  <ThemeProvider>
    <AuthProvider>
      <App />
    </AuthProvider>
  </ThemeProvider>
</StrictMode>

Common mistakes

  • Creating context inside the component — every render makes a new context and consumers lose the provider; create it at module scope.
  • Meaningful default values — a default of { theme: 'light' } silently masks a missing provider; default to null and throw.
  • Context as the default state store — two sibling components that share data only need lifting, not a global.
  • Unstable provider values — an inline object/array literal re-renders all consumers on every provider render; memoise the value.
  • One giant context for everything — splitting by concern prevents unrelated updates from re-rendering the whole tree.

Practice checklist

  1. Convert a drilled theme prop into ThemeContext + provider + useContext.
  2. Remove the intermediate props and confirm the tree still updates everywhere.
  3. Add a second context (current user) and verify theme changes do not re-render user UI.
  4. Write a useTheme hook that throws outside its provider; trigger the error.
  5. Memoise the provider value and check render counts in the DevTools profiler.

Global values solved — now give users multiple pages without full reloads: Routing with React Router.

Quiz: Managing Global State with Context

5 questions · 70% to pass

Frequently Asked Questions

React is a JavaScript library for building user interfaces from reusable components. It manages how your UI updates when data changes, so you describe what the screen should look like and React performs the efficient DOM updates. It is one of the most in-demand frontend skills and powers products from Netflix to Instagram, which makes it an excellent next step after core JavaScript.

You should be comfortable with functions, arrays and objects (map, filter, spread), destructuring, modules and basic async/await. React itself is written in plain JavaScript — JSX is a syntax layer on top. If any of those feel shaky, revisit the JavaScript fundamentals first and this series will go much faster.

React is a library focused only on the UI layer — you choose your own router, data layer and tooling. Angular is a full framework that ships routing, forms, HTTP client and testing conventions as one opinionated package. React's flexibility and huge ecosystem make it the common choice for teams that want to compose their own stack.

A component is a reusable function that returns UI. It receives input through props, keeps private data in state, and can render other components. Pages are composed by nesting small components (navbar, card list, footer) instead of one giant file, which keeps code testable and easy to change.

JSX is a syntax extension that looks like HTML but compiles to React's create() calls — it is a description of UI, not markup the browser parses. Small differences matter: class becomes className, style takes an object, and you embed JavaScript expressions inside curly braces. Once compiled, React turns that description into real DOM nodes.

Props are inputs passed from a parent — they are read-only inside the child, like function arguments. State is private, mutable data owned by a component; updating it triggers a re-render. The rule of thumb: values that come from outside the component are props; values that change over the component's lifetime are state.

Rendering means calling your component function to produce a description of the UI for the current state and props. It does not always mean DOM work — React compares the new description with the previous one (reconciliation) and applies only the differences to the real DOM. This is why updating state 100 times does not mean 100 full page rebuilds.

An effect runs after the component has committed to the screen — after render, not during it. You list dependencies in an array, and React re-runs the effect only when one of them changes. If the effect returns a cleanup function, React runs it before the next re-run and on unmount, which is where you cancel timers, abort fetches and remove listeners.

Use Vite. Create React App is effectively in maintenance mode, while Vite starts instantly (native ES modules) and builds fast, and it is the default choice for new React projects. This series uses Vite for every example — you only need Node.js installed to follow along.

React apps compile to static files (HTML, CSS and JavaScript), so any static host works: Netlify, Vercel, GitHub Pages or Cloudflare Pages. Run the production build, upload the dist folder, and configure a single-page-application fallback so unknown paths load index.html — routing chapters explain why that fallback is required.