useContext & the Context API
Reviewed & published by Brayan K
The Context API lets a component broadcast a value to all of its descendants without passing props through every layer in between. You create a context, wrap a subtree in a Provider that supplies the value, and any descendant reads it with useContext. It's the cure for prop drilling — and the standard way to share global data like theme, the current user, or language.
Part of the free React course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
What You'll Learn
- What prop drilling is and why it hurts
- The three pieces: createContext, Provider, useContext
- Read a context value in any descendant
- How the nearest provider's value wins
- When to use context vs a plain prop
- Why consumers re-render when the value changes
1️⃣ The Problem: Prop Drilling
When a deeply nested component needs a value, you normally pass it as a prop — but that means every component in between has to accept and forward it, even though they never use it. That's prop drilling: tedious, noisy, and easy to break.
// theme has to be threaded through every layer:
function Page({ theme }) { return <Toolbar theme={theme} />; }
function Toolbar({ theme }) { return <Button theme={theme} />; }
function Button({ theme }) { return <button className={theme}>OK</button>; }// PROP DRILLING: the theme has to pass through components that ignore it.
function Page(theme) { return Toolbar(theme); } // just forwards it
function Toolbar(theme) { return Button(theme); } // just forwards it
function Button(theme) { return "Button is " + theme; } // finally uses it
console.log(Page("dark")); // Button is dark
// Page and Toolbar don't care about theme — they only pass it along.
// With many layers this gets tedious and error-prone. Context fixes it.2️⃣ The Fix: createContext + Provider + useContext
Context has three parts. createContext makes the context object. A Provider wraps a subtree and supplies the value. Any descendant calls useContext to read it — no forwarding required.
const ThemeContext = createContext("light"); // 1) create
function App() {
return (
<ThemeContext.Provider value="dark">
<Toolbar />
</ThemeContext.Provider>
);
}
function Button() {
const theme = useContext(ThemeContext); // 3) consume directly
return <button className={theme}>OK</button>;
}// CONTEXT is like a shared value any descendant can read directly,
// without it being threaded through every layer.
// The provider sets the value once:
const themeContext = { value: "dark" }; // <ThemeContext.Provider value="dark">
// Any descendant reads it directly, skipping the layers in between:
function DeepButton() {
const theme = themeContext.value; // useContext(ThemeContext)
return "Button is " + theme;
}
console.log(DeepButton()); // Button is dark
// Change the provider's value and every reader sees the new value:
themeContext.value = "light";
console.log(DeepButton()); // Button is lightYour turn — read a user out of a context and greet them:
// 🎯 YOUR TURN — fill in each ___ then press "Try it Yourself".
// Model a tiny context: a provider holds a user, a consumer reads it.
const userContext = { value: { name: "Ada", role: "admin" } };
function Greeting() {
// 1) Read the user out of the context's value.
const user = userContext.___; // 👉 .value
// 2) Return a greeting using the user's name.
return "Hello, " + user.___; // 👉 .name
}
console.log(Greeting()); // Hello, Ada
// Change the provider, the consumer follows:
userContext.value = { name: "Grace", role: "admin" };
console.log(Greeting()); // Hello, Grace
// ✅ Expected output:
// Hello, Ada
// Hello, Grace3️⃣ The Nearest Provider Wins
When providers are nested, useContext reads the closest one above the consumer. That lets you override a context for just one part of the tree — for example, a "dark" section inside an otherwise "light" app.
<ThemeContext.Provider value="light">
<Header />
<ThemeContext.Provider value="dark">
<Sidebar />
</ThemeContext.Provider>
</ThemeContext.Provider>// useContext reads the NEAREST provider above it in the tree.
// Simulate nesting: an inner provider overrides the outer one.
function read(providers) {
// providers listed outermost -> innermost; nearest (last) wins
return providers[providers.length - 1];
}
console.log(read(["light"])); // light (only one provider)
console.log(read(["light", "dark"])); // dark (inner provider overrides)
console.log(read(["dark", "light"])); // light (nearest wins again)
// So you can override context for just one part of the tree.🧩 Reorder Challenge
These lines set up and consume a context. Put them in the right order:
A. <Toolbar />
B. const ThemeContext = createContext("light");
C. <ThemeContext.Provider value="dark">
D. const theme = useContext(ThemeContext);
E. </ThemeContext.Provider>Common Errors (and the fix)
- useContext returns the default value, not yours. — The consumer isn't inside the Provider. Wrap it (or a parent) in <Ctx.Provider>.
- All consumers re-render every render. — Your value is a new object each time. Wrap it in useMemo.
- Forgot the value prop. — <Ctx.Provider> without value supplies undefined.
- Overusing context. — For one or two levels, a plain prop is simpler and easier to trace.
📋 Quick Reference
| Step | Code |
|---|---|
| Create | const Ctx = createContext(default); |
| Provide | <Ctx.Provider value={v}>...</Ctx.Provider> |
| Consume | const v = useContext(Ctx); |
| Stabilize value | const v = useMemo(() => ({...}), [deps]); |
🧠 Quick Recall
1. Name the three pieces of the Context API.
createContext, <Ctx.Provider value={...}>, and useContext(Ctx).
2. A consumer is nested under two providers (outer "light", inner "dark"). What does it read?
"dark" — the nearest provider above it wins.
3. A consumer reads the default value even though you added a Provider. Likely cause?
The consumer isn't actually rendered inside that Provider's subtree.
Mini-Challenge: Theme-Aware Label
Read a theme out of a context object and return the matching label, then change the theme and re-read. Run it and check your output.
// 🎯 MINI-CHALLENGE: a theme-aware label
//
// 1. A "context" object holds { theme: "dark" }.
// 2. Write label() that reads context.theme and returns:
// "🌙 dark mode" when theme is "dark"
// "☀️ light mode" otherwise
// 3. Log label(), then switch context.theme to "light" and log again.
//
// ✅ Expected output:
// 🌙 dark mode
// ☀️ light mode
const context = { theme: "dark" };
// your code here🎉 Lesson Complete
- ✅ Context cures prop drilling for global-ish data
- ✅ Three pieces: createContext, Provider, useContext
- ✅ Any descendant reads the value directly — no forwarding
- ✅ The nearest provider wins; memoize the value to avoid extra re-renders
Practice quiz
What problem does the Context API primarily solve?
- Slow rendering of large lists
- Fetching data from a server
- Prop drilling — passing props through layers that don't use them
- Memoizing expensive calculations
Answer: Prop drilling — passing props through layers that don't use them. Context lets a provider broadcast a value so any descendant can read it directly, avoiding threading the same prop through every layer.
What are the three pieces of the Context API?
- createContext, a Provider, and useContext
- useState, useEffect, useRef
- connect, mapState, dispatch
- subscribe, getSnapshot, getServerSnapshot
Answer: createContext, a Provider, and useContext. createContext makes the context object, <Ctx.Provider value={...}> supplies the value, and useContext(Ctx) reads it in any descendant.
How does a descendant component read a context value?
- By importing it directly from the file
- By receiving it as a prop from its parent
- By calling createContext again
- By calling useContext(MyContext)
Answer: By calling useContext(MyContext). A descendant calls useContext(MyContext) to read the nearest provider's value — no prop forwarding required.
When providers are nested, which value does useContext read?
- The outermost provider's value
- The nearest (closest) provider above the consumer
- A merged combination of all providers
- The default passed to createContext
Answer: The nearest (closest) provider above the consumer. useContext reads the closest provider above it in the tree, so an inner provider can override the outer one for just that subtree.
What does the argument to createContext (e.g. createContext('light')) specify?
- The default value used when no matching Provider is found above
- The provider's value for the whole app
- The component that will consume it
- A required initial state
Answer: The default value used when no matching Provider is found above. That argument is the default value, used only when a consumer has no matching Provider above it in the tree.
Why might all consumers re-render every time the parent renders?
- Context always re-renders everything
- useContext is called too many times
- The Provider's value is a new object created on each render
- The default value is missing
Answer: The Provider's value is a new object created on each render. Every consumer re-renders when the provider value changes by identity. Passing a fresh object each render triggers needless re-renders; memoize it with useMemo.
A consumer reads the default value even though you added a Provider. Likely cause?
- The default value was set too high
- The consumer isn't actually rendered inside that Provider's subtree
- useContext was called at the top level
- createContext was called twice
Answer: The consumer isn't actually rendered inside that Provider's subtree. If the consumer is not a descendant of the Provider, useContext falls back to the default value passed to createContext.
When is a plain prop better than Context?
- When sharing the current theme app-wide
- When sharing the logged-in user everywhere
- When the value changes very rarely
- When passing data down just one or two levels
Answer: When passing data down just one or two levels. For one or two levels, a normal prop is simpler and easier to trace. Context shines for truly global data like theme, user, or language.
How can you avoid unnecessary re-renders from a context Provider's value?
- Call createContext inside the render
- Wrap the value in useMemo so its reference only changes when needed
- Remove the value prop
- Use useLayoutEffect instead
Answer: Wrap the value in useMemo so its reference only changes when needed. Memoizing the value with useMemo keeps the same reference across renders unless its dependencies change, preventing consumers from re-rendering needlessly.
What happens if you write <Ctx.Provider> without a value prop?
- It throws an error immediately
- Consumers receive the default from createContext
- Consumers receive undefined as the value
- It uses the previous render's value
Answer: Consumers receive undefined as the value. A Provider without a value prop supplies undefined to its consumers, which is a common source of bugs. Always pass value.
Continue this course
- Previous: useRef — DOM Refs & Persisting Values
- Next: useReducer — Manage complex state with actions and a reducer
- Quick reference: React cheat sheet › Hooks Rules & Patterns
Frequently asked questions
What problem does Context solve?
Prop drilling — passing the same prop down through many layers of components that don't use it, just to reach a deep child. Context lets a provider broadcast a value and any descendant read it directly with useContext.
What are the three pieces of the Context API?
createContext(default) makes the context object; <MyContext.Provider value={...}> wraps a subtree and supplies the value; useContext(MyContext) reads the nearest provider's value inside any descendant.
When should I NOT use Context?
For passing a prop down one or two levels — that's simpler as a normal prop. Context shines for truly global data: theme, current user, language. Overusing it can make data flow hard to trace and cause extra re-renders.
Why does my component re-render when context changes?
Every consumer of a context re-renders when the provider's value changes. If the value is a new object each render, all consumers re-render every time. Memoize the value (useMemo) to avoid needless re-renders.