English flagEnglish

11. gyakorlat – REST API, Postman és RTK Query

2026-05-04 11 perc olvasási idő GitHub

Bevezetés

Az előző gyakorlaton megismertük a Redux Toolkit alapjait: a Store, Action, Reducer és Dispatch fogalmakat, és a cartSlice-szal kezeltük a kosár state-et. Ma ezt a tudást kiterjesztjük: bekötjük az alkalmazást egy valódi REST API-ba.

A mai óra három nagy témára épül:

  • REST API alapok — HTTP metódusok, státuszkódok, JSON, hitelesítési módszerek
  • Postman — az API kipróbálása kód nélkül: Swagger importálás, Bearer token, environment változók
  • RTK Query — az API kommunikáció Redux-ba integrálva: query, mutation, automatikus cache és loading állapotok

A mai óra után képes leszel:

  • Megmondani, mit jelent egy GET /api/products, POST /api/orders vagy POST /api/auth/login kérés
  • Postmanben Bearer tokenes hitelesítéssel meghívni egy védett végpontot
  • createApi és fetchBaseQuery segítségével RTK Query API-t definiálni
  • useGetProductsQuery-val termékeket betölteni, loading és error állapotokat kezelni
  • usePlaceOrderMutation-nel rendelést leadni, a visszajelzést megjeleníteni
  • useLoginMutation + dispatch(setCredentials(...)) kombinációval bejelentkezési folyamatot implementálni

A projekt elindítása

A mai projekt két részből áll: egy backend szerverből és a React frontendjéből.

1. Backend (REST API)

cd gyak8_handout/rest-api
npm install
npm run db:setup   # csak első alkalommal — létrehozza az adatbázist és betölti a seed adatokat
npm run dev        # http://localhost:3000

A Swagger UI — ahol az összes végpont kipróbálható kód nélkül — elérhető: http://localhost:3000/docs

2. Frontend (React + Redux)

cd gyak8_handout/webshop-mf5m7s
npm install
npm run dev        # http://localhost:5173

Az alkalmazás betölt és renderel, de placeholder adatokkal dolgozik. A mai feladat: bekötni a Redux cartSlice hiányzó részeit és az API kommunikációt.


1. REST API alapok

Mielőtt kódot írnánk, értjük meg, mit hívunk meg — és miért úgy, ahogy.

Mi az a REST API?

A REST (Representational State Transfer) egy szerver és egy kliens közötti kommunikációs stílus. A szerver erőforrásokat (resources) kínál URL-eken keresztül, a kliens pedig HTTP-kérésekkel éri el őket.

Az erőforrás lehet bármi: termék, felhasználó, rendelés. Az URL azonosítja az erőforrást, a HTTP metódus megmondja, mit akarunk csinálni vele:

HTTP metódusJelentésPélda
GETLekérdez — nem módosít semmitGET /api/products — termékek listája
POSTLétrehoz egy új erőforrástPOST /api/orders — új rendelés
PUTTeljes csere — az egész erőforrást felülírjaPUT /api/products/1 — termék módosítása
PATCHRészleges módosítás — csak a megadott mezőket változtatjaPATCH /api/products/1
DELETETörlésDELETE /api/products/1
ℹ️

A REST nem szabvány, hanem konvenció. A szerver nem köteles pontosan ezeket a szabályokat követni — de egy jól tervezett REST API igen.

HTTP státuszkódok

Minden API válasz tartalmaz egy státuszkódot, amely megmondja, mi történt:

KódKategóriaMikor látod
200 OKSikerGET kérés sikeres
201 CreatedSikerPOST — az erőforrás létrejött
400 Bad RequestKliens hibaHiányzó vagy érvénytelen adatok a kérésben
401 UnauthorizedKliens hibaNincs érvényes hitelesítés (token hiányzik vagy lejárt)
403 ForbiddenKliens hibaVan token, de nincs jogosultság az erőforráshoz
404 Not FoundKliens hibaA keresett erőforrás nem létezik
500 Internal Server ErrorSzerver hibaA szerveren valami elromlott

Az 1xx5xx tartomány öt csoportra osztható: 2xx mindig jót jelent, 4xx a kliens hibázott, 5xx a szerver hibázott.

