Skip to content
Laviyx Learn

Try

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.

Intermediate9 min readUpdated
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#

  1. Create a context: const ThemeContext = createContext("light"); (the argument is the default value).
  2. Provide a value around part of the tree: <ThemeContext value="dark">...</ThemeContext>.
  3. Read it in any component below: const theme = useContext(ThemeContext);.
JSX
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>
  );
}
Result

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:

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

Two good habits shown here:

  • An AuthProvider component 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 children instead (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
JSX
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>
  );
}
Result

Key takeaway#

Next: Custom Hooks.

Finished this lesson?