Bab 4: Kamu Mungkin Tidak Butuh Effect

4 menit baca

Kenapa Bab Ini Penting?

Ini mungkin bab paling penting di seluruh seri "Escape Hatches". Kenapa? Karena kebanyakan pemula React terlalu sering pakai useEffect. Mereka pakai Effect buat hal-hal yang sebenernya bisa dilakuin dengan cara yang lebih simpel dan lebih performa.

Bayangin gini: kamu punya palu (useEffect). Tiba-tiba semua masalah keliatan kayak paku. Mau masak? Palu. Mau nyetir? Palu. Mau nulis? Palu. Padahal banyak masalah yang butuh alat lain.

Aturan emas: Kalau sesuatu bisa dihitung dari props atau state yang ada, jangan simpen di state lain dan jangan pakai Effect. Hitung langsung saat render.

Anti-Pattern 1: Transformasi Data untuk Render

Ini yang paling sering terjadi. Kamu punya data, mau ditransformasi sebelum ditampilin:

jsx
// ❌ ANTI-PATTERN: pakai Effect buat transformasi data
function DaftarProduk({ produk }) {
  const [produkMurah, setProdukMurah] = useState([]);
  
  useEffect(() => {
    // Filter produk yang harganya di bawah 50rb
    const hasil = produk.filter(p => p.harga < 50000);
    setProdukMurah(hasil);
  }, [produk]);
  
  return (
    <ul>
      {produkMurah.map(p => <li key={p.id}>{p.nama}</li>)}
    </ul>
  );
}

Kenapa ini jelek?

  1. Render pertama: produkMurah masih [] (kosong)
  2. Effect jalan setelah render → update produkMurah
  3. Komponen render LAGI buat nampilin data yang baru
  4. Total: 2 render padahal harusnya cukup 1!
jsx
// ✅ BENAR: hitung langsung saat render
function DaftarProduk({ produk }) {
  // Langsung hitung, nggak perlu state tambahan
  const produkMurah = produk.filter(p => p.harga < 50000);
  
  return (
    <ul>
      {produkMurah.map(p => <li key={p.id}>{p.nama}</li>)}
    </ul>
  );
}

Satu render, langsung beres. Simpel, cepat, nggak ada state tambahan yang harus di-maintain.

Anti-Pattern 2: Menghitung Nilai Turunan

jsx
// ❌ ANTI-PATTERN: state + Effect buat nilai yang bisa dihitung
function Keranjang({ items }) {
  const [totalHarga, setTotalHarga] = useState(0);
  const [jumlahItem, setJumlahItem] = useState(0);
  
  useEffect(() => {
    setTotalHarga(items.reduce((sum, item) => sum + item.harga * item.qty, 0));
    setJumlahItem(items.reduce((sum, item) => sum + item.qty, 0));
  }, [items]);
  
  return (
    <div>
      <p>Total item: {jumlahItem}</p>
      <p>Total harga: Rp{totalHarga.toLocaleString()}</p>
    </div>
  );
}

// ✅ BENAR: hitung langsung
function Keranjang({ items }) {
  // Dihitung setiap render, tapi itu OK! Ini cepat.
  const totalHarga = items.reduce((sum, item) => sum + item.harga * item.qty, 0);
  const jumlahItem = items.reduce((sum, item) => sum + item.qty, 0);
  
  return (
    <div>
      <p>Total item: {jumlahItem}</p>
      <p>Total harga: Rp{totalHarga.toLocaleString()}</p>
    </div>
  );
}

"Tapi kan itu dihitung ulang setiap render?"

Ya! Dan itu nggak masalah untuk kebanyakan kasus. Array.reduce di array 100 item itu cuma butuh microsecond. Jauh lebih cepat daripada render ulang yang disebabkan Effect.

Anti-Pattern 3: Reset State Saat Props Berubah

jsx
// ❌ ANTI-PATTERN: reset state pakai Effect
function FormProfil({ userId }) {
  const [nama, setNama] = useState('');
  const [email, setEmail] = useState('');
  
  // Reset form setiap userId berubah
  useEffect(() => {
    setNama('');
    setEmail('');
  }, [userId]);
  
  return (
    <form>
      <input value={nama} onChange={e => setNama(e.target.value)} />
      <input value={email} onChange={e => setEmail(e.target.value)} />
    </form>
  );
}

