Skip to main content

Context API

The Problem: Prop Drilling

Imagine you have a theme variable in your top-level App component, and a deeply nested Button component needs it. Without Context, you pass it through every component in between — even components that don't use it themselves:

App (has theme)
└── Layout (passes theme)
└── Sidebar (passes theme)
└── Button (finally uses theme)

This is prop drilling — passing props through layers of components just to get data where it's needed. It's tedious, error-prone, and makes refactoring painful.

The Solution: Context

React's Context API lets you broadcast a value to any component in the tree that wants it, without threading it through every level.

App (provides theme via Context)
└── Layout
└── Sidebar
└── Button (reads theme from Context directly)

Creating and Using Context

Context has three parts:

  1. Create the context with createContext
  2. Provide the value with <Context.Provider>
  3. Consume the value with useContext

Step 1: Create the Context

src/context/ThemeContext.js
import { createContext } from "react";

const ThemeContext = createContext("light"); // "light" is the default value

export default ThemeContext;
About the default value

The default value ("light" here) is only used when a component reads from context but has no matching Provider above it in the tree. In practice you'll almost always have a Provider, so this default is a safety net.

Step 2: Provide the Value

Wrap any part of the tree with <ThemeContext.Provider>. All components inside can read the value.

src/App.jsx
import { useState } from "react";
import ThemeContext from "./context/ThemeContext";
import Layout from "./components/Layout";

function App() {
const [theme, setTheme] = useState("light");

return (
<ThemeContext.Provider value={theme}>
<button onClick={() => setTheme(theme === "light" ? "dark" : "light")}>
Toggle Theme
</button>
<Layout />
</ThemeContext.Provider>
);
}

Step 3: Consume the Value

Any component inside the provider can read the value with useContext:

src/components/Button.jsx
import { useContext } from "react";
import ThemeContext from "../context/ThemeContext";

function Button({ children }) {
const theme = useContext(ThemeContext);

return (
<button
style={{
backgroundColor: theme === "dark" ? "#333" : "#fff",
color: theme === "dark" ? "#fff" : "#333",
border: "1px solid currentColor",
padding: "8px 16px",
}}
>
{children}
</button>
);
}

No props passed through Layout or SidebarButton reads the theme directly.

Real-World Pattern: Context + Custom Hook

Wrapping context in a custom hook is the professional way to use it. It hides implementation details and gives you a nice error message if someone forgets to add the Provider:

src/context/AuthContext.jsx
import { createContext, useContext, useState } from "react";

const AuthContext = createContext(null);

export function AuthProvider({ children }) {
const [user, setUser] = useState(null);

const login = (userData) => setUser(userData);
const logout = () => setUser(null);

return (
<AuthContext.Provider value={{ user, login, logout }}>
{children}
</AuthContext.Provider>
);
}

export function useAuth() {
const context = useContext(AuthContext);
if (!context) {
throw new Error("useAuth must be used inside <AuthProvider>");
}
return context;
}

Set up the provider at the top of your app:

src/main.jsx
import { createRoot } from "react-dom/client";
import { AuthProvider } from "./context/AuthContext";
import App from "./App";

createRoot(document.getElementById("root")).render(
<AuthProvider>
<App />
</AuthProvider>
);

Now any component can call useAuth() directly:

src/components/Navbar.jsx
import { useAuth } from "../context/AuthContext";

function Navbar() {
const { user, logout } = useAuth();

return (
<nav>
{user ? (
<>
<span>Welcome, {user.name}</span>
<button onClick={logout}>Logout</button>
</>
) : (
<a href="/login">Login</a>
)}
</nav>
);
}

Multiple Contexts

You can have as many contexts as you need. A common setup:

src/main.jsx
<AuthProvider>
<ThemeProvider>
<NotificationProvider>
<App />
</NotificationProvider>
</ThemeProvider>
</AuthProvider>

Each context is independent. Components only re-render when the specific context they subscribe to changes.

When to Use Context

Context is great for:

  • Theme (dark/light mode)
  • Authentication (logged-in user, permissions)
  • Language / locale (i18n)
  • Feature flags
Context is not a state management replacement

Context doesn't optimize re-renders. When the context value changes, every component that calls useContext for that context re-renders — even if the specific piece of data they use hasn't changed.

For complex global state with frequent updates (like a shopping cart with many operations), consider Redux or Zustand instead.

For state that only changes occasionally (theme, auth user), Context is perfect.

Quick Reference

// 1. Create
const MyContext = createContext(defaultValue);

// 2. Provide
<MyContext.Provider value={someValue}>
<ChildComponents />
</MyContext.Provider>

// 3. Consume (inside any child component)
const value = useContext(MyContext);
Use CaseTool
Avoid prop drilling for global valuesContext API
High-frequency state updatesRedux / Zustand
Server state (API data)React Query / SWR
Local component stateuseState