Bab 4: Menyimpan dan Mereset State
⏱ 6 menit bacaPendahuluan: State Itu Punya "Alamat"
Pernah gak kamu pindah kos? Barang-barang kamu (state) itu terikat sama KAMAR kamu (posisi di tree), bukan sama NAMA kamu (komponen). Kalau kamu pindah kamar, barang-barang di kamar lama hilang. Tapi kalau orang lain masuk ke kamar yang sama, dia gak dapet barang kamu... kecuali kamarnya masih "dianggap sama" oleh React.
Ini konsep yang sering bikin bingung pemula: state itu terikat ke posisi komponen di render tree, bukan ke komponen itu sendiri. Di bab ini, kamu bakal paham kapan React menyimpan state, kapan meressetnya, dan gimana cara mengontrol perilaku ini.
State Terikat ke Posisi di Tree
Bayangin loker di gym. Loker nomor 7 itu selalu loker nomor 7, gak peduli siapa yang pakai. Kalau kamu pakai loker 7 hari ini, barang kamu ada di sana. Besok kalau kamu pakai loker 7 lagi, barang kemarin masih ada (selama gak di-reset).
Tapi kalau kamu pindah ke loker 12, loker 7 dikosongkan. Barang kamu di loker 7 hilang.
Dalam React
React "melihat" komponen berdasarkan POSISI-nya di tree, bukan berdasarkan nama variabel atau referensi.
import { useState } from 'react';
function App() {
const [tampilkanA, setTampilkanA] = useState(true);
return (
<div>
{/* Posisi 0 di dalam div */}
{tampilkanA ? (
<Counter nama="Counter A" />
) : (
<Counter nama="Counter B" />
)}
<button onClick={() => setTampilkanA(!tampilkanA)}>
Ganti
</button>
</div>
);
}
function Counter({ nama }) {
const [angka, setAngka] = useState(0);
return (
<div>
<h3>{nama}</h3>
<p>Angka: {angka}</p>
<button onClick={() => setAngka(angka + 1)}>+1</button>
</div>
);
}Pertanyaan: Kalau kamu klik +1 beberapa kali di "Counter A", terus klik "Ganti" (jadi "Counter B")... apakah angkanya reset ke 0?
Jawaban: TIDAK! Angkanya tetap! Kenapa?
Karena dari sudut pandang React:
- Sebelum: Di posisi 0, ada komponen
<Counter> - Sesudah: Di posisi 0, MASIH ada komponen
<Counter>
React gak peduli props-nya beda (nama="A" vs nama="B"). Yang penting: komponen yang sama, di posisi yang sama = state dipertahankan.
Komponen Sama di Posisi Sama = State Dipertahankan
Visualisasi Tree
// Render 1: tampilkanA = true
App
└── div
├── Counter (nama="Counter A") ← posisi 0
└── button
// Render 2: tampilkanA = false
App
└── div
├── Counter (nama="Counter B") ← posisi 0 (SAMA!)
└── button
React lihat: "Oh, di posisi 0 masih ada Counter. Berarti ini komponen yang sama, state-nya dipertahankan."
Contoh Lain: Styling Berbeda, State Tetap
function App() {
const [merah, setMerah] = useState(false);
return (
<div>
{merah ? (
<Counter style={{ color: 'red' }} />
) : (
<Counter style={{ color: 'blue' }} />
)}
<button onClick={() => setMerah(!merah)}>
Ganti Warna
</button>
</div>
);
}Meskipun style-nya beda, ini TETAP Counter di posisi yang sama. State dipertahankan. Warna berubah, tapi angka counter tetap.
Komponen Berbeda di Posisi Sama = State Direset
Kalau kamar kos nomor 3 tadinya ditempatin Andi (komponen A), terus Andi keluar dan Budi (komponen B) masuk... barang-barang Andi udah dibuang. Budi mulai dari nol.
Dalam Kode
function App() {
const [tampilkanCounter, setTampilkanCounter] = useState(true);
return (
<div>
{tampilkanCounter ? (
<Counter /> {/* Komponen Counter */}
) : (
<InputTeks /> {/* Komponen BERBEDA! */}
)}
<button onClick={() => setTampilkanCounter(!tampilkanCounter)}>
Ganti Komponen
</button>
</div>
);
}
function Counter() {
const [angka, setAngka] = useState(0);
return (
<div>
<p>Counter: {angka}</p>
<button onClick={() => setAngka(angka + 1)}>+1</button>
</div>
);
}
function InputTeks() {
const [teks, setTeks] = useState('');
return <input value={teks} onChange={(e) => setTeks(e.target.value)} />;
}Kalau kamu:
- Klik +1 beberapa kali (counter jadi 5)
- Klik "Ganti Komponen" (sekarang tampil InputTeks)
- Ketik sesuatu di input
- Klik "Ganti Komponen" lagi (balik ke Counter)
Counter balik ke 0! Karena React lihat: "Di posisi ini tadinya Counter, sekarang InputTeks. Komponen BERBEDA. Buang state lama, mulai fresh."
Bahkan Wrapper Berbeda Pun Mereset State
function App() {
const [fancy, setFancy] = useState(false);
// ❗ Ini MERESET state Counter!
if (fancy) {
return (
<div>
<section> {/* ← wrapper berbeda */}
<Counter />
</section>
<button onClick={() => setFancy(false)}>Normal</button>
</div>
);
}
return (
<div>
<div> {/* ← wrapper berbeda */}
<Counter />
</div>
<button onClick={() => setFancy(true)}>Fancy</button>
</div>
);
}Meskipun dua-duanya render Counter, posisi di tree BERBEDA:
- Versi 1:
div > div > Counter - Versi 2:
div > section > Counter
Parent element berbeda (div vs section) = posisi berbeda = state direset.
Mereset State dengan Key Prop
Di bank, setiap nasabah dapet nomor antrian. Kalau nomor 45 selesai dilayani, counter teller di-reset untuk nasabah berikutnya. Nomor antrian itu kayak key di React.
Masalah: Komponen Sama, Posisi Sama, Tapi Mau Reset
Ingat contoh Counter A dan Counter B tadi? State-nya gak ke-reset karena React anggap itu komponen yang sama. Tapi gimana kalau kita MAHU state-nya reset?
function App() {
const [tampilkanA, setTampilkanA] = useState(true);
return (
<div>
{/* TANPA key: state dipertahankan (gak reset) */}
{tampilkanA ? (
<Counter nama="Counter A" />
) : (
<Counter nama="Counter B" />
)}
</div>
);
}Solusi: Tambahkan Key yang Berbeda
function App() {
const [tampilkanA, setTampilkanA] = useState(true);
return (
<div>
{/* DENGAN key: state DIRESET saat key berubah */}
{tampilkanA ? (
<Counter key="A" nama="Counter A" />
) : (
<Counter key="B" nama="Counter B" />
)}
</div>
);
}Sekarang, saat toggle:
- React lihat: "Key berubah dari 'A' ke 'B'. Ini komponen BARU. Buang state lama, mulai fresh."
Key itu kayak identitas unik. Kalau key berubah, React anggap itu komponen yang BERBEDA meskipun tipe-nya sama.
Contoh Praktis: Chat App
Ini contoh paling umum dimana kamu butuh reset state dengan key:
import { useState } from 'react';
const kontak = [
{ id: 1, nama: 'Andi', avatar: '👨' },
{ id: 2, nama: 'Budi', avatar: '👦' },
{ id: 3, nama: 'Citra', avatar: '👩' },
];
function ChatApp() {
const [kontakAktif, setKontakAktif] = useState(kontak[0]);
return (
<div style={{ display: 'flex', gap: '20px' }}>
{/* Sidebar: daftar kontak */}
<div style={{ width: '200px', borderRight: '1px solid #ddd', paddingRight: '20px' }}>
<h3>💬 Chat</h3>
{kontak.map(k => (
<button
key={k.id}
onClick={() => setKontakAktif(k)}
style={{
display: 'block',
width: '100%',
padding: '10px',
marginBottom: '5px',
background: kontakAktif.id === k.id ? '#e3f2fd' : 'white',
border: '1px solid #ddd',
borderRadius: '4px',
cursor: 'pointer',
textAlign: 'left',
}}
>
{k.avatar} {k.nama}
</button>
))}
</div>
{/* Area chat */}
<div style={{ flex: 1 }}>
{/* ⭐ KEY = kontakAktif.id → reset state saat ganti kontak */}
<PanelChat key={kontakAktif.id} kontak={kontakAktif} />
</div>
</div>
);
}
function PanelChat({ kontak }) {
const [pesan, setPesan] = useState('');
const [riwayat, setRiwayat] = useState([]);
function kirimPesan() {
if (pesan.trim() === '') return;
setRiwayat([...riwayat, { teks: pesan, waktu: new Date().toLocaleTimeString() }]);
setPesan('');
}
return (
<div>
<h3>{kontak.avatar} Chat dengan {kontak.nama}</h3>
{/* Riwayat pesan */}
<div style={{
height: '200px',
overflowY: 'auto',
border: '1px solid #ddd',
padding: '10px',
marginBottom: '10px',
borderRadius: '4px',
}}>
{riwayat.length === 0 ? (
<p style={{ color: '#999' }}>Belum ada pesan. Mulai ngobrol!</p>
) : (
riwayat.map((p, i) => (
<div key={i} style={{ marginBottom: '8px' }}>
<span style={{ color: '#666', fontSize: '12px' }}>{p.waktu}</span>
<p style={{ margin: '2px 0' }}>{p.teks}</p>
</div>
))
)}
</div>
{/* Input pesan */}
<div style={{ display: 'flex', gap: '8px' }}>
<input
value={pesan}
onChange={(e) => setPesan(e.target.value)}
placeholder={`Tulis pesan ke ${kontak.nama}...`}
onKeyDown={(e) => e.key === 'Enter' && kirimPesan()}
style={{ flex: 1, padding: '8px' }}
/>
<button onClick={kirimPesan}>Kirim</button>
</div>
</div>
);
}Tanpa key: Kalau kamu ketik pesan untuk Andi, terus pindah ke Budi, pesan yang kamu ketik MASIH ADA di input! Karena React anggap PanelChat di posisi yang sama = komponen yang sama.
Dengan key={kontakAktif.id}: Setiap ganti kontak, PanelChat di-reset total. Input kosong, riwayat kosong. Fresh start untuk setiap kontak.
Memindahkan State ke Posisi Berbeda
Teknik: Render di Posisi Berbeda untuk Reset
Selain pakai key, kamu juga bisa "memaksa" reset dengan menaruh komponen di posisi tree yang berbeda:
function App() {
const [pemain, setPemain] = useState(1);
return (
<div>
<h2>🎮 Game Giliran</h2>
{/* Teknik: render di posisi BERBEDA */}
{pemain === 1 && <SkorPemain nama="Pemain 1" />}
{pemain === 2 && <SkorPemain nama="Pemain 2" />}
<button onClick={() => setPemain(pemain === 1 ? 2 : 1)}>
Ganti Giliran
</button>
</div>
);
}Ini bekerja karena:
pemain === 1: renderSkorPemaindi "slot" pertama, slot kedua kosong (null)pemain === 2: slot pertama kosong (null), renderSkorPemaindi "slot" kedua
Posisi berbeda = komponen berbeda = state reset.
Tapi cara ini kurang elegan. Pakai key lebih jelas dan intentional:
function App() {
const [pemain, setPemain] = useState(1);
return (
<div>
<h2>🎮 Game Giliran</h2>
{/* Lebih jelas pakai key */}
<SkorPemain key={pemain} nama={`Pemain ${pemain}`} />
<button onClick={() => setPemain(pemain === 1 ? 2 : 1)}>
Ganti Giliran
</button>
</div>
);
}Aturan Lengkap: Kapan State Dipertahankan vs Direset
| Kondisi | State | Contoh |
|---|---|---|
| Komponen sama, posisi sama, key sama | ✅ Dipertahankan | <Counter /> → <Counter /> |
| Komponen sama, posisi sama, key BEDA | 🔄 Direset | <Counter key="a" /> → <Counter key="b" /> |
| Komponen BEDA, posisi sama | 🔄 Direset | <Counter /> → <Input /> |
| Komponen sama, posisi BEDA | 🔄 Direset | Pindah dari child ke-0 jadi child ke-1 |
| Komponen dihapus (unmount) | 🗑️ Dihancurkan | {show && <Counter />} saat show=false |
Contoh Mendalam: Form Edit Profil
import { useState } from 'react';
const users = [
{ id: 'u1', nama: 'Andi Pratama', email: 'andi@email.com', bio: 'Frontend developer' },
{ id: 'u2', nama: 'Budi Santoso', email: 'budi@email.com', bio: 'Backend engineer' },
{ id: 'u3', nama: 'Citra Dewi', email: 'citra@email.com', bio: 'UI/UX designer' },
];
function AdminPanel() {
const [userAktif, setUserAktif] = useState(users[0]);
return (
<div style={{ display: 'flex', gap: '30px' }}>
{/* Sidebar */}
<div>
<h3>👥 Users</h3>
{users.map(u => (
<div
key={u.id}
onClick={() => setUserAktif(u)}
style={{
padding: '10px',
cursor: 'pointer',
background: userAktif.id === u.id ? '#e8f5e9' : 'transparent',
borderRadius: '4px',
marginBottom: '4px',
}}
>
{u.nama}
</div>
))}
</div>
{/* Form edit - KEY memastikan form reset saat ganti user */}
<FormEditProfil key={userAktif.id} user={userAktif} />
</div>
);
}
function FormEditProfil({ user }) {
// State lokal untuk draft (dimulai dari data user)
const [nama, setNama] = useState(user.nama);
const [email, setEmail] = useState(user.email);
const [bio, setBio] = useState(user.bio);
const [sudahDiubah, setSudahDiubah] = useState(false);
function handleChange(setter) {
return (e) => {
setter(e.target.value);
setSudahDiubah(true);
};
}
function handleSimpan() {
alert(`Tersimpan!\nNama: ${nama}\nEmail: ${email}\nBio: ${bio}`);
setSudahDiubah(false);
}
function handleReset() {
setNama(user.nama);
setEmail(user.email);
setBio(user.bio);
setSudahDiubah(false);
}
return (
<div style={{ flex: 1 }}>
<h3>✏️ Edit Profil: {user.nama}</h3>
<div style={{ marginBottom: '10px' }}>
<label>Nama:</label>
<input value={nama} onChange={handleChange(setNama)} style={{ width: '100%' }} />
</div>
<div style={{ marginBottom: '10px' }}>
<label>Email:</label>
<input value={email} onChange={handleChange(setEmail)} style={{ width: '100%' }} />
</div>
<div style={{ marginBottom: '10px' }}>
<label>Bio:</label>
<textarea value={bio} onChange={handleChange(setBio)} style={{ width: '100%' }} />
</div>
{sudahDiubah && (
<div>
<button onClick={handleSimpan} style={{ marginRight: '8px' }}>
💾 Simpan
</button>
<button onClick={handleReset}>
↩️ Reset
</button>
<span style={{ marginLeft: '10px', color: 'orange' }}>
⚠️ Ada perubahan yang belum disimpan
</span>
</div>
)}
</div>
);
}Kenapa pakai key={userAktif.id}?
Tanpa key: Kalau kamu edit nama Andi jadi "Andi Baru", terus klik Budi... form masih nunjukin "Andi Baru" di field nama! Karena React anggap FormEditProfil di posisi yang sama = state dipertahankan.
Dengan key: Setiap ganti user, form di-reset total. useState(user.nama) dijalankan ulang dengan data user yang baru.
Visualisasi: React Tree dan State
// Saat userAktif = users[0] (Andi)
AdminPanel
├── div (sidebar)
│ ├── div (Andi) ← selected
│ ├── div (Budi)
│ └── div (Citra)
└── FormEditProfil [key="u1"] ← state: {nama: "Andi", email: "andi@...", ...}
// Saat userAktif = users[1] (Budi)
AdminPanel
├── div (sidebar)
│ ├── div (Andi)
│ ├── div (Budi) ← selected
│ └── div (Citra)
└── FormEditProfil [key="u2"] ← KEY BERUBAH! State di-reset!
state baru: {nama: "Budi", email: "budi@...", ...}
Pattern: Preserving State Intentionally
Kadang kamu MAHU state dipertahankan meskipun komponen "hilang" sebentar. Contoh: tab yang menyimpan scroll position.
function TabApp() {
const [tabAktif, setTabAktif] = useState('feed');
return (
<div>
<div>
<button onClick={() => setTabAktif('feed')}>Feed</button>
<button onClick={() => setTabAktif('profil')}>Profil</button>
<button onClick={() => setTabAktif('pesan')}>Pesan</button>
</div>
{/* ❌ Cara ini MERESET state setiap ganti tab */}
{tabAktif === 'feed' && <TabFeed />}
{tabAktif === 'profil' && <TabProfil />}
{tabAktif === 'pesan' && <TabPesan />}
</div>
);
}Kalau kamu mau state dipertahankan (misal: scroll position di feed), pakai CSS untuk "sembunyikan" bukan unmount:
function TabApp() {
const [tabAktif, setTabAktif] = useState('feed');
return (
<div>
<div>
<button onClick={() => setTabAktif('feed')}>Feed</button>
<button onClick={() => setTabAktif('profil')}>Profil</button>
<button onClick={() => setTabAktif('pesan')}>Pesan</button>
</div>
{/* ✅ Semua tab TETAP di-render, tapi yang gak aktif disembunyikan */}
<div style={{ display: tabAktif === 'feed' ? 'block' : 'none' }}>
<TabFeed />
</div>
<div style={{ display: tabAktif === 'profil' ? 'block' : 'none' }}>
<TabProfil />
</div>
<div style={{ display: tabAktif === 'pesan' ? 'block' : 'none' }}>
<TabPesan />
</div>
</div>
);
}Dengan display: none, komponen tetap ada di tree (state dipertahankan), cuma gak kelihatan.
Trade-off: Semua tab di-render sekaligus (lebih berat), tapi state gak hilang.
⚠️ Jebakan
Jebakan 1: Lupa Kasih Key Saat Ganti Data
// ❌ Form gak reset saat ganti item yang di-edit
function EditItem({ item }) {
const [draft, setDraft] = useState(item.nama);
// Kalau parent kirim item baru, draft MASIH isi item lama!
return <input value={draft} onChange={(e) => setDraft(e.target.value)} />;
}
// Di parent:
<EditItem item={itemTerpilih} /> // Gak ada key!
// ✅ Tambahkan key
<EditItem key={itemTerpilih.id} item={itemTerpilih} />Jebakan 2: Mengira Nama Komponen = Identitas
// ❌ Salah paham: "Kan namanya beda, harusnya reset?"
function App() {
const [mode, setMode] = useState('login');
return mode === 'login' ? <FormLogin /> : <FormRegister />;
// Ini MEMANG reset karena komponen BERBEDA (FormLogin vs FormRegister)
// Tapi...
}
// ❌ Ini TIDAK reset meskipun "terlihat beda"
function App() {
const [mode, setMode] = useState('login');
// Keduanya adalah <form> di posisi yang sama!
return mode === 'login' ? (
<form><input placeholder="Login" /></form>
) : (
<form><input placeholder="Register" /></form>
);
// React lihat: <form> di posisi 0 → <form> di posisi 0 = SAMA!
// State input TIDAK reset!
}Jebakan 3: Key yang Gak Stabil
// ❌ JANGAN pakai Math.random() sebagai key
{items.map(item => (
<Item key={Math.random()} data={item} />
// Setiap render, key berubah → state SELALU reset → performa buruk!
))}
// ❌ JANGAN pakai index kalau urutan bisa berubah
{items.map((item, index) => (
<Item key={index} data={item} />
// Kalau item dihapus/ditambah di tengah, state bisa "pindah" ke item lain!
))}
// ✅ Pakai ID yang stabil dan unik
{items.map(item => (
<Item key={item.id} data={item} />
))}Jebakan 4: Nesting Komponen di Dalam Komponen
// ❌ BAHAYA BESAR: Definisi komponen di dalam komponen lain
function Parent() {
const [count, setCount] = useState(0);
// Ini DIDEFINISIKAN ULANG setiap render!
function Child() {
const [teks, setTeks] = useState('');
return <input value={teks} onChange={(e) => setTeks(e.target.value)} />;
}
return (
<div>
<Child /> {/* State SELALU reset setiap Parent re-render! */}
<button onClick={() => setCount(count + 1)}>Re-render Parent</button>
</div>
);
}
// ✅ Definisikan komponen di LUAR
function Child() {
const [teks, setTeks] = useState('');
return <input value={teks} onChange={(e) => setTeks(e.target.value)} />;
}
function Parent() {
const [count, setCount] = useState(0);
return (
<div>
<Child /> {/* State dipertahankan dengan benar */}
<button onClick={() => setCount(count + 1)}>Re-render Parent</button>
</div>
);
}Ini jebakan yang SANGAT umum dan susah di-debug. Kalau komponen didefinisikan di dalam komponen lain, React menganggapnya sebagai komponen BARU setiap render (karena referensi fungsinya berubah). Akibatnya, state selalu reset.
Jebakan 5: Conditional Rendering yang Mengubah Posisi
// ❌ Menambah elemen di atas menggeser posisi Counter
function App() {
const [showBanner, setShowBanner] = useState(false);
return (
<div>
{showBanner && <div>Banner!</div>} {/* Ini menggeser Counter */}
<Counter /> {/* Posisi berubah: dari child ke-0 jadi child ke-1 */}
</div>
);
}
// Saat showBanner berubah, Counter RESET karena posisinya bergeser!
// ✅ Solusi 1: Pakai key
<Counter key="main-counter" />
// ✅ Solusi 2: Jangan geser posisi
<div>
<div style={{ display: showBanner ? 'block' : 'none' }}>Banner!</div>
<Counter /> {/* Selalu di posisi yang sama */}
</div>🏋️ Challenge
Challenge 1: Stopwatch yang Reset per Pemain
Bikin stopwatch sederhana dimana ada 2 pemain. Saat ganti pemain, stopwatch harus reset ke 0. Tapi kalau cuma start/stop, state harus dipertahankan.
Hint: Pakai key yang berubah saat ganti pemain.
Lihat Solusi
import { useState, useEffect, useRef } from 'react';
function GameStopwatch() {
const [pemainAktif, setPemainAktif] = useState(1);
return (
<div>
<h2>⏱️ Game Stopwatch</h2>
<div style={{ marginBottom: '20px' }}>
<button
onClick={() => setPemainAktif(1)}
style={{ fontWeight: pemainAktif === 1 ? 'bold' : 'normal' }}
>
Pemain 1
</button>
<button
onClick={() => setPemainAktif(2)}
style={{ fontWeight: pemainAktif === 2 ? 'bold' : 'normal' }}
>
Pemain 2
</button>
</div>
{/* Key berubah saat ganti pemain → stopwatch reset */}
<Stopwatch key={pemainAktif} nama={`Pemain ${pemainAktif}`} />
</div>
);
}
function Stopwatch({ nama }) {
const [waktu, setWaktu] = useState(0); // dalam milidetik
const [berjalan, setBerjalan] = useState(false);
const intervalRef = useRef(null);
useEffect(() => {
if (berjalan) {
intervalRef.current = setInterval(() => {
setWaktu(w => w + 10);
}, 10);
} else {
clearInterval(intervalRef.current);
}
return () => clearInterval(intervalRef.current);
}, [berjalan]);
function formatWaktu(ms) {
const detik = Math.floor(ms / 1000);
const milidetik = Math.floor((ms % 1000) / 10);
return `${detik.toString().padStart(2, '0')}.${milidetik.toString().padStart(2, '0')}`;
}
return (
<div style={{ textAlign: 'center' }}>
<h3>{nama}</h3>
<p style={{ fontSize: '48px', fontFamily: 'monospace' }}>
{formatWaktu(waktu)}
</p>
<button onClick={() => setBerjalan(!berjalan)}>
{berjalan ? '⏸️ Pause' : '▶️ Start'}
</button>
<button onClick={() => { setWaktu(0); setBerjalan(false); }}>
🔄 Reset
</button>
</div>
);
}Challenge 2: Form yang Bisa Di-reset dengan Key
Bikin form kontak dimana ada dropdown untuk pilih "Tipe Pesan" (Pertanyaan, Keluhan, Saran). Setiap ganti tipe, form harus reset total (semua field kosong lagi).
Hint: Bungkus form dalam komponen terpisah dan kasih key={tipe}.
Lihat Solusi
import { useState } from 'react';
function HalamanKontak() {
const [tipe, setTipe] = useState('pertanyaan');
return (
<div>
<h2>📬 Hubungi Kami</h2>
<div style={{ marginBottom: '20px' }}>
<label>Tipe Pesan: </label>
<select value={tipe} onChange={(e) => setTipe(e.target.value)}>
<option value="pertanyaan">❓ Pertanyaan</option>
<option value="keluhan">😤 Keluhan</option>
<option value="saran">💡 Saran</option>
</select>
</div>
{/* Key = tipe → form reset saat ganti tipe */}
<FormKontak key={tipe} tipe={tipe} />
</div>
);
}
function FormKontak({ tipe }) {
const [nama, setNama] = useState('');
const [email, setEmail] = useState('');
const [pesan, setPesan] = useState('');
const [prioritas, setPrioritas] = useState('normal');
const [status, setStatus] = useState('mengisi');
const placeholder = {
pertanyaan: 'Tulis pertanyaan kamu di sini...',
keluhan: 'Ceritakan masalah yang kamu alami...',
saran: 'Bagikan ide atau saran kamu...',
};
async function handleSubmit(e) {
e.preventDefault();
setStatus('mengirim');
await new Promise(resolve => setTimeout(resolve, 1000));
setStatus('terkirim');
}
if (status === 'terkirim') {
return (
<div style={{ padding: '20px', background: '#e8f5e9', borderRadius: '8px' }}>
<h3>✅ Pesan Terkirim!</h3>
<p>Terima kasih, {nama}. Tim kami akan merespons {tipe} kamu segera.</p>
</div>
);
}
return (
<form onSubmit={handleSubmit} style={{ maxWidth: '500px' }}>
<h3>Form {tipe.charAt(0).toUpperCase() + tipe.slice(1)}</h3>
<div style={{ marginBottom: '10px' }}>
<input
placeholder="Nama lengkap"
value={nama}
onChange={(e) => setNama(e.target.value)}
required
style={{ width: '100%', padding: '8px' }}
/>
</div>
<div style={{ marginBottom: '10px' }}>
<input
type="email"
placeholder="Email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
style={{ width: '100%', padding: '8px' }}
/>
</div>
{tipe === 'keluhan' && (
<div style={{ marginBottom: '10px' }}>
<label>Prioritas: </label>
<select value={prioritas} onChange={(e) => setPrioritas(e.target.value)}>
<option value="rendah">Rendah</option>
<option value="normal">Normal</option>
<option value="tinggi">Tinggi</option>
<option value="urgent">Urgent</option>
</select>
</div>
)}
<div style={{ marginBottom: '10px' }}>
<textarea
placeholder={placeholder[tipe]}
value={pesan}
onChange={(e) => setPesan(e.target.value)}
required
rows={5}
style={{ width: '100%', padding: '8px' }}
/>
</div>
<button type="submit" disabled={status === 'mengirim'}>
{status === 'mengirim' ? '⏳ Mengirim...' : '📤 Kirim'}
</button>
</form>
);
}Challenge 3: Scoreboard dengan Swap Pemain
Bikin scoreboard untuk 2 pemain. Ada tombol "Swap" yang menukar posisi pemain (Pemain 1 jadi di bawah, Pemain 2 jadi di atas). Skor masing-masing pemain harus TETAP mengikuti pemainnya, bukan posisinya.
Hint: Pakai key yang mengikuti identitas pemain, bukan posisi.
Lihat Solusi
import { useState } from 'react';
function Scoreboard() {
const [terbalik, setTerbalik] = useState(false);
const pemain1 = { id: 'p1', nama: 'Tim Merah 🔴', warna: '#ffebee' };
const pemain2 = { id: 'p2', nama: 'Tim Biru 🔵', warna: '#e3f2fd' };
// Tentukan urutan berdasarkan state
const atas = terbalik ? pemain2 : pemain1;
const bawah = terbalik ? pemain1 : pemain2;
return (
<div style={{ maxWidth: '400px', margin: '0 auto' }}>
<h2>🏆 Scoreboard</h2>
{/* Key mengikuti IDENTITAS pemain, bukan posisi */}
<SkorPemain key={atas.id} pemain={atas} />
<div style={{ textAlign: 'center', margin: '10px 0' }}>
<button onClick={() => setTerbalik(!terbalik)}>
🔄 Swap Posisi
</button>
</div>
<SkorPemain key={bawah.id} pemain={bawah} />
</div>
);
}
function SkorPemain({ pemain }) {
const [skor, setSkor] = useState(0);
return (
<div style={{
padding: '20px',
background: pemain.warna,
borderRadius: '8px',
textAlign: 'center',
marginBottom: '10px',
}}>
<h3>{pemain.nama}</h3>
<p style={{ fontSize: '36px', fontWeight: 'bold' }}>{skor}</p>
<button onClick={() => setSkor(skor + 1)}>+1</button>
<button onClick={() => setSkor(skor - 1)} disabled={skor === 0}>-1</button>
</div>
);
}Penjelasan: Karena key mengikuti pemain.id (bukan posisi), saat swap:
SkorPemain key="p1"tetap punya skor Tim Merah meskipun pindah posisiSkorPemain key="p2"tetap punya skor Tim Biru meskipun pindah posisi
React "melacak" komponen berdasarkan key, jadi state mengikuti key-nya.
Kesimpulan
Memahami kapan React menyimpan dan mereset state itu krusial untuk menghindari bug yang membingungkan. Ingat aturan utamanya:
- State terikat ke posisi di tree, bukan ke variabel atau nama komponen
- Komponen sama + posisi sama = state dipertahankan (meskipun props berubah)
- Komponen beda ATAU posisi beda = state direset
- Key prop adalah cara paling jelas untuk mengontrol identitas komponen
- Jangan definisikan komponen di dalam komponen lain karena ini menyebabkan reset yang gak disengaja
Kapan pakai key untuk reset:
- Form edit yang berganti data (ganti user, ganti item)
- Chat yang berganti kontak
- Wizard/step yang harus fresh di setiap langkah
- Game yang berganti pemain/level
Sudah paham materi ini?
Tandai sebagai selesai untuk melacak progress-mu.