Bab 5: Antrian Update State

6 menit baca

Masalah yang Kita Hadapi

Di bab sebelumnya, kita lihat masalah ini:

jsx
function Counter() {
  const [angka, setAngka] = useState(0);

  function handleKlik() {
    setAngka(angka + 1); // 0 + 1 = 1
    setAngka(angka + 1); // 0 + 1 = 1 (masih pakai snapshot!)
    setAngka(angka + 1); // 0 + 1 = 1 (masih pakai snapshot!)
  }
  // Hasil: angka = 1, bukan 3!
}

Kita mau angka naik 3, tapi cuma naik 1. Karena semua angka di handler itu snapshot (= 0). Gimana solusinya?

Jawabannya: Updater Function.


Updater Function: Antrian yang Rapi

Alih-alih passing nilai langsung ke setState, kamu bisa passing fungsi. Fungsi ini menerima state sebelumnya sebagai parameter dan mengembalikan state baru.

jsx
function Counter() {
  const [angka, setAngka] = useState(0);

  function handleKlik() {
    setAngka(n => n + 1); // "ambil nilai sebelumnya, tambah 1"
    setAngka(n => n + 1); // "ambil nilai sebelumnya, tambah 1"
    setAngka(n => n + 1); // "ambil nilai sebelumnya, tambah 1"
  }
  // Hasil: angka = 3! ✓
}

Sekarang hasilnya benar! Tapi kenapa?


Analogi: Antrian di Bank

Bayangin kamu di bank. Ada 3 orang antri di teller:

Cara lama (passing nilai langsung):

  • Orang 1: "Set saldo jadi Rp 100.000 + Rp 10.000"
  • Orang 2: "Set saldo jadi Rp 100.000 + Rp 10.000" (baca saldo yang sama!)
  • Orang 3: "Set saldo jadi Rp 100.000 + Rp 10.000" (baca saldo yang sama!)
  • Hasil akhir: Rp 110.000 (bukan Rp 130.000)

Cara baru (updater function):

  • Orang 1: "Ambil saldo SAAT INI, tambah Rp 10.000"
  • Orang 2: "Ambil saldo SAAT INI, tambah Rp 10.000"
  • Orang 3: "Ambil saldo SAAT INI, tambah Rp 10.000"
  • Hasil akhir: Rp 130.000 ✓

Updater function itu kayak instruksi: "Apapun saldo saat ini, tambah segini." Dia nggak peduli berapa saldonya sekarang, dia cuma tau operasinya.


Cara React Memproses Antrian

Saat kamu panggil setState berkali-kali, React memasukkan semuanya ke antrian (queue). Setelah event handler selesai, React memproses antrian satu per satu.

Contoh dengan Nilai Langsung

jsx
// State awal: angka = 0
setAngka(5);     // Antrian: [ganti jadi 5]
setAngka(5);     // Antrian: [ganti jadi 5, ganti jadi 5]

React proses:

  1. "Ganti jadi 5" → angka = 5
  2. "Ganti jadi 5" → angka = 5
  3. Hasil akhir: 5

Contoh dengan Updater Function

jsx
// State awal: angka = 0
setAngka(n => n + 1);  // Antrian: [n => n + 1]
setAngka(n => n + 1);  // Antrian: [n => n + 1, n => n + 1]
setAngka(n => n + 1);  // Antrian: [n => n + 1, n => n + 1, n => n + 1]

React proses:

  1. n => n + 1 dengan n = 0 → angka = 1
  2. n => n + 1 dengan n = 1 → angka = 2
  3. n => n + 1 dengan n = 2 → angka = 3
  4. Hasil akhir: 3

Contoh Campuran (Nilai + Updater)

jsx
// State awal: angka = 0
setAngka(angka + 5);   // Antrian: [ganti jadi 5]
setAngka(n => n + 1);  // Antrian: [ganti jadi 5, n => n + 1]
setAngka(42);          // Antrian: [ganti jadi 5, n => n + 1, ganti jadi 42]

