Bab 7: Update Array di State

5 menit baca

Array = 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)
Tambahpush, unshift[...arr, item], concat
Hapuspop, shift, splicefilter, slice
Gantisplice, arr[i] = xmap
Urutkansort, 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)

jsx
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)

jsx
function handleTambahDiAwal() {
  // Item baru di depan, lalu spread array lama
  setItems([inputBaru, ...items]);
}

Tambah di Posisi Tertentu

jsx
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:

jsx
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

jsx
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

jsx
// 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:

jsx
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

jsx
function handleToggleSelesai(id) {
  setTugas(tugas.map(t => 
    t.id === id ? { ...t, selesai: !t.selesai } : t
  ));
}

Ganti Item di Index Tertentu

jsx
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:

jsx
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!

jsx
// ❌ 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-render

Update 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.

jsx
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:

jsx
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:

jsx
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

jsx
// 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

jsx
// 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)

jsx
// 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)

jsx
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)

jsx
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

jsx
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:

jsx
// ❌ 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

jsx
// ❌ 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

jsx
// ❌ 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

jsx
// ❌ 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

jsx
// ❌ 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 juga

Jebakan 5: Concat vs spread untuk menggabungkan

jsx
// Dua cara yang sama-sama valid:
setItems([...items, ...itemsBaru]);        // Spread
setItems(items.concat(itemsBaru));          // Concat

// Keduanya return array baru, keduanya aman

Jebakan 6: Nested array update

jsx
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
jsx
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
jsx
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
jsx
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:

  1. Cara merespons event (klik, input, submit)
  2. State sebagai memori komponen
  3. Proses render dan commit React
  4. State sebagai snapshot yang tetap per render
  5. Cara mengantri update state dengan updater function
  6. Cara update object di state tanpa mutasi
  7. 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.