Bab 2: State — Memori Komponen
⏱ 5 menit bacaKenapa Variabel Biasa Nggak Cukup?
Coba perhatiin kode ini:
function Counter() {
let angka = 0;
function handleKlik() {
angka = angka + 1;
console.log(angka); // Angka naik di console...
}
return (
<div>
<p>Angka: {angka}</p>
<button onClick={handleKlik}>Tambah</button>
</div>
);
}Kalau kamu jalanin kode ini dan klik tombolnya, yang terjadi:
- Di console, angka naik: 1, 2, 3, 4...
- Tapi di layar? Tetap 0. Nggak berubah.
Kenapa? Dua alasan:
- Variabel lokal nggak bertahan antar render. Setiap kali React me-render komponen, dia mulai dari awal. Variabel
angkaselalu di-set ulang ke 0. - Mengubah variabel lokal nggak memicu re-render. React nggak tau kalau
angkaberubah, jadi dia nggak update tampilan.
Analoginya gini: bayangin kamu nulis angka di papan tulis. Setiap kali ada yang masuk ruangan (re-render), papan tulis dihapus bersih dan ditulis ulang dari awal. Variabel lokal itu kayak nulis di papan tulis yang selalu dihapus.
Yang kita butuhkan adalah sesuatu yang:
- Bertahan antar render (nggak di-reset)
- Memicu re-render saat berubah (biar tampilan update)
Itulah State.
useState: Hook Pertamamu
useState adalah sebuah Hook dari React. Hook itu fungsi spesial yang dimulai dengan kata use. Dia "mengaitkan" (hook into) fitur-fitur React ke komponen kamu.
import { useState } from 'react';
function Counter() {
// useState mengembalikan array dengan 2 elemen:
// 1. Nilai state saat ini
// 2. Fungsi untuk mengubah state
const [angka, setAngka] = useState(0);
// ^ ^ ^
// | | |
// | | Nilai awal (initial value)
// | Fungsi setter (untuk update)
// Nilai state saat ini
function handleKlik() {
setAngka(angka + 1); // Update state DAN trigger re-render!
}
return (
<div>
<p>Angka: {angka}</p>
<button onClick={handleKlik}>Tambah</button>
</div>
);
}Coba sendiri: Edit kode di bawah dan lihat hasilnya langsung!
Sekarang kalau klik tombol, angka di layar BENAR-BENAR berubah! 🎉
Apa yang Terjadi di Balik Layar?
- Komponen pertama kali di-render.
useState(0)mengembalikan[0, setAngka]. React "mengingat" bahwa state-nya adalah 0. - User klik tombol.
setAngka(1)dipanggil. - React tau state berubah (dari 0 ke 1), jadi dia me-render ulang komponen.
- Saat render ulang,
useState(0)sekarang mengembalikan[1, setAngka]. Nilai 0 di parameter diabaikan karena React sudah punya nilai tersimpan (1). - React update DOM sesuai hasil render baru.
Analoginya: useState itu kayak loker di gym. Kamu simpen barang (state) di loker, pergi latihan (render), balik lagi, barang masih ada. Nggak hilang kayak variabel biasa yang "dihapus" setiap render.
Anatomi useState
const [sesuatu, setSesuatu] = useState(nilaiAwal);Mari bedah satu per satu:
Destructuring Array
useState mengembalikan array dengan tepat 2 elemen. Kita pakai array destructuring untuk memberi nama:
// Ini sama aja dengan:
const hasilUseState = useState(0);
const angka = hasilUseState[0]; // Nilai state
const setAngka = hasilUseState[1]; // Fungsi setter
// Tapi kita tulis lebih ringkas:
const [angka, setAngka] = useState(0);Konvensi Penamaan
Polanya selalu: [sesuatu, setSesuatu]
const [nama, setNama] = useState('');
const [umur, setUmur] = useState(0);
const [aktif, setAktif] = useState(true);
const [items, setItems] = useState([]);
const [user, setUser] = useState(null);
const [formData, setFormData] = useState({ nama: '', email: '' });Kenapa pakai set + nama? Karena ini konvensi yang dipakai semua developer React. Kalau kamu baca kode orang lain, kamu langsung tau setNama itu fungsi untuk mengubah state nama.
Nilai Awal (Initial Value)
Nilai yang kamu passing ke useState() adalah nilai awal. Bisa tipe apa aja:
useState(0) // Number
useState('') // String kosong
useState('Budi') // String
useState(true) // Boolean
useState([]) // Array kosong
useState(null) // Null
useState({ x: 0 }) // ObjectNilai awal ini cuma dipakai di render pertama. Di render-render selanjutnya, React pakai nilai yang sudah tersimpan.
Cara State Memicu Re-render
Ini alur lengkapnya:
User klik tombol
↓
Event handler dipanggil
↓
setState() dipanggil dengan nilai baru
↓
React menandai: "Komponen ini perlu di-render ulang"
↓
React me-render ulang komponen (panggil fungsi komponen lagi)
↓
useState() mengembalikan nilai state yang BARU
↓
JSX baru dihasilkan dengan data baru
↓
React update DOM sesuai perubahan
↓
User lihat tampilan yang sudah update
Penting: setState TIDAK langsung mengubah variabel. Dia menjadwalkan re-render. Nilai state yang baru baru tersedia di render BERIKUTNYA. (Ini bakal dibahas lebih detail di bab tentang "State sebagai Snapshot".)
Multiple State Variables
Satu komponen bisa punya banyak state. Setiap useState independen satu sama lain.
import { useState } from 'react';
function ProfilUser() {
const [nama, setNama] = useState('');
const [umur, setUmur] = useState(0);
const [hobi, setHobi] = useState([]);
const [sedangEdit, setSedangEdit] = useState(false);
return (
<div>
<h2>Profil</h2>
{sedangEdit ? (
<div>
<input
value={nama}
onChange={(e) => setNama(e.target.value)}
placeholder="Nama"
/>
<input
type="number"
value={umur}
onChange={(e) => setUmur(Number(e.target.value))}
placeholder="Umur"
/>
<button onClick={() => setSedangEdit(false)}>Simpan</button>
</div>
) : (
<div>
<p>Nama: {nama || '(belum diisi)'}</p>
<p>Umur: {umur || '(belum diisi)'}</p>
<button onClick={() => setSedangEdit(true)}>Edit</button>
</div>
)}
</div>
);
}Kapan Pakai Satu State vs Banyak State?
Pakai state terpisah kalau data-datanya berubah secara independen:
// ✅ Bagus — x dan y bisa berubah sendiri-sendiri
const [x, setX] = useState(0);
const [y, setY] = useState(0);Pakai satu state (object) kalau data-datanya selalu berubah bareng:
// ✅ Bagus — firstName dan lastName biasanya diupdate bareng
const [formData, setFormData] = useState({
firstName: '',
lastName: '',
email: ''
});Aturan praktisnya: kalau kamu sering update dua state bersamaan, mungkin lebih baik digabung jadi satu object.
State Terisolasi Per Instance Komponen
Ini konsep yang sering bikin pemula bingung. Setiap instance (salinan) komponen punya state-nya sendiri yang terpisah.
import { useState } from 'react';
function Counter() {
const [angka, setAngka] = useState(0);
return (
<div style={{ border: '1px solid gray', padding: '10px', margin: '10px' }}>
<p>Angka: {angka}</p>
<button onClick={() => setAngka(angka + 1)}>+1</button>
</div>
);
}
function App() {
return (
<div>
<h1>Tiga Counter Independen</h1>
<Counter /> {/* Punya state sendiri */}
<Counter /> {/* Punya state sendiri */}
<Counter /> {/* Punya state sendiri */}
</div>
);
}Kalau kamu klik tombol di Counter pertama, cuma Counter pertama yang berubah. Counter kedua dan ketiga tetap 0. Mereka nggak berbagi state.
Analoginya: bayangin 3 orang beda punya HP masing-masing. Kalau satu orang ganti wallpaper, HP orang lain nggak ikut berubah. Setiap instance komponen itu kayak HP terpisah — punya "memori" sendiri.
// Visualisasi mental:
// <Counter /> instance 1 → state: { angka: 5 }
// <Counter /> instance 2 → state: { angka: 0 }
// <Counter /> instance 3 → state: { angka: 12 }Contoh Praktis: Aplikasi Todo Sederhana
import { useState } from 'react';
function TodoApp() {
const [todos, setTodos] = useState([]);
const [inputText, setInputText] = useState('');
function handleTambah() {
if (inputText.trim() === '') return; // Jangan tambah kalau kosong
// Buat todo baru dan tambahkan ke array
const todoBaru = {
id: Date.now(), // ID unik sederhana
teks: inputText,
selesai: false
};
setTodos([...todos, todoBaru]); // Tambah ke akhir array
setInputText(''); // Kosongkan input
}
function handleToggle(id) {
setTodos(todos.map(todo => {
if (todo.id === id) {
return { ...todo, selesai: !todo.selesai };
}
return todo;
}));
}
function handleHapus(id) {
setTodos(todos.filter(todo => todo.id !== id));
}
return (
<div>
<h2>Todo List Warung</h2>
<div>
<input
value={inputText}
onChange={(e) => setInputText(e.target.value)}
placeholder="Tambah tugas..."
onKeyDown={(e) => {
if (e.key === 'Enter') handleTambah();
}}
/>
<button onClick={handleTambah}>Tambah</button>
</div>
<ul>
{todos.map(todo => (
<li key={todo.id}>
<span
style={{
textDecoration: todo.selesai ? 'line-through' : 'none',
cursor: 'pointer'
}}
onClick={() => handleToggle(todo.id)}
>
{todo.teks}
</span>
<button onClick={() => handleHapus(todo.id)}>❌</button>
</li>
))}
</ul>
<p>Total: {todos.length} | Selesai: {todos.filter(t => t.selesai).length}</p>
</div>
);
}Contoh Praktis: Toggle Tema Gelap/Terang
import { useState } from 'react';
function AppDenganTema() {
const [gelap, setGelap] = useState(false);
const style = {
backgroundColor: gelap ? '#1a1a2e' : '#ffffff',
color: gelap ? '#eaeaea' : '#333333',
padding: '20px',
minHeight: '100vh',
transition: 'all 0.3s ease'
};
return (
<div style={style}>
<h1>Warung Kopi Digital</h1>
<p>Selamat datang di warung kami!</p>
<button onClick={() => setGelap(!gelap)}>
{gelap ? '☀️ Mode Terang' : '🌙 Mode Gelap'}
</button>
<p>Tema saat ini: {gelap ? 'Gelap' : 'Terang'}</p>
</div>
);
}Aturan Penggunaan Hook
Hook (termasuk useState) punya aturan ketat:
1. Hanya panggil di level teratas komponen
function Komponen() {
// ✅ BENAR — di level teratas
const [nama, setNama] = useState('');
// ❌ SALAH — di dalam if
if (true) {
const [umur, setUmur] = useState(0); // JANGAN!
}
// ❌ SALAH — di dalam loop
for (let i = 0; i < 3; i++) {
const [item, setItem] = useState(''); // JANGAN!
}
// ❌ SALAH — di dalam fungsi nested
function helper() {
const [data, setData] = useState(null); // JANGAN!
}
}Kenapa? Karena React melacak Hook berdasarkan urutan pemanggilannya. Kalau Hook ada di dalam if atau loop, urutannya bisa berubah antar render, dan React jadi bingung Hook mana yang mana.
Analoginya: bayangin kamu punya loker bernomor di gym (loker 1, 2, 3...). React "mengingat" state berdasarkan nomor urut. Kalau tiba-tiba urutan berubah (karena if), kamu buka loker 2 tapi isinya barang loker 3. Kacau!
2. Hanya panggil di dalam komponen React atau custom Hook
// ❌ SALAH — di luar komponen
const [data, setData] = useState(null);
function BukanKomponen() {
// ❌ SALAH — ini fungsi biasa, bukan komponen React
const [x, setX] = useState(0);
}
// ✅ BENAR — di dalam komponen (huruf kapital)
function KomponenReact() {
const [x, setX] = useState(0);
return <div>{x}</div>;
}State vs Props: Apa Bedanya?
Pemula sering bingung bedain state dan props. Ini perbandingannya:
| State | Props | |
|---|---|---|
| Siapa yang "punya"? | Komponen itu sendiri | Parent yang passing |
| Bisa diubah? | Ya, pakai setter | Tidak (read-only) |
| Trigger re-render? | Ya | Ya (kalau parent re-render) |
| Analoginya | Catatan pribadi | Surat dari atasan |
// Props = data dari parent (read-only)
// State = data internal komponen (bisa diubah)
function KartuProduk({ nama, harga }) { // props dari parent
const [jumlah, setJumlah] = useState(0); // state internal
return (
<div>
<h3>{nama}</h3> {/* dari props */}
<p>Rp {harga}</p> {/* dari props */}
<p>Jumlah: {jumlah}</p> {/* dari state */}
<button onClick={() => setJumlah(jumlah + 1)}>+</button>
</div>
);
}
function Toko() {
return (
<div>
{/* Parent passing props ke child */}
<KartuProduk nama="Kopi Susu" harga={25000} />
<KartuProduk nama="Es Teh" harga={10000} />
</div>
);
}⚠️ Jebakan
Jebakan 1: Lupa import useState
// ❌ Error: useState is not defined
function Counter() {
const [angka, setAngka] = useState(0);
// ...
}
// ✅ Jangan lupa import!
import { useState } from 'react';
function Counter() {
const [angka, setAngka] = useState(0);
// ...
}Jebakan 2: Langsung mutasi state
// ❌ SALAH — langsung ubah variabel state
function Salah() {
const [angka, setAngka] = useState(0);
function handleKlik() {
angka = angka + 1; // JANGAN! Ini nggak trigger re-render
// Dan akan error karena const
}
}
// ✅ BENAR — pakai setter function
function Benar() {
const [angka, setAngka] = useState(0);
function handleKlik() {
setAngka(angka + 1); // Ini yang benar
}
}Jebakan 3: Ekspektasi state langsung berubah
function Jebakan() {
const [angka, setAngka] = useState(0);
function handleKlik() {
setAngka(5);
console.log(angka); // Masih 0! Bukan 5!
// State baru tersedia di render BERIKUTNYA
}
}Ini bakal dibahas detail di bab "State sebagai Snapshot".
Jebakan 4: Hook di dalam kondisi
// ❌ SALAH — Hook di dalam if
function Komponen({ tampilkanNama }) {
if (tampilkanNama) {
const [nama, setNama] = useState(''); // JANGAN!
}
const [umur, setUmur] = useState(0);
}
// ✅ BENAR — semua Hook di level teratas, kondisi di dalam return
function Komponen({ tampilkanNama }) {
const [nama, setNama] = useState('');
const [umur, setUmur] = useState(0);
return (
<div>
{tampilkanNama && <p>Nama: {nama}</p>}
<p>Umur: {umur}</p>
</div>
);
}Jebakan 5: Tipe data initial value salah
// ❌ Nanti error saat .map() karena initial value bukan array
const [items, setItems] = useState('');
// ✅ Kalau mau pakai .map(), initial value harus array
const [items, setItems] = useState([]);
// ❌ Nanti error saat akses .nama karena initial value bukan object
const [user, setUser] = useState('');
// ✅ Kalau mau akses properti, initial value harus object atau null
const [user, setUser] = useState(null);
// Tapi jangan lupa handle null: user?.nama atau user && user.nama🏋️ Challenge
Challenge 1: Lampu Toggle
Buat komponen "Lampu" yang bisa dinyalakan dan dimatikan. Tampilkan emoji 💡 kalau nyala dan 🌑 kalau mati. Tombolnya bertuliskan "Nyalakan" atau "Matikan" sesuai kondisi.
💡 Hint
Pakai useState dengan nilai boolean. true = nyala, false = mati. Toggle dengan !nilaiSekarang.
✅ Solusi
import { useState } from 'react';
function Lampu() {
const [nyala, setNyala] = useState(false);
function handleToggle() {
setNyala(!nyala);
}
return (
<div style={{
textAlign: 'center',
padding: '40px',
backgroundColor: nyala ? '#fff9c4' : '#212121',
transition: 'all 0.3s'
}}>
<p style={{ fontSize: '80px' }}>
{nyala ? '💡' : '🌑'}
</p>
<button onClick={handleToggle}>
{nyala ? 'Matikan' : 'Nyalakan'}
</button>
</div>
);
}Challenge 2: Counter dengan Batas
Buat counter yang:
- Bisa ditambah dan dikurangi
- Nggak bisa kurang dari 0
- Nggak bisa lebih dari 10
- Tampilkan pesan "Maksimum!" atau "Minimum!" saat mencapai batas
💡 Hint
Pakai kondisi if di dalam handler. Cek apakah nilai baru masih dalam range sebelum memanggil setter.
✅ Solusi
import { useState } from 'react';
function CounterBerbatas() {
const [angka, setAngka] = useState(0);
const [pesan, setPesan] = useState('');
function handleTambah() {
if (angka >= 10) {
setPesan('Maksimum! Nggak bisa lebih dari 10.');
return;
}
setAngka(angka + 1);
setPesan('');
}
function handleKurang() {
if (angka <= 0) {
setPesan('Minimum! Nggak bisa kurang dari 0.');
return;
}
setAngka(angka - 1);
setPesan('');
}
return (
<div>
<h2>Counter Berbatas (0-10)</h2>
<p style={{ fontSize: '48px' }}>{angka}</p>
<button onClick={handleKurang}>➖</button>
<button onClick={handleTambah}>➕</button>
{pesan && <p style={{ color: 'red' }}>{pesan}</p>}
</div>
);
}Challenge 3: Galeri Foto Sederhana
Buat galeri yang menampilkan satu foto pada satu waktu. Ada tombol "Sebelumnya" dan "Selanjutnya" untuk navigasi. Tampilkan juga "Foto X dari Y".
💡 Hint
Simpan index foto aktif di state. Array foto bisa didefinisikan di luar state (karena nggak berubah). Navigasi = ubah index.
✅ Solusi
import { useState } from 'react';
function GaleriFoto() {
const fotos = [
{ judul: 'Warung Kopi Pagi', url: 'https://placekitten.com/400/300' },
{ judul: 'Pasar Tradisional', url: 'https://placekitten.com/401/300' },
{ judul: 'Sawah Hijau', url: 'https://placekitten.com/402/300' },
{ judul: 'Pantai Sore', url: 'https://placekitten.com/403/300' },
];
const [indexAktif, setIndexAktif] = useState(0);
function handleSebelumnya() {
if (indexAktif > 0) {
setIndexAktif(indexAktif - 1);
}
}
function handleSelanjutnya() {
if (indexAktif < fotos.length - 1) {
setIndexAktif(indexAktif + 1);
}
}
const fotoSekarang = fotos[indexAktif];
return (
<div style={{ textAlign: 'center' }}>
<h2>{fotoSekarang.judul}</h2>
<img
src={fotoSekarang.url}
alt={fotoSekarang.judul}
style={{ width: '400px', height: '300px', objectFit: 'cover' }}
/>
<p>Foto {indexAktif + 1} dari {fotos.length}</p>
<button
onClick={handleSebelumnya}
disabled={indexAktif === 0}
>
⬅️ Sebelumnya
</button>
<button
onClick={handleSelanjutnya}
disabled={indexAktif === fotos.length - 1}
>
Selanjutnya ➡️
</button>
</div>
);
}Ringkasan
- Variabel biasa nggak cukup karena: (1) di-reset setiap render, (2) nggak trigger re-render
- useState mengembalikan
[nilai, setNilai]— nilai state dan fungsi untuk mengubahnya - Memanggil setter (
setNilai) memicu re-render komponen - State terisolasi per instance komponen — setiap salinan punya state sendiri
- Hook harus dipanggil di level teratas komponen, nggak boleh di dalam if/loop
- Konvensi penamaan:
[sesuatu, setSesuatu]
Di bab selanjutnya, kita bakal menyelam lebih dalam ke proses render React. Gimana React memutuskan kapan harus update tampilan, dan apa yang sebenarnya terjadi saat kamu panggil setState.
Sudah paham materi ini?
Tandai sebagai selesai untuk melacak progress-mu.