React proses:

  1. "Ganti jadi 5" → angka = 5
  2. n => n + 1 dengan n = 5 → angka = 6
  3. "Ganti jadi 42" → angka = 42
  4. Hasil akhir: 42

Perhatiin: "ganti jadi nilai" itu menimpa apapun sebelumnya. Updater function membangun di atas nilai sebelumnya.


Tabel Referensi: Cara React Memproses Queue

Update yang di-queuen (nilai masuk)ReturnHasil
setAngka(5)(diabaikan)55
setAngka(n => n + 1)566
setAngka(n => n * 2)61212
setAngka(0)(diabaikan)00

Aturannya simpel:

  • Nilai langsung (number, string, dll): langsung replace, abaikan nilai sebelumnya
  • Updater function: terima nilai sebelumnya, return nilai baru

Konvensi Penamaan Parameter Updater

Parameter di updater function biasanya dinamai berdasarkan huruf pertama state-nya, atau singkatan yang masuk akal:

jsx
const [angka, setAngka] = useState(0);
setAngka(n => n + 1);        // 'n' dari 'angka' (number)
setAngka(prev => prev + 1);  // 'prev' = previous (umum dipakai)

const [umur, setUmur] = useState(25);
setUmur(u => u + 1);         // 'u' dari 'umur'
setUmur(prev => prev + 1);   // 'prev' juga oke

const [items, setItems] = useState([]);
setItems(prev => [...prev, itemBaru]);  // 'prev' untuk array

const [enabled, setEnabled] = useState(false);
setEnabled(e => !e);          // 'e' dari 'enabled'
setEnabled(prev => !prev);    // 'prev' juga oke

Yang paling umum dipakai:

  • prev — universal, selalu jelas
  • Huruf pertama state — n untuk angka, c untuk count, dll
  • Nama pendek yang deskriptif — prevItems, oldCount

Pilih yang paling mudah dibaca di konteks kamu. Yang penting konsisten.


Kapan Pakai Updater vs Nilai Langsung?

Pakai Updater Function Kalau:

1. Update bergantung pada nilai sebelumnya:

jsx
// Counter — tambah/kurang dari nilai sekarang
setAngka(n => n + 1);
setAngka(n => n - 1);

// Toggle boolean
setAktif(prev => !prev);

// Tambah item ke array
setItems(prev => [...prev, itemBaru]);

// Hapus item dari array
setItems(prev => prev.filter(item => item.id !== idHapus));

2. Multiple update dalam satu handler:

jsx
function handleKlikCepat() {
  // Kalau mau increment 3x, HARUS pakai updater
  setAngka(n => n + 1);
  setAngka(n => n + 1);
  setAngka(n => n + 1);
}

3. Update di dalam setTimeout/setInterval:

jsx
// Di dalam timer, state bisa sudah berubah
setTimeout(() => {
  setAngka(n => n + 1); // Selalu pakai nilai terbaru
}, 3000);

setInterval(() => {
  setDetik(d => d + 1); // Selalu increment dari nilai terbaru
}, 1000);

Pakai Nilai Langsung Kalau:

1. Set ke nilai yang nggak bergantung pada state sebelumnya:

jsx
// Reset ke nilai tertentu
setAngka(0);
setNama('');
setItems([]);

// Set berdasarkan input user
setNama(e.target.value);
setUmur(Number(e.target.value));

// Set berdasarkan data dari server
setUser(dataFromServer);

2. Replace seluruh state:

jsx
// Ganti total, nggak peduli nilai lama
setFormData({ nama: 'Budi', email: 'budi@mail.com' });

Contoh Praktis: Keranjang Belanja

jsx
import { useState } from 'react';

