Magento 2 Experten — Hyvä Theme, Tailwind CSS & SEO aus einer Hand ›

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):

src/context/AuthContext.jsx
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:

src/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:

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.jsx

login('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.