Context
Props are the normal way to pass data, but some data is needed almost everywhere: the logged-in user, the colour theme, the chosen language. Passing these through every level of the tree, even through components that do not use them, is called prop drilling, and it quickly becomes tiring and fragile.
Context lets a parent component make a value available to every component below it, however deep, without passing props through the middle. In this lesson you will create a context, provide a value, read it, combine it with state, and learn when context is the right tool.
Three steps: create, provide, use
import { createContext, useContext, useState } from "react";
import { createRoot } from "react-dom/client";
// 1. Create the context, with a default value
const ThemeContext = createContext("light");
const colours = {
light: { background: "#ffffff", color: "#1f2937", border: "#d1d5db" },
dark: { background: "#1f2937", color: "#f9fafb", border: "#4b5563" },
};
function ThemedButton({ children }) {
// 3. Use the context: no props needed
const theme = useContext(ThemeContext);
const c = colours[theme];
return (
<button style={{ background: c.background, color: c.color, border: `1px solid ${c.border}`, padding: "6px 12px" }}>
{children}
</button>
);
}
function Toolbar() {
// Toolbar does not know or care about the theme
return (
<div>
<ThemedButton>Book tickets</ThemedButton> <ThemedButton>My trips</ThemedButton>
</div>
);
}
function App() {
const [theme, setTheme] = useState("light");
return (
// 2. Provide a value to everything inside
<ThemeContext value={theme}>
<label>
<input type="checkbox" checked={theme === "dark"} onChange={(e) => setTheme(e.target.checked ? "dark" : "light")} />
Dark mode
</label>
<Toolbar />
</ThemeContext>
);
}
createRoot(document.getElementById("root")).render(<App />);createContext("light")creates a context object. The argument is the default value, used only when a component reads the context with no provider above it.<ThemeContext value={theme}>provides a value to every component inside it. Whenthemechanges, React re-renders every component that reads this context.useContext(ThemeContext)reads the value from the nearest provider above the component.
Notice that Toolbar passes nothing to ThemedButton. The value "teleports" from App to the buttons.
Context with state: a provider component
Context passes values down, and state lets values change. Together they make a small "global store". A common pattern is to wrap both in a provider component and a custom hook, so the rest of the app never touches the context object directly:
import { createContext, useContext, useState } from "react";
import { createRoot } from "react-dom/client";
const UserContext = createContext(null);
function UserProvider({ children }) {
const [user, setUser] = useState(null);
const login = (name, city) => setUser({ name, city });
const logout = () => setUser(null);
return <UserContext value={{ user, login, logout }}>{children}</UserContext>;
}
function useUser() {
const value = useContext(UserContext);
if (value === null) throw new Error("useUser must be used inside <UserProvider>");
return value;
}
function Header() {
const { user, logout } = useUser();
return (
<header style={{ borderBottom: "1px solid #ddd", paddingBottom: 6 }}>
{user ? (
<>Namaste, {user.name} ({user.city}) <button onClick={logout}>Log out</button></>
) : (
"Not logged in"
)}
</header>
);
}
function LoginPanel() {
const { user, login } = useUser();
if (user) return <p>You can now book your Rohtak to Jaipur bus.</p>;
return <button onClick={() => login("Priya", "Rohtak")}>Log in as Priya</button>;
}
function App() {
return (
<UserProvider>
<Header />
<main>
<LoginPanel />
</main>
</UserProvider>
);
}
createRoot(document.getElementById("root")).render(<App />);Header and LoginPanel are siblings, yet both see the same user, and logging in from one updates the other. The useUser hook gives a clear error if someone forgets the provider, which is better than mysterious undefined values. You will learn more about custom hooks in Custom Hooks.
Overriding context for part of the tree
A component reads the nearest provider above it. You can nest providers to override a value for one section, for example a Hindi section inside an English page:
import { createContext, useContext } from "react";
import { createRoot } from "react-dom/client";
const LanguageContext = createContext("en");
const greetings = { en: "Welcome", hi: "स्वागत है", ml: "സ്വാഗതം" };
function Greeting({ name }) {
const lang = useContext(LanguageContext);
return <p>{greetings[lang]}, {name}! ({lang})</p>;
}
function App() {
return (
<LanguageContext value="en">
<Greeting name="Aarav" />
<LanguageContext value="hi">
<Greeting name="Meera" />
</LanguageContext>
<LanguageContext value="ml">
<Greeting name="Anil" />
</LanguageContext>
</LanguageContext>
);
}
createRoot(document.getElementById("root")).render(<App />);Reading context with use()
React 19 added the use API, which can read a context too: const theme = use(ThemeContext);. It behaves like useContext, with one difference: use may be called inside an if statement or a loop, so you can read context only when you need it. use can also read promises, which you will see in Suspense, lazy & use().
import { use } from "react";
function Price({ amount, showTax }) {
if (!showTax) return <span>₹{amount}</span>;
const { gstRate } = use(TaxContext); // allowed inside a condition
return <span>₹{amount} + {gstRate}% GST</span>;
}When to use context
Context is good for values that many components at different depths need and that change rarely: theme, language, current user, feature flags, a shopping cart. Before reaching for it, consider simpler options:
- Pass props. A few levels of props are explicit and easy to follow.
- Pass JSX as
children. Often a middle component does not need the data at all; let the parent render the child directly and pass it down aschildren.
Also remember the cost: when a provider's value changes, every component that reads that context re-renders. Passing a new object such as value={{ user, login }} on every render counts as a change. Split unrelated data into separate contexts, and see Performance & React Compiler for memoising values.
Common mistakes
- Reading context outside its provider. You silently get the default value. A custom hook that throws, like
useUser, catches this early. - Using context for everything. Data used by one or two nearby components should stay in props or state.
- Putting the provider too low. Components outside the provider cannot see the value.
- One giant context. Mixing theme, user and cart in one value re-renders every reader when any part changes.
Key points
- Context passes a value to all components below a provider, avoiding prop drilling.
createContext(default)creates it;<MyContext value={...}>provides it (React 19);useContext(MyContext)reads it.<MyContext.Provider>is the older provider syntax you will see in existing code.- Combine context with state in a provider component, and expose it through a custom hook.
- Components read the nearest provider, so providers can be nested to override values.
use(MyContext)reads context and may be called conditionally.
Exercise
Create a CartContext with a CartProvider that stores an array of item names and offers addItem and clear. Build three components inside the provider: ProductList with "Add" buttons for three snacks, CartBadge showing "Cart (N)", and ClearButton. None of them should receive cart props.
Show solution
import { createContext, useContext, useState } from "react";
import { createRoot } from "react-dom/client";
const CartContext = createContext(null);
function CartProvider({ children }) {
const [items, setItems] = useState([]);
const addItem = (name) => setItems((prev) => [...prev, name]);
const clear = () => setItems([]);
return <CartContext value={{ items, addItem, clear }}>{children}</CartContext>;
}
const useCart = () => useContext(CartContext);
function CartBadge() {
const { items } = useCart();
return <strong>Cart ({items.length})</strong>;
}
function ProductList() {
const { addItem } = useCart();
return ["Samosa", "Kachori", "Dhokla"].map((s) => (
<button key={s} onClick={() => addItem(s)}>Add {s}</button>
));
}
function ClearButton() {
const { clear } = useCart();
return <button onClick={clear}>Clear cart</button>;
}
createRoot(document.getElementById("root")).render(
<CartProvider>
<CartBadge />
<p><ProductList /></p>
<ClearButton />
</CartProvider>
);All three components get what they need from useCart(), so the cart data never travels through props.
Quick Quiz
1. What problem does context solve?
Context delivers a value to deep components directly from a provider above them.
2. When is the default value of createContext(defaultValue) used?
The default is a fallback for components outside any provider.
3. How do you provide a context value in React 19?
React 19 lets you render the context as its own provider; .Provider is the older form.
4. Which can be called inside an if statement?
use is the one React API that may be called conditionally; hooks must stay at the top level.
Learn React with a mentor
Free lessons get you started. Our Full Stack Web Development training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
