useReducer a reducer pattern
Za každým komplexním nástrojem stojí jednoduchý princip. Stejně jako je věta složená ze slov a dům z cihel, je i useReducer postavený na základním funkcionálním vzoru: reduceru.
- •Co je reducer pattern v JavaScriptu a jak souvisí s Array.prototype.reduce
- •Jak se liší práce se stavem v čase v Reactu oproti práci s poli
- •Jak navrhnout reducer funkci a akce pro složitější stav
- •Jak oddělit logiku (reducer) od pohledu (komponenta) pomocí useReducer
- •Kdy použít useReducer místo useState v reálných aplikacích
1. Základ v JavaScriptu
Než začneme psát React s useReducer, musíme pochopit mechanismus v JavaScriptu, na kterém je postavený: reducer pattern, známý z Array.prototype.reduce.
Představte si jednoduchý e‑shop košík. Máte pole položek, z nichž každá má svou cenu:
const cart = [
{ item: "Book", price: 10 },
{ item: "Pen", price: 2 },
{ item: "Notebook", price: 5 },
]Triviální způsob, jak spočítat celkovou částku, je použít smyčku, která mění externí proměnnou:
let total = 0 // External state
cart.forEach(product => {
total += product.price // Mutation
})
console.log(total) // 17To sice funguje, ale je to nečisté: funkce, která zpracovává každou položku, čte i mění total – proměnnou mimo svůj scope. Ve větších aplikacích takové roztroušené mutace ztěžují pochopení, kde a jak se stav mění.
Reducer pattern to řeší tak, že stav i transformační logika zůstanou uvnitř čisté funkce:
const cart = [
{ item: "Book", price: 10 },
{ item: "Pen", price: 2 },
{ item: "Notebook", price: 5 },
]
// The reducer function:
// accepts current state (accumulatedTotal) and the current item (product)
function cartReducer(accumulatedTotal, product) {
return accumulatedTotal + product.price
}
const initialTotal = 0
const total = cart.reduce(cartReducer, initialTotal)
console.log(total) // 17Funkce cartReducer je čistá: nečte ani nemění nic mimo sebe. Dostane aktuální stav (akumulátor), aktuální hodnotu (product) a vrátí nový stav. To je celý reducer pattern v jediné větě:
nextState = reducer(currentState, value)
2. Posun v Reactu: stav v čase
V JavaScriptu reduce prochází pole hodnot, které existují současně. V Reactu useReducer pracuje se sekvencí akcí, které přicházejí v čase.
Místo pole produktů si představte proud uživatelských interakcí: kliknutí, stisky kláves, přepínače, odeslání formuláře. Každá interakce je modelovaná jako akce. Reducer vezme aktuální stav UI a poslední akci a vrátí nový stav:
nextState = reducer(currentState, action)
React hook useReducer zrcadlí Array.prototype.reduce, ale místo iterace přes pole reaguje na postupně přicházející akce po celou dobu života komponenty:
const [state, dispatch] = React.useReducer(reducer, initialState)
// state: current snapshot of your data
// dispatch: function you call to send an action
// reducer: pure function deciding how state changes
// initialState: starting value for the state machine3. Reálný příklad: RPG postava
Odpoutejme se od jednoduchých čítačů. Představme si herní HUD pro postavu, jejíž stav zahrnuje životy (HP), manu (MP) a příznak, zda je naživu.
Budeme řešit tři druhy akcí: zranění, léčení a seslání kouzla (které spotřebovává manu). Reducer se stane jediným zdrojem pravdy o tom, jak tyto akce mění stav.
const initialState = {
hp: 100,
mp: 50,
isAlive: true,
}
type GameAction =
| { type: "TAKE_DAMAGE"; amount: number }
| { type: "HEAL" }
| { type: "CAST_SPELL" }
function gameReducer(state: typeof initialState, action: GameAction) {
// If the character is dead, most actions stop having any effect.
if (!state.isAlive && action.type !== "HEAL") {
return state
}
switch (action.type) {
case "TAKE_DAMAGE": {
const newHp = state.hp - action.amount
return {
...state,
hp: newHp > 0 ? newHp : 0,
isAlive: newHp > 0,
}
}
case "HEAL": {
return {
...state,
hp: Math.min(state.hp + 20, 100), // Cap HP at 100
isAlive: true,
}
}
case "CAST_SPELL": {
if (state.mp < 10) return state // Not enough mana
return {
...state,
mp: state.mp - 10,
}
}
default: {
return state
}
}
}Všimněte si, že reducer je zcela soběstačný: dostane stav a akci a vrátí nový stav. Neví nic o tlačítkách, kliknutích ani o DOMu.
Komponenta pak pouze překládá uživatelské interakce na akce. Sama neprovádí žádné počítání se stavem – jen „hlásí, co se stalo“:
import React from "react"
export default function CharacterProfile() {
const [state, dispatch] = React.useReducer(gameReducer, initialState)
return (
<div className="card space-y-4">
<h2>
Hero Status:{" "}
<span className={state.isAlive ? "text-emerald-500" : "text-red-500"}>
{state.isAlive ? "Alive" : "Fallen"}
</span>
</h2>
<p>
HP: {state.hp} | MP: {state.mp}
</p>
<div className="flex gap-2">
<button
onClick={() => dispatch({ type: "TAKE_DAMAGE", amount: 15 })}
>
Attack Enemy (take 15 damage)
</button>
<button onClick={() => dispatch({ type: "CAST_SPELL" })}>
Cast Fireball (cost 10 MP)
</button>
<button onClick={() => dispatch({ type: "HEAL" })}>
Drink Potion (+20 HP)
</button>
</div>
</div>
)
}Toto čisté oddělení – komponenty dispatchují akce, reducer rozhoduje o vývoji stavu – škáluje mnohem lépe než rozházené volání setState a ad‑hoc logika napříč stromem.
4. Kdy použít useReducer vs useState
Scénář A: Složitý, provázaný stav
Klasický příklad je načítání dat: často sledujete loading, error a data. Tyto tři kusy stavu jsou úzce provázané – rozhodně byste neměli být „loading“ a zároveň zobrazovat chybu.
// With separate useState calls, you manually juggle invariants:
setLoading(false)
setError(null)
setData(result)S reducerem místo toho popíšete, co se stalo, a samotný reducer zajistí, že kombinace příznaků zůstane konzistentní:
type FetchState<T> = {
loading: boolean
error: string | null
data: T | null
}
type FetchAction<T> =
| { type: "FETCH_START" }
| { type: "FETCH_SUCCESS"; payload: T }
| { type: "FETCH_ERROR"; error: string }
function fetchReducer<T>(
state: FetchState<T>,
action: FetchAction<T>,
): FetchState<T> {
switch (action.type) {
case "FETCH_START":
return { loading: true, error: null, data: null }
case "FETCH_SUCCESS":
return { loading: false, error: null, data: action.payload }
case "FETCH_ERROR":
return { loading: false, error: action.error, data: null }
default:
return state
}
}
// Somewhere in a component:
// dispatch({ type: "FETCH_SUCCESS", payload: result })Scénář B: Únik z dependency pasti useEffectu
Představte si semafor, který každé dvě sekundy cykluje Zelená → Oranžová → Červená → Zelená. Naivní implementace s useState často skončí efektem, který s každým renderem znovu vytváří vlastní interval:
// ❌ Problematic: effect depends on currentLight, so it tears
function TrafficLightWithState() {
const [currentLight, setCurrentLight] = React.useState<"green" | "yellow" | "red">("green")
React.useEffect(() => {
const id = window.setInterval(() => {
setCurrentLight((prev) =>
prev === "green" ? "yellow" : prev === "yellow" ? "red" : "green",
)
}, 2000)
return () => window.clearInterval(id)
}, [currentLight]) // Effect restarts every time the light changes
return <div className={`light ${currentLight}`} />
}Protože je currentLight v dependency poli, efekt při každém tiknutí interval zruší a vytvoří nový. To může vést k jemným chybám v časování i k plýtvání výkonem.
S useReducer je dispatch stabilní (nemění se), takže efekt může běžet jen jednou a jednoduše dispatchovat obecnou akci „NEXT_LIGHT“:
type Light = "green" | "yellow" | "red"
type LightAction = { type: "NEXT_LIGHT" }
function lightReducer(state: Light, _action: LightAction): Light {
if (state === "green") return "yellow"
if (state === "yellow") return "red"
return "green"
}
function TrafficLight() {
const [light, dispatch] = React.useReducer(lightReducer, "green")
React.useEffect(() => {
const id = window.setInterval(() => {
// We don't care what the current light is here,
// we just say: move to the next state.
dispatch({ type: "NEXT_LIGHT" })
}, 2000)
return () => window.clearInterval(id)
}, []) // ✅ Stable: effect runs only once
return <div className={`light ${light}`} />
}Tento vzor – přechody stavu patří do reduceru, efekty a handlery pouze dispatchují akce – vede k předvídatelnému chování a jednodušším dependency polím.
Shrnutí
useReducer není jen „chytřejší useState“, ale způsob, jak zorganizovat změny stavu kolem čisté reducer funkce, zatímco komponenty pouze popisují, co se stalo.
Staví na stejném reducer patternu, který znáte z Array.prototype.reduce, a poskytuje přirozený nástroj pro modelování složitého, provázaného stavu bez křehké sítě setState volání.
Kdykoli máte stav s více poli, která se mění společně, připomíná stavový automat nebo trpí na problémy s dependency poli v useEffect, stojí za otázku: nebylo by to přehlednější jako reducer?