9. gyakorlat – React Context és useCart hook
Bevezetés
Az előző két gyakorlaton felépítettük a webshop alapját, és elkészítettük a kosár oldalpanelt. Volt azonban egy probléma: a cartItems és az addToCart függvényt le kellett adogatni az App-ból a Navbar-ba, onnan tovább a ProductSheet-be — minden közbülső komponensen keresztül, még ha nem is volt szüksége rájuk.
Erre a problémára ad megoldást a React Context API.
A mai óra témái:
- Prop drilling — mi a probléma, és mikor lesz zavaró
- React Context — globális state, amely bármelyik komponensből elérhető
useCarthook — a context elérése egy egyedi hookkal
1. A prop drilling probléma
Az előző állapotban a kosár state az App-ban élt, és minden komponens, amely hozzáfért, propként kapta meg:
App
├── Navbar (cartItems ← prop)
│ └── ProductSheet (cartItems ← prop)
└── ProductCard (addToCart ← prop)
Ha mélyen egymásba ágyazott komponensek is kell, hogy lássák a kosarat, minden szinten tovább kell adni a propokat — akkor is, ha egy közbülső komponensnek nincs szüksége rá. Ezt hívják prop drilling-nek.
Miért gond ez?
- Ha a
cartItemstípusa megváltozik, minden érintett komponens props-interfész-ét frissíteni kell - Egy közbülső komponens anélkül függ a kosár state-től, hogy ténylegesen használná
- Nehezebb átlátni, hogy egy prop honnan jön és kinek szól
2. A Context API megoldása
A React Context lehetővé teszi, hogy egy értéket az egész komponensfán elérhetővé tegyünk — anélkül, hogy propként kellene átadni minden szinten.
A három alapelem:
| Elem | Szerepe |
|---|---|
createContext | Létrehoz egy context objektumot |
<Provider> | Becsomagolja a komponensfát, és szolgáltatja az értéket |
useContext | Beolvassa az értéket bármelyik leszármazott komponensből |
3. CartContext megvalósítása
Az egész kosárlogika egy fájlba került: src/context/CartContext.tsx.
// src/context/CartContext.tsx
import {
createContext,
useContext,
useState,
type ReactNode,
} from "react"
import { products, type Product } from "../data/products"
interface CartContextType {
cartItems: Product[]
addToCart: (id: number) => void
}
const CartContext = createContext<CartContextType | null>(null)
A createContext<CartContextType | null>(null) létrehoz egy üres context-et. A null az alapértelmezett érték — akkor lenne érvényes, ha valaki Provider nélkül próbálná használni. Az useCart hookban ezt kezeljük le.
CartProvider
export function CartProvider({ children }: { children: ReactNode }) {
const [cartItems, setCartItems] = useState<Product[]>([])
function addToCart(id: number): void {
const product = products.find((x) => x.id === id)
if (!product) return
setCartItems((state) => [...state, product])
}
return (
<CartContext.Provider value={{ cartItems, addToCart }}>
{children}
</CartContext.Provider>
)
}
A CartProvider egy egyszerű React komponens: tartalmazza a state-et és a logikát, majd a CartContext.Provider-rel közzéteszi az értéket az összes leszármazottjának. A children prop azt jelenti, hogy bármit becsomagolhat — általában az egész alkalmazást.
useCart hook
export function useCart(): CartContextType {
const context = useContext(CartContext)
if (!context) {
throw new Error("useCart csak CartProvideren belül használható")
}
return context
}
Az useCart elrejti az useContext hívást, és egyetlen helyen tartalmaz egy biztonsági ellenőrzést: ha valaki véletlenül CartProvider-en kívül használja, értő hibaüzenetet kap. Ez a szokásos minta egyedi context hookokhoz.
Az useCart tulajdonképpen csak egy wrapper az useContext(CartContext) körül — de mivel TypeScript null-t zár ki és hibát dob, a hívó oldalon már mindig biztos lehet abban, hogy érvényes értéket kap (nem kell ? operátor vagy null-ellenőrzés).
4. CartProvider beillesztése a fába
A Provider-t a lehető legfelső szinten kell elhelyezni — általában a main.tsx-ben, az egész App körül:
// src/main.tsx
import { CartProvider } from "./context/CartContext.tsx"
createRoot(document.getElementById("root")!).render(
<StrictMode>
<ThemeProvider>
<CartProvider>
<App />
</CartProvider>
</ThemeProvider>
</StrictMode>
)
A CartProvider becsomagolja az App-ot — ettől kezdve minden komponens, amely az App fájában van, elérheti a kosár state-t az useCart hookkal.
5. Változások a komponensekben
App.tsx — state nélkül
A régi state és addToCart függvény helyett egyetlen sor:
// régi (gyak8)
const [cartItems, setCartItems] = useState<Product[]>([])
function addToCart(id: number): void { ... }
// új (gyak9)
const { cartItems, addToCart } = useCart()
A Navbar-nak ezentúl nem kell propot átadni — nincs több cartCount vagy cartItems prop:
// régi
<Navbar cartItems={cartItems} />
// új
<Navbar />
Navbar.tsx — prop nélkül
const Navbar = () => {
return (
<header className="sticky top-0 z-10 border-b bg-background">
<div className="mx-auto flex h-16 max-w-5xl items-center justify-between px-4">
<span>Webshop</span>
<ProductSheet />
</div>
</header>
)
}
A Navbar most egyetlen propot sem kap — a ProductSheet maga intézi a kosár elérését.
ProductSheet.tsx — közvetlen context-hozzáférés
const ProductSheet = () => {
const { cartItems } = useCart()
return (
<Sheet>
<SheetTrigger asChild>
<Button>
<ShoppingCart />
Kosár ({cartItems.length})
</Button>
</SheetTrigger>
<SheetContent>
<SheetHeader>
<SheetTitle>Kosár összesítő</SheetTitle>
</SheetHeader>
<div className="grid flex-1 auto-rows-min gap-6 px-4">
{cartItems.map((p, index) => (
<div
key={"product-total-" + index}
className="flex w-full items-center justify-between gap-4"
>
<h4 className="text-lg">{p.name}</h4>
<p className="font-bold">{p.price} Ft</p>
</div>
))}
</div>
<SheetFooter>
<SheetClose asChild>
<Button variant="outline">Kosár bezárása</Button>
</SheetClose>
</SheetFooter>
</SheetContent>
</Sheet>
)
}
A ProductSheet egyetlen sor (useCart()) segítségével közvetlenül hozzáfér a kosárhoz — a Navbar-nak és az App-nak nem kell tudnia erről.
Összefoglalás
Előtte és utána
| Jellemző | Gyak8 (prop drilling) | Gyak9 (Context) |
|---|---|---|
cartItems helye | App state | CartProvider state |
addToCart elérhetősége | prop lánc: App → Navbar → ProductSheet | useCart() bárhonnan |
Navbar props | cartItems: Product[] | nincs |
ProductSheet props | cartItems: Product[] | nincs |
App.tsx | useState + addToCart | const { cartItems, addToCart } = useCart() |
A megvalósítás lépései
1. createContext<T | null>(null) → context létrehozása
2. CartProvider komponens → state + logika + Provider
3. <CartProvider> a main.tsx-ben → az egész App körül
4. useCart() hook → context elérése + null-ellenőrzés
5. useCart() hívás a komponensekben → props helyett
A Context nem helyettesít mindent: ha egy prop csak egy szintet “utazik”, nincs értelme context-be tenni. A Context akkor jön jól, ha ugyanaz az adat több, egymástól független komponensnél is megjelenik — mint a kosár, amelyet a ProductSheet is és a ProductCard is el kell, hogy érjen.