In 04-01 you lifted state to the common ancestor and CicloUrbano's catalogue started filtering for real, but the lesson closed by naming the price of that technique: prop drilling. When a piece of data lives up top and gets used down below, it has to cross every component in between, each of which receives it without using it and forwards it without understanding it. With two levels it's a nuisance; with five, every change to a component's signature forces a manual trek through half the application. useContext is React's answer: a direct channel between a component and any of its descendants, with no stops in between. In this lesson you'll see the concrete problem in CicloUrbano, the three pieces of the mechanism (createContext, the provider, and useContext), how React looks for the nearest provider, the professional provider + access-hook pattern applied to the current user and the visual theme, how to nest and override providers, and — very importantly — when context is the wrong tool.

A scope note before we start: here we study the mechanism of context and apply it to two concrete cases. Context as a strategy for managing state across the entire application — the full pattern, when it scales, when it doesn't, performance, and splitting into several contexts — is the subject of 07-02, in the module devoted to state management. Don't exhaust that debate here: first you need to master the tool.

Contents

  1. The problem: the current user crossing four levels
  2. What context is, and what it isn't
  3. The three pieces of the mechanism
  4. createContext and the default value
  5. The provider in React 19
  6. useContext and finding the nearest provider
  7. The professional pattern: provider + access hook
  8. The complete UserContext for CicloUrbano
  9. ThemeContext and switching appearance
  10. Nesting and overriding providers
  11. When to use context, and when not to
  12. The performance warning

  1. The problem: the current user crossing four levels

CicloUrbano needs to show who's signed in, with a dropdown menu, in the header. The user lives in App, and the menu is four levels down:

// src/App.jsx
function App() {
  const [user, setUser] = useState(users[0]);   // usr-01, Ana Ribera
  const [theme, setTheme] = useState('light');

  return (
    <Layout user={user} theme={theme} onThemeChange={setTheme}>
      {/* … */}
    </Layout>
  );
}

// src/components/Layout.jsx — uses NONE of the three props
function Layout({ user, theme, onThemeChange, children }) {
  return (
    <div className={styles.layout}>
      <Header user={user} theme={theme} onThemeChange={onThemeChange} />
      <main className={styles.main}>{children}</main>
      <Footer />
    </div>
  );
}

// src/components/Header.jsx — DOESN'T use them either
function Header({ user, theme, onThemeChange }) {
  return (
    <header className={styles.header}>
      <h1>CicloUrbano</h1>
      <nav>
        <a href="#catalogue">Catalogue</a>
        <a href="#stations">Stations</a>
        <a href="#bookings">My bookings</a>
      </nav>
      <UserMenu user={user} theme={theme} onThemeChange={onThemeChange} />
    </header>
  );
}

// src/components/UserMenu.jsx — finally, here they get used
function UserMenu({ user, theme, onThemeChange }) {
  return (
    <div className={styles.menu}>
      <span>{user.name}</span>
      {user.role === 'operario' && <a href="#workshop">Workshop panel</a>}
      <button type="button" onClick={() => onThemeChange(theme === 'light' ? 'dark' : 'light')}>
        {theme === 'light' ? 'Dark' : 'Light'} theme
      </button>
    </div>
  );
}

Seen as a tree:

flowchart TD
    APP["App<br/><b>state: user, theme</b>"] -->|"user, theme, onThemeChange"| LAY["Layout<br/>❌ doesn't use them"]
    LAY -->|"user, theme, onThemeChange"| HEA["Header<br/>❌ doesn't use them"]
    LAY --> MAIN["main / children"]
    HEA -->|"user, theme, onThemeChange"| MEN["UserMenu<br/>✅ used HERE"]
    HEA --> NAV["nav"]
    style LAY fill:#fde68a
    style HEA fill:#fde68a
    style MEN fill:#dcfce7

The two yellow components are plumbing, not components. And the cost is real:

  • Polluted signatures. Layout declares three props it doesn't care about. Anyone reading its code has to follow the trail to understand what they're for.
  • Cascading changes. If UserMenu needs the locale tomorrow, you have to touch App, Layout, Header, and UserMenu. Four files for one piece of data.
  • Broken reuse. You can't use Layout on a screen without a user without inventing a fake value.
  • Test noise. Testing Header forces you to fabricate a fake user even when the test has nothing to do with that.

  1. What context is, and what it isn't

