useReducer — React

Pas state cuma 1-2 nilai sederhana, useState paling clean. Tapi bayangin shopping cart: tambah item, hapus item, update quantity, terapkan kupon, reset, restore

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: paham useReducer = 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

🎯 useState vs useReducer — pilih mana?

  • State 1-2 nilai independenuseState (counter, toggle, input)
  • Object dengan 3+ fielduseState masih OK kalau update simple
  • 5+ jenis aksi update atau action interrelateduseReducer
  • State kompleks dengan validasi updateuseReducer (validasi di reducer)
  • Multi-step form, wizarduseReducer (predictable transitions)
  • Time-travel debugging, undo/redouseReducer (action log = history)
  • Global app stateuseReducer + Context, atau Zustand/Redux Toolkit

Aturan praktis: kalau handler udah punya 5+ jenis setState calls dan logic update bertambah rumit — refactor ke useReducer. 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.

Yang akan kamu pelajari