Kérés és válasz felépítése

Egy HTTP kérés három részből áll:

POST /api/auth/login
Content-Type: application/json
Authorization: Bearer abc123token

{
  "email": "admin@webshop.dev",
  "password": "admin"
}
RészMagyarázat
URL + metódusMit akarunk, hol
Headers (fejlécek)Metaadatok — pl. tartalom típusa, hitelesítési token
Body (törzs)Az elküldött adatok — csak POST, PUT, PATCH esetén van

A válasz hasonló felépítésű: státuszkód + fejlécek + JSON törzs.

Ha egy végpont védett, a szervernek tudnia kell, ki küldi a kérést. Erre két elterjedt módszer létezik:

Bearer token (fejlécben):

Authorization: Bearer eyJhbGciOi...

A bejelentkezéskor kapott tokent minden kérésnél az Authorization fejlécbe kell tenni. A szerver ellenőrzi, érvényes-e.

  • A tokent a kliensnek kell tárolnia (pl. Redux store, localStorage)
  • Minden kérésnél manuálisan kell csatolni
  • Jól működik SPA-kban és mobilalkalmazásokban

Cookie alapú hitelesítés:

Set-Cookie: auth_token=abc123; HttpOnly; SameSite=Lax

A szerver a bejelentkezéskor egy HttpOnly sütit állít be a böngészőben. A böngésző ezt automatikusan elküldi minden kérésnél.

  • A fejlesztőnek nem kell foglalkoznia a token tárolásával — a böngésző kezeli
  • A JavaScript nem tudja elolvasni (HttpOnly miatt) — véd az XSS ellen
  • CORS esetén a credentials: 'include' beállítás szükséges

A mai projektben mindkét módszer megvalósítva van. A kódban kommentekkel jelölve látod, melyik sor melyik megközelítéshez tartozik.

💡

Egyszerű hüvelykujjszabály: ha az alkalmazásodnak más domainű API-val kell kommunikálnia, Bearer token az egyszerűbb. Ha az API és a frontend azonos domainen fut (pl. Next.js), a cookie alapú megközelítés kényelmesebb.


2. Postman — az API kipróbálása kód nélkül

Mielőtt React-ban kódolnánk az API hívásokat, érdemes kipróbálni őket egy dedikált eszközzel. A Postman erre való: HTTP kéréseket küldhetünk, a válaszokat megtekinthetjük, és az egész folyamatot elmenthetjük.

OpenAPI / Swagger importálása

A mai szerver egy Swagger UI-t kínál: http://localhost:3000/docs. Ez egy interaktív dokumentáció, ahol az összes végpont kipróbálható közvetlenül a böngészőből.

A Swagger dokumentációt exportálhatjuk és Postmanbe importálhatjuk:

  1. Nyisd meg a http://localhost:3000/docs oldalt a böngészőben
  2. Keresd meg az openapi.json linket a Swagger UI tetején, és töltsd le a fájlt — ez tartalmazza az összes végpont leírását géppel olvasható formátumban
  3. Postmanben: Import gomb → húzd be a fájlt
  4. Postman automatikusan létrehozza a végpontok gyűjteményét (Collection)

Environment változók

A Postman Environment segítségével elkerüljük, hogy minden kérésben kézzel gépeljük be az URL-t és a tokent.

  1. Postmanben kattints a Environments fülre (bal oldalt)
  2. Hozz létre egy új environment-et: pl. webshop-local
  3. Add hozzá a következő változókat:
VáltozóÉrték
baseUrlhttp://localhost:3000/api
token(egyelőre üres — a bejelentkezés után töltjük ki)
  1. A kérésekben a változókat dupla kapcsos zárójelbe téve hivatkozhatsz rájuk: {{baseUrl}}/products

Bearer token beállítása

  1. Hívd meg a POST {{baseUrl}}/auth/login végpontot:
    • Body → raw → JSON:
    { "email": "admin@webshop.dev", "password": "admin" }
  2. A válaszban megjelenik a token mező — másold ki az értékét
  3. Az Environment-ben illesszd be a token változó értékeként
  4. A védett végpontoknál: Authorization fül → Type: Bearer Token → Token: {{token}}

