Understanding useContext: Avoiding Prop Drilling
Understanding useContext
~11 Min. Lesezeit Zuletzt aktualisiert am August 8, 2026
So far, every example we've written has passed data top-down via props. But what if lots of far-apart components need the same value – for example, whether a user is currently logged in? That's what context is for.
The problem: "prop drilling"
Imagine App knows whether the user is logged in, but a deeply nested component six levels down (e.g. a logout button in the header) needs that information. Without context, you'd have to pass the value as a prop through EVERY intermediate component – even ones that don't need the value themselves, just pass it along. This is called "prop drilling" and gets messy fast.
Creating a context: AuthContext
Create a new folder src/context/ with the file AuthContext.jsx inside it. We'll build a simple login state that we'll use for a protected area starting in chapter 17 (Protected Routes):
import { createContext, useState } from 'react';
export const AuthContext = createContext(null);
export function AuthProvider({ children }) {
const [user, setUser] = useState(null);
function login(username) {
setUser({ username });
}
function logout() {
setUser(null);
}
return (
<AuthContext.Provider value={{ user, login, logout }}>
{children}
</AuthContext.Provider>
);
}Three parts work together here: createContext(null) creates the context itself (null is just the starting value before a provider overrides it). AuthProvider is a completely normal component that wraps its children prop (everything between its own opening/closing tags) in <AuthContext.Provider value={{...}}> – ANY component inside this provider can read the value, no matter how deeply nested.
Updating main.jsx: wrapping the whole app
For truly every component to have access, we wrap the entire app in the entry point main.jsx:
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import App from './App.jsx';
import { AuthProvider } from './context/AuthContext.jsx';
import './index.css';
createRoot(document.getElementById('root')).render(
<StrictMode>
<AuthProvider>
<App />
</AuthProvider>
</StrictMode>
);Updating App.jsx: using useContext
Now let's read the context value with useContext. Add the import and a small login display at the top of the page in src/App.jsx:
import { useContext, useEffect, useMemo, useRef, useState } from 'react';
import ProductCard from './components/ProductCard';
import { AuthContext } from './context/AuthContext';
const SAMPLE_PRODUCTS = [
{ sku: 'boots-01', name: 'Hiking Boots', price: 89.99, imageUrl: 'https://picsum.photos/seed/1/300' },
{ sku: 'backpack-01', name: 'Trekking Backpack', price: 59.5, imageUrl: 'https://picsum.photos/seed/2/300' },
{ sku: 'jacket-01', name: 'Rain Jacket', price: 74.0, imageUrl: 'https://picsum.photos/seed/3/300' },
];
function App() {
const { user, login, logout } = useContext(AuthContext);
const [query, setQuery] = useState('');
const searchInputRef = useRef(null);
useEffect(() => {
document.title = `Product Catalog (${SAMPLE_PRODUCTS.length} products)`;
searchInputRef.current.focus();
}, []);
const filtered = SAMPLE_PRODUCTS.filter((product) =>
product.name.toLowerCase().includes(query.toLowerCase())
);
const mostExpensiveProduct = useMemo(() => {
return filtered.reduce(
(max, p) => (p.price > (max?.price ?? 0) ? p : max),
null
);
}, [filtered]);
return (
<div className="app">
<header className="app-header">
<h1>Product Catalog</h1>
{user ? (
<p>Logged in as {user.username} <button onClick={logout}>Log out</button></p>
) : (
<button onClick={() => login('Jane Doe')}>Log in</button>
)}
</header>
<input
ref={searchInputRef}
type="text"
placeholder="Search products..."
value={query}
onChange={(event) => setQuery(event.target.value)}
/>
{mostExpensiveProduct && (
<p>Most expensive product: {mostExpensiveProduct.name} (${mostExpensiveProduct.price.toFixed(2)})</p>
)}
<div className="product-grid">
{filtered.map((product) => (
<ProductCard
key={product.sku}
name={product.name}
price={product.price}
imageUrl={product.imageUrl}
onSelect={() => alert(product.name + ' tapped')}
/>
))}
</div>
</div>
);
}
export default App;Project structure after this chapter
produktkatalog-web/
├── index.html
├── package.json
├── vite.config.js
└── src/
├── main.jsx (updated: AuthProvider)
├── App.jsx (updated: useContext)
├── index.css
├── context/
│ └── AuthContext.jsx ← NEW
└── components/
└── ProductCard.jsxlogin('Jane Doe') is just a demo login here, with no real check – we'll build a real login form with validation in chapters 13–14, and a genuinely protected area in chapter 17. For now, this is enough to see useContext in action: login status is available everywhere, without passing it as a prop.
Tipp: useContext does NOT replace useState – it only transports state that already exists (from the provider) to places that need it, without prop drilling. For purely local state within a single component, useState remains the right choice.