Context with useContext
Context lets you pass data through the component tree without manually passing props.
Creating Context
import { createContext, useContext } from "react";
const ThemeContext = createContext("light");
Providing Context
Wrap components with the Provider:
function App() {
return (
<ThemeContext.Provider value="dark">
<Toolbar />
</ThemeContext.Provider>
);
}
Consuming with useContext
function Button() {
const theme = useContext(ThemeContext);
return <button className={theme}>Click</button>;
}
useContext finds the nearest Provider up the tree.
Avoiding Prop Drilling
Without context, passing theme through every level:
// Bad — prop drilling
<App theme="dark">
<Layout theme="dark">
<Sidebar theme="dark">
<Button theme="dark" />
</Sidebar>
</Layout>
</App>
// Good — context
<App>
<Layout>
<Sidebar>
<Button /> {/* reads theme directly */}
</Sidebar>
</Layout>
</App>
Dynamic Context
Combine with useState for dynamic values:
function App() {
const [theme, setTheme] = useState("light");
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
<Main />
</ThemeContext.Provider>
);
}
Full example
A complete theme switcher: context in its own file, a provider, a toggle, and components that read the theme without any props.
import { createContext, useContext, useState } from "react";
const ThemeContext = createContext(null);
export function ThemeProvider({ children }) {
const [theme, setTheme] = useState("light");
const toggle = () => setTheme((t) => (t === "light" ? "dark" : "light"));
return (
<ThemeContext.Provider value={{ theme, toggle }}>
{children}
</ThemeContext.Provider>
);
}
export function useTheme() {
const value = useContext(ThemeContext);
if (!value) throw new Error("useTheme must be used inside ThemeProvider");
return value;
}
import { useTheme } from "../context/ThemeContext.jsx";
export default function ThemeToggle() {
const { theme, toggle } = useTheme();
return (
<button className="toggle" onClick={toggle}>
Switch to {theme === "light" ? "dark" : "light"} mode
</button>
);
}
import { useTheme } from "../context/ThemeContext.jsx";
export default function Card({ title, children }) {
const { theme } = useTheme();
return (
<div className={`card card--${theme}`}>
<h2>{title}</h2>
<p>{children}</p>
</div>
);
}
import Card from "./components/Card.jsx";
import ThemeToggle from "./components/ThemeToggle.jsx";
export default function App() {
return (
<main className="app">
<ThemeToggle />
<Card title="Context">This card themes itself.</Card>
<Card title="No props">Nothing was drilled through the tree.</Card>
</main>
);
}
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { ThemeProvider } from "./context/ThemeContext.jsx";
import App from "./App.jsx";
import "./index.css";
createRoot(document.getElementById("root")).render(
<StrictMode>
<ThemeProvider>
<App />
</ThemeProvider>
</StrictMode>
);
Best Practices
- Use context for global data (theme, auth, locale)
- Don’t use context for high-frequency updates
- Split large contexts into smaller ones
Common Mistakes
- Passing new objects/values on every render defeats memoization
- Using context for every prop — use props for local data
- Not wrapping with Provider causes default values to be used