Mostantól minden védett kérés automatikusan a megfelelő tokent küldi.

💡

A Postmanben is elvégezheted a CRUD tesztelést: próbáld meg meghívni a GET /api/orders végpontot token nélkül — 401-et kapsz. Majd add hozzá a tokent, és 200-at kapsz.


3. A projekt struktúrája

webshop-mf5m7s/
└── src/
    ├── App.tsx                  # BrowserRouter, Routes (/, /cart, /admin)
    ├── main.tsx                 # Redux Provider becsomagolás
    ├── store/
    │   ├── store.ts             # configureStore — cart + auth + webshopApi
    │   ├── cartSlice.ts         ← TODO [1]: removeItem, clearCart, selectorok
    │   ├── authSlice.ts         # setCredentials, logout — kész
    │   ├── webshopApi.ts        ← TODO [2] + [3]: RTK Query API
    │   └── hooks.ts             # useAppDispatch, useAppSelector — típusos hookök
    ├── components/
    │   ├── Navbar.tsx           ← TODO [1]: useAppSelector a kosár számlálóhoz
    │   └── RequireAuth.tsx      # Login form + route guard — kész
    └── pages/
        ├── ProductsPage.tsx     ← TODO [2]: useGetProductsQuery bekötése
        ├── CartPage.tsx         ← TODO [1] + [3]: selector + usePlaceOrderMutation
        └── AdminPage.tsx        # Rendelések listája — kész

A store.ts a három részt köti össze:

// src/store/store.ts
import { configureStore } from '@reduxjs/toolkit'
import cartReducer from './cartSlice'
import authReducer from './authSlice'
import { webshopApi } from './webshopApi'

export const store = configureStore({
  reducer: {
    cart: cartReducer,
    auth: authReducer,
    [webshopApi.reducerPath]: webshopApi.reducer,  // RTK Query saját slice-ja
  },
  middleware: (getDefaultMiddleware) =>
    getDefaultMiddleware().concat(webshopApi.middleware),  // cache, polling, invalidation
})

export type RootState = ReturnType<typeof store.getState>
export type AppDispatch = typeof store.dispatch
ℹ️

A webshopApi.middleware az RTK Query belső logikájához szükséges: kezeli a cache-elést, az automatikus újralekérdezéseket és a providesTags / invalidatesTags mechanizmust. Nélküle a Query hookök nem működnek.


4. TODO [1] — cartSlice kiegészítése

A cartSlice.ts-ben az addItem reducer már kész. A removeItem reducer és néhány selector hiányzik.

removeItem — törlés

// src/store/cartSlice.ts

reducers: {
  addItem(state, action: PayloadAction<Product>) {
    const product = action.payload
    const existing = state.items.find((p) => p.id === product.id)
    if (existing) {
      existing.quantity += 1
    } else {
      state.items.push({ ...product, quantity: 1 })
    }
  },

  // Kommenteld ki (távolítsd el a // jeleket):
  removeItem(state, action: PayloadAction<number>) {
    state.items = state.items.filter((i) => i.id !== action.payload)
  },

  clearCart(state) {
    state.items = []
  },
},
ℹ️

Figyeld meg, hogy az Immer-rel írt reducer közvetlen mutációnak tűnik (state.items = ...), de az Immer a háttérben immutábilis update-et végez. Nem kell [...state.items] másolatot csinálni.

Szelektorok és akció exportok

// src/store/cartSlice.ts — a slice után

export const { addItem, removeItem, clearCart } = cartSlice.actions

// selectCart — a kosár teljes tartalma
export const selectCart = (state: RootState) => state.cart.items

// selectCartTotal — a végösszeg
export const selectCartTotal = (state: RootState) =>
  state.cart.items.reduce((sum, i) => sum + i.price * i.quantity, 0)

A szelektoros minta (selector) szétválasztja az adathozzáférési logikát a komponensektől. Ha a state struktúrája megváltozik, csak a selectort kell frissíteni — az összes komponens, amely azt használja, automatikusan frissül.