function KeranjangBelanja() {
  const [items, setItems] = useState([]);
  const [total, setTotal] = useState(0);

  const menuWarung = [
    { id: 1, nama: 'Kopi Susu', harga: 25000 },
    { id: 2, nama: 'Es Teh Manis', harga: 10000 },
    { id: 3, nama: 'Roti Bakar', harga: 15000 },
    { id: 4, nama: 'Nasi Goreng', harga: 20000 },
  ];

  function handleTambah(menu) {
    // Pakai updater karena bergantung pada items sebelumnya
    setItems(prev => {
      const existing = prev.find(item => item.id === menu.id);
      if (existing) {
        // Kalau sudah ada, tambah jumlahnya
        return prev.map(item =>
          item.id === menu.id
            ? { ...item, jumlah: item.jumlah + 1 }
            : item
        );
      }
      // Kalau belum ada, tambah baru
      return [...prev, { ...menu, jumlah: 1 }];
    });

    // Total juga pakai updater
    setTotal(prev => prev + menu.harga);
  }

  function handleKurang(menu) {
    setItems(prev => {
      const existing = prev.find(item => item.id === menu.id);
      if (existing && existing.jumlah === 1) {
        // Kalau jumlah 1, hapus dari keranjang
        return prev.filter(item => item.id !== menu.id);
      }
      // Kalau lebih dari 1, kurangi jumlah
      return prev.map(item =>
        item.id === menu.id
          ? { ...item, jumlah: item.jumlah - 1 }
          : item
      );
    });

    setTotal(prev => prev - menu.harga);
  }

  function handleReset() {
    // Reset — pakai nilai langsung karena nggak bergantung pada sebelumnya
    setItems([]);
    setTotal(0);
  }

  return (
    <div>
      <h2>Menu Warung</h2>
      {menuWarung.map(menu => (
        <div key={menu.id} style={{ marginBottom: '8px' }}>
          <span>{menu.nama} - Rp {menu.harga.toLocaleString()}</span>
          <button onClick={() => handleTambah(menu)}>➕</button>
        </div>
      ))}

      <hr />
      <h2>Keranjang</h2>
      {items.length === 0 ? (
        <p>Keranjang kosong</p>
      ) : (
        <ul>
          {items.map(item => (
            <li key={item.id}>
              {item.nama} x{item.jumlah} = Rp {(item.harga * item.jumlah).toLocaleString()}
              <button onClick={() => handleKurang(item)}>➖</button>
              <button onClick={() => handleTambah(item)}>➕</button>
            </li>
          ))}
        </ul>
      )}

      <p><strong>Total: Rp {total.toLocaleString()}</strong></p>
      <button onClick={handleReset}>Kosongkan Keranjang</button>
    </div>
  );
}

Contoh: Rapid Fire Button

jsx
import { useState } from 'react';

function RapidFire() {
  const [skor, setSkor] = useState(0);

  function handleRapidKlik() {
    // Simulasi "rapid fire" — 10 increment sekaligus
    for (let i = 0; i < 10; i++) {
      setSkor(prev => prev + 1); // Updater: selalu pakai nilai terbaru
    }
  }

  function handleRapidKlikSalah() {
    // Ini SALAH — cuma naik 1
    for (let i = 0; i < 10; i++) {
      setSkor(skor + 1); // Snapshot: selalu pakai nilai yang sama
    }
  }

  return (
    <div>
      <p style={{ fontSize: '48px' }}>Skor: {skor}</p>
      <button onClick={handleRapidKlik}>
        Rapid Fire +10 (Benar)
      </button>
      <button onClick={handleRapidKlikSalah}>
        Rapid Fire +10 (Salah — cuma +1)
      </button>
      <button onClick={() => setSkor(0)}>Reset</button>
    </div>
  );
}

Replacing vs Updating: Kapan Pakai Yang Mana

Ini ringkasan visual:

┌─────────────────────────────────────────────────────┐ │ REPLACING (Nilai Langsung) │ │ │ │ setAngka(5) → "Ganti jadi 5, titik." │ │ setNama('Budi') → "Ganti jadi Budi, titik." │ │ setItems([]) → "Ganti jadi array kosong." │ │ │ │ Cocok untuk: reset, set dari input, set dari API │ ├─────────────────────────────────────────────────────┤ │ UPDATING (Updater Function) │ │ │ │ setAngka(n => n+1) → "Tambah 1 dari sekarang" │ │ setAktif(a => !a) → "Balik dari sekarang" │ │ setItems(i => [...i, x]) → "Tambah x ke yang ada" │ │ │ │ Cocok untuk: increment, toggle, append, filter │ └─────────────────────────────────────────────────────┘