Masalahnya: Render pertama dengan userId baru masih nunjukin data lama (sebelum Effect jalan). Baru setelah Effect jalan, form di-reset dan render lagi.

jsx
// ✅ BENAR: pakai key buat reset seluruh komponen
function HalamanProfil({ userId }) {
  // key berubah = React unmount komponen lama, mount yang baru (state fresh)
  return <FormProfil key={userId} userId={userId} />;
}

function FormProfil({ userId }) {
  const [nama, setNama] = useState('');
  const [email, setEmail] = useState('');
  
  // Nggak perlu Effect! State otomatis fresh karena key berubah
  
  return (
    <form>
      <input value={nama} onChange={e => setNama(e.target.value)} />
      <input value={email} onChange={e => setEmail(e.target.value)} />
    </form>
  );
}

Trik key: Kalau kamu kasih key yang berbeda ke komponen, React anggap itu komponen yang beda. Jadi dia unmount yang lama dan mount yang baru dengan state fresh. Nggak perlu Effect sama sekali!

Anti-Pattern 4: Fetch di Effect yang Harusnya di Event Handler

jsx
// ❌ ANTI-PATTERN: fetch saat form submit pakai Effect
function FormPesanan() {
  const [pesanan, setPesanan] = useState(null);
  const [kirim, setKirim] = useState(false);
  
  useEffect(() => {
    if (kirim) {
      fetch('/api/pesanan', {
        method: 'POST',
        body: JSON.stringify(pesanan)
      });
      setKirim(false);
    }
  }, [kirim, pesanan]);
  
  function handleSubmit() {
    setPesanan({ item: 'Nasi Goreng', qty: 2 });
    setKirim(true); // Trigger Effect
  }
  
  return <button onClick={handleSubmit}>Pesan</button>;
}

Kenapa ini salah? Karena mengirim pesanan itu respons langsung dari aksi user (klik tombol). Ini bukan sinkronisasi. Ini event handling!

jsx
// ✅ BENAR: fetch langsung di event handler
function FormPesanan() {
  async function handleSubmit() {
    const pesanan = { item: 'Nasi Goreng', qty: 2 };
    
    const res = await fetch('/api/pesanan', {
      method: 'POST',
      body: JSON.stringify(pesanan)
    });
    
    if (res.ok) {
      alert('Pesanan berhasil!');
    }
  }
  
  return <button onClick={handleSubmit}>Pesan</button>;
}

Lebih simpel, lebih jelas, lebih mudah di-debug.

Anti-Pattern 5: Sinkronisasi Dua State

jsx
// ❌ ANTI-PATTERN: sinkronisasi state satu sama lain pakai Effect
function PilihProvinsiKota() {
  const [provinsi, setProvinsi] = useState('');
  const [kota, setKota] = useState('');
  const [daftarKota, setDaftarKota] = useState([]);
  
  // Reset kota setiap provinsi berubah
  useEffect(() => {
    setKota(''); // Reset kota
    // Ini trigger render tambahan!
  }, [provinsi]);
  
  // Fetch daftar kota berdasarkan provinsi
  useEffect(() => {
    if (provinsi) {
      fetch(`/api/kota?provinsi=${provinsi}`)
        .then(res => res.json())
        .then(data => setDaftarKota(data));
    }
  }, [provinsi]);
  
  // ...
}

// ✅ BENAR: reset di event handler langsung
function PilihProvinsiKota() {
  const [provinsi, setProvinsi] = useState('');
  const [kota, setKota] = useState('');
  const [daftarKota, setDaftarKota] = useState([]);
  
  function handleProvinsiChange(e) {
    const provinsiBaru = e.target.value;
    setProvinsi(provinsiBaru);
    setKota(''); // Reset kota langsung di sini!
    
    // Fetch daftar kota
    fetch(`/api/kota?provinsi=${provinsiBaru}`)
      .then(res => res.json())
      .then(data => setDaftarKota(data));
  }
  
  return (
    <div>
      <select value={provinsi} onChange={handleProvinsiChange}>
        <option value="">Pilih Provinsi</option>
        <option value="jawa-barat">Jawa Barat</option>
        <option value="jawa-tengah">Jawa Tengah</option>
      </select>
      
      <select value={kota} onChange={e => setKota(e.target.value)}>
        <option value="">Pilih Kota</option>
        {daftarKota.map(k => (
          <option key={k.id} value={k.id}>{k.nama}</option>
        ))}
      </select>
    </div>
  );
}