// src/components/Navbar.tsx
import { useAppSelector } from '@/store/hooks'
import { selectCart } from '@/store/cartSlice'

const Navbar = () => {
  const cartItems = useAppSelector(selectCart)
  const totalItems = cartItems.reduce((sum, i) => sum + i.quantity, 0)

  // ...
  // A gomb feliratában: Kosár ({totalItems})
}

CartPage — törlés és dispatch

// src/pages/CartPage.tsx
import { useAppDispatch, useAppSelector } from '@/store/hooks'
import { selectCart, selectCartTotal, removeItem, clearCart } from '@/store/cartSlice'

const CartPage = () => {
  const dispatch = useAppDispatch()
  const items = useAppSelector(selectCart)
  const total = useAppSelector(selectCartTotal)

  // ...

  // Törlés gomb onClick-jén:
  onClick={() => dispatch(removeItem(item.id))}
}

5. Aszinkron JavaScript: Promise és async/await

A REST API hívások — és általában minden hálózati kommunikáció — aszinkron műveletek. Mielőtt rátérünk az RTK Query-re, meg kell érteni, hogyan kezeli a JavaScript az aszinkron kódot.

Szinkron vs. aszinkron kód

A JavaScript alapvetően egyszálú: egyszerre csak egy dolgot csinál. Ha egy szinkron művelet sokáig tart (pl. egy adatbázis-lekérdezés), az egész program megáll és vár.

// Szinkron — a második sor CSAK akkor fut, ha az első végzett
const eredmeny = szamitasEzer()   // 2 másodpercig fut
console.log(eredmeny)             // megvárja a fenti sort

Az aszinkron kód ezt feloldja: a hosszú műveletet “elindítja”, a program fut tovább, és majd értesítést kap, ha az eredmény megérkezett.

// Aszinkron — a fetchData fut, de a program nem vár rá
fetchData()                        // elindul, de nem blokkolja
console.log('Ez azonnal lefut')    // fetch bevégzése előtt jelenik meg!

Mi az a Promise?

Egy Promise egy aszinkron művelet jövőbeli eredményét képviseli. Olyan, mint egy “ígérvény”: “majd adok neked egy értéket, de most még dolgozom rajta”.

Egy Promise háromféle állapotban lehet:

ÁllapotJelentés
pendingFolyamatban — az eredmény még nem ismert
fulfilledSikeres — az eredmény elérhető
rejectedHibás — valami elromlott

A fetch API például Promist ad vissza:

// fetch() azonnal visszatér egy Promise-szal — nem vár a válaszra
const promise = fetch('https://api.example.com/products')
console.log(promise)  // Promise { <pending> }

Az eredményt a .then() / .catch() metódusokkal lehet “elkapni”:

fetch('https://api.example.com/products')
  .then(response => response.json())   // ha megérkezett, parse-olja
  .then(data => console.log(data))     // ha parse kész, feldolgozza
  .catch(error => console.error(error)) // ha bármi hibázott

Ez a Promise-láncolás — minden .then() egy újabb Promise-t ad vissza. Működik, de mélyen egymásba ágyazva nehezen olvasható.

async / await — a modern megközelítés

Az async/await szintaktikai cukor a Promise-ok fölé: ugyanazt csinálja, de szinkron kódhoz hasonlóan olvasható.

Szabályok:

  1. Az await csak async függvényen belül használható
  2. Az await megvárja a Promise teljesülését, de NEM blokkolja a böngészőt
  3. A hibát try/catch-csel kell kezelni
// Ugyanaz, mint a fenti .then()/.catch() lánc — de sokkal olvashatóbb
async function termekeketTolt() {
  try {
    const response = await fetch('https://api.example.com/products')
    const data = await response.json()
    console.log(data)             // [{ id: 1, name: '...' }, ...]
  } catch (error) {
    console.error('Hiba:', error)  // hálózati hiba, CORS, stb.
  }
}

termekeketTolt()  // meghívás — a függvény aszinkron fut
ℹ️

Az await csak a saját függvény futását függeszti fel — a böngésző többi teendőjét (scroll, kattintás, animáció) nem blokkolja. Ez az aszinkron modell lényege.

Trivial példa: időzítő