Deep Dive: Bagaimana Queue Diproses

Mari kita trace secara detail bagaimana React memproses queue:

jsx
function ContohKompleks() {
  const [angka, setAngka] = useState(0);

  function handleKlik() {
    setAngka(angka + 5);     // A: replace dengan 5
    setAngka(n => n + 1);    // B: updater, n + 1
    setAngka(n => n * 2);    // C: updater, n * 2
  }
}

Queue setelah handler selesai: [5, n => n + 1, n => n * 2]

Proses:

StepQueue ItemNilai MasukOperasiHasil
15 (replace)0 (state awal)Ganti jadi 55
2n => n + 1 (updater)55 + 16
3n => n * 2 (updater)66 * 212

Hasil akhir: angka = 12

Satu lagi:

jsx
function ContohLain() {
  const [angka, setAngka] = useState(0);

  function handleKlik() {
    setAngka(n => n + 1);    // A: updater
    setAngka(n => n + 1);    // B: updater
    setAngka(n => n + 1);    // C: updater
    setAngka(0);             // D: replace!
    setAngka(n => n + 1);    // E: updater
  }
}

Proses:

StepQueue ItemNilai MasukOperasiHasil
1n => n + 100 + 11
2n => n + 111 + 12
3n => n + 122 + 13
40 (replace)3Ganti jadi 00
5n => n + 100 + 11

Hasil akhir: angka = 1

Perhatiin step 4: replace "menimpa" semua progress sebelumnya!


Pola Umum dengan Updater

Toggle

jsx
const [buka, setBuka] = useState(false);

// Toggle: balik nilai boolean
function handleToggle() {
  setBuka(prev => !prev);
}

Increment/Decrement dengan Batas

jsx
const [stok, setStok] = useState(10);

function handleBeli() {
  setStok(prev => {
    if (prev <= 0) return 0; // Jangan kurang dari 0
    return prev - 1;
  });
}

function handleRestock() {
  setStok(prev => {
    if (prev >= 100) return 100; // Jangan lebih dari 100
    return prev + 10;
  });
}

Append ke Array

jsx
const [pesan, setPesan] = useState([]);

function handleKirim(pesanBaru) {
  setPesan(prev => [...prev, {
    id: Date.now(),
    teks: pesanBaru,
    waktu: new Date().toLocaleTimeString()
  }]);
}

Remove dari Array

jsx
const [todos, setTodos] = useState([
  { id: 1, teks: 'Beli kopi' },
  { id: 2, teks: 'Bikin laporan' },
]);

function handleHapus(id) {
  setTodos(prev => prev.filter(todo => todo.id !== id));
}

Update Item di Array

jsx
const [produk, setProduk] = useState([
  { id: 1, nama: 'Kopi', stok: 10 },
  { id: 2, nama: 'Teh', stok: 5 },
]);

function handleKurangiStok(id) {
  setProduk(prev => prev.map(item => {
    if (item.id === id) {
      return { ...item, stok: item.stok - 1 };
    }
    return item;
  }));
}

⚠️ Jebakan

Jebakan 1: Campur aduk replace dan updater tanpa sadar

jsx
// ❌ Bug halus: replace di tengah menghapus progress updater sebelumnya
function handleKlik() {
  setAngka(n => n + 1);  // 0 → 1
  setAngka(n => n + 1);  // 1 → 2
  setAngka(angka + 1);   // angka = 0 (snapshot!), jadi replace ke 1!
  setAngka(n => n + 1);  // 1 → 2
}
// Hasil: 2, bukan 4!

// ✅ Konsisten pakai updater
function handleKlik() {
  setAngka(n => n + 1);  // 0 → 1
  setAngka(n => n + 1);  // 1 → 2
  setAngka(n => n + 1);  // 2 → 3
  setAngka(n => n + 1);  // 3 → 4
}
// Hasil: 4 ✓

