Passing data deeply with context
Stop passing props through every level: create a context, provide it from a parent, read it with useContext or use, and learn when props are still better.
- 01Replace prop drilling with contextCreate a context, provide a value with React 19's
<ThemeContext value>, and read it withuseContextoruse. - 02Predict what a component readsFind the nearest provider above a component, override a value for part of the tree, and know when the default is used.
- 03Know the cost and the alternativesExplain why every reader renders again when the value changes, and choose props or
childrenwhen context isn't needed.
A theme that every level has to pass along
A shop lets you pick light or dark buttons. The choice lives in App, at the top, but the buttons sit four components down. With props, the only way to get it there is to hand it from each component to the next. Tick Dark buttons in the result:
import { useState } from "react"; export default function App() { const [theme, setTheme] = useState("light"); return ( <> <label> <input type="checkbox" checked={theme === "dark"} onChange={(e) => setTheme(e.target.checked ? "dark" : "light")} /> Dark buttons </label> <ShopPage theme={theme} /> </> );} function ShopPage({ theme }) { return ( <main> <h3>Tea shop</h3> <ProductList theme={theme} /> </main> );} function ProductList({ theme }) { return ( <ul> <ProductCard name="Green tea" theme={theme} /> <ProductCard name="Chai" theme={theme} /> </ul> );} function ProductCard({ name, theme }) { return ( <li> {name} <AddButton theme={theme} /> </li> );} function AddButton({ theme }) { return <button className={theme}>Add to cart</button>;}Line 4 keeps theme in App's state, and lines 9 and 10 switch it with the checkbox. Line 13 passes it to ShopPage, which passes it to ProductList on line 22, which passes it to each ProductCard on lines 30 and 31, which passes it to AddButton on line 39. Only line 45 uses it, to pick the button's className; the styles are in the styles.css tab.
It works, but three of the five components take a theme prop they never use, only to pass it on. This is called prop drilling. React has a way to skip the middle: context. A component high in the tree provides a value, and any component below it, however deep, reads it directly.
theme travels through ShopPage, ProductList, and ProductCard, which only pass it on. With context, App provides it once, and AddButton reads it directly.To get the internet to a laptop in the back bedroom, you could run a cable from the router through the hall, the kitchen, and the living room. Or the router can send the signal through the air, and the laptop joins it from wherever it is. The rooms in between don't carry anything.
- In real life: Running a cable through every room to reach one laptop
- In React: Passing
themethrough every component to reachAddButton - In real life: A router that sends the signal through the house
- In React: A provider:
<ThemeContext value={theme}> - In real life: Any device in range connects, in any room
- In React: Any component below the provider calls
useContext(ThemeContext) - In real life: A device joins the nearest router
- In React: A component reads the nearest provider above it
- In real life: Outside the house, a device falls back to its own data plan
- In React: With no provider above, a component gets the default value
Context lets a component provide a value to every component below it in the tree, without passing props through each level. You create a context with createContext, provide a value with <ThemeContext value={…}>, and read it with useContext(ThemeContext) or use(ThemeContext).
The problem with passing props through every level
Passing props is React's normal way to move data down, and most of the time it's the right way: you can read any component and see exactly what it gets. Drilling becomes a problem when a value has to go deep, or to many places, through components that don't care about it. In the shop above:
ShopPage,ProductList, andProductCardeach listthemein their props (lines 18, 27, and 36), and each hands it on. If the header later needs the theme too, every component on the way to it needs a new prop.- Renaming
thememeans editing all five components, even though only one of them uses it. - Moving
AddButtonsomewhere else in the tree means building a new path of props to its new place.
Drilling often grows out of lifting state up: once state moves to a common parent near the top, every component that needs it is far away. How to read the table: each row is one change to the shop, and what it costs with props and with context.
| When you… | With props passed through every level | With context |
|---|---|---|
| Add a new component deep down that needs the theme | Add a theme prop to every component on the way to it. | Call useContext(ThemeContext) in the new component. |
Rename theme | Edit every component that passes it, even ones that never use it. | Edit the provider and the components that read it. |
Move AddButton to another part of the page | Build a new path of props to its new place. | Nothing, as long as it's still below the provider. |
| Read a component to see where its data comes from | Its props say so: follow them up to the parent. | Find the nearest provider above it, which may be far away. |
The next three steps replace the drilled prop with context: create it, read it, and provide it.
Step 1: create a context
A context is made once, outside any component, with createContext. Most apps put it in its own file, so the component that provides it and the components that read it can all import it. This file is the ThemeContext.js tab in every example below; it only runs as part of them:
import { createContext } from "react";
export const ThemeContext = createContext("light");Line 1 imports createContext from React. Line 3 calls it and exports what it returns as ThemeContext. Two things to know about that line:
- The context object,
ThemeContext, doesn't hold the current theme. It's a name both sides agree on: a provider says "here is the value forThemeContext", and a reader asks "what isThemeContexthere?". - The argument,
"light", is the default value. A component gets it only when there is no provider forThemeContextanywhere above it. It never changes. Usenullwhen there's no sensible default, like the signed-in user.
Step 2: read it with useContext or use
A component reads a context by passing the context object to useContext. Here a button reads the theme, and nothing provides one yet:
import { useContext } from "react";import { ThemeContext } from "./ThemeContext.js"; export default function App() { return ( <ul> <li> Green tea <AddButton /> </li> </ul> );} function AddButton() { const theme = useContext(ThemeContext); return <button className={theme}>Add to cart ({theme})</button>;}Line 2 imports ThemeContext from its file. Line 15 calls useContext(ThemeContext) and names the result theme. Line 16 uses it for the class and the label. The button says Add to cart (light): React looked up the tree for a ThemeContext provider, found none, and returned the default from createContext("light"). There is no error and no warning, so a forgotten provider can go unnoticed; Exercise 2 has one.
useContext is a hook, so it follows the rules from State: call it at the top level of the component, not inside an if or after an early return. React 19 added a second way to read a context, use, which doesn't have that limit. Here, a sold-out product returns early, and only an available one reads the theme:
import { use } from "react";import { ThemeContext } from "./ThemeContext.js"; export default function App() { return ( <ThemeContext value="dark"> <ul> <li>Green tea <AddButton soldOut={false} /></li> <li>Chai <AddButton soldOut={true} /></li> </ul> </ThemeContext> );} function AddButton({ soldOut }) { if (soldOut) { return <span>Sold out</span>; } const theme = use(ThemeContext); return <button className={theme}>Add to cart ({theme})</button>;}Lines 16 to 18 return Sold out for Chai. Line 19 runs only for Green tea, and calls use(ThemeContext) after that early return, which useContext isn't allowed to do. Line 6 provides "dark", so Green tea's button says Add to cart (dark). Step 3 explains that line.
How to read the table: each row is one question, with the answer for each way of reading a context.
| Question | useContext(ThemeContext) | use(ThemeContext) |
|---|---|---|
| Where can you call it? | Only at the top level of a component or hook. | Anywhere in a component or hook, even inside an if or after an early return, but not inside try and catch. |
| What does it return for a context? | The nearest provider's value, or the default. | The same: the nearest provider's value, or the default. |
| Which React versions have it? | Every version with hooks, so most code you read uses it. | React 19 and later. |
| What else can it read? | Only a context. | A Promise too, which The use API lesson covers. |
For a context, both return the same value: the nearest provider's, or the default. use can also read a Promise, which The use API lesson covers. This lesson uses useContext where a component reads at the top level, as most code you'll read does, and use where the read comes after a condition.
Step 3: provide a value from a parent
To give the components below a value, render the context object itself as a component, with a value prop, around them. That's a provider. Here is the overview's shop with context. Tick Dark buttons: both buttons change, and no component in the middle mentions the theme.
import { useContext, useState } from "react";import { ThemeContext } from "./ThemeContext.js"; export default function App() { const [theme, setTheme] = useState("light"); return ( <ThemeContext value={theme}> <label> <input type="checkbox" checked={theme === "dark"} onChange={(e) => setTheme(e.target.checked ? "dark" : "light")} /> Dark buttons </label> <ShopPage /> </ThemeContext> );} function ShopPage() { return ( <main> <h3>Tea shop</h3> <ProductList /> </main> );} function ProductList() { return ( <ul> <ProductCard name="Green tea" /> <ProductCard name="Chai" /> </ul> );} function ProductCard({ name }) { return ( <li> {name} <AddButton /> </li> );} function AddButton() { const theme = useContext(ThemeContext); return <button className={theme}>Add to cart</button>;}Line 8 opens <ThemeContext value={theme}> and line 15 closes it. Everything between them, and everything those components render, however deep, can read theme. ShopPage, ProductList, and ProductCard (lines 19 to 43) no longer take or pass a theme prop. AddButton reads it on line 46.
When you tick the box, setTheme gives App a new theme, and line 8 provides the new value. React renders again every component that reads ThemeContext, so both buttons turn dark. A context isn't only for values that never change: put state in the value, and the readers follow it.
Providers in code written before React 19
Rendering <ThemeContext> directly as a provider is new in React 19. Before that, and in much of the code you'll read today, a provider is written with .Provider:
<ThemeContext.Provider value={theme}>
<ShopPage />
</ThemeContext.Provider>It means exactly the same thing, and still works in React 19. The React team has said it will deprecate .Provider in a future version, so write new providers as <ThemeContext value={…}>.
The nearest provider wins
A provider can sit inside another provider of the same context. A component reads the nearest one above it. That lets you override a value for one part of the page. This page is light, but its promo box is always dark. Press Switch page theme a few times:
import { useContext, useState } from "react";import { ThemeContext } from "./ThemeContext.js"; export default function App() { const [theme, setTheme] = useState("light"); return ( <ThemeContext value={theme}> <button onClick={() => setTheme(theme === "light" ? "dark" : "light")}> Switch page theme </button> <p> Green tea <AddButton /> </p> <section className="promo"> <ThemeContext value="dark"> <p> Today only: Chai <AddButton /> </p> </ThemeContext> </section> </ThemeContext> );} function AddButton() { const theme = useContext(ThemeContext); return <button className={theme}>Add to cart ({theme})</button>;}Line 8 provides the page's theme to everything inside it. Lines 16 to 20 put a second provider, with the value "dark", around the promo. The Green tea button on line 13 has one provider above it, and follows the switch: light, then dark. The Chai button on line 18 has two, and reads the nearer one on line 16, so it says dark whatever the page theme is.
AddButton reads the closest ThemeContext above it. The Green tea button finds the page's light; the Chai button finds the promo's dark first, so the page's value never reaches it.Try it with the providers switched on and off. The playground renders both buttons with real context. Choose a value for each provider, or none to take it out, and read what each button gets and from where.
const ThemeContext = createContext("light");
function Page() {
return (
<ThemeContext value="light">
<header>
Green tea <AddButton />
</header>
<section>
<ThemeContext value="dark">
Chai <AddButton />
</ThemeContext>
</section>
</ThemeContext>
);
}useContext(ThemeContext)The Green tea button reads light, from the page provider. The Chai button has two providers above it, and the nearest one, the promo provider, wins: dark.
Take the promo provider out, and Chai reads the page's value like Green tea. Take both out, and both buttons fall back to the default, "light". Each context is separate, too: a provider of ThemeContext never hides the value of another context, such as a UserContext.
Every reader renders again when the value changes
When a provider's value changes, React renders again every component below it that reads that context. To see it, each component in this shop shows how many times it has rendered. The counter comes from the RenderCount.jsx tab; you don't need to read it to use it. Press Switch theme twice:
import { createContext, useContext, useState } from "react";import RenderCount from "./RenderCount.jsx"; const ThemeContext = createContext("light"); function ThemeSwitch({ children }) { const [theme, setTheme] = useState("light"); return ( <ThemeContext value={theme}> <button onClick={() => setTheme(theme === "light" ? "dark" : "light")}> Switch theme </button> {children} </ThemeContext> );} function Header() { return <h3>Tea shop <RenderCount /></h3>;} function ThemeLabel() { const theme = useContext(ThemeContext); return <p>Theme: {theme} <RenderCount /></p>;} function ProductList() { return ( <section> <p>Products <RenderCount /></p> <AddButton /> </section> );} function AddButton() { const theme = useContext(ThemeContext); return <button className={theme}>Add to cart <RenderCount /></button>;} export default function App() { return ( <ThemeSwitch> <Header /> <ThemeLabel /> <ProductList /> </ThemeSwitch> );}ThemeSwitch (lines 6 to 17) keeps the theme in state and provides it on line 10. ThemeLabel (line 24) and AddButton (line 38) read it. Header and ProductList don't. After two clicks, Theme: light and Add to cart say renders: 3, while Tea shop and Products still say renders: 1.
ThemeSwitch's theme changes, React renders again every component that reads ThemeContext: ThemeLabel and AddButton. Header and ProductList don't read it, and React doesn't call them again.Why do Header and ProductList stay at 1? App made them, on lines 45 to 47, and passed them to ThemeSwitch as children. When the theme changes, only ThemeSwitch renders again; its children are the same as last time, so React doesn't call them. But it does call every component that reads the context, even AddButton, deep inside ProductList, which didn't render.
React decides whether the value changed by comparing the old and new value with Object.is. A string like "dark" compares by its text. An object compares by identity: if a provider gets a new object, like value={{ theme, user }} written in the component, every reader renders again, even one that only uses user and even when nothing in it changed.
For most apps, this costs nothing you would notice: a theme or a signed-in user changes rarely. When it does matter, there are tools for it, like splitting one big context into several, which the next lesson does, and reading the context lower down, which When React re-renders covers. Press Edit & run and add useContext(ThemeContext); as the first line of Header: it now reads the context, and its counter follows the clicks.
Before you reach for context
Context is easy to overuse. A value passed through two or three components is often clearer as props, because each component shows what it gets. Before you add a context, try these two things first:
- Pass props. Many props through several components is normal, and the data flow stays easy to follow.
- Pass JSX as
children. If components in the middle only place the data on the page, let them takechildren, and give the data to the component that needs it directly.
Here, a list of teas passes through a Layout and a Main that only pass it on:
const teas = ["Green tea", "Chai", "Mint tea"]; export default function App() { return <Layout teas={teas} />;} function Layout({ teas }) { return ( <div className="layout"> <h3>Tea shop</h3> <Main teas={teas} /> </div> );} function Main({ teas }) { return ( <main> <TeaList teas={teas} /> </main> );} function TeaList({ teas }) { return <p>{teas.length} teas: {teas.join(", ")}</p>;}Line 4 hands teas to Layout, line 11 hands it to Main, and line 19 to TeaList, the only component that uses it. Now the same page with children:
const teas = ["Green tea", "Chai", "Mint tea"]; export default function App() { return ( <Layout> <Main> <TeaList teas={teas} /> </Main> </Layout> );} function Layout({ children }) { return ( <div className="layout"> <h3>Tea shop</h3> {children} </div> );} function Main({ children }) { return <main>{children}</main>;} function TeaList({ teas }) { return <p>{teas.length} teas: {teas.join(", ")}</p>;}Lines 5 to 9 put TeaList inside Main inside Layout, right in App, and give it teas on line 7. Layout and Main show whatever children they get (lines 17 and 23) and know nothing about teas. The result is the same: 3 teas: Green tea, Chai, Mint tea.
teas passes through Layout and Main. Right, App puts TeaList inside them as children and gives it teas itself: no context, and no layers in between.Context is the right tool when many components in different parts of the tree need the same value. Common cases:
- A theme, or other display settings like a language, that buttons and cards all over the page read.
- The signed-in user, read by the header, the cart, and a review form.
- The current page address: routing libraries keep it in a context, so every link can tell whether it's the active one.
- State managed near the top that far-away components need to read and change. The next lesson combines a reducer with context for this.
A shop with a theme and a signed-in user
Real apps often have a few contexts side by side. This shop has two: the theme, and the signed-in user. Press Sign in, then Switch theme, then Sign out.
import { use, useState } from "react";import { ThemeContext, UserContext } from "./contexts.js"; export default function App() { const [theme, setTheme] = useState("light"); const [user, setUser] = useState(null); return ( <ThemeContext value={theme}> <UserContext value={user}> <Header onSwitchTheme={() => setTheme(theme === "light" ? "dark" : "light")} onSignIn={() => setUser({ name: "Asha" })} onSignOut={() => setUser(null)} /> <p> Green tea <AddButton /> </p> <ThemeContext value="dark"> <footer> Need help? <AddButton label="Chat with us" /> </footer> </ThemeContext> </UserContext> </ThemeContext> );} function Header({ onSwitchTheme, onSignIn, onSignOut }) { const user = use(UserContext); return ( <header> <button onClick={onSwitchTheme}>Switch theme</button>{" "} {user ? ( <> Hi, {user.name} <button onClick={onSignOut}>Sign out</button> </> ) : ( <button onClick={onSignIn}>Sign in</button> )} </header> );} function AddButton({ label = "Add to cart" }) { const theme = use(ThemeContext); const user = use(UserContext); return ( <button className={theme} disabled={!user}> {user ? label : "Sign in to " + label.toLowerCase()} </button> );}The contexts.js tab creates both: ThemeContext with the default "light", and UserContext with null, for nobody signed in. App keeps both in state (lines 5 and 6) and provides them on lines 9 and 10. The header's buttons call functions App passes as props on lines 12 to 14, because App owns the state; the next lesson shows how to change state through context instead.
Headerreads the user on line 30 and showsHi, Ashawith a Sign out button, or a Sign in button.AddButtonreads both contexts on lines 47 and 48. While nobody is signed in, it's turned off and saysSign in to add to cart.- Lines 19 to 23 override the theme for the footer, so its button stays dark while the page switches. The footer's button still reads the user from line 10: overriding one context leaves the other alone.
- The light or dark theme every button and card reads
- The signed-in user, read by the header, the cart, and the reviews
- A product's price, passed from ProductCard to its PriceTag
- The language the whole page is shown in
- An onAdd handler that a card passes to its own button
- The current page address, read by every link to mark the active one
- The search text, shared by the search box and the list beside it
- A list that passes through a Layout that only places it on the page
Sort each piece of data: is it a good fit for context, or should it travel as props or children?
Common mix-ups
- "The context object holds the value." No.
ThemeContextis only a name both sides share. The value comes from the nearest provider, or the default. - "Forgetting the provider causes an error." No. The reader quietly gets the default value, like the button that said
lightwith no provider. - "A component can read the value it provides." No.
useContextlooks for providers above the component. A provider in the same component's output only reaches its children. - "Context replaces props." No. Props are still the normal way to pass data. Context is for values many components far apart need.
- "Only components that use part of the value render again." No. When the value changes, every component that reads that context renders again.
- "You need
.Providerto provide a context." Not in React 19. Render<ThemeContext value={…}>directly;.Provideris the older form.
Practice exercises
Each exercise shows its code's result under it, or when you press Edit & run, and updates as you type. Guess first, then use the hints.
Read the code without running it. What does the Footer line show? Then press Edit & run to check.
import { createContext, useContext } from "react";
const ThemeContext = createContext("light");
export default function App() {
return (
<>
<Label place="Header" />
<ThemeContext value="dark">
<Label place="Page" />
<ThemeContext value="light">
<Label place="Banner" />
</ThemeContext>
<Label place="Footer" />
</ThemeContext>
</>
);
}
function Label({ place }) {
const theme = useContext(ThemeContext);
return <p>{place}: {theme}</p>;
}import { createContext, useContext } from "react"; const ThemeContext = createContext("light"); export default function App() { return ( <> <Label place="Header" /> <ThemeContext value="dark"> <Label place="Page" /> <ThemeContext value="light"> <Label place="Banner" /> </ThemeContext> <Label place="Footer" /> </ThemeContext> </> );} function Label({ place }) { const theme = useContext(ThemeContext); return <p>{place}: {theme}</p>;}Header has no provider above it, so it gets the default, light. Page is inside the dark provider. Banner is inside the light provider nested in it. Footer comes after that inner provider closes, so its nearest provider is the dark one: Footer: dark.
App starts with a dark theme, but the button says light, and Switch theme does nothing. Fix it. What does the button say before you click anything?
import { createContext, useContext, useState } from "react"; const ThemeContext = createContext("light"); export default function App() { const [theme, setTheme] = useState("dark"); return ( <> <button onClick={() => setTheme(theme === "light" ? "dark" : "light")}> Switch theme </button> <Toolbar /> </> );} function Toolbar() { return ( <p> <ThemeButton /> </p> );} function ThemeButton() { const theme = useContext(ThemeContext); return <button className={theme}>Theme: {theme}</button>;}import { createContext, useContext, useState } from "react"; const ThemeContext = createContext("light"); export default function App() { const [theme, setTheme] = useState("dark"); return ( <ThemeContext value={theme}> <button onClick={() => setTheme(theme === "light" ? "dark" : "light")}> Switch theme </button> <Toolbar /> </ThemeContext> );} function Toolbar() { return ( <p> <ThemeButton /> </p> );} function ThemeButton() { const theme = useContext(ThemeContext); return <button className={theme}>Theme: {theme}</button>;}Nothing provided ThemeContext, so ThemeButton got the default, light, and the switch changed state no one read. With the provider around the page, the button reads App's state: Theme: dark at first, and Switch theme flips it.
The user passes through Page and Sidebar to reach Greeting. Give it to Greeting with a UserContext instead. What does the page show when you're done?
const user = { name: "Asha" }; export default function App() { return <Page user={user} />;} function Page({ user }) { return ( <main> <Sidebar user={user} /> </main> );} function Sidebar({ user }) { return ( <aside> <Greeting user={user} /> </aside> );} function Greeting({ user }) { return <p>Hi, {user.name}</p>;}import { createContext, useContext } from "react"; const UserContext = createContext(null);const user = { name: "Asha" }; export default function App() { return ( <UserContext value={user}> <Page /> </UserContext> );} function Page() { return ( <main> <Sidebar /> </main> );} function Sidebar() { return ( <aside> <Greeting /> </aside> );} function Greeting() { const user = useContext(UserContext); return <p>Hi, {user.name}</p>;}App provides the user once. Page and Sidebar lose a prop they never used, and Greeting reads the user itself. The page still shows Hi, Asha.
Keep the page light, but make only the footer's button dark, without changing ThemeButton. What does the footer button say?
import { createContext, useContext } from "react"; const ThemeContext = createContext("light"); export default function App() { return ( <ThemeContext value="light"> <p> Green tea <ThemeButton label="Add to cart" /> </p> <footer> <ThemeButton label="Contact us" /> </footer> </ThemeContext> );} function ThemeButton({ label }) { const theme = useContext(ThemeContext); return <button className={theme}>{label} ({theme})</button>;}import { createContext, useContext } from "react"; const ThemeContext = createContext("light"); export default function App() { return ( <ThemeContext value="light"> <p> Green tea <ThemeButton label="Add to cart" /> </p> <ThemeContext value="dark"> <footer> <ThemeButton label="Contact us" /> </footer> </ThemeContext> </ThemeContext> );} function ThemeButton({ label }) { const theme = useContext(ThemeContext); return <button className={theme}>{label} ({theme})</button>;}The new provider is nearer to the footer's button than the page's, so that button reads dark. The Add to cart button is outside it and still reads light.
Every comment says depth 0, so the replies don't indent. Make each Comment provide one more than its own depth to its replies. What depth does Add milk, then. show?
import { createContext, useContext } from "react"; const DepthContext = createContext(0); const thread = { text: "Is this tea strong?", replies: [ { text: "Quite strong.", replies: [{ text: "Add milk, then.", replies: [] }], }, ],}; export default function App() { return <Comment comment={thread} />;} function Comment({ comment }) { const depth = useContext(DepthContext); return ( <div style={{ marginLeft: depth * 16 }}> <p> {comment.text} (depth {depth}) </p> {comment.replies.map((reply) => ( <Comment key={reply.text} comment={reply} /> ))} </div> );}import { createContext, useContext } from "react"; const DepthContext = createContext(0); const thread = { text: "Is this tea strong?", replies: [ { text: "Quite strong.", replies: [{ text: "Add milk, then.", replies: [] }], }, ],}; export default function App() { return <Comment comment={thread} />;} function Comment({ comment }) { const depth = useContext(DepthContext); return ( <div style={{ marginLeft: depth * 16 }}> <p> {comment.text} (depth {depth}) </p> <DepthContext value={depth + 1}> {comment.replies.map((reply) => ( <Comment key={reply.text} comment={reply} /> ))} </DepthContext> </div> );}Each Comment reads the depth from the nearest provider above it and provides one more to its replies. The top comment reads the default, 0; its reply reads 1; the reply to that reply reads 2, and is indented 32 pixels.
This is the drilled layout from above. Without using context, change it so teas goes straight from App to TeaList. After your change, how many components take a teas prop?
const teas = ["Green tea", "Chai", "Mint tea"]; export default function App() { return <Layout teas={teas} />;} function Layout({ teas }) { return ( <div className="layout"> <h3>Tea shop</h3> <Main teas={teas} /> </div> );} function Main({ teas }) { return ( <main> <TeaList teas={teas} /> </main> );} function TeaList({ teas }) { return <p>{teas.length} teas: {teas.join(", ")}</p>;}const teas = ["Green tea", "Chai", "Mint tea"]; export default function App() { return ( <Layout> <Main> <TeaList teas={teas} /> </Main> </Layout> );} function Layout({ children }) { return ( <div className="layout"> <h3>Tea shop</h3> {children} </div> );} function Main({ children }) { return <main>{children}</main>;} function TeaList({ teas }) { return <p>{teas.length} teas: {teas.join(", ")}</p>;}Only TeaList takes teas now; Layout and Main take children. You removed two layers of passing without a context.
Check your understanding
For each code question, find the reader, then walk up the tree to the first provider of that context. Press Edit & run on a question's code to check your answer.
Question 1 of 8What does the argument to
createContext("light")do?Choose an answer to see the explanation.
Question 2 of 8What does
Statusshow?Read the code, then predictimport { createContext, useContext } from "react"; const ThemeContext = createContext("light"); const LanguageContext = createContext("English"); export default function App() { return ( <ThemeContext value="dark"> <LanguageContext value="Hindi"> <ThemeContext value="light"> <Status /> </ThemeContext> </LanguageContext> </ThemeContext> ); } function Status() { const theme = useContext(ThemeContext); const language = useContext(LanguageContext); return <p>{theme}, {language}</p>; }Choose an answer to see the explanation.
Question 3 of 8
Toolbarreads the theme and also provides"dark". What does the paragraph show?Read the code, then predictimport { createContext, useContext } from "react"; const ThemeContext = createContext("light"); export default function App() { return <Toolbar />; } function Toolbar() { const theme = useContext(ThemeContext); return ( <ThemeContext value="dark"> <p>Toolbar theme: {theme}</p> </ThemeContext> ); }Choose an answer to see the explanation.
Question 4 of 8In a React 19 app, how should you write a new provider for
ThemeContext?Choose an answer to see the explanation.
Question 5 of 8A component returns early when a product is sold out, and reads the theme after that
return. Which call is allowed there?Choose an answer to see the explanation.
Question 6 of 8In the render-counter shop, you press Switch theme twice. What does the
Tea shopheading's counter say?Choose an answer to see the explanation.
Question 7 of 8A provider's value changes from
"light"to"dark". Which components render again because of it?Choose an answer to see the explanation.
Question 8 of 8A list passes through a
Layoutthat only places it on the page. What should you try before a context?Choose an answer to see the explanation.
Key takeaways
- Prop drilling is passing a prop through components that only hand it on. Context lets a value skip them.
createContext(defaultValue)makes the context object, outside any component. The object holds no value; the default is used only when no provider is above.- Provide a value with
<ThemeContext value={…}>(React 19; older code uses.Provider). Read it withuseContext(ThemeContext)at the top level, oruse(ThemeContext), which also works after an early return or inside anif. - A component reads the nearest provider above it, so a provider inside another overrides the value for its part of the tree.
- When the value changes, every component that reads the context renders again. Before using context, try props, or passing JSX as
children.
Remember the one-liner.
Provide it once up high; read it anywhere below.
Coming next: Scaling up with a reducer and context. It puts a reducer's state and its dispatch in context, so any component on the screen can read the state and send actions.