Bab 5: Antrian Update State
⏱ 6 menit bacaMasalah yang Kita Hadapi
Di bab sebelumnya, kita lihat masalah ini:
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.
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
// State awal: angka = 0
setAngka(5); // Antrian: [ganti jadi 5]
setAngka(5); // Antrian: [ganti jadi 5, ganti jadi 5]React proses:
- "Ganti jadi 5" → angka = 5
- "Ganti jadi 5" → angka = 5
- Hasil akhir: 5
Contoh dengan Updater Function
// 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:
n => n + 1dengan n = 0 → angka = 1n => n + 1dengan n = 1 → angka = 2n => n + 1dengan n = 2 → angka = 3- Hasil akhir: 3 ✓
Contoh Campuran (Nilai + Updater)
// 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:
- "Ganti jadi 5" → angka = 5
n => n + 1dengan n = 5 → angka = 6- "Ganti jadi 42" → angka = 42
- 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-queue | n (nilai masuk) | Return | Hasil |
|---|---|---|---|
setAngka(5) | (diabaikan) | 5 | 5 |
setAngka(n => n + 1) | 5 | 6 | 6 |
setAngka(n => n * 2) | 6 | 12 | 12 |
setAngka(0) | (diabaikan) | 0 | 0 |
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:
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 okeYang paling umum dipakai:
prev— universal, selalu jelas- Huruf pertama state —
nuntuk angka,cuntuk 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:
// 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:
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:
// 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:
// 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:
// Ganti total, nggak peduli nilai lama
setFormData({ nama: 'Budi', email: 'budi@mail.com' });Contoh Praktis: Keranjang Belanja
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
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:
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:
| Step | Queue Item | Nilai Masuk | Operasi | Hasil |
|---|---|---|---|---|
| 1 | 5 (replace) | 0 (state awal) | Ganti jadi 5 | 5 |
| 2 | n => n + 1 (updater) | 5 | 5 + 1 | 6 |
| 3 | n => n * 2 (updater) | 6 | 6 * 2 | 12 |
Hasil akhir: angka = 12
Satu lagi:
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:
| Step | Queue Item | Nilai Masuk | Operasi | Hasil |
|---|---|---|---|---|
| 1 | n => n + 1 | 0 | 0 + 1 | 1 |
| 2 | n => n + 1 | 1 | 1 + 1 | 2 |
| 3 | n => n + 1 | 2 | 2 + 1 | 3 |
| 4 | 0 (replace) | 3 | Ganti jadi 0 | 0 |
| 5 | n => n + 1 | 0 | 0 + 1 | 1 |
Hasil akhir: angka = 1
Perhatiin step 4: replace "menimpa" semua progress sebelumnya!
Pola Umum dengan Updater
Toggle
const [buka, setBuka] = useState(false);
// Toggle: balik nilai boolean
function handleToggle() {
setBuka(prev => !prev);
}Increment/Decrement dengan Batas
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
const [pesan, setPesan] = useState([]);
function handleKirim(pesanBaru) {
setPesan(prev => [...prev, {
id: Date.now(),
teks: pesanBaru,
waktu: new Date().toLocaleTimeString()
}]);
}Remove dari Array
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
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
// ❌ 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
// ❌ 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
// ❌ 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
// ❌ 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):
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
| Step | Queue Item | Nilai Masuk | Operasi | Hasil |
|---|---|---|---|---|
| A | n => n + 2 | 0 | 0 + 2 | 2 |
| B | n => n * 3 | 2 | 2 * 3 | 6 |
| C | 10 (replace) | 6 | Ganti jadi 10 | 10 |
| D | n => n - 1 | 10 | 10 - 1 | 9 |
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
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
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.