11. gyakorlat – REST API, Postman és RTK Query
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/ordersvagyPOST /api/auth/loginkérés - Postmanben Bearer tokenes hitelesítéssel meghívni egy védett végpontot
createApiésfetchBaseQuerysegítségével RTK Query API-t definiálniuseGetProductsQuery-val termékeket betölteni, loading és error állapotokat kezelniusePlaceOrderMutation-nel rendelést leadni, a visszajelzést megjeleníteniuseLoginMutation+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ódus | Jelentés | Példa |
|---|---|---|
GET | Lekérdez — nem módosít semmit | GET /api/products — termékek listája |
POST | Létrehoz egy új erőforrást | POST /api/orders — új rendelés |
PUT | Teljes csere — az egész erőforrást felülírja | PUT /api/products/1 — termék módosítása |
PATCH | Részleges módosítás — csak a megadott mezőket változtatja | PATCH /api/products/1 |
DELETE | Törlés | DELETE /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ód | Kategória | Mikor látod |
|---|---|---|
200 OK | Siker | GET kérés sikeres |
201 Created | Siker | POST — az erőforrás létrejött |
400 Bad Request | Kliens hiba | Hiányzó vagy érvénytelen adatok a kérésben |
401 Unauthorized | Kliens hiba | Nincs érvényes hitelesítés (token hiányzik vagy lejárt) |
403 Forbidden | Kliens hiba | Van token, de nincs jogosultság az erőforráshoz |
404 Not Found | Kliens hiba | A keresett erőforrás nem létezik |
500 Internal Server Error | Szerver hiba | A szerveren valami elromlott |
Az 1xx–5xx 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ész | Magyarázat |
|---|---|
| URL + metódus | Mit 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.
Hitelesítés: Bearer token vs. Cookie
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 (
HttpOnlymiatt) — 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:
- Nyisd meg a
http://localhost:3000/docsoldalt a böngészőben - Keresd meg az
openapi.jsonlinket 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 - Postmanben: Import gomb → húzd be a fájlt
- 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.
- Postmanben kattints a Environments fülre (bal oldalt)
- Hozz létre egy új environment-et: pl.
webshop-local - Add hozzá a következő változókat:
| Változó | Érték |
|---|---|
baseUrl | http://localhost:3000/api |
token | (egyelőre üres — a bejelentkezés után töltjük ki) |
- 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
- Hívd meg a
POST {{baseUrl}}/auth/loginvégpontot:- Body → raw → JSON:
{ "email": "admin@webshop.dev", "password": "admin" } - A válaszban megjelenik a
tokenmező — másold ki az értékét - Az Environment-ben illesszd be a
tokenváltozó értékeként - 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.
Navbar — kosár darabszám
// 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:
| Állapot | Jelentés |
|---|---|
pending | Folyamatban — az eredmény még nem ismert |
fulfilled | Sikeres — az eredmény elérhető |
rejected | Hibá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:
- Az
awaitcsakasyncfüggvényen belül használható - Az
awaitmegvárja a Promise teljesülését, de NEM blokkolja a böngészőt - 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:
- A Vercel projektben nyisd meg a Settings → Environment Variables oldalt
- Add hozzá:
VITE_API_URL= a backend éles URL-je (pl.https://api.sajatdomain.com/api) - 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:
| Query | Mutation | |
|---|---|---|
| HTTP metódus | GET (általában) | POST, PUT, PATCH, DELETE |
| Mire való | Adatok lekérése | Adatok módosítása, létrehozása |
| Hook neve | use[Endpoint]Query | use[Endpoint]Mutation |
| Visszatér | { data, isLoading, isError } | [triggerFn, { isLoading, data, error }] |
| Mikor fut? | Automatikusan, mountoláskor | Csak ha manuálisan hívod a trigger függvényt |
| Cache | Igen — azonos paraméternél nem kér le újra | Nem 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 vs. Cookie — összehasonlítás
// 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',
| Szempont | Bearer token | Cookie (HttpOnly) |
|---|---|---|
| Tárolás | Redux store / localStorage | Böngésző kezeli automatikusan |
| Frissítés után | Elvész (hacsak localStorage-ba nem mentjük) | Megmarad |
| JavaScript elérheti? | Igen | Nem (HttpOnly esetén) |
| XSS védelemhez | Gyengébb | Erősebb |
| Más domain (CORS) | Egyszerűbb | credentials: 'include' + szerver CORS config |
| Mai projektben | ✅ Elsődleges | Kommentekkel 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ájl | Mit csináltunk |
|---|---|
store/cartSlice.ts | removeItem, clearCart, selectCart, selectCartTotal hozzáadva |
store/webshopApi.ts | getProducts query, placeOrder mutation definiálva |
store/authSlice.ts | setCredentials, logout — token és user tárolása |
components/Navbar.tsx | useAppSelector(selectCart) — kosár számláló Redux-ból |
pages/ProductsPage.tsx | useGetProductsQuery() — loading/error állapotokkal |
pages/CartPage.tsx | useAppSelector + usePlaceOrderMutation + visszaigazolás |
components/RequireAuth.tsx | Bearer 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()