Context and State Management · Lesson 19 of 31
React Context and useContext
Passing props works well for a level or two. But some data is needed all over the app – the logged-in user, the theme, the language. Threading it through every component in between is called prop drilling, and it makes components carry props they don't use. Context lets a parent make a value available to its whole subtree, and any component below can read it directly.
In this lesson
- The prop drilling problem
createContext, providing a value anduseContext- Updating context: a theme switcher and an auth provider
- Performance and when not to use Context
The problem: prop drilling
// user has to pass through Layout and Header, which don't use it
function App() {
const user = { name: "Asha" };
return <Layout user={user} />;
}
function Layout({ user }) {
return <Header user={user} />;
}
function Header({ user }) {
return <Avatar user={user} />;
}
function Avatar({ user }) {
return <span>{user.name}</span>;
}
Context in three steps
import { createContext, useContext } from "react";
// 1. Create the context (with a default used when there's no provider)
const UserContext = createContext(null);
function Avatar() {
const user = useContext(UserContext); // 3. Read it anywhere below the provider
return <span>👤 {user.name}</span>;
}
function Header() {
return <header>LearnFrenzy · <Avatar /></header>; // no user prop
}
function Layout() {
return (
<div>
<Header />
<main>Your courses</main>
</div>
);
}
export default function App() {
const user = { name: "Asha", plan: "pro" };
return (
<UserContext value={user}> {/* 2. Provide a value (React 19; older: UserContext.Provider) */}
<Layout />
</UserContext>
);
}
LearnFrenzy · 👤 Asha Your courses
The value skips the middle components
- UserContext value={user}
- Layout – doesn't know about user
- Header – doesn't know about user
- Avatar – useContext(UserContext) ✔
- Header – doesn't know about user
- Layout – doesn't know about user
useContext finds the nearest provider above the component. Without one, it returns the default passed to createContext.
Updating context: a theme switcher
To let components change the value, keep it in state in the provider and put the setter in context too:
import { createContext, useContext, useState } from "react";
const ThemeContext = createContext({ theme: "light", toggleTheme: () => {} });
function ThemeProvider({ children }) {
const [theme, setTheme] = useState("light");
const toggleTheme = () => setTheme((t) => (t === "light" ? "dark" : "light"));
return <ThemeContext value={{ theme, toggleTheme }}>{children}</ThemeContext>;
}
function ThemeButton() {
const { theme, toggleTheme } = useContext(ThemeContext);
return <button onClick={toggleTheme}>Switch to {theme === "light" ? "dark" : "light"}</button>;
}
function Card() {
const { theme } = useContext(ThemeContext);
return <p className={`card card--${theme}`}>This card uses the {theme} theme</p>;
}
export default function App() {
return (
<ThemeProvider>
<ThemeButton />
<Card />
<Card />
</ThemeProvider>
);
}
[Switch to dark] This card uses the light theme This card uses the light theme › click “Switch to dark” [Switch to light] This card uses the dark theme This card uses the dark theme › click “Switch to light” [Switch to dark] This card uses the light theme This card uses the light theme
A reusable auth provider
A common pattern is a provider component plus a custom hook that reads the context and fails loudly if the provider is missing:
import { createContext, useContext, useState } from "react";
const AuthContext = createContext(null);
export function AuthProvider({ children }) {
const [user, setUser] = useState(null);
const login = (name) => setUser({ name }); // real apps call an API here
const logout = () => setUser(null);
return <AuthContext value={{ user, login, logout }}>{children}</AuthContext>;
}
export function useAuth() {
const ctx = useContext(AuthContext);
if (!ctx) throw new Error("useAuth must be used inside <AuthProvider>");
return ctx;
}
function NavBar() {
const { user, login, logout } = useAuth();
return user ? (
<nav>Hi, {user.name} <button onClick={logout}>Log out</button></nav>
) : (
<nav><button onClick={() => login("Asha")}>Log in</button></nav>
);
}
function Dashboard() {
const { user } = useAuth();
if (!user) return <p>Please log in to see your progress.</p>;
return <p>{user.name}, you have completed 12 lessons.</p>;
}
export default function App() {
return (
<AuthProvider>
<NavBar />
<Dashboard />
</AuthProvider>
);
}
[Log in] Please log in to see your progress. › click “Log in” Hi, Asha [Log out] Asha, you have completed 12 lessons. › click “Log out” [Log in] Please log in to see your progress.
Performance and pitfalls
When a provider's value changes, every component that reads that context re-renders. Keep that in mind:
- Split contexts by how often they change: a rarely-changing
ThemeContextseparate from a frequently-changingCartContext. - Stabilise the value.
value={{ theme, toggleTheme }}is a new object every render of the provider. If the provider re-renders often, wrap it inuseMemo(and the functions inuseCallback). With the React Compiler this is done for you. - Don't use Context for everything. For one or two levels, props are simpler and clearer. Composition – passing
children– often removes drilling without Context.
Good uses of Context
Theme, locale, the current user, feature flags, a router or a query client – values many components need that change rarely. For fast-changing, app-wide data with lots of consumers, a state library (next lesson) usually performs better.
Practice
Try it yourself
Create a
LanguageContextwith "en" and "hi", a toggle button, and a greeting that shows "Hello" or "नमस्ते".Show solution
import { createContext, useContext, useState } from "react"; const LanguageContext = createContext(null); const messages = { en: { hello: "Hello" }, hi: { hello: "नमस्ते" } }; function Greeting() { const { lang } = useContext(LanguageContext); return <h2>{messages[lang].hello}, Asha</h2>; } function LanguageToggle() { const { lang, setLang } = useContext(LanguageContext); return <button onClick={() => setLang(lang === "en" ? "hi" : "en")}>Language: {lang}</button>; } export default function App() { const [lang, setLang] = useState("en"); return ( <LanguageContext value={{ lang, setLang }}> <LanguageToggle /> <Greeting /> </LanguageContext> ); }[Language: en] Hello, Asha › click “Language: en” [Language: hi] नमस्ते, Asha
Quick check
What does useContext return when there's no provider above?
The default value passed to createContext.
Which components re-render when a context value changes?
All components that read that context.
How do you provide a context in React 19?
Render the context itself: <MyContext value={…}> (MyContext.Provider still works).
Summary
- Context passes a value to a whole subtree, removing prop drilling.
- createContext → <Ctx value> → useContext(Ctx); keep state in the provider to allow updates.
- Wrap it in a provider component plus a useX hook; split contexts and keep values stable.
Finished this lesson?
Mark it complete to track your progress, then carry on with the next lesson.