React course menu
React Context API: useContext Without Prop Drilling
Pass data deeply through the React component tree with context: the prop drilling problem, createContext, providing a value, reading it with useContext, updating context with state, theme and logged-in user examples, combining context with a reducer, and when not to use context.
On this page
Props pass data one level at a time. When many components at different depths need the same data (the current theme, the logged-in user, the language), passing it through every component in between becomes tedious. This is called prop drilling. Context lets a parent make a value available to its entire subtree, so any component below can read it directly.
Three steps#
- Create a context:
const ThemeContext = createContext("light");(the argument is the default value). - Provide a value around part of the tree:
<ThemeContext value="dark">...</ThemeContext>. - Read it in any component below:
const theme = useContext(ThemeContext);.
import { createContext, useContext, useState } from "react";
const ThemeContext = createContext("light");
const COLORS = {
light: { background: "#ffffff", color: "#222222", border: "#dddddd" },
dark: { background: "#1f2430", color: "#e6e6e6", border: "#3a4152" },
};
function Button({ children }) {
const theme = useContext(ThemeContext);
const c = COLORS[theme];
return <button style={{ background: c.background, color: c.color, border: `1px solid ${c.border}`, padding: "4px 10px" }}>{children}</button>;
}
function Card({ title }) {
const theme = useContext(ThemeContext);
const c = COLORS[theme];
return (
<div style={{ background: c.background, color: c.color, border: `1px solid ${c.border}`, padding: 12, marginTop: 8 }}>
<h4 style={{ margin: "0 0 8px" }}>{title}</h4>
<Button>Open</Button>
</div>
);
}
function Dashboard() {
return (
<div>
<Card title="Orders" />
<Card title="Messages" />
</div>
);
}
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 theme
</label>
<Dashboard />
</ThemeContext>
);
}Dashboard does not receive or pass any theme prop, yet Card and Button deep inside it read the theme. Because the provided value comes from state, toggling the checkbox re-renders every component that reads the context.
In React 19 you render the context itself as the provider (<ThemeContext value=...>). Older code writes <ThemeContext.Provider value=...>, which still works.
Providing values and functions#
Often you provide both data and functions to change it, so any component can update the shared state. A logged-in user is a typical case:
import { createContext, useContext, useState } from "react";
const AuthContext = createContext(null);
function useAuth() {
const value = useContext(AuthContext);
if (!value) throw new Error("useAuth must be used inside <AuthProvider>");
return value;
}
function AuthProvider({ children }) {
const [user, setUser] = useState(null);
const login = (name) => setUser({ name });
const logout = () => setUser(null);
return <AuthContext value={{ user, login, logout }}>{children}</AuthContext>;
}
function Header() {
const { user, logout } = useAuth();
return (
<header style={{ borderBottom: "1px solid #ddd", paddingBottom: 6 }}>
ShopKart {user ? <>· Hi, {user.name} <button onClick={logout}>Log out</button></> : "· Guest"}
</header>
);
}
function LoginForm() {
const { user, login } = useAuth();
if (user) return <p>You are logged in.</p>;
return <button onClick={() => login("Farah")} style={{ marginTop: 8 }}>Log in as Farah</button>;
}
export default function App() {
return (
<AuthProvider>
<Header />
<LoginForm />
</AuthProvider>
);
}Two good habits shown here:
- An
AuthProvidercomponent keeps the state and the context together. - A custom hook (
useAuth) reads the context and throws a clear error if it is used outside the provider.
Context with a reducer#
For app-wide state with many kinds of updates (a cart, a to-do list), combine useReducer with context: the provider holds [state, dispatch] from the reducer, and any component can read the state or dispatch actions. This covers what many apps reach for a state library to do.
When not to use context#
- For data only one or two levels down: pass props. They make data flow explicit and components easier to reuse.
- To avoid passing JSX through layers: pass components as
childreninstead (see Composition). - For fast-changing values used by many components: every reader re-renders when the value changes. Split contexts by concern (theme, user, cart), or use a library designed for frequent updates (such as Zustand or Redux Toolkit) for large apps.
Common mistakes#
Practice#
Practice exercise
Create a LanguageContext with the values "en" and "hi". Provide it from App with a toggle button, and build a nested Greeting component that shows "Hello" or "नमस्ते" depending on the language, without passing any props.
Show hint
createContext("en"), wrap the tree in <LanguageContext value={lang}>, and read it with useContext(LanguageContext) inside Greeting.
Show solution
import { createContext, useContext, useState } from "react";
const LanguageContext = createContext("en");
const TEXT = { en: "Hello", hi: "नमस्ते" };
function Greeting() {
const lang = useContext(LanguageContext);
return <h3>{TEXT[lang]}</h3>;
}
function Page() {
return (
<section>
<Greeting />
</section>
);
}
export default function App() {
const [lang, setLang] = useState("en");
return (
<LanguageContext value={lang}>
<button onClick={() => setLang(lang === "en" ? "hi" : "en")}>Switch language</button>
<Page />
</LanguageContext>
);
}Key takeaway#
Next: Custom Hooks.
Finished this lesson?