kodingup.camp

Beranda/React Mastery: Dasar, Rendering & Hooks

Manajemen State Lokal

Modul 6 dari 124 menit bacaLocal State Management

Isi modul

    Kenapa ini penting

    Props membuat komponen bisa menampilkan data dari luar, tapi aplikasi nyata juga perlu mengingat sesuatu yang berubah karena interaksi pengguna: isi kolom pencarian, jumlah item di keranjang, tab yang sedang aktif, apakah menu terbuka. Ingatan milik komponen ini disebut state. Begitu state berubah, React menggambar ulang komponen dengan nilai baru. useState adalah hook yang paling sering kamu pakai seumur karier React, dan memahami aturannya (terutama soal immutability dan updater function) akan menyelamatkanmu dari bug yang membingungkan.

    Konsep inti

    Kenapa variabel biasa tidak cukup

    Variabel lokal biasa (let jumlah = 0) punya dua masalah:

    1. Tidak bertahan antar render. Setiap kali React memanggil ulang fungsi komponen, variabel lokal dibuat dari awal lagi.
    2. Mengubahnya tidak memicu render. React tidak tahu variabel itu berubah, jadi layar tidak diperbarui.

    useState menyelesaikan keduanya: nilainya disimpan oleh React di luar fungsi komponen, dan memanggil fungsi setter-nya memberi sinyal ke React untuk render ulang.

    Sintaks useState

    const [nilai, setNilai] = useState(nilaiAwal);
    

    Konvensi nama: [sesuatu, setSesuatu]. Bentuk [a, b] adalah array destructuring biasa.

    Aturan hooks

    Hooks (semua fungsi berawalan use) hanya boleh dipanggil di level teratas function component atau custom hook, tidak di dalam if, loop, atau fungsi bersarang. React mengenali setiap state berdasarkan urutan pemanggilan, jadi urutannya harus selalu sama di setiap render.

    State sebagai snapshot

    Ini konsep yang sering mengejutkan pemula: di dalam satu render, nilai state tetap. Memanggil setJumlah(jumlah + 1) tidak langsung mengubah jumlah di baris berikutnya; ia meminta React membuat render baru dengan nilai baru. Analogi: state seperti foto. Kamu bisa meminta foto baru diambil, tapi foto yang sedang kamu pegang tidak berubah.

    Batching

    React mengumpulkan (batching) semua pemanggilan setter dalam satu event handler, lalu melakukan satu render setelah handler selesai. Analogi pelayan restoran: ia tidak lari ke dapur setiap kamu menyebut satu menu, tapi menunggu kamu selesai memesan lalu membawa semua pesanan sekaligus. Akibatnya:

    setJumlah(jumlah + 1);
    setJumlah(jumlah + 1);
    setJumlah(jumlah + 1);
    // Jika jumlah = 0, hasilnya 1, bukan 3!
    

    Ketiganya membaca jumlah yang sama (0), jadi tiga kali meminta "jadikan 1".

    Updater function

    Kalau nilai baru bergantung pada nilai sebelumnya, kirim fungsi ke setter: setJumlah(n => n + 1). React mengantrekan fungsi ini dan menjalankannya berurutan, masing-masing menerima hasil fungsi sebelumnya. Tiga kali setJumlah(n => n + 1) dari 0 menghasilkan 3.

    Antrean n yang diterima Hasil
    n => n + 1 0 1
    n => n + 1 1 2
    n => n + 1 2 3

    Immutability: object dan array

    React memutuskan perlu render ulang atau tidak dengan membandingkan nilai lama dan baru menggunakan Object.is. Kalau kamu mengubah isi objek secara langsung (user.nama = 'Budi') lalu memanggil setUser(user), referensinya sama, React menganggap tidak ada perubahan. Aturan emasnya: perlakukan state sebagai read-only, selalu buat salinan baru.

    Operasi ❌ Mutasi (hindari) ✅ Salinan baru
    Ubah properti objek user.nama = 'Budi' { ...user, nama: 'Budi' }
    Tambah item array arr.push(x) [...arr, x]
    Hapus item array arr.splice(i, 1) arr.filter(item => item.id !== id)
    Ubah item array arr[i].done = true arr.map(item => item.id === id ? { ...item, done: true } : item)
    Urutkan arr.sort() [...arr].sort() atau arr.toSorted()

    Untuk objek bersarang, salin setiap lapis yang berubah: { ...user, alamat: { ...user.alamat, kota: 'Medan' } }.

    Contoh kode

    // src/App.jsx, counter, batching, updater, objek dan array
    import { useState } from 'react';
    
    function Counter() {
      const [jumlah, setJumlah] = useState(0);
    
      function tambahTigaSalah() {
        setJumlah(jumlah + 1);
        setJumlah(jumlah + 1);
        setJumlah(jumlah + 1);
        console.log(jumlah); // → masih nilai lama (snapshot)
      }
    
      function tambahTigaBenar() {
        setJumlah(n => n + 1);
        setJumlah(n => n + 1);
        setJumlah(n => n + 1);
      }
    
      return (
        <div>
          <h2>Jumlah: {jumlah}</h2>
          <button onClick={tambahTigaSalah}>+3 (salah, hasilnya +1)</button>
          <button onClick={tambahTigaBenar}>+3 (benar)</button>
          <button onClick={() => setJumlah(0)}>Reset</button>
        </div>
      );
    }
    
    function Profil() {
      const [user, setUser] = useState({
        nama: 'Sari',
        alamat: { kota: 'Yogyakarta', kodePos: '55281' },
      });
    
      function pindahKota() {
        // Salin objek luar DAN objek alamat yang diubah
        setUser({ ...user, alamat: { ...user.alamat, kota: 'Semarang' } });
      }
    
      return (
        <div>
          <p>{user.nama} tinggal di {user.alamat.kota}</p>
          <button onClick={pindahKota}>Pindah ke Semarang</button>
        </div>
      );
    }
    // Klik → "Sari tinggal di Semarang"
    
    function DaftarBelanja() {
      const [items, setItems] = useState([
        { id: 1, nama: 'Gula', beli: false },
        { id: 2, nama: 'Kopi', beli: false },
      ]);
      const [nextId, setNextId] = useState(3);
    
      function tambah() {
        setItems([...items, { id: nextId, nama: `Barang ${nextId}`, beli: false }]);
        setNextId(nextId + 1);
      }
    
      function toggle(id) {
        setItems(items.map(item => (item.id === id ? { ...item, beli: !item.beli } : item)));
      }
    
      function hapus(id) {
        setItems(items.filter(item => item.id !== id));
      }
    
      return (
        <div>
          <button onClick={tambah}>Tambah barang</button>
          <ul>
            {items.map(item => (
              <li key={item.id}>
                <span style={{ textDecoration: item.beli ? 'line-through' : 'none' }}>{item.nama}</span>{' '}
                <button onClick={() => toggle(item.id)}>Centang</button>
                <button onClick={() => hapus(item.id)}>Hapus</button>
              </li>
            ))}
          </ul>
        </div>
      );
    }
    
    export default function App() {
      return (
        <>
          <Counter />
          <Profil />
          <DaftarBelanja />
        </>
      );
    }
    

    Variabel biasa vs state, supaya perbedaannya terasa:

    import { useState } from 'react';
    
    export function Perbandingan() {
      let biasa = 0;                       // dibuat ulang setiap render
      const [state, setState] = useState(0);
    
      return (
        <>
          <button onClick={() => { biasa++; console.log('biasa:', biasa); }}>
            Variabel biasa: {biasa}
          </button>
          {/* Klik berkali-kali → console: biasa: 1, layar tetap 0 */}
    
          <button onClick={() => setState(state + 1)}>State: {state}</button>
          {/* Klik → layar 1, 2, 3, ... */}
        </>
      );
    }
    

    Kesalahan umum

    1. Mutasi langsung lalu memanggil setter dengan objek yang sama. items.push(x); setItems(items) tidak memicu render yang benar. Pakai setItems([...items, x]).
    2. Mengharapkan state langsung berubah setelah setter. setNama('Budi'); console.log(nama) masih mencetak nilai lama. Nilai baru baru tersedia di render berikutnya.
    3. Tidak memakai updater saat nilai bergantung pada nilai sebelumnya. Untuk penambahan berturut-turut atau update dari timer, pakai setN(n => n + 1).
    4. Memanggil hook di dalam if atau loop. Melanggar aturan hooks dan menyebabkan state tertukar. Panggil di level teratas.
    5. Lupa menyalin objek bersarang. { ...user, alamat.kota: 'X' } bukan sintaks valid; salin setiap lapis: { ...user, alamat: { ...user.alamat, kota: 'X' } }.

    Latihan

    1. Mudah, Buat komponen Lampu dengan state boolean. Tombol "Saklar" mengganti teks antara "Lampu Nyala 💡" dan "Lampu Mati".
    2. Sedang, Buat form profil dengan state objek { nama, email } dan dua input. Setiap input mengubah satu field tanpa menghapus field lainnya.
    3. Menantang, Buat komponen Skor dengan tombol "+1", "+5 (lima kali +1)", dan "Undo". Simpan riwayat skor dalam array state agar Undo bisa kembali ke nilai sebelumnya.
    Lihat solusi

    1.

    import { useState } from 'react';
    
    export default function Lampu() {
      const [nyala, setNyala] = useState(false);
      return (
        <>
          <p>{nyala ? 'Lampu Nyala 💡' : 'Lampu Mati'}</p>
          <button onClick={() => setNyala(n => !n)}>Saklar</button>
        </>
      );
    }
    

    2.

    import { useState } from 'react';
    
    export default function FormProfil() {
      const [form, setForm] = useState({ nama: '', email: '' });
    
      function handleChange(e) {
        // e.target.name berisi 'nama' atau 'email'
        setForm({ ...form, [e.target.name]: e.target.value });
      }
    
      return (
        <form>
          <input name="nama" value={form.nama} onChange={handleChange} placeholder="Nama" />
          <input name="email" value={form.email} onChange={handleChange} placeholder="Email" />
          <p>Halo {form.nama || '...'} ({form.email || 'belum ada email'})</p>
        </form>
      );
    }
    

    3.

    import { useState } from 'react';
    
    export default function Skor() {
      const [riwayat, setRiwayat] = useState([0]); // nilai terakhir = skor sekarang
      const skor = riwayat[riwayat.length - 1];
    
      function tambah(n) {
        setRiwayat(r => [...r, r[r.length - 1] + n]);
      }
    
      function tambahLima() {
        for (let i = 0; i < 5; i++) tambah(1); // updater → benar-benar +5
      }
    
      function undo() {
        setRiwayat(r => (r.length > 1 ? r.slice(0, -1) : r));
      }
    
      return (
        <>
          <h2>Skor: {skor}</h2>
          <button onClick={() => tambah(1)}>+1</button>
          <button onClick={tambahLima}>+5</button>
          <button onClick={undo}>Undo</button>
        </>
      );
    }
    // +1, +5 → Skor 6; Undo → 5 (setiap +1 dari tombol +5 tercatat sebagai satu langkah)
    

    Ringkasan

    Kuis pemahaman

    Pilih satu jawaban per soal. Penjelasan muncul setelah kamu menjawab.