LearnFrenzy
Sign in Take a free test

Context and State Management · Lesson 19 of 31

React Context and useContext

6 min read Updated Nov 8, 2026 Saurabh Samir

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 and useContext
  • 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>
  );
}

Result

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) ✔

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>
  );
}

Result

[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>
  );
}

Result

[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 ThemeContext separate from a frequently-changing CartContext.
  • Stabilise the value. value={{ theme, toggleTheme }} is a new object every render of the provider. If the provider re-renders often, wrap it in useMemo (and the functions in useCallback). 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

  1. Create a LanguageContext with "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>
      );
    }

    Result

    [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.

Next lesson

Popular