Kapan useMemo Diperlukan?

Kalau perhitungan kamu beneran mahal (ribuan item, operasi kompleks), baru pakai useMemo:

jsx
import { useMemo } from 'react';

function DaftarBesar({ items, filter }) {
  // ❌ Tanpa memo: dihitung ulang setiap render (meskipun filter nggak berubah)
  // const hasilFilter = items.filter(item => cocok(item, filter));
  
  // ✅ Dengan useMemo: cuma dihitung ulang kalau items atau filter berubah
  const hasilFilter = useMemo(() => {
    console.log('Menghitung ulang filter...');
    return items.filter(item => {
      // Simulasi operasi mahal
      return item.nama.toLowerCase().includes(filter.toLowerCase());
    });
  }, [items, filter]);
  
  return (
    <ul>
      {hasilFilter.map(item => (
        <li key={item.id}>{item.nama}</li>
      ))}
    </ul>
  );
}

Kapan pakai useMemo?

  • Array/object dengan ribuan item yang di-filter/sort
  • Perhitungan matematika yang kompleks
  • Bikin tree/graph structure dari data mentah

Kapan NGGAK perlu useMemo?

  • Filter array kecil (< 100 item)
  • Operasi string sederhana
  • Penjumlahan/pengurangan biasa
  • Kalau kamu nggak yakin, jangan pakai dulu. Optimasi prematur itu musuh.

Decision Tree: Effect vs Event Handler vs Langsung

Pakai flowchart ini buat menentukan di mana naruh logic:

Apakah ini respons langsung dari aksi user (klik, ketik, submit)? ├── YA → Taruh di EVENT HANDLER │ └── TIDAK → Apakah ini perlu sinkronisasi dengan sistem luar? │ (API, WebSocket, DOM, timer, localStorage) │ ├── YA → Taruh di useEffect │ └── TIDAK → Apakah ini bisa dihitung dari state/props yang ada? │ ├── YA → HITUNG LANGSUNG saat render (atau useMemo kalau mahal) │ └── TIDAK → Mungkin kamu butuh state baru

Contoh Lengkap: Refactoring dari Effect ke Tanpa Effect

Mari kita refactor komponen yang penuh Effect jadi lebih bersih:

jsx
// ❌ SEBELUM: penuh Effect yang nggak perlu
function TokoOnline({ produk, kategoriAktif, urutkan }) {
  const [produkFiltered, setProdukFiltered] = useState([]);
  const [produkSorted, setProdukSorted] = useState([]);
  const [totalHarga, setTotalHarga] = useState(0);
  const [jumlahProduk, setJumlahProduk] = useState(0);
  
  // Effect 1: filter berdasarkan kategori
  useEffect(() => {
    const hasil = produk.filter(p => 
      kategoriAktif === 'semua' || p.kategori === kategoriAktif
    );
    setProdukFiltered(hasil);
  }, [produk, kategoriAktif]);
  
  // Effect 2: sort setelah filter
  useEffect(() => {
    const sorted = [...produkFiltered].sort((a, b) => {
      if (urutkan === 'harga-asc') return a.harga - b.harga;
      if (urutkan === 'harga-desc') return b.harga - a.harga;
      return a.nama.localeCompare(b.nama);
    });
    setProdukSorted(sorted);
  }, [produkFiltered, urutkan]);
  
  // Effect 3: hitung total
  useEffect(() => {
    setTotalHarga(produkSorted.reduce((sum, p) => sum + p.harga, 0));
    setJumlahProduk(produkSorted.length);
  }, [produkSorted]);
  
  return (
    <div>
      <p>{jumlahProduk} produk, total Rp{totalHarga.toLocaleString()}</p>
      <ul>
        {produkSorted.map(p => (
          <li key={p.id}>{p.nama} - Rp{p.harga.toLocaleString()}</li>
        ))}
      </ul>
    </div>
  );
}

