Bab 7: Update Array di State
⏱ 5 menit bacaArray = Object, Aturan Sama!
Array di JavaScript itu sebenarnya object. Jadi aturan yang sama berlaku: jangan mutasi, buat yang baru.
Ini artinya method-method array yang mengubah array asli (mutating methods) nggak boleh dipakai langsung di state. Kamu harus pakai method yang mengembalikan array baru (non-mutating methods).
Tabel Referensi: Boleh vs Nggak Boleh
| Operasi | ❌ Mutasi (JANGAN) | ✅ Non-mutasi (PAKAI INI) |
|---|---|---|
| Tambah | push, unshift | [...arr, item], concat |
| Hapus | pop, shift, splice | filter, slice |
| Ganti | splice, arr[i] = x | map |
| Urutkan | sort, reverse | [...arr].sort(), [...arr].reverse() |
Analoginya: bayangin array itu daftar pesanan di warung. Kalau mau ubah daftar, kamu nggak boleh coret-coret daftar yang sudah ada. Kamu harus tulis ulang daftar baru yang sudah dimodifikasi, lalu ganti daftar lama dengan yang baru.
Menambah Item ke Array
Tambah di Akhir (seperti push)
import { useState } from 'react';
function DaftarBelanja() {
const [items, setItems] = useState(['Kopi', 'Gula', 'Susu']);
const [inputBaru, setInputBaru] = useState('');
function handleTambah() {
if (!inputBaru.trim()) return;
// ✅ Spread array lama + item baru di akhir
setItems([...items, inputBaru]);
setInputBaru(''); // Reset input
}
return (
<div>
<h2>Daftar Belanja</h2>
<input
value={inputBaru}
onChange={(e) => setInputBaru(e.target.value)}
placeholder="Tambah item..."
onKeyDown={(e) => e.key === 'Enter' && handleTambah()}
/>
<button onClick={handleTambah}>Tambah</button>
<ul>
{items.map((item, index) => (
<li key={index}>{item}</li>
))}
</ul>
</div>
);
}Tambah di Awal (seperti unshift)
function handleTambahDiAwal() {
// Item baru di depan, lalu spread array lama
setItems([inputBaru, ...items]);
}Tambah di Posisi Tertentu
function handleTambahDiPosisi(posisi, itemBaru) {
setItems([
...items.slice(0, posisi), // Ambil dari awal sampai posisi
itemBaru, // Sisipkan item baru
...items.slice(posisi) // Ambil dari posisi sampai akhir
]);
}
// Contoh: sisipkan "Teh" di posisi 1 (index 1)
// ['Kopi', 'Gula', 'Susu'] → ['Kopi', 'Teh', 'Gula', 'Susu']
handleTambahDiPosisi(1, 'Teh');Analoginya: slice itu kayak gunting. Kamu gunting daftar jadi dua bagian, sisipkan item baru di tengah, lalu gabungkan lagi.
Menghapus Item dari Array
Hapus Berdasarkan Kondisi (filter)
filter mengembalikan array baru yang hanya berisi item yang lolos kondisi:
function DaftarTugas() {
const [tugas, setTugas] = useState([
{ id: 1, teks: 'Beli kopi', selesai: false },
{ id: 2, teks: 'Bikin laporan', selesai: true },
{ id: 3, teks: 'Meeting jam 3', selesai: false },
]);
function handleHapus(id) {
// filter: "ambil semua yang ID-nya BUKAN id ini"
setTugas(tugas.filter(t => t.id !== id));
}
function handleHapusSelesai() {
// Hapus semua yang sudah selesai
setTugas(tugas.filter(t => !t.selesai));
}
return (
<div>
<h2>Daftar Tugas</h2>
<ul>
{tugas.map(t => (
<li key={t.id}>
<span style={{ textDecoration: t.selesai ? 'line-through' : 'none' }}>
{t.teks}
</span>
<button onClick={() => handleHapus(t.id)}>🗑️</button>
</li>
))}
</ul>
<button onClick={handleHapusSelesai}>Hapus yang Selesai</button>
</div>
);
}Hapus Berdasarkan Index
function handleHapusByIndex(index) {
setItems(items.filter((_, i) => i !== index));
// _ artinya "parameter ini nggak dipakai"
// i = index saat ini, skip kalau sama dengan index yang mau dihapus
}Hapus Item Pertama atau Terakhir
// Hapus item pertama (seperti shift)
setItems(items.slice(1));
// Hapus item terakhir (seperti pop)
setItems(items.slice(0, -1));Mengganti/Mengubah Item di Array
Ganti Item dengan map
map mengembalikan array baru di mana setiap item bisa diubah:
function DaftarProduk() {
const [produk, setProduk] = useState([
{ id: 1, nama: 'Kopi Susu', harga: 25000 },
{ id: 2, nama: 'Es Teh', harga: 10000 },
{ id: 3, nama: 'Roti Bakar', harga: 15000 },
]);
function handleNaikHarga(id) {
setProduk(produk.map(p => {
if (p.id === id) {
// Buat object baru untuk item yang diubah
return { ...p, harga: p.harga + 5000 };
}
// Item lain dikembalikan tanpa perubahan
return p;
}));
}
function handleUbahNama(id, namaBaru) {
setProduk(produk.map(p =>
p.id === id ? { ...p, nama: namaBaru } : p
));
}
return (
<div>
<h2>Menu Warung</h2>
{produk.map(p => (
<div key={p.id} style={{ marginBottom: '10px' }}>
<span>{p.nama} - Rp {p.harga.toLocaleString()}</span>
<button onClick={() => handleNaikHarga(p.id)}>+5rb</button>
</div>
))}
</div>
);
}Toggle Boolean di Item
function handleToggleSelesai(id) {
setTugas(tugas.map(t =>
t.id === id ? { ...t, selesai: !t.selesai } : t
));
}Ganti Item di Index Tertentu
function handleGantiDiIndex(index, itemBaru) {
setItems(items.map((item, i) =>
i === index ? itemBaru : item
));
}Mengurutkan Array
sort() dan reverse() memutasi array asli! Jadi kamu harus copy dulu:
function DaftarSortable() {
const [siswa, setSiswa] = useState([
{ nama: 'Citra', nilai: 85 },
{ nama: 'Ani', nilai: 92 },
{ nama: 'Budi', nilai: 78 },
{ nama: 'Deni', nilai: 88 },
]);
function handleSortNama() {
// ✅ Copy dulu dengan spread, baru sort
const sorted = [...siswa].sort((a, b) => a.nama.localeCompare(b.nama));
setSiswa(sorted);
}
function handleSortNilai() {
// ✅ Sort berdasarkan nilai (tertinggi dulu)
const sorted = [...siswa].sort((a, b) => b.nilai - a.nilai);
setSiswa(sorted);
}
function handleBalik() {
// ✅ Reverse juga harus copy dulu
const reversed = [...siswa].reverse();
setSiswa(reversed);
}
return (
<div>
<h2>Daftar Siswa</h2>
<button onClick={handleSortNama}>Sort Nama (A-Z)</button>
<button onClick={handleSortNilai}>Sort Nilai (Tertinggi)</button>
<button onClick={handleBalik}>Balik Urutan</button>
<ol>
{siswa.map((s, i) => (
<li key={i}>{s.nama} — Nilai: {s.nilai}</li>
))}
</ol>
</div>
);
}Penting: [...siswa].sort(...) — spread DULU baru sort. Kalau siswa.sort(...) langsung, kamu mutasi state!
// ❌ SALAH — mutasi langsung
siswa.sort((a, b) => a.nama.localeCompare(b.nama));
setSiswa(siswa); // Referensi sama, React mungkin skip
// ✅ BENAR — copy dulu
const sorted = [...siswa].sort((a, b) => a.nama.localeCompare(b.nama));
setSiswa(sorted); // Referensi baru, React pasti re-renderUpdate Object di Dalam Array
Ini kombinasi dari bab sebelumnya (update object) dan bab ini (update array). Pola dasarnya: pakai map untuk menemukan item, lalu spread object untuk update.
function KeranjangBelanja() {
const [keranjang, setKeranjang] = useState([
{ id: 1, nama: 'Kopi Susu', harga: 25000, jumlah: 1 },
{ id: 2, nama: 'Roti Bakar', harga: 15000, jumlah: 2 },
{ id: 3, nama: 'Es Teh', harga: 10000, jumlah: 1 },
]);
function handleTambahJumlah(id) {
setKeranjang(keranjang.map(item =>
item.id === id
? { ...item, jumlah: item.jumlah + 1 } // Object baru!
: item // Item lain nggak diubah
));
}
function handleKurangJumlah(id) {
setKeranjang(keranjang.map(item => {
if (item.id === id) {
const jumlahBaru = item.jumlah - 1;
if (jumlahBaru <= 0) return null; // Tandai untuk dihapus
return { ...item, jumlah: jumlahBaru };
}
return item;
}).filter(Boolean)); // Hapus yang null
}
function handleHapus(id) {
setKeranjang(keranjang.filter(item => item.id !== id));
}
const total = keranjang.reduce(
(sum, item) => sum + (item.harga * item.jumlah), 0
);
return (
<div>
<h2>Keranjang Belanja</h2>
{keranjang.map(item => (
<div key={item.id} style={{
display: 'flex',
alignItems: 'center',
gap: '10px',
marginBottom: '8px'
}}>
<span>{item.nama}</span>
<button onClick={() => handleKurangJumlah(item.id)}>-</button>
<span>{item.jumlah}</span>
<button onClick={() => handleTambahJumlah(item.id)}>+</button>
<span>= Rp {(item.harga * item.jumlah).toLocaleString()}</span>
<button onClick={() => handleHapus(item.id)}>🗑️</button>
</div>
))}
<hr />
<p><strong>Total: Rp {total.toLocaleString()}</strong></p>
</div>
);
}Pola Lengkap: CRUD Array
Berikut semua operasi CRUD (Create, Read, Update, Delete) untuk array di state:
import { useState } from 'react';
function CrudLengkap() {
const [items, setItems] = useState([
{ id: 1, teks: 'Item pertama', aktif: true },
{ id: 2, teks: 'Item kedua', aktif: false },
{ id: 3, teks: 'Item ketiga', aktif: true },
]);
let nextId = 4;
// CREATE — tambah item baru
function handleCreate(teks) {
setItems(prev => [...prev, {
id: nextId++,
teks: teks,
aktif: true
}]);
}
// READ — sudah otomatis via render (items.map)
// UPDATE — ubah item yang ada
function handleUpdate(id, teksBaru) {
setItems(prev => prev.map(item =>
item.id === id ? { ...item, teks: teksBaru } : item
));
}
// UPDATE — toggle aktif
function handleToggle(id) {
setItems(prev => prev.map(item =>
item.id === id ? { ...item, aktif: !item.aktif } : item
));
}
// DELETE — hapus item
function handleDelete(id) {
setItems(prev => prev.filter(item => item.id !== id));
}
// REORDER — pindah item ke atas
function handleMoveUp(index) {
if (index === 0) return; // Sudah paling atas
setItems(prev => {
const copy = [...prev];
// Tukar posisi dengan item di atasnya
[copy[index - 1], copy[index]] = [copy[index], copy[index - 1]];
return copy;
});
}
return (
<div>
<h2>CRUD Array</h2>
<button onClick={() => handleCreate(`Item baru ${Date.now()}`)}>
+ Tambah Item
</button>
<ul>
{items.map((item, index) => (
<li key={item.id} style={{ opacity: item.aktif ? 1 : 0.5 }}>
<input
value={item.teks}
onChange={(e) => handleUpdate(item.id, e.target.value)}
/>
<button onClick={() => handleToggle(item.id)}>
{item.aktif ? '✅' : '❌'}
</button>
<button onClick={() => handleMoveUp(index)}>⬆️</button>
<button onClick={() => handleDelete(item.id)}>🗑️</button>
</li>
))}
</ul>
</div>
);
}Immer untuk Array
Sama seperti object, Immer juga bikin update array jauh lebih simpel:
import { useImmer } from 'use-immer';
function TodoDenganImmer() {
const [todos, updateTodos] = useImmer([
{ id: 1, teks: 'Beli kopi', selesai: false },
{ id: 2, teks: 'Bikin laporan', selesai: false },
]);
function handleTambah(teks) {
updateTodos(draft => {
// Dengan Immer, push BOLEH!
draft.push({ id: Date.now(), teks, selesai: false });
});
}
function handleToggle(id) {
updateTodos(draft => {
// Cari dan mutasi langsung — Immer yang handle
const todo = draft.find(t => t.id === id);
todo.selesai = !todo.selesai;
});
}
function handleHapus(id) {
updateTodos(draft => {
// Cari index dan splice — Immer yang handle
const index = draft.findIndex(t => t.id === id);
draft.splice(index, 1);
});
}
function handleUbahTeks(id, teksBaru) {
updateTodos(draft => {
const todo = draft.find(t => t.id === id);
todo.teks = teksBaru;
});
}
function handleSort() {
updateTodos(draft => {
// Sort langsung — Immer yang handle
draft.sort((a, b) => a.teks.localeCompare(b.teks));
});
}
return (
<div>
<h2>Todo (Immer)</h2>
<button onClick={() => handleTambah('Todo baru')}>+ Tambah</button>
<button onClick={handleSort}>Sort A-Z</button>
<ul>
{todos.map(todo => (
<li key={todo.id}>
<input
type="checkbox"
checked={todo.selesai}
onChange={() => handleToggle(todo.id)}
/>
<input
value={todo.teks}
onChange={(e) => handleUbahTeks(todo.id, e.target.value)}
style={{ textDecoration: todo.selesai ? 'line-through' : 'none' }}
/>
<button onClick={() => handleHapus(todo.id)}>🗑️</button>
</li>
))}
</ul>
</div>
);
}Perhatiin betapa bersihnya kode dengan Immer. Kamu bisa pakai push, splice, assignment langsung (todo.selesai = ...), dan sort tanpa khawatir mutasi. Immer yang bikin object/array baru di belakang layar.
Pola-Pola Umum
Tambah Item dengan ID Unik
// Cara 1: Date.now() (simpel, cukup untuk kebanyakan kasus)
const itemBaru = { id: Date.now(), teks: 'Baru' };
// Cara 2: Counter (lebih predictable)
const [nextId, setNextId] = useState(1);
function handleTambah() {
setItems(prev => [...prev, { id: nextId, teks: 'Baru' }]);
setNextId(prev => prev + 1);
}
// Cara 3: crypto.randomUUID() (paling unik)
const itemBaru = { id: crypto.randomUUID(), teks: 'Baru' };Filter dengan Multiple Kondisi
// Hapus item yang selesai DAN sudah lebih dari 7 hari
function handleBersihkan() {
const semingguLalu = Date.now() - 7 * 24 * 60 * 60 * 1000;
setTodos(prev => prev.filter(todo =>
!(todo.selesai && todo.tanggalSelesai < semingguLalu)
));
}Batch Update (Update Banyak Item Sekaligus)
// Tandai semua sebagai selesai
function handleSelesaikanSemua() {
setTodos(prev => prev.map(todo => ({ ...todo, selesai: true })));
}
// Toggle semua
function handleToggleSemua() {
const semuaSelesai = todos.every(t => t.selesai);
setTodos(prev => prev.map(todo => ({ ...todo, selesai: !semuaSelesai })));
}Drag and Drop (Pindah Posisi)
function handlePindah(dariIndex, keIndex) {
setItems(prev => {
const copy = [...prev];
const [item] = copy.splice(dariIndex, 1); // Ambil item
copy.splice(keIndex, 0, item); // Sisipkan di posisi baru
return copy;
});
}Catatan: di sini kita mutasi copy (bukan state langsung). Ini aman karena copy adalah array baru yang dibuat dengan [...prev]. Kita nggak pernah menyentuh prev langsung.
Deduplikasi (Hapus Duplikat)
function handleHapusDuplikat() {
setItems(prev => {
const seen = new Set();
return prev.filter(item => {
if (seen.has(item.nama)) return false;
seen.add(item.nama);
return true;
});
});
}Contoh Lengkap: Aplikasi Catatan
import { useState } from 'react';
function AplikasiCatatan() {
const [catatan, setCatatan] = useState([]);
const [judulBaru, setJudulBaru] = useState('');
const [isiBaru, setIsiBaru] = useState('');
const [editId, setEditId] = useState(null);
function handleTambah() {
if (!judulBaru.trim()) return;
setCatatan(prev => [...prev, {
id: Date.now(),
judul: judulBaru,
isi: isiBaru,
tanggal: new Date().toLocaleDateString('id-ID'),
warna: getWarnaRandom()
}]);
setJudulBaru('');
setIsiBaru('');
}
function handleHapus(id) {
setCatatan(prev => prev.filter(c => c.id !== id));
}
function handleEdit(id) {
const target = catatan.find(c => c.id === id);
setJudulBaru(target.judul);
setIsiBaru(target.isi);
setEditId(id);
}
function handleSimpanEdit() {
setCatatan(prev => prev.map(c =>
c.id === editId
? { ...c, judul: judulBaru, isi: isiBaru }
: c
));
setJudulBaru('');
setIsiBaru('');
setEditId(null);
}
function getWarnaRandom() {
const warna = ['#fff3cd', '#d1ecf1', '#d4edda', '#f8d7da', '#e2d9f3'];
return warna[Math.floor(Math.random() * warna.length)];
}
return (
<div>
<h2>Catatan Saya</h2>
{/* Form Input */}
<div style={{ marginBottom: '20px' }}>
<input
value={judulBaru}
onChange={(e) => setJudulBaru(e.target.value)}
placeholder="Judul catatan..."
style={{ display: 'block', marginBottom: '8px', width: '100%' }}
/>
<textarea
value={isiBaru}
onChange={(e) => setIsiBaru(e.target.value)}
placeholder="Isi catatan..."
style={{ display: 'block', marginBottom: '8px', width: '100%' }}
/>
{editId ? (
<div>
<button onClick={handleSimpanEdit}>💾 Simpan Edit</button>
<button onClick={() => { setEditId(null); setJudulBaru(''); setIsiBaru(''); }}>
❌ Batal
</button>
</div>
) : (
<button onClick={handleTambah}>+ Tambah Catatan</button>
)}
</div>
{/* Daftar Catatan */}
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(200px, 1fr))', gap: '12px' }}>
{catatan.map(c => (
<div key={c.id} style={{
backgroundColor: c.warna,
padding: '12px',
borderRadius: '8px',
boxShadow: '0 2px 4px rgba(0,0,0,0.1)'
}}>
<h3 style={{ margin: '0 0 8px 0' }}>{c.judul}</h3>
<p style={{ margin: '0 0 8px 0' }}>{c.isi}</p>
<small>{c.tanggal}</small>
<div>
<button onClick={() => handleEdit(c.id)}>✏️</button>
<button onClick={() => handleHapus(c.id)}>🗑️</button>
</div>
</div>
))}
</div>
{catatan.length === 0 && <p style={{ color: '#999' }}>Belum ada catatan. Tambah yang pertama!</p>}
</div>
);
}Tips Performa: Key yang Benar
Saat render array dengan map, selalu kasih key yang unik dan stabil:
// ❌ BURUK — pakai index sebagai key
{items.map((item, index) => (
<li key={index}>{item.teks}</li>
))}
// ✅ BAGUS — pakai ID unik
{items.map(item => (
<li key={item.id}>{item.teks}</li>
))}Kenapa index buruk? Karena kalau kamu hapus atau sisipkan item di tengah, index bergeser. React jadi bingung item mana yang berubah, mana yang baru, mana yang dihapus. Ini bisa menyebabkan bug visual dan masalah performa.
Analoginya: bayangin kamu punya 5 anak dan kamu panggil mereka "Anak 1", "Anak 2", dst. Kalau "Anak 2" pindah sekolah, tiba-tiba "Anak 3" jadi "Anak 2". Bingung kan? Lebih baik panggil pakai nama asli (ID unik).
⚠️ Jebakan
Jebakan 1: Pakai push/pop/splice langsung
// ❌ SALAH — mutasi state langsung!
function handleTambah() {
items.push('Item baru'); // MUTASI!
setItems(items); // Referensi sama, React mungkin skip
}
// ✅ BENAR — buat array baru
function handleTambah() {
setItems([...items, 'Item baru']);
}Jebakan 2: Sort tanpa copy
// ❌ SALAH — sort() mutasi array asli!
function handleSort() {
items.sort((a, b) => a - b); // MUTASI!
setItems(items);
}
// ✅ BENAR — copy dulu baru sort
function handleSort() {
const sorted = [...items].sort((a, b) => a - b);
setItems(sorted);
}Jebakan 3: Mutasi object di dalam array
// ❌ SALAH — mutasi object di dalam array
function handleToggle(id) {
const todo = todos.find(t => t.id === id);
todo.selesai = !todo.selesai; // MUTASI object!
setTodos([...todos]); // Array baru tapi object-nya masih dimutasi
}
// ✅ BENAR — buat object baru untuk item yang diubah
function handleToggle(id) {
setTodos(todos.map(t =>
t.id === id ? { ...t, selesai: !t.selesai } : t
));
}Ini jebakan yang halus! Meskipun kamu bikin array baru dengan [...todos], object-object di dalamnya masih referensi yang sama. Kamu HARUS bikin object baru (dengan spread) untuk item yang diubah.
Jebakan 4: Lupa bahwa filter/map return array baru
// ❌ Nggak perlu spread lagi setelah filter/map — mereka sudah return array baru
setItems([...items.filter(i => i.aktif)]); // Spread nggak perlu!
// ✅ Cukup langsung
setItems(items.filter(i => i.aktif)); // filter sudah return array baru
setItems(items.map(i => ({ ...i, aktif: true }))); // map jugaJebakan 5: Concat vs spread untuk menggabungkan
// Dua cara yang sama-sama valid:
setItems([...items, ...itemsBaru]); // Spread
setItems(items.concat(itemsBaru)); // Concat
// Keduanya return array baru, keduanya amanJebakan 6: Nested array update
const [data, setData] = useState({
kategori: [
{ nama: 'Minuman', items: ['Kopi', 'Teh'] },
{ nama: 'Makanan', items: ['Roti', 'Nasi'] }
]
});
// ❌ SALAH
function handleTambahItem(kategoriIndex, itemBaru) {
data.kategori[kategoriIndex].items.push(itemBaru); // MUTASI!
}
// ✅ BENAR — spread di setiap level
function handleTambahItem(kategoriIndex, itemBaru) {
setData({
...data,
kategori: data.kategori.map((kat, i) =>
i === kategoriIndex
? { ...kat, items: [...kat.items, itemBaru] }
: kat
)
});
}🏋️ Challenge
Challenge 1: Todo List dengan Filter
Buat todo list yang:
- Bisa tambah todo baru
- Bisa toggle selesai/belum
- Bisa hapus todo
- Punya filter: "Semua", "Aktif", "Selesai"
- Tampilkan jumlah todo aktif
💡 Hint
Simpan semua todos di state. Filter hanya mempengaruhi TAMPILAN (apa yang di-render), bukan data yang disimpan. Buat state terpisah untuk filter aktif.
✅ Solusi
import { useState } from 'react';
function TodoListFilter() {
const [todos, setTodos] = useState([
{ id: 1, teks: 'Belajar React', selesai: false },
{ id: 2, teks: 'Bikin project', selesai: false },
{ id: 3, teks: 'Baca dokumentasi', selesai: true },
]);
const [input, setInput] = useState('');
const [filter, setFilter] = useState('semua'); // 'semua' | 'aktif' | 'selesai'
function handleTambah() {
if (!input.trim()) return;
setTodos(prev => [...prev, {
id: Date.now(),
teks: input,
selesai: false
}]);
setInput('');
}
function handleToggle(id) {
setTodos(prev => prev.map(t =>
t.id === id ? { ...t, selesai: !t.selesai } : t
));
}
function handleHapus(id) {
setTodos(prev => prev.filter(t => t.id !== id));
}
// Filter hanya untuk tampilan
const todosTampil = todos.filter(t => {
if (filter === 'aktif') return !t.selesai;
if (filter === 'selesai') return t.selesai;
return true; // 'semua'
});
const jumlahAktif = todos.filter(t => !t.selesai).length;
return (
<div>
<h2>Todo List</h2>
<div>
<input
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="Tambah todo..."
onKeyDown={(e) => e.key === 'Enter' && handleTambah()}
/>
<button onClick={handleTambah}>Tambah</button>
</div>
<div style={{ margin: '10px 0' }}>
<button
onClick={() => setFilter('semua')}
style={{ fontWeight: filter === 'semua' ? 'bold' : 'normal' }}
>
Semua ({todos.length})
</button>
<button
onClick={() => setFilter('aktif')}
style={{ fontWeight: filter === 'aktif' ? 'bold' : 'normal' }}
>
Aktif ({jumlahAktif})
</button>
<button
onClick={() => setFilter('selesai')}
style={{ fontWeight: filter === 'selesai' ? 'bold' : 'normal' }}
>
Selesai ({todos.length - jumlahAktif})
</button>
</div>
<ul>
{todosTampil.map(todo => (
<li key={todo.id}>
<input
type="checkbox"
checked={todo.selesai}
onChange={() => handleToggle(todo.id)}
/>
<span style={{
textDecoration: todo.selesai ? 'line-through' : 'none',
color: todo.selesai ? '#999' : '#333'
}}>
{todo.teks}
</span>
<button onClick={() => handleHapus(todo.id)}>🗑️</button>
</li>
))}
</ul>
<p>{jumlahAktif} todo tersisa</p>
</div>
);
}Challenge 2: Playlist Manager
Buat playlist musik yang bisa:
- Tambah lagu (judul + artis)
- Hapus lagu
- Pindah lagu ke atas/bawah (reorder)
- Shuffle (acak urutan)
- Tampilkan "Now Playing" (lagu pertama di list)
💡 Hint
Untuk reorder, tukar posisi dua item di array (copy dulu!). Untuk shuffle, pakai algoritma Fisher-Yates pada copy array.
✅ Solusi
import { useState } from 'react';
function PlaylistManager() {
const [playlist, setPlaylist] = useState([
{ id: 1, judul: 'Bohemian Rhapsody', artis: 'Queen' },
{ id: 2, judul: 'Hotel California', artis: 'Eagles' },
{ id: 3, judul: 'Imagine', artis: 'John Lennon' },
]);
const [judulBaru, setJudulBaru] = useState('');
const [artisBaru, setArtisBaru] = useState('');
function handleTambah() {
if (!judulBaru.trim() || !artisBaru.trim()) return;
setPlaylist(prev => [...prev, {
id: Date.now(),
judul: judulBaru,
artis: artisBaru
}]);
setJudulBaru('');
setArtisBaru('');
}
function handleHapus(id) {
setPlaylist(prev => prev.filter(lagu => lagu.id !== id));
}
function handlePindahAtas(index) {
if (index === 0) return;
setPlaylist(prev => {
const copy = [...prev];
[copy[index - 1], copy[index]] = [copy[index], copy[index - 1]];
return copy;
});
}
function handlePindahBawah(index) {
if (index === playlist.length - 1) return;
setPlaylist(prev => {
const copy = [...prev];
[copy[index], copy[index + 1]] = [copy[index + 1], copy[index]];
return copy;
});
}
function handleShuffle() {
setPlaylist(prev => {
const copy = [...prev];
// Fisher-Yates shuffle
for (let i = copy.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[copy[i], copy[j]] = [copy[j], copy[i]];
}
return copy;
});
}
return (
<div>
<h2>Playlist Manager</h2>
{playlist.length > 0 && (
<div style={{
backgroundColor: '#e8f5e9',
padding: '12px',
borderRadius: '8px',
marginBottom: '16px'
}}>
<strong>Now Playing:</strong> {playlist[0].judul} — {playlist[0].artis}
</div>
)}
<div style={{ marginBottom: '16px' }}>
<input
value={judulBaru}
onChange={(e) => setJudulBaru(e.target.value)}
placeholder="Judul lagu"
/>
<input
value={artisBaru}
onChange={(e) => setArtisBaru(e.target.value)}
placeholder="Artis"
/>
<button onClick={handleTambah}>+ Tambah</button>
<button onClick={handleShuffle}>🔀 Shuffle</button>
</div>
<ol>
{playlist.map((lagu, index) => (
<li key={lagu.id} style={{ marginBottom: '8px' }}>
<strong>{lagu.judul}</strong> — {lagu.artis}
{' '}
<button onClick={() => handlePindahAtas(index)} disabled={index === 0}>
⬆️
</button>
<button onClick={() => handlePindahBawah(index)} disabled={index === playlist.length - 1}>
⬇️
</button>
<button onClick={() => handleHapus(lagu.id)}>🗑️</button>
</li>
))}
</ol>
{playlist.length === 0 && <p>Playlist kosong. Tambah lagu!</p>}
<p>Total: {playlist.length} lagu</p>
</div>
);
}Challenge 3: Tabel Sortable dengan Multi-Column
Buat tabel data siswa yang bisa di-sort berdasarkan kolom mana pun (nama, nilai, kelas). Klik header kolom untuk sort. Klik lagi untuk balik urutan (ascending/descending).
💡 Hint
Simpan sortBy (kolom mana) dan sortOrder ('asc' atau 'desc') di state. Saat header diklik: kalau kolom sama, flip order. Kalau kolom beda, set kolom baru dengan order 'asc'. Sort data saat render (jangan simpan data yang sudah di-sort di state).
✅ Solusi
import { useState } from 'react';
function TabelSortable() {
const [siswa, setSiswa] = useState([
{ id: 1, nama: 'Budi Santoso', nilai: 85, kelas: 'XII-A' },
{ id: 2, nama: 'Ani Wijaya', nilai: 92, kelas: 'XII-B' },
{ id: 3, nama: 'Citra Dewi', nilai: 78, kelas: 'XII-A' },
{ id: 4, nama: 'Deni Pratama', nilai: 88, kelas: 'XII-C' },
{ id: 5, nama: 'Eka Putri', nilai: 95, kelas: 'XII-B' },
{ id: 6, nama: 'Fajar Rahman', nilai: 72, kelas: 'XII-C' },
]);
const [sortBy, setSortBy] = useState('nama');
const [sortOrder, setSortOrder] = useState('asc'); // 'asc' atau 'desc'
function handleSort(kolom) {
if (sortBy === kolom) {
// Kolom sama — flip order
setSortOrder(prev => prev === 'asc' ? 'desc' : 'asc');
} else {
// Kolom beda — set kolom baru, default ascending
setSortBy(kolom);
setSortOrder('asc');
}
}
// Sort saat render (derived state)
const siswaSorted = [...siswa].sort((a, b) => {
let comparison = 0;
if (sortBy === 'nama' || sortBy === 'kelas') {
comparison = a[sortBy].localeCompare(b[sortBy]);
} else if (sortBy === 'nilai') {
comparison = a.nilai - b.nilai;
}
return sortOrder === 'asc' ? comparison : -comparison;
});
function getHeaderIcon(kolom) {
if (sortBy !== kolom) return '↕️';
return sortOrder === 'asc' ? '⬆️' : '⬇️';
}
return (
<div>
<h2>Data Siswa (Klik header untuk sort)</h2>
<table style={{ borderCollapse: 'collapse', width: '100%' }}>
<thead>
<tr>
<th
onClick={() => handleSort('nama')}
style={{ cursor: 'pointer', padding: '8px', borderBottom: '2px solid #333' }}
>
Nama {getHeaderIcon('nama')}
</th>
<th
onClick={() => handleSort('nilai')}
style={{ cursor: 'pointer', padding: '8px', borderBottom: '2px solid #333' }}
>
Nilai {getHeaderIcon('nilai')}
</th>
<th
onClick={() => handleSort('kelas')}
style={{ cursor: 'pointer', padding: '8px', borderBottom: '2px solid #333' }}
>
Kelas {getHeaderIcon('kelas')}
</th>
</tr>
</thead>
<tbody>
{siswaSorted.map(s => (
<tr key={s.id}>
<td style={{ padding: '8px', borderBottom: '1px solid #ddd' }}>{s.nama}</td>
<td style={{ padding: '8px', borderBottom: '1px solid #ddd' }}>{s.nilai}</td>
<td style={{ padding: '8px', borderBottom: '1px solid #ddd' }}>{s.kelas}</td>
</tr>
))}
</tbody>
</table>
<p style={{ color: '#666', fontSize: '12px' }}>
Sorted by: {sortBy} ({sortOrder === 'asc' ? 'A-Z / Rendah-Tinggi' : 'Z-A / Tinggi-Rendah'})
</p>
</div>
);
}Perhatiin: kita TIDAK menyimpan data yang sudah di-sort di state. Data asli tetap di siswa. Sorting dilakukan saat render (siswaSorted). Ini pola yang disebut derived state — state yang dihitung dari state lain. Keuntungannya: data asli tetap utuh, dan kamu bisa sort/filter dengan cara berbeda tanpa kehilangan urutan asli.
Ringkasan
- Jangan mutasi array di state — selalu buat array baru
- Tambah:
[...arr, item]atau[item, ...arr] - Hapus:
arr.filter(item => kondisi) - Ganti:
arr.map(item => kondisi ? itemBaru : item) - Sort:
[...arr].sort(compareFn)(copy dulu!) - Object di array: pakai
map+ spread object - Immer menyederhanakan semua operasi di atas
- Selalu pakai key unik (bukan index) saat render array
- Pertimbangkan derived state untuk sorting/filtering tampilan
Penutup Part 3
Selamat! Kamu sudah menyelesaikan Part 3: Menambah Interaktivitas. Sekarang kamu paham:
- Cara merespons event (klik, input, submit)
- State sebagai memori komponen
- Proses render dan commit React
- State sebagai snapshot yang tetap per render
- Cara mengantri update state dengan updater function
- Cara update object di state tanpa mutasi
- Cara update array di state tanpa mutasi
Konsep-konsep ini adalah fondasi dari SEMUA aplikasi React. Setiap form, setiap list, setiap interaksi user — semuanya pakai konsep yang sudah kamu pelajari di part ini. Latihan terus, dan jangan takut eksperimen!
Sudah paham materi ini?
Tandai sebagai selesai untuk melacak progress-mu.