Kenapa ini penting
Props membuat komponen bisa menampilkan data dari luar, tapi aplikasi nyata juga perlu mengingat sesuatu yang berubah karena interaksi pengguna: isi kolom pencarian, jumlah item di keranjang, tab yang sedang aktif, apakah menu terbuka. Ingatan milik komponen ini disebut state. Begitu state berubah, React menggambar ulang komponen dengan nilai baru. useState adalah hook yang paling sering kamu pakai seumur karier React, dan memahami aturannya (terutama soal immutability dan updater function) akan menyelamatkanmu dari bug yang membingungkan.
Konsep inti
Kenapa variabel biasa tidak cukup
Variabel lokal biasa (let jumlah = 0) punya dua masalah:
- Tidak bertahan antar render. Setiap kali React memanggil ulang fungsi komponen, variabel lokal dibuat dari awal lagi.
- Mengubahnya tidak memicu render. React tidak tahu variabel itu berubah, jadi layar tidak diperbarui.
useState menyelesaikan keduanya: nilainya disimpan oleh React di luar fungsi komponen, dan memanggil fungsi setter-nya memberi sinyal ke React untuk render ulang.
Sintaks useState
const [nilai, setNilai] = useState(nilaiAwal);
nilai: state saat render ini.setNilai: fungsi untuk meminta nilai baru dan memicu render ulang.nilaiAwal: dipakai hanya pada render pertama.
Konvensi nama: [sesuatu, setSesuatu]. Bentuk [a, b] adalah array destructuring biasa.
Aturan hooks
Hooks (semua fungsi berawalan use) hanya boleh dipanggil di level teratas function component atau custom hook, tidak di dalam if, loop, atau fungsi bersarang. React mengenali setiap state berdasarkan urutan pemanggilan, jadi urutannya harus selalu sama di setiap render.
State sebagai snapshot
Ini konsep yang sering mengejutkan pemula: di dalam satu render, nilai state tetap. Memanggil setJumlah(jumlah + 1) tidak langsung mengubah jumlah di baris berikutnya; ia meminta React membuat render baru dengan nilai baru. Analogi: state seperti foto. Kamu bisa meminta foto baru diambil, tapi foto yang sedang kamu pegang tidak berubah.
Batching
React mengumpulkan (batching) semua pemanggilan setter dalam satu event handler, lalu melakukan satu render setelah handler selesai. Analogi pelayan restoran: ia tidak lari ke dapur setiap kamu menyebut satu menu, tapi menunggu kamu selesai memesan lalu membawa semua pesanan sekaligus. Akibatnya:
setJumlah(jumlah + 1);
setJumlah(jumlah + 1);
setJumlah(jumlah + 1);
// Jika jumlah = 0, hasilnya 1, bukan 3!
Ketiganya membaca jumlah yang sama (0), jadi tiga kali meminta "jadikan 1".
Updater function
Kalau nilai baru bergantung pada nilai sebelumnya, kirim fungsi ke setter: setJumlah(n => n + 1). React mengantrekan fungsi ini dan menjalankannya berurutan, masing-masing menerima hasil fungsi sebelumnya. Tiga kali setJumlah(n => n + 1) dari 0 menghasilkan 3.
| Antrean | n yang diterima |
Hasil |
|---|---|---|
n => n + 1 |
0 | 1 |
n => n + 1 |
1 | 2 |
n => n + 1 |
2 | 3 |
Immutability: object dan array
React memutuskan perlu render ulang atau tidak dengan membandingkan nilai lama dan baru menggunakan Object.is. Kalau kamu mengubah isi objek secara langsung (user.nama = 'Budi') lalu memanggil setUser(user), referensinya sama, React menganggap tidak ada perubahan. Aturan emasnya: perlakukan state sebagai read-only, selalu buat salinan baru.
| Operasi | ❌ Mutasi (hindari) | ✅ Salinan baru |
|---|---|---|
| Ubah properti objek | user.nama = 'Budi' |
{ ...user, nama: 'Budi' } |
| Tambah item array | arr.push(x) |
[...arr, x] |
| Hapus item array | arr.splice(i, 1) |
arr.filter(item => item.id !== id) |
| Ubah item array | arr[i].done = true |
arr.map(item => item.id === id ? { ...item, done: true } : item) |
| Urutkan | arr.sort() |
[...arr].sort() atau arr.toSorted() |
Untuk objek bersarang, salin setiap lapis yang berubah: { ...user, alamat: { ...user.alamat, kota: 'Medan' } }.
Contoh kode
// src/App.jsx, counter, batching, updater, objek dan array
import { useState } from 'react';
function Counter() {
const [jumlah, setJumlah] = useState(0);
function tambahTigaSalah() {
setJumlah(jumlah + 1);
setJumlah(jumlah + 1);
setJumlah(jumlah + 1);
console.log(jumlah); // → masih nilai lama (snapshot)
}
function tambahTigaBenar() {
setJumlah(n => n + 1);
setJumlah(n => n + 1);
setJumlah(n => n + 1);
}
return (
<div>
<h2>Jumlah: {jumlah}</h2>
<button onClick={tambahTigaSalah}>+3 (salah, hasilnya +1)</button>
<button onClick={tambahTigaBenar}>+3 (benar)</button>
<button onClick={() => setJumlah(0)}>Reset</button>
</div>
);
}
function Profil() {
const [user, setUser] = useState({
nama: 'Sari',
alamat: { kota: 'Yogyakarta', kodePos: '55281' },
});
function pindahKota() {
// Salin objek luar DAN objek alamat yang diubah
setUser({ ...user, alamat: { ...user.alamat, kota: 'Semarang' } });
}
return (
<div>
<p>{user.nama} tinggal di {user.alamat.kota}</p>
<button onClick={pindahKota}>Pindah ke Semarang</button>
</div>
);
}
// Klik → "Sari tinggal di Semarang"
function DaftarBelanja() {
const [items, setItems] = useState([
{ id: 1, nama: 'Gula', beli: false },
{ id: 2, nama: 'Kopi', beli: false },
]);
const [nextId, setNextId] = useState(3);
function tambah() {
setItems([...items, { id: nextId, nama: `Barang ${nextId}`, beli: false }]);
setNextId(nextId + 1);
}
function toggle(id) {
setItems(items.map(item => (item.id === id ? { ...item, beli: !item.beli } : item)));
}
function hapus(id) {
setItems(items.filter(item => item.id !== id));
}
return (
<div>
<button onClick={tambah}>Tambah barang</button>
<ul>
{items.map(item => (
<li key={item.id}>
<span style={{ textDecoration: item.beli ? 'line-through' : 'none' }}>{item.nama}</span>{' '}
<button onClick={() => toggle(item.id)}>Centang</button>
<button onClick={() => hapus(item.id)}>Hapus</button>
</li>
))}
</ul>
</div>
);
}
export default function App() {
return (
<>
<Counter />
<Profil />
<DaftarBelanja />
</>
);
}
Variabel biasa vs state, supaya perbedaannya terasa:
import { useState } from 'react';
export function Perbandingan() {
let biasa = 0; // dibuat ulang setiap render
const [state, setState] = useState(0);
return (
<>
<button onClick={() => { biasa++; console.log('biasa:', biasa); }}>
Variabel biasa: {biasa}
</button>
{/* Klik berkali-kali → console: biasa: 1, layar tetap 0 */}
<button onClick={() => setState(state + 1)}>State: {state}</button>
{/* Klik → layar 1, 2, 3, ... */}
</>
);
}
Kesalahan umum
- Mutasi langsung lalu memanggil setter dengan objek yang sama.
items.push(x); setItems(items)tidak memicu render yang benar. PakaisetItems([...items, x]). - Mengharapkan state langsung berubah setelah setter.
setNama('Budi'); console.log(nama)masih mencetak nilai lama. Nilai baru baru tersedia di render berikutnya. - Tidak memakai updater saat nilai bergantung pada nilai sebelumnya. Untuk penambahan berturut-turut atau update dari timer, pakai
setN(n => n + 1). - Memanggil hook di dalam
ifatau loop. Melanggar aturan hooks dan menyebabkan state tertukar. Panggil di level teratas. - Lupa menyalin objek bersarang.
{ ...user, alamat.kota: 'X' }bukan sintaks valid; salin setiap lapis:{ ...user, alamat: { ...user.alamat, kota: 'X' } }.
Latihan
- Mudah, Buat komponen
Lampudengan state boolean. Tombol "Saklar" mengganti teks antara "Lampu Nyala 💡" dan "Lampu Mati". - Sedang, Buat form profil dengan state objek
{ nama, email }dan dua input. Setiap input mengubah satu field tanpa menghapus field lainnya. - Menantang, Buat komponen
Skordengan tombol "+1", "+5 (lima kali +1)", dan "Undo". Simpan riwayat skor dalam array state agar Undo bisa kembali ke nilai sebelumnya.
Lihat solusi
1.
import { useState } from 'react';
export default function Lampu() {
const [nyala, setNyala] = useState(false);
return (
<>
<p>{nyala ? 'Lampu Nyala 💡' : 'Lampu Mati'}</p>
<button onClick={() => setNyala(n => !n)}>Saklar</button>
</>
);
}
2.
import { useState } from 'react';
export default function FormProfil() {
const [form, setForm] = useState({ nama: '', email: '' });
function handleChange(e) {
// e.target.name berisi 'nama' atau 'email'
setForm({ ...form, [e.target.name]: e.target.value });
}
return (
<form>
<input name="nama" value={form.nama} onChange={handleChange} placeholder="Nama" />
<input name="email" value={form.email} onChange={handleChange} placeholder="Email" />
<p>Halo {form.nama || '...'} ({form.email || 'belum ada email'})</p>
</form>
);
}
3.
import { useState } from 'react';
export default function Skor() {
const [riwayat, setRiwayat] = useState([0]); // nilai terakhir = skor sekarang
const skor = riwayat[riwayat.length - 1];
function tambah(n) {
setRiwayat(r => [...r, r[r.length - 1] + n]);
}
function tambahLima() {
for (let i = 0; i < 5; i++) tambah(1); // updater → benar-benar +5
}
function undo() {
setRiwayat(r => (r.length > 1 ? r.slice(0, -1) : r));
}
return (
<>
<h2>Skor: {skor}</h2>
<button onClick={() => tambah(1)}>+1</button>
<button onClick={tambahLima}>+5</button>
<button onClick={undo}>Undo</button>
</>
);
}
// +1, +5 → Skor 6; Undo → 5 (setiap +1 dari tombol +5 tercatat sebagai satu langkah)
Ringkasan
useStatememberi komponen ingatan yang bertahan antar render dan memicu render ulang saat diubah.- State adalah snapshot: nilainya tetap di dalam satu render, nilai baru muncul di render berikutnya.
- React melakukan batching beberapa setter dalam satu event menjadi satu render.
- Gunakan updater function
setX(prev => ...)saat nilai baru bergantung pada nilai sebelumnya. - Jangan mutasi state; buat salinan baru dengan spread,
map, danfilter.