Kode di atas punya 3 Effect dan 4 state tambahan yang semuanya nggak perlu! Setiap kali produk berubah, komponen render 4 kali (render awal + 3 Effect yang masing-masing trigger render).

jsx
// ✅ SESUDAH: bersih, cepat, mudah dipahami
function TokoOnline({ produk, kategoriAktif, urutkan }) {
  // Semua dihitung langsung dari props. Nggak perlu state tambahan!
  const produkFiltered = produk.filter(p => 
    kategoriAktif === 'semua' || p.kategori === kategoriAktif
  );
  
  const produkSorted = [...produkFiltered].sort((a, b) => {
    if (urutkan === 'harga-asc') return a.harga - b.harga;
    if (urutkan === 'harga-desc') return b.harga - a.harga;
    return a.nama.localeCompare(b.nama);
  });
  
  const totalHarga = produkSorted.reduce((sum, p) => sum + p.harga, 0);
  const jumlahProduk = produkSorted.length;
  
  return (
    <div>
      <p>{jumlahProduk} produk, total Rp{totalHarga.toLocaleString()}</p>
      <ul>
        {produkSorted.map(p => (
          <li key={p.id}>{p.nama} - Rp{p.harga.toLocaleString()}</li>
        ))}
      </ul>
    </div>
  );
}

Dari 3 Effect + 4 state → 0 Effect + 0 state tambahan. Satu render, langsung beres.

Contoh: Validasi Form Tanpa Effect

jsx
// ❌ ANTI-PATTERN: validasi pakai Effect
function FormRegistrasi() {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [errorEmail, setErrorEmail] = useState('');
  const [errorPassword, setErrorPassword] = useState('');
  const [formValid, setFormValid] = useState(false);
  
  useEffect(() => {
    if (!email.includes('@')) {
      setErrorEmail('Email harus mengandung @');
    } else {
      setErrorEmail('');
    }
  }, [email]);
  
  useEffect(() => {
    if (password.length < 8) {
      setErrorPassword('Password minimal 8 karakter');
    } else {
      setErrorPassword('');
    }
  }, [password]);
  
  useEffect(() => {
    setFormValid(errorEmail === '' && errorPassword === '' && email !== '' && password !== '');
  }, [errorEmail, errorPassword, email, password]);
  
  // 3 Effect, 3 state tambahan... berantakan!
}

// ✅ BENAR: hitung langsung
function FormRegistrasi() {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  
  // Validasi dihitung langsung dari state yang ada
  const errorEmail = email && !email.includes('@') ? 'Email harus mengandung @' : '';
  const errorPassword = password && password.length < 8 ? 'Password minimal 8 karakter' : '';
  const formValid = email !== '' && password !== '' && !errorEmail && !errorPassword;
  
  function handleSubmit(e) {
    e.preventDefault();
    if (formValid) {
      // Kirim ke server (di event handler, bukan Effect!)
      fetch('/api/register', {
        method: 'POST',
        body: JSON.stringify({ email, password })
      });
    }
  }
  
  return (
    <form onSubmit={handleSubmit}>
      <div>
        <input
          type="email"
          value={email}
          onChange={e => setEmail(e.target.value)}
          placeholder="Email"
        />
        {errorEmail && <span style={{ color: 'red' }}>{errorEmail}</span>}
      </div>
      
      <div>
        <input
          type="password"
          value={password}
          onChange={e => setPassword(e.target.value)}
          placeholder="Password"
        />
        {errorPassword && <span style={{ color: 'red' }}>{errorPassword}</span>}
      </div>
      
      <button type="submit" disabled={!formValid}>Daftar</button>
    </form>
  );
}

Kapan Effect MEMANG Diperlukan?

Jangan salah paham, Effect tetap punya tempatnya. Ini kasus-kasus yang memang butuh Effect:

1. Sinkronisasi dengan Sistem Luar

jsx
// ✅ Koneksi WebSocket = sistem luar
useEffect(() => {
  const ws = new WebSocket(url);
  ws.onmessage = handleMessage;
  return () => ws.close();
}, [url]);

2. Subscribe ke Event Browser