Jebakan 2: Updater function harus return nilai

jsx
// ❌ Lupa return — state jadi undefined!
setAngka(n => {
  n + 1; // Ini statement, bukan return!
});

// ✅ Explicit return
setAngka(n => {
  return n + 1;
});

// ✅ Atau implicit return (arrow function tanpa curly braces)
setAngka(n => n + 1);

Jebakan 3: Side effect di dalam updater

jsx
// ❌ JANGAN lakukan side effect di updater!
setAngka(n => {
  console.log('Ini bisa dipanggil berkali-kali!'); // Side effect!
  fetch('/api/log'); // Side effect!
  return n + 1;
});

// ✅ Side effect di luar updater
console.log('Logging...');
fetch('/api/log');
setAngka(n => n + 1);

Updater function harus pure — cuma hitung dan return nilai baru. Jangan fetch data, jangan console.log (kecuali debugging), jangan ubah variabel luar.

Jebakan 4: Mengira updater selalu diperlukan

jsx
// ❌ Overkill — updater nggak perlu di sini
function handleInputChange(e) {
  setNama(prev => e.target.value); // prev nggak dipakai!
}

// ✅ Cukup pakai nilai langsung
function handleInputChange(e) {
  setNama(e.target.value); // Lebih simpel dan jelas
}

Pakai updater HANYA kalau kamu butuh nilai sebelumnya. Kalau nggak butuh, pakai nilai langsung aja.


🏋️ Challenge

Challenge 1: Prediksi Hasil

Tanpa menjalankan kode, prediksi nilai akhir angka setelah tombol diklik (state awal = 0):

jsx
function Quiz() {
  const [angka, setAngka] = useState(0);

  function handleKlik() {
    setAngka(n => n + 2);   // A
    setAngka(n => n * 3);   // B
    setAngka(10);           // C
    setAngka(n => n - 1);   // D
  }

  return <button onClick={handleKlik}>{angka}</button>;
}
💡 Hint

Proses queue dari atas ke bawah. Updater pakai hasil sebelumnya. Replace langsung ganti tanpa peduli sebelumnya.

✅ Solusi
StepQueue ItemNilai MasukOperasiHasil
An => n + 200 + 22
Bn => n * 322 * 36
C10 (replace)6Ganti jadi 1010
Dn => n - 11010 - 19

Hasil akhir: angka = 9

Step C (replace) menimpa semua progress dari A dan B. Tapi D (updater) masih bisa membangun di atas hasil C.

Challenge 2: Counter dengan Fitur Undo

Buat counter yang:

  • Bisa increment (+1) dan decrement (-1)
  • Punya tombol "Undo" yang mengembalikan ke nilai sebelumnya
  • Simpan history perubahan (minimal 5 langkah terakhir)
💡 Hint

Simpan history sebagai array di state terpisah. Setiap kali angka berubah, push nilai lama ke history. Undo = pop dari history dan set sebagai angka baru.

✅ Solusi
jsx
import { useState } from 'react';

function CounterDenganUndo() {
  const [angka, setAngka] = useState(0);
  const [history, setHistory] = useState([]);

  function handleIncrement() {
    setHistory(prev => [...prev, angka].slice(-5)); // Simpan max 5
    setAngka(n => n + 1);
  }

  function handleDecrement() {
    setHistory(prev => [...prev, angka].slice(-5));
    setAngka(n => n - 1);
  }

  function handleUndo() {
    if (history.length === 0) return;
    
    // Ambil nilai terakhir dari history
    const nilaiSebelumnya = history[history.length - 1];
    setHistory(prev => prev.slice(0, -1)); // Hapus item terakhir
    setAngka(nilaiSebelumnya); // Replace ke nilai lama
  }

  return (
    <div>
      <p style={{ fontSize: '48px' }}>{angka}</p>
      <button onClick={handleDecrement}>-1</button>
      <button onClick={handleIncrement}>+1</button>
      <button onClick={handleUndo} disabled={history.length === 0}>
        Undo ({history.length})
      </button>
      <p>History: [{history.join(', ')}]</p>
    </div>
  );
}