Context is a mechanism that lets a component make a value available to its entire subtree of descendants, so any of them can read it directly, without receiving it through props.

It's also worth stating what it isn't, since it's frequently misunderstood:

  • It isn't a state store. Context transports a value; whoever keeps it still uses useState or useReducer in some component.
  • It doesn't replace props. Props remain the normal way to pass data. Context is the exception for what's "ambient".
  • It isn't a global channel. It only reaches the provider's descendants. A component outside that branch sees nothing.
  • It doesn't break the one-way data flow. Data still flows from ancestor to descendant; the only thing that disappears is the intermediate stops.

The useful mental image: if props are a parcel passed hand to hand down a line of people, context is a PA system. Whoever's provider is speaking; whoever wants to listen, listens; whoever's outside the room hears nothing.

  1. The three pieces of the mechanism

They're always the same three, in the same order:

flowchart LR
    A["1. createContext(defaultValue)<br/><i>creates the channel</i>"] --> B["2. &lt;Context value={x}&gt;<br/><i>emits the value</i>"]
    B --> C["3. useContext(Context)<br/><i>reads it, at any depth</i>"]
    style A fill:#e0f2fe
    style B fill:#fde68a
    style C fill:#dcfce7
Piece Where it lives What it does
createContext(defaultValue) In a separate module, outside components Creates the context object. Imported wherever it's needed
The provider At the top of the subtree that must see the value Emits the value to all its descendants
useContext(Context) In any descendant component Reads the value from the nearest provider

  1. createContext and the default value

// src/contexts/ThemeContext.js
import { createContext } from 'react';

export const ThemeContext = createContext('light');

createContext gets called exactly once per context, at module scope. Never inside a component: it would be recreated on every render and every consumer would lose the connection.

The argument is the default value, and its rule is counterintuitive: it's only used when a component calls useContext and finds no provider above it. If there's a provider, the default value is irrelevant, even if the provider emits undefined.

You have two strategies, and both are legitimate:

// Strategy A: a USEFUL default value. The component works even without a provider.
export const ThemeContext = createContext('light');

// Strategy B: an IMPOSSIBLE default value. It's there to catch the mistake.
export const UserContext = createContext(null);

Strategy A fits data that has a sensible default (a visual theme, a locale). Strategy B fits data whose absence is always a mounting error (the signed-in user, an API client): you set null and check for it in the access hook, as you'll see in section 7.

  1. The provider in React 19

import { ThemeContext } from './contexts/ThemeContext.js';

<ThemeContext value={theme}>
  {/* everything hanging off here can read the theme */}
</ThemeContext>

In React 19 the context object is used directly as a component. It used to be written <ThemeContext.Provider>, and you'll see that form in practically all existing code and library documentation:

// Older form: still works in React 19, marked as deprecated
<ThemeContext.Provider value={theme}>
  …
</ThemeContext.Provider>
React 18 React 19
Providing a value <Context.Provider value={x}> <Context value={x}>
Consuming with the hook useContext(Context) useContext(Context) (same)
Consuming without the hook <Context.Consumer>{(v) => …}</Context.Consumer> Deprecated: use the hook

The prop is always called value, whatever language the rest of the project is written in: it's part of React's API, just like children or key.

Two more things about the provider:

  • Its reach is its JSX subtree, not its file or module. Anything outside its children doesn't see the value.
  • The value can be anything: a string, an object, a function, or an object holding both data and functions. That's the normal case when the subtree also needs to be able to change the value.

  1. useContext and finding the nearest provider

import { useContext } from 'react';
import { ThemeContext } from '../contexts/ThemeContext.js';

function UserMenu() {
  const theme = useContext(ThemeContext);
  …
}

useContext receives the context object, not the provider and not the value. And it does exactly this: it climbs up the component tree from wherever it's called, looking for the first provider of that same context.

flowchart TD
    APP["App"] --> PROV["&lt;ThemeContext value='dark'&gt;"]
    PROV --> LAY["Layout"]
    LAY --> HEA["Header"]
    HEA --> MEN["UserMenu<br/>useContext(ThemeContext)"]
    MEN -. "searches upward" .-> HEA
    HEA -. " " .-> LAY
    LAY -. "found it!" .-> PROV
    PROV -. "returns 'dark'" .-> MEN
    style PROV fill:#fde68a
    style MEN fill:#dcfce7