jsx
// ✅ Event listener = sistem luar
useEffect(() => {
  window.addEventListener('resize', handleResize);
  return () => window.removeEventListener('resize', handleResize);
}, []);

3. Integrasi Library Non-React

jsx
// ✅ Library peta = sistem luar
useEffect(() => {
  const map = new MapLibrary(containerRef.current);
  map.setCenter(lat, lng);
  return () => map.destroy();
}, [lat, lng]);

4. Fetch Data Awal (tapi pertimbangkan library)

jsx
// ✅ Fetch data saat mount (tapi lebih baik pakai React Query/SWR)
useEffect(() => {
  let aktif = true;
  fetchData().then(data => {
    if (aktif) setData(data);
  });
  return () => { aktif = false; };
}, []);

Analogi: Warung Makan

Bayangin komponen React itu warung makan:

  • Render = masak makanan sesuai pesanan (props/state)
  • Hitung langsung = potong bawang sambil masak (bagian dari proses masak)
  • Event handler = pelanggan minta tambah sambal (respons ke aksi spesifik)
  • Effect = nyalain AC karena suhu ruangan berubah (sinkronisasi dengan kondisi luar)

Kamu nggak perlu "Effect" buat motong bawang. Itu bagian dari masak! Sama kayak kamu nggak perlu Effect buat filter array atau hitung total.

Checklist: Apakah Kamu Butuh Effect?

Sebelum nulis useEffect, tanya diri sendiri:

  1. ❓ "Bisa nggak ini dihitung langsung dari state/props yang ada?"

    • Kalau ya → Hitung langsung, nggak perlu Effect
  2. ❓ "Apakah ini respons dari aksi user?"

    • Kalau ya → Taruh di event handler
  3. ❓ "Apakah ini cuma perlu jalan sekali saat mount?"

    • Pertimbangkan: apakah bisa dipindah ke level yang lebih tinggi? (route loader, server component)
  4. ❓ "Apakah ini beneran sinkronisasi dengan sistem di luar React?"

    • Kalau ya → OK, pakai Effect
  5. ❓ "Apakah Effect ini cuma set state berdasarkan state/props lain?"

    • Kalau ya → Hampir pasti nggak butuh Effect!

⚠️ Jebakan

Jebakan 1: Effect Chain (Rantai Effect)

jsx
// ❌ Effect yang trigger Effect yang trigger Effect...
function JebakanChain() {
  const [a, setA] = useState(1);
  const [b, setB] = useState(0);
  const [c, setC] = useState(0);
  
  useEffect(() => { setB(a * 2); }, [a]);     // a berubah → set b
  useEffect(() => { setC(b + 10); }, [b]);    // b berubah → set c
  // Render 3 kali! a→render→b→render→c→render
}

// ✅ Hitung langsung
function Benar() {
  const [a, setA] = useState(1);
  const b = a * 2;      // Langsung
  const c = b + 10;     // Langsung
  // Render 1 kali!
}

Jebakan 2: Fetch yang Harusnya di Event Handler

jsx
// ❌ Pakai state flag buat trigger fetch
function Salah() {
  const [harusFetch, setHarusFetch] = useState(false);
  
  useEffect(() => {
    if (harusFetch) {
      fetch('/api/submit');
      setHarusFetch(false);
    }
  }, [harusFetch]);
  
  return <button onClick={() => setHarusFetch(true)}>Submit</button>;
}

// ✅ Fetch langsung di handler
function Benar() {
  function handleSubmit() {
    fetch('/api/submit');
  }
  
  return <button onClick={handleSubmit}>Submit</button>;
}

Jebakan 3: useMemo yang Nggak Perlu

jsx
// ❌ Overkill: useMemo buat operasi murah
function Overkill({ nama }) {
  const namaKapital = useMemo(() => {
    return nama.toUpperCase(); // Ini cepet banget, nggak perlu memo
  }, [nama]);
}

// ✅ Langsung aja
function Simpel({ nama }) {
  const namaKapital = nama.toUpperCase(); // Nggak perlu memo
}

Jebakan 4: Inisialisasi State dari Props Pakai Effect

jsx
// ❌ Pakai Effect buat inisialisasi
function Salah({ nilaiAwal }) {
  const [nilai, setNilai] = useState(0);
  
  useEffect(() => {
    setNilai(nilaiAwal); // Render 2x!
  }, []);
}