Challenge 3: Traffic Light (Lampu Lalu Lintas)

Buat simulasi lampu lalu lintas:

  • 3 state: Merah → Hijau → Kuning → Merah → ...
  • Tombol "Next" untuk pindah ke state berikutnya
  • Tombol "Auto" yang otomatis ganti setiap 2 detik
  • Tombol "Stop" untuk menghentikan auto
  • Tampilkan warna yang sesuai (bulatan merah/kuning/hijau)
💡 Hint

Pakai updater function untuk cycle: merah → hijau → kuning → merah. Untuk auto mode, pakai setInterval dengan updater function (karena di dalam interval, state bisa sudah berubah).

✅ Solusi
jsx
import { useState, useRef } from 'react';

function TrafficLight() {
  const [warna, setWarna] = useState('merah');
  const [autoMode, setAutoMode] = useState(false);
  const intervalRef = useRef(null);

  const urutan = ['merah', 'hijau', 'kuning'];

  function getWarnaBerikutnya(warnaSekarang) {
    const index = urutan.indexOf(warnaSekarang);
    const indexBerikutnya = (index + 1) % urutan.length;
    return urutan[indexBerikutnya];
  }

  function handleNext() {
    // Updater: ambil warna sekarang, ganti ke berikutnya
    setWarna(prev => getWarnaBerikutnya(prev));
  }

  function handleAuto() {
    if (autoMode) return; // Sudah jalan
    setAutoMode(true);

    intervalRef.current = setInterval(() => {
      // HARUS pakai updater! Kalau pakai 'warna' langsung,
      // dia akan selalu pakai snapshot saat handleAuto dipanggil
      setWarna(prev => getWarnaBerikutnya(prev));
    }, 2000);
  }

  function handleStop() {
    if (intervalRef.current) {
      clearInterval(intervalRef.current);
      intervalRef.current = null;
    }
    setAutoMode(false);
  }

  const warnaCSS = {
    merah: '#ff0000',
    kuning: '#ffcc00',
    hijau: '#00cc00'
  };

  return (
    <div style={{ textAlign: 'center' }}>
      <div style={{
        width: '100px',
        height: '100px',
        borderRadius: '50%',
        backgroundColor: warnaCSS[warna],
        margin: '20px auto',
        border: '3px solid #333',
        boxShadow: `0 0 20px ${warnaCSS[warna]}`
      }} />
      
      <p style={{ fontSize: '24px', textTransform: 'uppercase' }}>
        {warna}
      </p>

      <button onClick={handleNext} disabled={autoMode}>
        Next
      </button>
      <button onClick={handleAuto} disabled={autoMode}>
        Auto
      </button>
      <button onClick={handleStop} disabled={!autoMode}>
        Stop
      </button>
    </div>
  );
}

Perhatiin: di setInterval, kita HARUS pakai updater setWarna(prev => ...). Kalau pakai setWarna(getWarnaBerikutnya(warna)), warna akan selalu "merah" (snapshot saat handleAuto dipanggil), dan lampu akan stuck bolak-balik merah → hijau → merah → hijau.


Ringkasan

  • Updater function (n => n + 1) memungkinkan update berdasarkan nilai sebelumnya
  • React memproses semua setState sebagai antrian (queue) setelah handler selesai
  • Replace (nilai langsung) menimpa apapun sebelumnya
  • Updater membangun di atas hasil sebelumnya
  • Pakai updater kalau: multiple update, bergantung pada nilai lama, di dalam timer
  • Pakai nilai langsung kalau: reset, set dari input, nggak butuh nilai lama
  • Updater function harus pure — cuma hitung dan return, tanpa side effect
  • Konvensi parameter: prev, n, atau huruf pertama nama state

Di bab berikutnya, kita bakal bahas cara update object di state. Karena object itu reference type, ada aturan khusus yang harus diikuti supaya React bisa mendeteksi perubahan.

Sudah paham materi ini?

Tandai sebagai selesai untuk melacak progress-mu.