English flagEnglish

10. gyakorlat – Redux Toolkit bevezetés

2026-04-26 5 perc olvasási idő GitHub

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:

A Redux egyirányú adatfolyam: Komponens → dispatch → Action → Reducer → Store → useSelector (következő óra)
A Redux adatfolyam. A szaggatott vonal (useSelector) a következő órán kerül sorra.
A Redux egyirányú adatfolyam: Komponens → dispatch → Action → Reducer → Store → useSelector (következő óra)

A Redux adatfolyam. A szaggatott vonal (useSelector) a következő órán kerül sorra.

Az adatfolyam mindig egyirányú:

  1. A komponens meghívja a dispatch(action) függvényt
  2. Az action leírja, mi történt — van egy neve (type) és opcionálisan adatot visz (payload)
  3. A reducer megkapja az action-t és kiszámolja az új state-t
  4. 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é?

SzempontContextRedux Toolkit
CélközönségKisebb, ritkán változó globális stateKomplex, sokat változó state
Update-logika helyeSzétszórva a ProviderekbenEgy slice-ban összefogva
DebuggolhatóságNehéz nyomon követniRedux DevTools: minden action loggolva
TypeScript-integrációKézzel kell gondozniPayloadAction<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>
  )
}
  1. useAppDispatch() — megkapjuk a dispatch függvényt
  2. addItem({ id, name, price, emoji }) — action creator meghívása: ez adja a payload-ot
  3. dispatch(...) — 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: dispatchaddItem 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ájlSzerepe
store/store.tsStore összeállítása, RootState és AppDispatch típusok
store/hooks.tsTípusbiztos useAppDispatch és useAppSelector
store/cartSlice.tsCartState, addItem reducer, action creator
store/counterSlice.tsTanítási példa — a Redux-minta számlálón
main.tsx<Provider store={store}> — store elérhetővé tétele
components/ProductCard.tsxuseAppDispatch + 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.