// ✅ Inisialisasi langsung di useState
function Benar({ nilaiAwal }) {
  const [nilai, setNilai] = useState(nilaiAwal); // Render 1x!
}

Ringkasan

  1. Jangan pakai Effect buat transformasi data yang bisa dihitung langsung
  2. Jangan pakai Effect buat respons ke aksi user (pakai event handler)
  3. Jangan pakai Effect buat reset state (pakai key)
  4. Jangan pakai Effect buat sinkronisasi state satu sama lain (hitung langsung)
  5. Pakai useMemo hanya kalau perhitungan beneran mahal
  6. Pakai Effect hanya buat sinkronisasi dengan sistem di luar React

Prinsip: Kalau bisa dihitung, jangan disimpan. Kalau bisa di event handler, jangan di Effect.

🏋️ Challenge

Challenge 1: Refactor Effect Chains

Refactor komponen berikut yang punya 3 Effect berantai jadi tanpa Effect sama sekali:

jsx
function KonversiSuhu() {
  const [celsius, setCelsius] = useState(0);
  const [fahrenheit, setFahrenheit] = useState(32);
  const [kategori, setKategori] = useState('dingin');
  
  useEffect(() => {
    setFahrenheit(celsius * 9/5 + 32);
  }, [celsius]);
  
  useEffect(() => {
    if (fahrenheit < 50) setKategori('dingin');
    else if (fahrenheit < 80) setKategori('nyaman');
    else setKategori('panas');
  }, [fahrenheit]);
}

Hint: Semua bisa dihitung langsung dari celsius.

Lihat Solusi
jsx
import { useState } from 'react';

function KonversiSuhu() {
  const [celsius, setCelsius] = useState(0);
  
  // Hitung langsung, nggak perlu state atau Effect!
  const fahrenheit = celsius * 9/5 + 32;
  const kategori = fahrenheit < 50 ? 'dingin' 
                 : fahrenheit < 80 ? 'nyaman' 
                 : 'panas';
  
  return (
    <div>
      <input
        type="number"
        value={celsius}
        onChange={e => setCelsius(Number(e.target.value))}
      />
      <span>°C</span>
      
      <p>{celsius}°C = {fahrenheit.toFixed(1)}°F</p>
      <p>Kategori: {kategori}</p>
      <p style={{
        color: kategori === 'dingin' ? 'blue' : kategori === 'panas' ? 'red' : 'green'
      }}>
        {kategori === 'dingin' && '🥶 Brrr... dingin!'}
        {kategori === 'nyaman' && '😊 Suhu nyaman'}
        {kategori === 'panas' && '🥵 Panas banget!'}
      </p>
    </div>
  );
}

Penjelasan: Dari 2 state + 2 Effect → 0 state tambahan + 0 Effect. fahrenheit dan kategori cuma turunan dari celsius, jadi nggak perlu disimpan terpisah.

Challenge 2: Search dengan Filter Tanpa Effect

Bikin komponen pencarian produk yang bisa filter berdasarkan kategori DAN keyword. Jangan pakai Effect sama sekali.

Hint: Filter dan search itu cuma transformasi data. Hitung langsung dari state.

Lihat Solusi
jsx
import { useState } from 'react';

const PRODUK = [
  { id: 1, nama: 'Laptop ASUS', kategori: 'elektronik', harga: 8000000 },
  { id: 2, nama: 'Kemeja Batik', kategori: 'fashion', harga: 150000 },
  { id: 3, nama: 'Mouse Wireless', kategori: 'elektronik', harga: 200000 },
  { id: 4, nama: 'Celana Jeans', kategori: 'fashion', harga: 300000 },
  { id: 5, nama: 'Keyboard Mechanical', kategori: 'elektronik', harga: 500000 },
  { id: 6, nama: 'Sepatu Sneakers', kategori: 'fashion', harga: 450000 },
  { id: 7, nama: 'Headphone Sony', kategori: 'elektronik', harga: 1200000 },
  { id: 8, nama: 'Tas Ransel', kategori: 'fashion', harga: 250000 },
];