Important points about how it behaves:

  • The search goes upward, never sideways or downward. A sibling of the provider sees nothing.
  • The nearest provider wins. If two are nested, the inner one shadows the outer one (section 10).
  • If there's none, the default value from createContext gets returned. This is the dangerous case: there's no notice, no error, no console warning. The component just quietly runs on wrong data.

That last point is the whole reason for the pattern in the next section.

  1. The professional pattern: provider + access hook

Using createContext and useContext on their own scattered across the app has three drawbacks: every component has to import the context, nobody catches a missing provider, and the state logic ends up spread out everywhere. The ecosystem's standard pattern groups everything into one module with three exports: the context (sometimes kept private), a provider component, and an access hook.

// Skeleton of the pattern
const Context = createContext(null);              // 1. the channel (may go unexported)

export function XProvider({ children }) {          // 2. the provider, with the state inside
  const [value, setValue] = useState(initial);
  return <Context value={{ value, setValue }}>{children}</Context>;
}

export function useX() {                            // 3. the access hook, with validation
  const context = useContext(Context);
  if (context === null) {
    throw new Error('useX must be used inside <XProvider>');
  }
  return context;
}

The three payoffs, which more than make up for the extra ten lines:

  • A missing provider is caught immediately, with a message that says exactly what's missing and where. Without this, forgetting the provider produces a Cannot read properties of null ten components away.
  • Consumers never import the context, only the hook. If the context gets split into two for performance tomorrow (07-02), consumers never notice.
  • The state lives next to its provider, not scattered around App.

  1. The complete UserContext for CicloUrbano

// src/contexts/UserContext.jsx
import { createContext, useContext, useState } from 'react';
import { users } from '../data/domain.js';

// null as the default value: the absence of a provider is ALWAYS a bug
const UserContext = createContext(null);

/**
 * Provider for the user currently signed in to CicloUrbano.
 * Props:
 *  - children     (application content)
 *  - initialUser  (User object, optional, defaults to usr-01)
 */
