Pas state cuma 1-2 nilai sederhana, useState paling clean. Tapi bayangin shopping cart: tambah item, hapus item, update quantity, terapkan kupon, reset, restore from localStorage. Pakai 5 useState terpisah + 5 setter? Bug magnet — sering lupa sync, race condition antar update. Plus state interrelated (total dihitung dari items + kupon). useReducer sentralisasi state + semua aksi update di SATU function (reducer) — pattern yang udah ada 60 tahun di Lisp/functional programming, populer di React lewat Redux. Update jadi predictable, debug-able, time-travel-able.
💡 Pemberi nyawa Redux. Pas mendalami
useReducer, kamu sebenernya udah belajar 80% Redux. Pattern reducer + dispatch + action object identik. Skill transferable: pahamuseReducer= siap pake Redux Toolkit, Zustand actions, atau state machine library kayak XState.
function reducer(state, action) {
switch (action.type) {
case "increment": return { count: state.count + 1 }
case "decrement": return { count: state.count - 1 }
case "reset": return { count: 0 }
default: return state
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, { count: 0 })
return (
<div>
<span>{state.count}</span>
<button onClick={() => dispatch({ type: "increment" })}>+</button>
<button onClick={() => dispatch({ type: "decrement" })}>-</button>
<button onClick={() => dispatch({ type: "reset" })}>Reset</button>
</div>
)
}
Anatomi Reducer
function reducer(state, action) {
// ^^^^^^ ^^^^^^^
// state action object
// lama { type: "...", payload: ... }
switch (action.type) {
case "X": return /* state baru */
default: return state // SELALU return state lama untuk action gak dikenal
}
}
const [state, dispatch] = useReducer(reducer, initialState)
// ^^^^^ ^^^^^^^^ ^^^^^^^^^^^^
// state function buat trigger nilai awal
// terkini update — dispatch(action)
Aturan reducer: PURE function — gak boleh side effect (fetch, timer, mutate state lama). Cuma return state baru berdasar state lama + action.
Real Use Case: Shopping Cart
function cartReducer(state, action) {
switch (action.type) {
case "ADD_ITEM":
const existing = state.items.find(i => i.id === action.payload.id)
if (existing) {
return {
...state,
items: state.items.map(i =>
i.id === action.payload.id ? {...i, qty: i.qty + 1} : i
)
}
}
return { ...state, items: [...state.items, {...action.payload, qty: 1}] }
case "REMOVE_ITEM":
return { ...state, items: state.items.filter(i => i.id !== action.payload) }
case "UPDATE_QTY":
return {
...state,
items: state.items.map(i =>
i.id === action.payload.id ? {...i, qty: action.payload.qty} : i
)
}
case "APPLY_COUPON":
return { ...state, coupon: action.payload, discount: calcDiscount(action.payload) }
case "CLEAR":
return { items: [], coupon: null, discount: 0 }
default:
return state
}
}
function Cart() {
const [cart, dispatch] = useReducer(cartReducer, { items: [], coupon: null, discount: 0 })
return (
<div>
<button onClick={() => dispatch({ type: "ADD_ITEM", payload: product })}>
Tambah ke Cart
</button>
<button onClick={() => dispatch({ type: "CLEAR" })}>Reset</button>
</div>
)
}
Logic update di SATU tempat (cartReducer), UI tinggal dispatch.
useReducer + Context = Mini Redux
const CartContext = createContext()
function CartProvider({ children }) {
const [cart, dispatch] = useReducer(cartReducer, initial)
return (
<CartContext.Provider value={{ cart, dispatch }}>
{children}
</CartContext.Provider>
)
}
// Di komponen mana saja:
function ProductCard({ product }) {
const { dispatch } = useContext(CartContext)
return <button onClick={() => dispatch({ type: "ADD_ITEM", payload: product })}>Beli</button>
}
State global tanpa Redux library!
🎭 Analogi sehari-hari
useState itu kayak dompet pribadi: kamu sendiri yang tarik/setor uang, gak ada catatan resmi. Cocok buat duit jajan harian. useReducer itu kayak buku tabungan bank: tiap transaksi (action) tercatat dengan jelas — "setor 50rb", "tarik 20rb", "transfer ke X 30rb". Kasir (reducer) yang validasi + eksekusi tiap permintaan. Hasilnya predictable: lihat buku tabungan = paham riwayat lengkap. Bug? Trace transaksi mana yang salah. Cocok buat duit penting yang banyak transaksi: cart, form multi-step, board game state, undo/redo. State kompleks dengan banyak action = buku tabungan yang clear lebih baik dari dompet ngacak.
⚠️ Jebakan yang sering ditemui
- Mutate state di reducer —
state.count++; return stateSALAH. Reducer harus PURE — return state BARU. Pakai spread. - Side effect di reducer —
fetch(),setTimeout(),console.log()di dalam reducer = anti-pattern. Reducer pure (input → output). Side effect di useEffect atau event handler. - Action type typo —
dispatch({ type: "INCREMNT" })(typo) = silent fall to default. Pakai TypeScript atau action constant:const INCREMENT = "increment". - State terlalu nested —
{ ui: { modal: { open: true } } }susah update (deep clone). Flatten kalau bisa, atau pakai library Immer. - Reducer di dalam component — re-create tiap render. Define di luar component (top-level file).
default: return undefined— bug. Selaludefault: return stateuntuk action gak dikenal.- dispatch di useEffect tanpa deps yang jelas — bisa infinite loop. Awas.
🎯
useStatevsuseReducer— pilih mana?
- State 1-2 nilai independen →
useState(counter, toggle, input)- Object dengan 3+ field →
useStatemasih OK kalau update simple- 5+ jenis aksi update atau action interrelated →
useReducer✓- State kompleks dengan validasi update →
useReducer(validasi di reducer)- Multi-step form, wizard →
useReducer(predictable transitions)- Time-travel debugging, undo/redo →
useReducer(action log = history)- Global app state →
useReducer+ Context, atau Zustand/Redux ToolkitAturan praktis: kalau handler udah punya 5+ jenis
setStatecalls dan logic update bertambah rumit — refactor keuseReducer. Komponen jadi declarative ("dispatch action"), logic terpusat (reducer), test-able.
TL;DR: useReducer(reducer, initial) return [state, dispatch]. Reducer = PURE function (state, action) => newState. Update via dispatch({ type: "X", payload }). Cocok untuk state kompleks dengan banyak action atau validasi update. Aturan: reducer gak boleh mutate state lama, gak boleh side effect, default case return state. Combine dengan Context = state global tanpa Redux. Skill transferable ke Redux/Zustand.