function PencarianProduk() {
  const [keyword, setKeyword] = useState('');
  const [kategori, setKategori] = useState('semua');
  const [urutkan, setUrutkan] = useState('nama');
  
  // Semua dihitung langsung! Nggak perlu Effect!
  const produkFiltered = PRODUK
    .filter(p => kategori === 'semua' || p.kategori === kategori)
    .filter(p => p.nama.toLowerCase().includes(keyword.toLowerCase()))
    .sort((a, b) => {
      if (urutkan === 'nama') return a.nama.localeCompare(b.nama);
      if (urutkan === 'harga-asc') return a.harga - b.harga;
      return b.harga - a.harga;
    });
  
  const totalHarga = produkFiltered.reduce((sum, p) => sum + p.harga, 0);
  
  return (
    <div>
      <h2>Pencarian Produk</h2>
      
      <input
        value={keyword}
        onChange={e => setKeyword(e.target.value)}
        placeholder="Cari produk..."
      />
      
      <select value={kategori} onChange={e => setKategori(e.target.value)}>
        <option value="semua">Semua Kategori</option>
        <option value="elektronik">Elektronik</option>
        <option value="fashion">Fashion</option>
      </select>
      
      <select value={urutkan} onChange={e => setUrutkan(e.target.value)}>
        <option value="nama">Urutkan: Nama</option>
        <option value="harga-asc">Harga: Rendah ke Tinggi</option>
        <option value="harga-desc">Harga: Tinggi ke Rendah</option>
      </select>
      
      <p>Ditemukan: {produkFiltered.length} produk (Total: Rp{totalHarga.toLocaleString()})</p>
      
      <ul>
        {produkFiltered.map(p => (
          <li key={p.id}>
            {p.nama} - Rp{p.harga.toLocaleString()} [{p.kategori}]
          </li>
        ))}
      </ul>
    </div>
  );
}

Penjelasan: Nol Effect! Semua filter, sort, dan perhitungan total dilakukan langsung saat render. Setiap kali user ganti keyword/kategori/urutan, state berubah → komponen render → hasil baru dihitung langsung.

Challenge 3: Identifikasi Mana yang Butuh Effect

Dari daftar berikut, tentukan mana yang butuh Effect dan mana yang nggak. Jelaskan kenapa.

  1. Update document.title setiap halaman berubah
  2. Filter array berdasarkan input pencarian
  3. Koneksi ke WebSocket saat komponen mount
  4. Hitung diskon dari harga dan persentase
  5. Kirim data form ke server saat user klik Submit
  6. Subscribe ke window resize event
Lihat Solusi
  1. Update document.title → ✅ BUTUH Effect

    • document.title itu DOM API (sistem luar). React nggak punya cara deklaratif buat set title.
    jsx
    useEffect(() => { document.title = halaman; }, [halaman]);
  2. Filter array → ❌ NGGAK butuh Effect

    • Ini cuma transformasi data. Hitung langsung.
    jsx
    const hasilFilter = items.filter(i => i.nama.includes(keyword));
  3. Koneksi WebSocket → ✅ BUTUH Effect

    • WebSocket itu sistem luar yang perlu setup dan cleanup.
    jsx
    useEffect(() => {
      const ws = new WebSocket(url);
      return () => ws.close();
    }, [url]);
  4. Hitung diskon → ❌ NGGAK butuh Effect

    • Ini cuma matematika dari data yang ada.
    jsx
    const hargaDiskon = harga * (1 - diskonPersen / 100);
  5. Kirim form saat Submit → ❌ NGGAK butuh Effect

    • Ini respons ke aksi user. Taruh di event handler.
    jsx
    function handleSubmit() { fetch('/api/submit', { ... }); }
  6. Subscribe resize event → ✅ BUTUH Effect

    • Event listener browser itu sistem luar yang perlu setup dan cleanup.
    jsx
    useEffect(() => {
      window.addEventListener('resize', handler);
      return () => window.removeEventListener('resize', handler);
    }, []);

Ringkasan: Yang butuh Effect = interaksi dengan sistem di luar React (DOM API, WebSocket, event listener browser). Yang nggak = transformasi data, respons ke aksi user.

Sudah paham materi ini?

Tandai sebagai selesai untuk melacak progress-mu.