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:
- Create the context with
createContext - Provide the value with
<Context.Provider> - Consume the value with
useContext
Step 1: Create the Context
import { createContext } from "react";
const ThemeContext = createContext("light"); // "light" is the default value
export default ThemeContext;
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.
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:
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 Sidebar — Button 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:
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:
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:
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:
<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 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 Case | Tool |
|---|---|
| Avoid prop drilling for global values | Context API |
| High-frequency state updates | Redux / Zustand |
| Server state (API data) | React Query / SWR |
| Local component state | useState |