export function UserProvider({ children, initialUser = users[0] }) {
  const [user, setUser] = useState(initialUser);

  function signIn(id) {
    const found = users.find((candidate) => candidate.id === id);
    if (found) setUser(found);
  }

  function signOut() {
    setUser(null);
  }

  const value = {
    user,
    isOperator: user?.role === 'operario',
    signIn,
    signOut
  };

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

/**
 * Access to the current user. Throws if used outside the provider.
 */
export function useUser() {
  const context = useContext(UserContext);
  if (context === null) {
    throw new Error('useUser must be used inside <UserProvider>');
  }
  return context;
}

Design details worth explaining:

  • The file is .jsx, not .js, because it contains JSX. And its name has no accented characters, following the project's convention.
  • UserContext isn't exported. Nobody outside needs it: the provider uses it and the hook reads it. That makes it impossible to skip the validation.
  • isOperator is a derived value computed inside the provider. It avoids repeating user.role === 'operario' in every consumer, with the risk of getting it wrong somewhere.
  • The functions travel inside the value. Context doesn't just carry data: it also carries the way to change it, which is what lets you stop passing onUserChange down through props.

Now UserMenu reads without searching for anything:

// src/components/UserMenu.jsx
import { useUser } from '../contexts/UserContext.jsx';
import styles from './UserMenu.module.css';

function UserMenu() {
  const { user, isOperator, signOut } = useUser();

  if (!user) {
    return <a href="#sign-in" className={styles.signIn}>Sign in</a>;
  }

  return (
    <div className={styles.menu}>
      <span className={styles.name}>{user.name}</span>
      {isOperator && <a href="#workshop">Workshop panel</a>}
      <button type="button" onClick={signOut}>Sign out</button>
    </div>
  );
}

export default UserMenu;

And Layout and Header go back to what they were before prop drilling took over:

// src/components/Layout.jsx — not a single extra prop
function Layout({ children }) {
  return (
    <div className={styles.layout}>
      <Header />
      <main className={styles.main}>{children}</main>
      <Footer />
    </div>
  );
}

The tree, afterward:

flowchart TD
    APP["App"] --> PU["&lt;UserProvider&gt;<br/><b>state: user</b>"]
    PU --> LAY["Layout<br/>✅ no props"]
    LAY --> HEA["Header<br/>✅ no props"]
    HEA --> MEN["UserMenu<br/>useUser()"]
    PU -. "direct channel" .-> MEN
    style PU fill:#fde68a
    style LAY fill:#dcfce7
    style HEA fill:#dcfce7
    style MEN fill:#dcfce7

And here's main.jsx, with the provider above App and the global error boundary from 04-05 above everything:

// src/main.jsx
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import App from './App.jsx';
import ErrorBoundary from './components/ErrorBoundary.jsx';
import { UserProvider } from './contexts/UserContext.jsx';
import { ThemeProvider } from './contexts/ThemeContext.jsx';
import './index.css';

createRoot(document.getElementById('root')).render(
  <StrictMode>
    <ErrorBoundary title="CicloUrbano is unavailable">
      <ThemeProvider>
        <UserProvider>
          <App />
        </UserProvider>
      </ThemeProvider>
    </ErrorBoundary>
  </StrictMode>
);

  1. ThemeContext and switching appearance

The second canonical case. The visual theme gets read by half the application and changed by a single button: the exact definition of "ambient" data.

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

const ThemeContext = createContext(null);

const THEMES = ['light', 'dark'];

/**
 * Provider for CicloUrbano's visual theme.
 * Props:
 *  - children      (content)
 *  - initialTheme  (string, optional, 'light' or 'dark')
 */
export function ThemeProvider({ children, initialTheme = 'light' }) {
  const [theme, setTheme] = useState(() => {
    const saved = localStorage.getItem('ciclourbano:tema');
    return THEMES.includes(saved) ? saved : initialTheme;
  });

  // Syncs the <html> attribute and the browser's storage (05-02)
  useEffect(() => {
    document.documentElement.dataset.theme = theme;
    localStorage.setItem('ciclourbano:tema', theme);
  }, [theme]);

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

  return (
    <ThemeContext value={{ theme, isDark: theme === 'dark', toggleTheme }}>
      {children}
    </ThemeContext>
  );
}

export function useTheme() {
  const context = useContext(ThemeContext);
  if (context === null) {
    throw new Error('useTheme must be used inside <ThemeProvider>');
  }
  return context;
}

The initial state uses lazy initialization (05-01) so it doesn't read localStorage on every render, and the effect synchronizes with two external systems (05-02): the <html> element's data-theme attribute and the browser's storage. index.css's CSS variables respond to that attribute:

/* src/index.css (excerpt) */
:root {
  --color-brand: #12805c;
  --color-bg: #f5f7fa;
  --color-surface: #ffffff;
  --color-text: #1f2933;
  --color-border: #d9e2ec;
}

:root[data-theme='dark'] {
  --color-bg: #111a22;
  --color-surface: #1c2733;
  --color-text: #e6edf3;
  --color-border: #2d3b48;
}

Notice the split of responsibilities: React manages one piece of data ('light' or 'dark') and the CSS does all the visual work through variables. No component needs useTheme just to render differently; it's only needed by whoever has to decide something based on the theme, such as the button that toggles it:

// src/components/ThemeButton.jsx
import { useTheme } from '../contexts/ThemeContext.jsx';

function ThemeButton() {
  const { isDark, toggleTheme } = useTheme();

  return (
    <button type="button" onClick={toggleTheme} aria-pressed={isDark}>
      <span aria-hidden="true">{isDark ? '☀' : '☾'}</span>
      {isDark ? 'Light theme' : 'Dark theme'}
    </button>
  );
}

export default ThemeButton;

The aria-pressed comes from 03-06 and the convention already established in TypeSelector: a button that represents a toggleable state must announce it.

  1. Nesting and overriding providers

The same context can have several providers in different branches, or even nested. The nearest one upward always wins.

function App() {
  return (
    <ThemeProvider initialTheme="light">
      <Layout>
        <CataloguePanel />          {/* reads 'light' */}

        {/* The workshop panel is always shown in dark mode, without touching the global one */}
        <ThemeProvider initialTheme="dark">
          <WorkshopPanel />         {/* reads 'dark' */}
        </ThemeProvider>
      </Layout>
    </ThemeProvider>
  );
}
flowchart TD
    PT1["&lt;ThemeProvider 'light'&gt;"] --> LAY["Layout"]
    LAY --> CAT["CataloguePanel<br/>useTheme() → light"]
    LAY --> PT2["&lt;ThemeProvider 'dark'&gt;"]
    PT2 --> WOR["WorkshopPanel<br/>useTheme() → dark"]
    WOR --> SUB["Subcomponents<br/>useTheme() → dark"]
    style PT1 fill:#e0f2fe
    style PT2 fill:#334155,color:#ffffff

Cases where this genuinely comes in handy: a preview that must display with the opposite theme, a section with a different locale, or — very common — testing (Module 9), where you wrap the component under test in a provider with controlled values.

When there are several different contexts, you just nest them. If the staircase gets awkward, a component that groups every provider fixes it:

// src/contexts/Providers.jsx
export function Providers({ children }) {
  return (
    <ThemeProvider>
      <UserProvider>
        <BookingsProvider>{children}</BookingsProvider>
      </UserProvider>
    </ThemeProvider>
  );
}

  1. When to use context, and when not to

Context has a cost that doesn't show up in the code: it turns an explicit dependency into an implicit one. Reading <UserMenu /> no longer tells you where its data comes from; you have to open the file. In a component used in only one place, that's worse than a prop.

Situation Right tool Why
A direct child needs data from its parent Props Explicit, traceable, no ceremony
Two siblings share a piece of data Lift state up (04-01) Their common ancestor is one step away
The data only crosses one or two levels Props Context doesn't pay for itself
An intermediate component only passes the data because there's no other way Composition with children (04-02) Usually removes the drilling without context
Many components across the whole tree read the data, and few change it Context This is exactly its use case
The data is "ambient": user, theme, locale, permissions, currency format Context Ambient = read everywhere
Server state with caching and revalidation Dedicated libraries (07-06) Context doesn't cache or revalidate

The fourth row deserves an example, because plenty of people reach for context when composition would have been enough. This Layout receives user only to hand it to the header:

// Drilling caused by missing composition
<Layout user={user}>
  <CataloguePanel />
</Layout>

With a named slot (04-02), the data stops crossing anything at all:

// src/components/Layout.jsx
function Layout({ header, children }) {
  return (
    <div className={styles.layout}>
      {header}
      <main className={styles.main}>{children}</main>
      <Footer />
    </div>
  );
}

// Usage: App builds the header with the user and hands it over already assembled
<Layout header={<Header user={user} />}>
  <CataloguePanel />
</Layout>

Layout no longer knows anything about the user: it receives an already-built element. Before setting up a context, check whether composition solves the case; it's simpler and keeps dependencies visible.

The rule that sums up this section: context is for ambient data that many read and few change. When the data is specific to one concrete interaction, props remain the answer.

  1. The performance warning

One sentence, and we'll expand on it in its own place later: when a context's value changes, every component that consumes it re-renders, even if it only uses part of the value and that part didn't change. With a theme that gets toggled twice a day, this is irrelevant; with a value that changes on every keystroke, it matters.

The techniques for managing it — splitting a context into several by how often each part changes, separating data from the functions that modify it, and memoizing the provider's value — belong to 07-02 and to Module 8. For now, just hold on to the idea that the cost exists, and get in the habit of not lumping things that change at very different rates into the same context.

Common Mistakes and Tips

  • Calling createContext inside a component. A new context gets created on every render, and consumers stop finding the provider. It always belongs at module scope.
  • Forgetting the provider. Without the pattern from section 7, there's no error: the component gets the default value and silently misbehaves. With the access hook that throws, the failure shows up on the very first render with a clear message.
  • Believing the default value is used when the provider emits undefined. It isn't: it's only used if there's no provider anywhere in the chain.
  • Providing from the wrong component. The provider has to sit above every consumer. Put it inside Header, and the catalogue won't see it.
  • Cramming the whole application's state into a single context. Any change re-renders every consumer. One context per concern.
  • Using context to pass data to a direct child. That's over-engineering a prop. Context starts paying off at three levels or more, and only when there are several consumers.
  • Exporting both the context and the hook without a reason. Exporting only the provider and the hook stops anyone from bypassing the validation.
  • Tip: name the hook use + the noun (useUser, useTheme). Beyond the convention, it's required for the linter's hook rules to recognize it as a hook (04-04).
  • Tip: in tests, wrap the component in its provider with fixed values. If that feels hard, it's usually a sign the context has too many responsibilities.
  • Tip: put the derived values several consumers would need (isOperator) in the context's value, not just the raw data. It avoids repeating the same condition everywhere.

Exercises

Exercise 1. This CicloUrbano code fails at runtime with "Cannot destructure property 'user' of null". Find the two causes and fix them.

// src/App.jsx
import { UserProvider, useUser } from './contexts/UserContext.jsx';

function App() {
  const { user } = useUser();

  return (
    <UserProvider>
      <Layout>
        <p>Welcome, {user.name}</p>
        <CataloguePanel />
      </Layout>
    </UserProvider>
  );
}

Exercise 2. Create NoticesContext with the provider + access hook pattern. It must let any component in the tree show a global notice without receiving props: the provider keeps a list of notices { id, tone, text } (the tones are Notice's: info, success, warning, error), exposes showNotice(tone, text) and dismissNotice(id), and renders the notice stack above children. Also show how BookingForm would use it when creating a booking.

Exercise 3. BikeCard should show a "Send to workshop" button only if the current user is an operator (usr-02, Marc Solé). Today it receives user as a prop from App, crossing through BikeList. Rewrite it using useUser, and explain which props disappear from each component in the chain.

Solutions

Solution 1.

The two causes:

  1. App calls useUser(), but App itself is the one rendering <UserProvider>. A component can't consume a context that it provides itself: useContext's search goes upward, and the provider sits below the line where the hook is called. useContext returns null (the default value), and the destructuring blows up.
  2. user.name is read without checking that a user exists. The provider allows signOut(), which sets user to null; that <p> would fail the moment someone signed out.

The fix moves the provider above App (in main.jsx) and extracts the greeting into a descendant component:

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

// src/App.jsx — no longer calls the hook: it just composes
function App() {
  return (
    <Layout>
      <Welcome />
      <CataloguePanel />
    </Layout>
  );
}

// src/components/Welcome.jsx — a descendant of the provider: this is where it belongs
import { useUser } from '../contexts/UserContext.jsx';

function Welcome() {
  const { user } = useUser();

  if (!user) return <p>Welcome to CicloUrbano. Sign in to book a bike.</p>;
  return <p>Welcome, {user.name}</p>;
}

Solution 2.

// src/contexts/NoticesContext.jsx
import { createContext, useContext, useState } from 'react';
import Notice from '../components/Notice.jsx';
import styles from './NoticesContext.module.css';

const NoticesContext = createContext(null);

/**
 * Provider for CicloUrbano's global notice stack.
 * Props:
 *  - children (application content)
 */
export function NoticesProvider({ children }) {
  const [notices, setNotices] = useState([]);

  function showNotice(tone, text) {
    const id = `ntc-${crypto.randomUUID().slice(0, 8)}`;
    setNotices((prev) => [...prev, { id, tone, text }]);
    return id;
  }

  function dismissNotice(id) {
    setNotices((prev) => prev.filter((notice) => notice.id !== id));
  }

  return (
    <NoticesContext value={{ notices, showNotice, dismissNotice }}>
      <div className={styles.stack} role="status" aria-live="polite">
        {notices.map((notice) => (
          <Notice key={notice.id} tone={notice.tone}>
            {notice.text}
            <button type="button" onClick={() => dismissNotice(notice.id)}>
              Dismiss
            </button>
          </Notice>
        ))}
      </div>
      {children}
    </NoticesContext>
  );
}

export function useNotices() {
  const context = useContext(NoticesContext);
  if (context === null) {
    throw new Error('useNotices must be used inside <NoticesProvider>');
  }
  return context;
}

Used from BookingForm, without a single new prop added to the chain:

// src/components/BookingForm.jsx (excerpt)
import { useNotices } from '../contexts/NoticesContext.jsx';

function BookingForm({ onCreateBooking, userId = 'usr-01' }) {
  const { showNotice } = useNotices();

  function handleSubmit(event) {
    event.preventDefault();
    if (Object.keys(errors).length > 0) {
      showNotice('error', 'Check the highlighted fields before continuing.');
      return;
    }
    const booking = {
      id: `res-${crypto.randomUUID().slice(0, 8)}`,
      bicicletaId: data.bicicletaId,
      user: userId,
      startDate: data.startDate,
      hours: data.hours,
      status: 'activa'
    };
    onCreateBooking(booking);
    showNotice('success', `Booking ${booking.id} created for ${booking.hours} hours.`);
    setData(INITIAL_DATA);
  }
  …
}

This is context's ideal use case: notices are ambient (any component can raise one), they render in a single place, and nobody needs to know how they get there. Without context, showNotice would have to be passed as a prop from App down to every form and panel in the application. Also notice the role="status" with aria-live="polite" from 03-06: notices appear without moving focus, so they need to be announced.

Solution 3.

// src/components/BikeCard.jsx
import { useUser } from '../contexts/UserContext.jsx';
import StatusBadge from './StatusBadge.jsx';
import styles from './BikeCard.module.css';

/**
 * Props:
 *  - bike            (Bike object, required)
 *  - stationName     (string, optional)
 *  - onSelect        (function, optional)
 *  - onBook          (function, optional)
 *  - onSendToWorkshop (function, optional): only used if the user is an operator
 */
function BikeCard({ bike, stationName, onSelect, onBook, onSendToWorkshop }) {
  const { isOperator } = useUser();
  const formattedPrice = bike.pricePerHour.toFixed(2);

  return (
    <article className={styles.card}>
      <h3>{bike.model}</h3>
      <StatusBadge status={bike.status} />
      <p>{stationName} · €{formattedPrice} / hour</p>

      <button type="button" onClick={() => onSelect?.(bike)}>View details</button>
      <button
        type="button"
        onClick={() => onBook?.(bike)}
        disabled={bike.status !== 'disponible'}
      >
        Book
      </button>

      {isOperator && bike.status !== 'mantenimiento' && (
        <button type="button" onClick={() => onSendToWorkshop?.(bike)}>
          Send to workshop
        </button>
      )}
    </article>
  );
}

export default BikeCard;

Props that disappear from the chain:

Component Before After
App Passed user down to BikeList Nothing: the provider lives in main.jsx
BikeList Received user and forwarded it without using it Back to its real contract: bikes, stations, onSelect, onBook
BikeCard Received user as a prop Reads it with useUser()

Notice what hasn't changed: onSendToWorkshop is still a normal prop. The user is ambient data, but "what to do when this specific button on this specific card gets pressed" is the parent's call, and that's props territory. Mixing the two up — stuffing everything into context because it's convenient — is the mistake that turns an application into a tangle.

Conclusion

The prop drilling that 04-01 left hanging now has a name and a fix. Context is a direct channel between an ancestor and all of its descendants, always built from the same three pieces: createContext(defaultValue) in a separate module, a provider that in React 19 is written <Context value={…}> — with <Context.Provider> still working in code you inherit — and useContext(Context), which climbs the tree to the nearest provider and, if it finds none, silently returns the default value. That's why the professional pattern wraps all three pieces in a module with provider + access hook that throws a clear error when the provider is missing: that's exactly how you built UserContext and ThemeContext, with Layout and Header getting their clean signatures back. You know how to nest providers to override the value in one branch, and — most importantly — you know when not to use it: for direct children, props; for siblings, lifting state up; for plumbing, composition with children. Context is for what's ambient: user, theme, locale, permissions, notices.

One front is still open. UserProvider managed a single simple value, but CicloUrbano's booking state isn't simple: there's a list of bookings, a draft in progress, a submission phase, and a possible error, and all those pieces change together and by rules. With useState you'd end up with five loose variables and handlers that touch three at once — exactly the situation 05-01 flagged as the limit of that tool. React offers an alternative that gathers every transition into a single pure function, easy to read, to test, and to reason about. The next lesson is The useReducer Hook.

React Course

Module 1: Getting Started with React

Module 2: React Components

Module 3: Working with Events

Module 4: Advanced Component Concepts

Module 5: React Hooks

Module 6: Routing in React

Module 7: State Management

Module 8: Performance Optimization

Module 9: Testing React Applications

Module 10: Advanced Topics

Module 11: Project: Building a Complete Application

© Copyright 2026. All rights reserved