A legtriviálisabb Promise egy setTimeout-ból:

// Csinálunk egy Promise-t, amely 1 másodperc múlva teljesül
function varakozas(ms) {
  return new Promise(resolve => setTimeout(resolve, ms))
}

async function sorrend() {
  console.log('1. lépés')
  await varakozas(1000)        // 1 másodpercig vár
  console.log('2. lépés')     // csak ezután fut
  await varakozas(500)
  console.log('3. lépés')
}

sorrend()
// Kimenet (idővel):
// 1. lépés
// (1 mp szünet)
// 2. lépés
// (0.5 mp szünet)
// 3. lépés

React event handlerekben

Egy onClick handler is lehet aszinkron — pontosan így épül a mai projekt rendelésleadó és bejelentkezési logikája:

// Sima szinkron handler
function handleClick() {
  console.log('Kattintás!')
}

// Aszinkron handler — ugyanolyan szintaxissal adható át
async function handlePlaceOrder() {
  try {
    const result = await placeOrder({ items: [...] })   // megvárja a választ
    dispatch(clearCart())                               // csak siker után fut
  } catch {
    // hibakezelés
  }
}

<Button onClick={handlePlaceOrder}>Rendelés leadása</Button>
⚠️

Az onClick={async () => { await ... }} szintaxis is helyes — a React nem foglalkozik azzal, hogy a handler aszinkron-e. Azonban az aszinkron event handlerekben keletkező kivételeket a React nem kapja el — mindig írj try/catch-et, ha await-et használsz eseménykezelőben.


6. RTK Query — API kommunikáció Redux-ban

Az előző gyakorlatokon a fetch API-t ismertük meg. Az RTK Query egy magasabb szintű eszköz: a kérés állapotát (loading, error, data) automatikusan a Redux store-ban tárolja, cache-eli a válaszokat, és React hookokat generál.

createApi és fetchBaseQuery

Az egész API-t egyetlen createApi hívással definiáljuk:

// src/store/webshopApi.ts
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
import type { RootState } from './store'

export const webshopApi = createApi({
  reducerPath: 'webshopApi',       // a Redux slice neve
  tagTypes: ['Auth'],              // cache invalidáció tagekhez

  baseQuery: fetchBaseQuery({
    baseUrl: 'http://localhost:3000/api',

    // Bearer token: minden kérésnél hozzáfűzi az Authorization fejlécet
    prepareHeaders: (headers, { getState }) => {
      const token = (getState() as RootState).auth.token
      if (token) headers.set('Authorization', `Bearer ${token}`)
      return headers
    },

    // Cookie alapú megoldás (alternatíva):
    // credentials: 'include',
  }),

  endpoints: (builder) => ({
    // ... ide jönnek a végpontok
  }),
})

A prepareHeaders callback fut minden egyes kérés előtt — előveszi a Redux store-ból az aktuális tokent, és beilleszti az Authorization fejlécbe. Így soha nem kell manuálisan csatolni a tokent a kérésekhez.

Környezeti változók — .env és Vercel

A baseUrl-t ne égessük be a kódba. Ha az alkalmazást Vercelre deployoljuk, a backend valószínűleg más URL-en fut, mint localhost:3000.

1. lépés — .env fájl létrehozása a projekt gyökerében:

# .env
VITE_API_URL=http://localhost:3000/api
⚠️

Vite projektekben a környezeti változókat VITE_ előtaggal kell ellátni, különben nem lesznek elérhetők a böngészőben futó kódból. A process.env Vite-ban nem működik — helyette import.meta.env kell.

2. lépés — Használd a baseUrl-ban:

// src/store/webshopApi.ts
baseQuery: fetchBaseQuery({
  baseUrl: import.meta.env.VITE_API_URL ?? 'http://localhost:3000/api',
  // ...
}),

A ?? (nullish coalescing) operátor: ha a környezeti változó nincs beállítva (undefined), a localhost-os URL-re esik vissza — így helyi fejlesztés közben az .env fájl nélkül is működik.

