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
- The problem: the current user crossing four levels
- What context is, and what it isn't
- The three pieces of the mechanism
createContextand the default value- The provider in React 19
useContextand finding the nearest provider- The professional pattern: provider + access hook
- The complete
UserContextfor CicloUrbano ThemeContextand switching appearance- Nesting and overriding providers
- When to use context, and when not to
- The performance warning
- 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.
Layoutdeclares 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
UserMenuneeds the locale tomorrow, you have to touchApp,Layout,Header, andUserMenu. Four files for one piece of data. - Broken reuse. You can't use
Layouton a screen without a user without inventing a fake value. - Test noise. Testing
Headerforces you to fabricate a fakeusereven when the test has nothing to do with that.
- 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
useStateoruseReducerin 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.
- 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. <Context value={x}><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 |
createContext and the default value
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.
- 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
childrendoesn'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.
useContext and finding the nearest provider
useContext and finding the nearest providerimport { 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["<ThemeContext value='dark'>"]
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
createContextgets 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.
- 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 nullten 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.
- The complete
UserContext for CicloUrbano
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. UserContextisn't exported. Nobody outside needs it: the provider uses it and the hook reads it. That makes it impossible to skip the validation.isOperatoris a derived value computed inside the provider. It avoids repeatinguser.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
onUserChangedown 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["<UserProvider><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>
);
ThemeContext and switching appearance
ThemeContext and switching appearanceThe 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.
- 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["<ThemeProvider 'light'>"] --> LAY["Layout"]
LAY --> CAT["CataloguePanel<br/>useTheme() → light"]
LAY --> PT2["<ThemeProvider 'dark'>"]
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>
);
}
- 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:
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.
- 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
createContextinside 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:
AppcallsuseUser(), butAppitself 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.useContextreturnsnull(the default value), and the destructuring blows up.user.nameis read without checking that a user exists. The provider allowssignOut(), which setsusertonull; 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
- What Is React?
- Setting Up the Development Environment
- Hello World in React
- JSX: A JavaScript Syntax Extension
- How React Renders: Virtual DOM and Reconciliation
Module 2: React Components
- Understanding Components
- Function vs Class Components
- Props: Passing Data to Components
- State: Managing Component State
- Styling Components: CSS, Modules and Utilities
Module 3: Working with Events
- Handling Events in React
- Conditional Rendering
- Lists and Keys
- Forms and Controlled Components
- Form Validation and Uncontrolled Components
- Accessibility in Interactive Components
Module 4: Advanced Component Concepts
- Lifting State Up
- Composition vs Inheritance
- React Lifecycle Methods
- Hooks: Introduction and Basic Use
- Error Boundaries: Catching Failures in the UI
Module 5: React Hooks
- The useState Hook
- The useEffect Hook
- The useRef Hook and DOM Access
- The useContext Hook
- The useReducer Hook
- Custom Hooks
Module 6: Routing in React
- Introducing React Router
- Setting Up React Router
- Nested Routes
- Programmatic Navigation
- Protected Routes and Access Control
Module 7: State Management
- Introduction to State Management
- The Context API
- Redux: Introduction and Setup
- Redux: Actions and Reducers
- Redux: Connecting to React
- Server State: Fetching, Caching and Syncing
Module 8: Performance Optimization
- Performance Optimization Techniques in React
- Memoization with React.memo
- The useMemo and useCallback Hooks
- Code Splitting and Lazy Loading
- Measuring Performance with React DevTools Profiler
Module 9: Testing React Applications
- Introduction to Testing
- Unit Testing with Jest
- Component Testing with React Testing Library
- Testing Asynchronous Code and Mocking APIs
- End-to-End Testing with Cypress
Module 10: Advanced Topics
- Server-Side Rendering (SSR) with Next.js
- Static Site Generation (SSG) with Next.js
- Suspense and React Server Components
- TypeScript with React
- React Native: Building Mobile Apps
