kodingup.camp

Beranda/React Mastery: Styling, Pola Komponen, Data & Optimasi

Mengangkat State ke Atas

Modul 3 dari 134 menit bacaLifting State Up

Isi modul

    Kenapa ini penting

    Bayangkan halaman toko: ada kotak pencarian di atas dan daftar produk di bawah. Kalau state teks pencarian disimpan di dalam komponen SearchBox, komponen ProductList tidak bisa tahu apa yang diketik user. Dua komponen bersaudara (sibling) tidak bisa saling membaca state satu sama lain. Data di React mengalir satu arah, dari atas ke bawah lewat props. Jadi kalau dua komponen butuh data yang sama, state itu harus "diangkat" ke parent terdekat yang membawahi keduanya. Teknik ini disebut lifting state up, dan hampir semua aplikasi React nyata memakainya setiap hari.

    Konsep inti

    Single source of truth

    Prinsip utamanya: setiap data hanya punya satu pemilik. Kalau teks pencarian disimpan di dua tempat (misalnya di SearchBox dan juga di App), cepat atau lambat keduanya akan berbeda dan muncul bug yang membingungkan. Dengan satu pemilik, semua komponen lain hanya membaca lewat props atau meminta perubahan lewat fungsi callback.

    Analogi: papan skor pertandingan. Hanya ada satu papan resmi yang dipegang wasit. Penonton, komentator, dan pemain semua melihat papan yang sama. Kalau tiap orang mencatat skor sendiri, pasti ada yang beda.

    Langkah mengangkat state

    1. Identifikasi komponen mana saja yang membutuhkan data itu.
    2. Cari parent terdekat yang membawahi semuanya.
    3. Pindahkan useState ke parent tersebut.
    4. Turunkan nilai ke anak lewat props (misalnya query).
    5. Turunkan fungsi pengubah lewat props (misalnya onQueryChange), agar anak bisa meminta parent mengubah state.

    Komponen anak yang menerima nilai dan callback lewat props disebut controlled component: dia tidak punya state sendiri untuk data itu, dia "dikendalikan" oleh parent.

    Data turunan (derived data)

    Setelah state diangkat, sering muncul godaan menyimpan hasil filter di state baru: const [hasil, setHasil] = useState([]) lalu di-update lewat useEffect. Ini tidak perlu dan rawan bug. Hasil filter bisa dihitung langsung saat render dari produk dan query. Aturannya: kalau sebuah nilai bisa dihitung dari state atau props yang sudah ada, jangan jadikan state.

    Simpan di state Hitung saat render
    Teks yang diketik user Daftar produk hasil filter
    Kategori yang dipilih Jumlah produk yang tampil
    Data mentah dari API Total harga keranjang

    Seberapa tinggi state diangkat?

    Angkat secukupnya, yaitu ke parent terdekat. Jangan semua state ditaruh di App "biar aman". Semakin tinggi state, semakin banyak komponen yang ikut re-render saat state berubah, dan semakin panjang rantai props. Kalau rantai props mulai terlalu panjang, ada solusi lain (komposisi dan Context) yang dibahas di modul 5 dan 6.

    Kapan state justru diturunkan kembali?

    Lifting state up bukan jalan satu arah. Kadang setelah aplikasi berubah, state yang dulu dibutuhkan dua komponen ternyata sekarang hanya dipakai satu. Pada saat itu, pindahkan state kembali ke bawah, ke komponen yang memakainya. Prinsipnya tetap sama: state tinggal di komponen terendah yang masih bisa melayani semua pemakainya. Ini juga berpengaruh ke performa (dibahas di modul 11), karena perubahan state hanya me-render ulang komponen pemilik dan anak-anaknya.

    Penamaan props callback

    Konvensi yang umum di komunitas React: props berisi fungsi untuk event diberi awalan on (onQueryChange, onSelect, onDelete), sedangkan fungsi pengendali di parent diberi awalan handle (handleQueryChange). Penamaan yang konsisten membuat orang lain langsung tahu mana yang data dan mana yang "jalur pelaporan" dari anak ke parent.

    Mengirim informasi lewat callback

    Callback boleh membawa argumen. Anak cukup melaporkan apa yang terjadi (misalnya onDelete(id)), dan parent yang memutuskan bagaimana state berubah (memfilter array). Dengan begitu logika pengubahan data terkumpul di satu tempat, yaitu pemilik state. Ini memudahkan debugging: kalau data salah, kamu tahu harus mencari di komponen mana.

    Contoh kode

    Aplikasi filter produk dengan kotak pencarian dan pilihan kategori. State ada di App, dua anaknya membaca dan mengubah lewat props.

    // App.jsx
    import { useState } from 'react';
    
    const PRODUK = [
      { id: 1, nama: 'Kopi Gayo', kategori: 'minuman', harga: 85000 },
      { id: 2, nama: 'Teh Melati', kategori: 'minuman', harga: 25000 },
      { id: 3, nama: 'Keripik Singkong', kategori: 'camilan', harga: 15000 },
      { id: 4, nama: 'Kacang Mete', kategori: 'camilan', harga: 60000 },
    ];
    
    function FilterBar({ query, onQueryChange, kategori, onKategoriChange }) {
      // controlled: nilai dari props, perubahan dilaporkan ke parent
      return (
        <div>
          <input
            value={query}
            onChange={(e) => onQueryChange(e.target.value)}
            placeholder="Cari produk..."
          />
          <select value={kategori} onChange={(e) => onKategoriChange(e.target.value)}>
            <option value="semua">Semua</option>
            <option value="minuman">Minuman</option>
            <option value="camilan">Camilan</option>
          </select>
        </div>
      );
    }
    
    function ProductList({ items }) {
      if (items.length === 0) return <p>Produk tidak ditemukan.</p>;
      return (
        <ul>
          {items.map((p) => (
            <li key={p.id}>
              {p.nama} - Rp {p.harga.toLocaleString('id-ID')}
            </li>
          ))}
        </ul>
      );
    }
    
    export default function App() {
      // single source of truth: dua state ini milik App saja
      const [query, setQuery] = useState('');
      const [kategori, setKategori] = useState('semua');
    
      // data turunan: dihitung saat render, BUKAN disimpan di state
      const hasil = PRODUK.filter((p) => {
        const cocokNama = p.nama.toLowerCase().includes(query.toLowerCase());
        const cocokKategori = kategori === 'semua' || p.kategori === kategori;
        return cocokNama && cocokKategori;
      });
    
      return (
        <main>
          <FilterBar
            query={query}
            onQueryChange={setQuery}
            kategori={kategori}
            onKategoriChange={setKategori}
          />
          <p>{hasil.length} produk ditemukan</p>
          <ProductList items={hasil} />
        </main>
      );
    }
    
    // Ketik "ke" + kategori "camilan"
    // → 1 produk ditemukan: Keripik Singkong, Rp 15.000
    

    Kesalahan umum

    1. Menduplikasi state di anak. Misalnya FilterBar punya useState(query) sendiri yang diisi dari props. Setelah itu keduanya bisa beda. Biarkan anak memakai props langsung.
    2. Menyimpan hasil filter di state + useEffect. Ini membuat render ganda dan risiko data basi. Hitung saja saat render.
    3. Memanggil callback saat render. onChange={onQueryChange(e.target.value)} akan langsung dieksekusi. Yang benar membungkus dengan arrow function: onChange={(e) => onQueryChange(e.target.value)}.
    4. Mengangkat state terlalu tinggi. State yang hanya dipakai satu komponen (misalnya dropdown terbuka/tertutup) sebaiknya tetap lokal.
    5. Mengubah array state secara langsung. produk.push(...) tidak memicu render. Buat array baru: setProduk([...produk, baru]).

    Latihan

    1. Mudah, Buat dua komponen Counter bersaudara yang menampilkan angka yang sama dan tombol tambah di masing-masing. Klik di salah satu harus menaikkan keduanya.
    2. Sedang, Buat konverter suhu: dua input (Celsius dan Fahrenheit). Mengetik di salah satu otomatis mengisi yang lain. Simpan hanya satu state.
    3. Menantang, Tambahkan ke contoh filter produk sebuah pengurutan (urut: termurah / termahal) di FilterBar, tanpa menambah state turunan.
    Lihat solusi
    // 1. Dua counter berbagi state
    import { useState } from 'react';
    
    function Counter({ nilai, onTambah }) {
      return <button onClick={onTambah}>Nilai: {nilai}</button>;
    }
    
    export default function App() {
      const [nilai, setNilai] = useState(0);
      const tambah = () => setNilai((n) => n + 1);
      return (
        <>
          <Counter nilai={nilai} onTambah={tambah} />
          <Counter nilai={nilai} onTambah={tambah} />
        </>
      );
    }
    // klik salah satu → keduanya menampilkan "Nilai: 1"
    
    // 2. Konverter suhu: satu state (celsius), fahrenheit diturunkan
    import { useState } from 'react';
    
    export default function Konverter() {
      const [celsius, setCelsius] = useState('');
      const fahrenheit = celsius === '' ? '' : String((Number(celsius) * 9) / 5 + 32);
    
      return (
        <>
          <label>
            Celsius
            <input value={celsius} onChange={(e) => setCelsius(e.target.value)} />
          </label>
          <label>
            Fahrenheit
            <input
              value={fahrenheit}
              onChange={(e) =>
                setCelsius(e.target.value === '' ? '' : String(((Number(e.target.value) - 32) * 5) / 9))
              }
            />
          </label>
        </>
      );
    }
    // ketik 100 di Celsius → Fahrenheit 212
    
    // 3. Tambahan pengurutan di App
    const [urut, setUrut] = useState('termurah');
    
    const hasil = PRODUK.filter((p) => {
      const cocokNama = p.nama.toLowerCase().includes(query.toLowerCase());
      const cocokKategori = kategori === 'semua' || p.kategori === kategori;
      return cocokNama && cocokKategori;
    }).toSorted((a, b) => (urut === 'termurah' ? a.harga - b.harga : b.harga - a.harga));
    // toSorted membuat array baru, jadi PRODUK asli tidak berubah
    
    // di FilterBar tambahkan props urut & onUrutChange:
    <select value={urut} onChange={(e) => onUrutChange(e.target.value)}>
      <option value="termurah">Termurah</option>
      <option value="termahal">Termahal</option>
    </select>
    

    Ringkasan

    Kuis pemahaman

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