3. lépés — Vercel konfigurálása:

  1. A Vercel projektben nyisd meg a Settings → Environment Variables oldalt
  2. Add hozzá: VITE_API_URL = a backend éles URL-je (pl. https://api.sajatdomain.com/api)
  3. Deployolás után a Vite build automatikusan beégeti az értéket a bundle-be
ℹ️

Az .env fájlt add hozzá a .gitignore-hoz, ha érzékeny adatokat tartalmaz. Közös fejlesztésnél érdemes létrehozni egy .env.example fájlt a kötelező változók listájával (értékek nélkül), amit beversionálsz.

Query vs. Mutation

Az RTK Query kétféle végpontot különböztet meg:

QueryMutation
HTTP metódusGET (általában)POST, PUT, PATCH, DELETE
Mire valóAdatok lekéréseAdatok módosítása, létrehozása
Hook neveuse[Endpoint]Queryuse[Endpoint]Mutation
Visszatér{ data, isLoading, isError }[triggerFn, { isLoading, data, error }]
Mikor fut?Automatikusan, mountoláskorCsak ha manuálisan hívod a trigger függvényt
CacheIgen — azonos paraméternél nem kér le újraNem cache-el

7. TODO [2] — getProducts: termékek betöltése API-ból

Az endpoint definiálása

// src/store/webshopApi.ts — endpoints belül
getProducts: builder.query<Product[], void>({
  query: () => `/products`
  // => GET http://localhost:3000/api/products
}),

A generikus paraméterek: <ReturnType, ArgumentType>. A void azt jelenti, hogy a querynak nincs paramétere.

Hook exportálása

export const {
  useGetProductsQuery,
  // ... többi hook
} = webshopApi

Bekötés a komponensbe

// src/pages/ProductsPage.tsx
import { useGetProductsQuery } from '@/store/webshopApi'

const ProductsPage = () => {
  const { data: products = [], isLoading, isError } = useGetProductsQuery()

  if (isLoading) {
    return (
      <main className="max-w-5xl mx-auto px-4 py-8">
        <p className="text-muted-foreground">Termékek betöltése...</p>
      </main>
    )
  }

  if (isError) {
    return (
      <main className="max-w-5xl mx-auto px-4 py-8">
        <p className="text-destructive">Hiba a termékek betöltésekor. Elindult a szerver?</p>
      </main>
    )
  }

  return (
    <main className="max-w-5xl mx-auto px-4 py-8">
      <h1 className="text-3xl font-bold mb-8">Termékek</h1>
      <div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
        {products.map((product) => (
          <ProductCard key={product.id} {...product} />
        ))}
      </div>
    </main>
  )
}

A data: products = [] egy alias + default érték: a hook data mezőjét products névvel olvassuk, és ha még nem érkezett meg az adat (pl. loading alatt), üres tömbként kezeljük — így nem kapunk undefined hibát a map-nél.

💡

Nyisd meg a böngésző Network fülét (F12 → Network), és figyeld meg a GET /api/products kérést. Az RTK Query cache-eli a választ — ha más komponens is meghívja ugyanezt a queryt, nem indul új kérés, hanem a tárolt adatot kapja vissza.


8. TODO [3] — placeOrder: rendelés leadása

Az endpoint definiálása

// src/store/webshopApi.ts — endpoints belül
placeOrder: builder.mutation<Order, { items: { productId: number; quantity: number }[] }>({
  query: (body) => ({
    url: '/orders',
    method: 'POST',
    body,  // RTK Query automatikusan JSON-ná alakítja és beállítja a Content-Type fejlécet
  })
}),

A mutation generikus paraméterei: <ReturnType, ArgumentType>. A body értékét a hook trigger függvénye kapja majd meg.

Hook exportálása

export const {
  useGetProductsQuery,
  usePlaceOrderMutation,
  // ... többi hook
} = webshopApi

Bekötés a komponensbe

// src/pages/CartPage.tsx
import { usePlaceOrderMutation } from '@/store/webshopApi'

const CartPage = () => {
  const dispatch = useAppDispatch()
  const items = useAppSelector(selectCart)
  const total = useAppSelector(selectCartTotal)

  const [placeOrder, { isLoading, data: confirmedOrder, reset }] = usePlaceOrderMutation()

  // Rendelés visszaigazolása — ha a mutation sikeres volt
  if (confirmedOrder) {
    return (
      <main className="max-w-5xl mx-auto px-4 py-8">
        <div className="rounded-lg border p-8 text-center space-y-4">
          <p className="text-4xl"></p>
          <h2 className="text-2xl font-bold">Rendelés leadva!</h2>
          <p className="text-muted-foreground">
            Azonosító: <span className="font-mono font-bold">#{confirmedOrder.id}</span>
          </p>
          <p className="text-muted-foreground">
            Összeg: <span className="font-bold">{confirmedOrder.total.toLocaleString('hu-HU')} Ft</span>
          </p>
          <Button onClick={() => reset()} variant="outline">Új rendelés</Button>
        </div>
      </main>
    )
  }

  async function handlePlaceOrder() {
    await placeOrder({
      items: items.map((i) => ({ productId: i.id, quantity: i.quantity }))
    })
    dispatch(clearCart())
  }

  return (
    // ...
    <Button className="w-full" size="lg" onClick={handlePlaceOrder} disabled={isLoading}>
      {isLoading ? 'Feldolgozás...' : 'Rendelés leadása'}
    </Button>
  )
}
ℹ️

A reset() függvény törli a mutation belső állapotát — confirmedOrder visszaáll undefined-ra, így az alkalmazás visszatér a kosár nézetbe. Ez hasznos, ha a felhasználó új rendelést szeretne leadni.

A mutation triggerelés folyamata:

handlePlaceOrder()
  → placeOrder({ items: [...] })      // POST /api/orders
  → szerver válasz: 201 Created + { id, total, ... }
  → confirmedOrder = { id: 42, total: 42800, ... }
  → komponens újrarenderelődik: megjelenik a visszaigazolás
  → dispatch(clearCart())              // kosár kiürítése

9. Hitelesítés — login, token tárolás, védett útvonalak

Az auth flow lépései

[Felhasználó kitölti a login formot]

[login mutation: POST /api/auth/login]

[Szerver visszaadja: { token, user }]

[dispatch(setCredentials({ token, user }))]

[Redux store: auth.token = "abc123", auth.user = { id, email, role }]

[prepareHeaders: minden következő kérésnél: Authorization: Bearer abc123]

[Védett végpontok elérhetők]

authSlice — token és user tárolása

// src/store/authSlice.ts
import { createSlice, type PayloadAction } from '@reduxjs/toolkit'
import type { RootState } from './store'
import type { AuthUser } from './webshopApi'

interface AuthState {
  token: string | null
  user: AuthUser | null
}

export const authSlice = createSlice({
  name: 'auth',
  initialState: { token: null, user: null } as AuthState,
  reducers: {
    setCredentials(state, action: PayloadAction<{ token: string; user: AuthUser }>) {
      state.token = action.payload.token
      state.user = action.payload.user
    },
    logout(state) {
      state.token = null
      state.user = null
    },
  },
})

export const { setCredentials, logout } = authSlice.actions
export const selectToken = (state: RootState) => state.auth.token
export const selectUser = (state: RootState) => state.auth.user

login mutation és setCredentials

// src/store/webshopApi.ts — endpoints belül
login: builder.mutation<{ token: string; user: AuthUser }, { email: string; password: string }>({
  query: (body) => ({ url: '/auth/login', method: 'POST', body }),
  invalidatesTags: ['Auth'],
}),
// src/components/RequireAuth.tsx — LoginForm belül
import { useLoginMutation } from '@/store/webshopApi'
import { setCredentials } from '@/store/authSlice'

function LoginForm() {
  const dispatch = useAppDispatch()
  const [login, { isLoading, error }] = useLoginMutation()
  const [email, setEmail] = useState('')
  const [password, setPassword] = useState('')

  async function handleSubmit(e: React.FormEvent) {
    e.preventDefault()
    try {
      const result = await login({ email, password }).unwrap()
      dispatch(setCredentials({ token: result.token, user: result.user }))
    } catch {
      // a hibát az RTK Query `error` state-je kezeli — megjelenik a form alatt
    }
  }

  // ...
}

A .unwrap() fontos részlet: a mutation trigger alapból sosem dob hibát (a hiba az RTK Query error mezőjében jelenik meg). Az .unwrap() viszont kidobja a hibát kivételként, így a try/catch mintát használhatjuk — és a catch blokkban nem kell mást tenni, mert az error state automatikusan frissül.

RequireAuth — route guard

// src/components/RequireAuth.tsx
import { useAppSelector } from '@/store/hooks'
import { selectToken } from '@/store/authSlice'

const RequireAuth = ({ children }: { children: ReactNode }) => {
  const token = useAppSelector(selectToken)

  // Bearer token alapú: ha nincs token a store-ban, mutatja a login formot
  if (!token) {
    return <LoginForm />
  }

  return <>{children}</>
}

Az AdminPage-ben becsomagolva:

// src/pages/AdminPage.tsx
const AdminPage = () => (
  <RequireAuth>
    <OrderList />
  </RequireAuth>
)

Ha a felhasználó nincs bejelentkezve (token === null), a RequireAuth a login formot rendereli a gyerekek helyett. Bejelentkezés után a token bekerül a store-ba, a RequireAuth újrarenderelődik, és megjelenik az <OrderList />.

⚠️

A token a Redux store-ban (memóriában) él — oldal frissítéskor elvész! Éles alkalmazásban a tokent localStorage-ba kell menteni, és mountoláskor visszaolvasni. A cookie alapú megközelítésnél ez a probléma nem áll fenn, mert a böngésző automatikusan kezeli a sütit.

// Bearer token: prepareHeaders (webshopApi.ts)
prepareHeaders: (headers, { getState }) => {
  const token = (getState() as RootState).auth.token
  if (token) headers.set('Authorization', `Bearer ${token}`)
  return headers
},

// Cookie alapú: egyetlen sor az egész hitelesítéshez
credentials: 'include',
SzempontBearer tokenCookie (HttpOnly)
TárolásRedux store / localStorageBöngésző kezeli automatikusan
Frissítés utánElvész (hacsak localStorage-ba nem mentjük)Megmarad
JavaScript elérheti?IgenNem (HttpOnly esetén)
XSS védelemhezGyengébbErősebb
Más domain (CORS)Egyszerűbbcredentials: 'include' + szerver CORS config
Mai projektben✅ ElsődlegesKommentekkel jelölve (alternatíva)

Összefoglalás

A teljes adatfolyam

[Redux Store]
  ├── cart: { items: CartItem[] }          ← cartSlice
  ├── auth: { token, user }                ← authSlice
  └── webshopApi: { queries, mutations }   ← RTK Query (auto)

[Komponens]
  ├── useAppSelector(selectCart)           → olvassa a kosarat
  ├── dispatch(addItem(product))           → módosítja a kosarat
  ├── useGetProductsQuery()                → GET /api/products
  ├── usePlaceOrderMutation()              → POST /api/orders
  └── useLoginMutation()                   → POST /api/auth/login

Amit ma megvalósítottunk

FájlMit csináltunk
store/cartSlice.tsremoveItem, clearCart, selectCart, selectCartTotal hozzáadva
store/webshopApi.tsgetProducts query, placeOrder mutation definiálva
store/authSlice.tssetCredentials, logout — token és user tárolása
components/Navbar.tsxuseAppSelector(selectCart) — kosár számláló Redux-ból
pages/ProductsPage.tsxuseGetProductsQuery() — loading/error állapotokkal
pages/CartPage.tsxuseAppSelector + usePlaceOrderMutation + visszaigazolás
components/RequireAuth.tsxBearer token alapú route guard + login form

RTK Query gyorslap

// Query definiálása
getProducts: builder.query<ReturnType, ArgType>({
  query: (arg) => `/endpoint/${arg}`
})

// Mutation definiálása
placeOrder: builder.mutation<ReturnType, BodyType>({
  query: (body) => ({ url: '/endpoint', method: 'POST', body })
})

// Query használata komponensben
const { data, isLoading, isError } = useGetProductsQuery()

// Mutation használata komponensben
const [placeOrder, { isLoading, data, error }] = usePlaceOrderMutation()
await placeOrder(body).unwrap()