10. gyakorlat – Redux Toolkit bevezetés
Bevezetés
Az előző két gyakorlaton a kosár state-t először propként adogattuk le, majd React Context segítségével tettük elérhetővé. A Context egy kisebb alkalmazáshoz tökéletes — de ahogy az alkalmazás nő, egyre több state kerülhet bele, az update-logika szétszóródik, és nehezebb nyomon követni, melyik komponens változtat mit és mikor.
A Redux egy dedikált state management könyvtár, amely erre ad struktúrált megoldást. Az összes globális state egy helyen él, a módosítások mindig egy jól definiált úton haladnak, és az egész folyamat nyomon követhető.
A mai óra témái:
- A Redux gondolkodásmód — Store, Action, Reducer, Dispatch
- Redux Toolkit — a modern, hivatalosan ajánlott Redux API
- counterSlice — a minta megértése egy egyszerű példán
- cartSlice — a webshop kosár Redux-ban
- useAppDispatch — action küldése egy komponensből
1. A Redux gondolkodásmód
Mielőtt kódot látnánk, érdemes megérteni, hogyan gondolkodik a Redux. A state csak egy meghatározott úton változhat:
Az adatfolyam mindig egyirányú:
- A komponens meghívja a
dispatch(action)függvényt - Az action leírja, mi történt — van egy neve (
type) és opcionálisan adatot visz (payload) - A reducer megkapja az action-t és kiszámolja az új state-t
- A store tárolja az eredményt — az egész alkalmazásban elérhető
Ez az egyirányú folyamat teszi a Redux state-változásokat kiszámíthatóvá és nyomon követhetővé.
2. Miért Redux a Context helyett?
A Context API-t az előző órán tanultuk — miért van szükség valamire mellé?
| Szempont | Context | Redux Toolkit |
|---|---|---|
| Célközönség | Kisebb, ritkán változó globális state | Komplex, sokat változó state |
| Update-logika helye | Szétszórva a Providerekben | Egy slice-ban összefogva |
| Debuggolhatóság | Nehéz nyomon követni | Redux DevTools: minden action loggolva |
| TypeScript-integráció | Kézzel kell gondozni | PayloadAction<T> + generált típusok |
A kettő nem zárja ki egymást. A mai projektben a Context még bent maradt — ez egy valós refaktoring folyamat: párhuzamosan él a régi és az új megoldás, és fokozatosan cserélődik le.
3. Telepítés
npm install @reduxjs/toolkit react-redux
@reduxjs/toolkit— a Redux modern, opinionált API-ja (createSlice,configureStore)react-redux— a React–Redux összekötő (Provider,useDispatch,useSelector)
4. A minta: counterSlice
Mielőtt a webshopba érünk, nézzük meg a Redux-mintát a lehető legegyszerűbb példán: egy számlálón.
// src/store/counterSlice.ts
import { createSlice, type PayloadAction } from '@reduxjs/toolkit'
export interface CounterState {
value: number
}
const initialState: CounterState = {
value: 0
}
export const counterSlice = createSlice({
name: 'counter',
initialState,
reducers: {
increment: state => {
state.value += 1
},
decrement: state => {
state.value -= 1
},
incrementByAmount: (state, action: PayloadAction<number>) => {
state.value += action.payload
}
}
})
export const { increment, decrement, incrementByAmount } = counterSlice.actions
export default counterSlice.reducer
A createSlice egy helyen definiálja a state típusát, a kezdeti értéket és az összes reducer-t. A függvény neve (increment, decrement) lesz az action type értéke — nem kell string konstansokat kézzel írni.
Mit csinál a PayloadAction<number>?
Amikor egy action adatot is visz magával, a payload típusát PayloadAction<T>-vel deklaráljuk:
// dispatch híváskor:
dispatch(incrementByAmount(5))
// az action, amit a reducer megkap:
{ type: 'counter/incrementByAmount', payload: 5 }
A Redux Toolkit Immer könyvtárat használ a háttérben — ezért írhatunk state.value += 1-et úgy, mintha közvetlenül mutálnánk az objektumot. Valójában Immer egy új, immutable state-et hoz létre. Soha ne mutáld a state-et Reduxon kívül.
5. A store összeállítása
A store a Redux “adatbázisa” — az összes slice-t összefogjuk benne:
// src/store/store.ts
import { configureStore } from '@reduxjs/toolkit'
import cartReducer from './cartSlice'
export const store = configureStore({
reducer: {
cart: cartReducer
}
})
export type RootState = ReturnType<typeof store.getState>
export type AppDispatch = typeof store.dispatch
export type AppStore = typeof store
A reducer objektum kulcsai adják meg a store alakját. Ha a cart reducert adjuk meg cart kulcson, a state így néz ki:
{
cart: {
items: [...]
}
}
A RootState és AppDispatch TypeScript típusok — ezeket fogjuk használni a típusbiztos hook-okban.
6. Típusbiztos hookok
A useDispatch és useSelector alapból nem ismeri a store típusát. A megoldás: egyszer definiálunk typed verziókat, és ezeket használjuk mindenhol:
// src/store/hooks.ts
import { useDispatch, useSelector } from 'react-redux'
import type { AppDispatch, RootState } from './store'
export const useAppDispatch = useDispatch.withTypes<AppDispatch>()
export const useAppSelector = useSelector.withTypes<RootState>()
Ezentúl az alkalmazásban nem useDispatch-t, hanem useAppDispatch-et importálunk.
7. Provider a main.tsx-ben
A Redux store-t a Provider komponenssel tesszük elérhetővé az egész alkalmazásban — ugyanúgy, mint a Context Provider-t:
// src/main.tsx
import { Provider } from "react-redux"
import { store } from "./store/store.ts"
createRoot(document.getElementById("root")!).render(
<StrictMode>
<ThemeProvider>
<CartProvider>
<Provider store={store}>
<App />
</Provider>
</CartProvider>
</ThemeProvider>
</StrictMode>
)
A CartProvider és a Redux Provider most párhuzamosan él. Ez a fokozatos átállás természetes állapota — a következő órákon a Context kivezethető, ha a Redux átveszi a szerepét.
8. cartSlice — a webshop kosár Redux-ban
A számlálóval megértettük a mintát, most alkalmazzuk a webshopra. Az első változás az adatmodellben van:
// src/data/products.ts
export interface Product {
id: number
name: string
price: number
emoji: string
}
export interface CartItem extends Product {
quantity: number
}
A CartItem örökli a Product összes mezőjét, és hozzáad egy quantity számot. Ez az a modell, amelyről a gyak8-ban még lemondtunk — most a Redux reducer elég egyszerűen tudja kezelni.
// src/store/cartSlice.ts
import { createSlice, type PayloadAction } from '@reduxjs/toolkit'
import type { CartItem, Product } from '@/data/products'
export interface CartState {
items: CartItem[]
}
const initialState: CartState = {
items: []
}
export const cartSlice = createSlice({
name: 'cart',
initialState,
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 })
}
}
}
})
export const { addItem } = cartSlice.actions
export default cartSlice.reducer
Az addItem reducer két esetet kezel:
- Ha a termék már a kosárban van, növeli a
quantity-t - Ha nem, hozzáadja
quantity: 1-gyel
Ezt a logikát korábban a useCart hookban vagy az App-ban kellett tartani. Most a slice-ban van — pontosan ott, ahol az adatot kezeljük.
9. dispatch az action — ProductCard
Egy komponensből actiont küldeni három lépés:
// src/components/ProductCard.tsx
import { useAppDispatch } from "@/store/hooks"
import { addItem } from "@/store/cartSlice"
const ProductCard = ({ id, name, price, emoji, addToCart }: ProductCardProps) => {
const dispatch = useAppDispatch()
return (
<Card>
{/* ... */}
<CardFooter>
<Button
className="w-full"
onClick={() => dispatch(addItem({ id, name, price, emoji }))}
>
Kosárba
</Button>
</CardFooter>
</Card>
)
}
useAppDispatch()— megkapjuk a dispatch függvénytaddItem({ id, name, price, emoji })— action creator meghívása: ez adja apayload-otdispatch(...)— elküldjük az action-t a store-nak; a reducer kiszámolja az új state-t
A gombra kattintás lehetővé teszi az egész folyamatot: dispatch → addItem action → cartSlice reducer → store frissül.
Összefoglalás
A Redux adatfolyam a webshopban
ProductCard gomb kattintás
↓
dispatch(addItem({ id, name, price, emoji }))
↓
cartSlice reducer: addItem(state, action)
→ ha már van: existing.quantity += 1
→ ha új: state.items.push({ ...product, quantity: 1 })
↓
store frissül: { cart: { items: [...] } }
↓
(következő óra: useSelector → komponens újrarenderelődik)
Fájlok és szerepük
| Fájl | Szerepe |
|---|---|
store/store.ts | Store összeállítása, RootState és AppDispatch típusok |
store/hooks.ts | Típusbiztos useAppDispatch és useAppSelector |
store/cartSlice.ts | CartState, addItem reducer, action creator |
store/counterSlice.ts | Tanítási példa — a Redux-minta számlálón |
main.tsx | <Provider store={store}> — store elérhetővé tétele |
components/ProductCard.tsx | useAppDispatch + dispatch(addItem(...)) |
Amit a következő órán tanulunk
Az action elküldése után a store frissül — de a komponens még nem látja az új értéket. Ehhez kell a useSelector: ez olvassa ki az adatot a store-ból, és automatikusan újrarendereli a komponenst, ha az az adat megváltozik.