English flagEnglish

9. gyakorlat – React Context és useCart hook

2026-04-19 4 perc olvasási idő GitHub

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ő
  • useCart hook — 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 cartItems tí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:

ElemSzerepe
createContextLétrehoz egy context objektumot
<Provider>Becsomagolja a komponensfát, és szolgáltatja az értéket
useContextBeolvassa 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 />
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 helyeApp stateCartProvider state
addToCart elérhetőségeprop lánc: App → Navbar → ProductSheetuseCart() bárhonnan
Navbar propscartItems: Product[]nincs
ProductSheet propscartItems: Product[]nincs
App.tsxuseState + addToCartconst { 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.