Bab 7: Menghapus Dependency Effect

4 menit baca

Kenapa React Komplain tentang Dependency?

Kamu pasti pernah lihat warning ini di console:

React Hook useEffect has a missing dependency: 'count'. Either include it or remove the dependency array.

React nggak cuma iseng kasih warning. Dia serius. Kalau kamu pakai nilai reactive di dalam Effect tapi nggak masukin ke dependency array, Effect kamu bakal pakai nilai basi (stale). Ini bug yang susah di-debug karena kadang "keliatan jalan" tapi sebenernya salah.

Analogi: Bayangin kamu punya resep masak yang bilang "pakai bumbu yang ada di meja". Tapi kamu cuma cek meja sekali pas awal masak. Kalau ada yang ganti bumbu di meja, kamu tetap pakai bumbu lama. Hasilnya? Masakan yang rasanya nggak sesuai!

Dependency array itu kayak bilang ke React: "Cek ulang bumbu di meja setiap kali bumbu ini berubah."

Cara yang SALAH: Suppress Warning

jsx
// ❌ JANGAN PERNAH lakuin ini!
useEffect(() => {
  setInterval(() => {
    setCount(count + 1);
  }, 1000);
  // eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

Suppress warning itu kayak nutup lampu "check engine" di mobil pakai selotip. Masalahnya tetap ada, kamu cuma nggak lihat aja.

Cara yang BENAR: Hapus Dependency dengan Mengubah Kode

Kuncinya: jangan hapus dependency dari array, tapi ubah kode supaya dependency itu nggak dibutuhkan lagi.

Ada beberapa strategi:

Strategi 1: Pakai Updater Function

Ini yang paling sering dipakai. Kalau Effect kamu cuma perlu state sebelumnya buat menghitung state baru, pakai updater function:

jsx
// ❌ count di dependency → Effect re-run setiap detik → interval di-reset terus
function Timer() {
  const [count, setCount] = useState(0);
  
  useEffect(() => {
    const id = setInterval(() => {
      setCount(count + 1); // Butuh count → harus di dependency
    }, 1000);
    return () => clearInterval(id);
  }, [count]); // Effect jalan ulang setiap count berubah = interval baru setiap detik!
  
  return <p>{count}</p>;
}

// ✅ Updater function: nggak perlu count di dependency
function Timer() {
  const [count, setCount] = useState(0);
  
  useEffect(() => {
    const id = setInterval(() => {
      setCount(c => c + 1); // c = nilai sebelumnya, nggak perlu baca count dari luar
    }, 1000);
    return () => clearInterval(id);
  }, []); // Bersih! Nggak ada dependency
  
  return <p>{count}</p>;
}

Kenapa ini jalan? setCount(c => c + 1) bilang ke React: "Ambil nilai count saat ini (apapun itu), tambah 1." Kamu nggak perlu "tahu" berapa count sekarang di dalam Effect. React yang urus.

Strategi 2: Pindahkan Kode ke Dalam Effect

Kalau kamu bikin object atau function di luar Effect, terus pakai di dalam Effect, object/function itu jadi dependency. Solusi: pindahkan ke dalam Effect.

jsx
// ❌ options dibuat di luar Effect → jadi dependency → berubah setiap render!
function Chat({ roomId }) {
  const options = {
    serverUrl: 'https://chat.example.com',
    roomId: roomId
  };
  
  useEffect(() => {
    const koneksi = buatKoneksi(options);
    koneksi.connect();
    return () => koneksi.disconnect();
  }, [options]); // options = object baru setiap render → infinite loop!
}

// ✅ Pindahkan object ke dalam Effect
function Chat({ roomId }) {
  useEffect(() => {
    // Bikin object di dalam Effect
    const options = {
      serverUrl: 'https://chat.example.com',
      roomId: roomId
    };
    
    const koneksi = buatKoneksi(options);
    koneksi.connect();
    return () => koneksi.disconnect();
  }, [roomId]); // Cuma depend pada roomId (primitive, bisa dibandingkan)
}

Strategi 3: Pindahkan Fungsi ke Dalam Effect

Sama kayak object, fungsi yang dibuat di dalam komponen juga berubah setiap render:

jsx
// ❌ buatUrl dibuat di luar Effect → jadi dependency → berubah setiap render!
function Chat({ roomId }) {
  function buatUrl() {
    return `https://chat.example.com/room/${roomId}`;
  }
  
  useEffect(() => {
    const url = buatUrl();
    const koneksi = buatKoneksi(url);
    koneksi.connect();
    return () => koneksi.disconnect();
  }, [buatUrl]); // buatUrl = fungsi baru setiap render!
}

// ✅ Pindahkan fungsi ke dalam Effect
function Chat({ roomId }) {
  useEffect(() => {
    // Definisikan fungsi di dalam Effect
    function buatUrl() {
      return `https://chat.example.com/room/${roomId}`;
    }
    
    const url = buatUrl();
    const koneksi = buatKoneksi(url);
    koneksi.connect();
    return () => koneksi.disconnect();
  }, [roomId]); // Bersih!
}

Strategi 4: Pindahkan ke Luar Komponen (Kalau Nggak Pakai Props/State)

Kalau fungsi atau nilai nggak bergantung pada props atau state, pindahkan ke luar komponen:

jsx
// ✅ Konstanta di luar komponen = nggak reactive = nggak perlu di dependency
const SERVER_URL = 'https://chat.example.com';

function buatUrl(roomId) {
  return `${SERVER_URL}/room/${roomId}`;
}

function Chat({ roomId }) {
  useEffect(() => {
    const url = buatUrl(roomId); // buatUrl stabil, nggak perlu di dependency
    const koneksi = buatKoneksi(url);
    koneksi.connect();
    return () => koneksi.disconnect();
  }, [roomId]); // Cuma roomId
}

Strategi 5: Destructure Props/Object

Kadang kamu terima object sebagai prop, dan cuma butuh beberapa field-nya:

jsx
// ❌ user object berubah setiap render (meskipun isinya sama)
function Profil({ user }) {
  useEffect(() => {
    document.title = `Profil: ${user.nama}`;
  }, [user]); // user = object, bisa berubah referensinya setiap render
}

// ✅ Destructure: ambil yang dibutuhkan aja
function Profil({ user }) {
  const { nama } = user; // atau langsung di parameter: ({ user: { nama } })
  
  useEffect(() => {
    document.title = `Profil: ${nama}`;
  }, [nama]); // nama = string, stabil kalau nilainya sama
}

// Atau lebih baik lagi: destructure di parameter
function Profil({ user: { nama, email } }) {
  useEffect(() => {
    document.title = `Profil: ${nama}`;
  }, [nama]);
}

Jebakan Besar: Object dan Array sebagai Dependency

Ini sumber bug nomor satu buat pemula. Di JavaScript, dua object yang "isinya sama" itu BUKAN object yang sama:

jsx
// Di JavaScript:
{ nama: 'Budi' } === { nama: 'Budi' } // false! Beda referensi!
[1, 2, 3] === [1, 2, 3]               // false! Beda referensi!

// Tapi:
'Budi' === 'Budi'  // true! Primitive dibandingkan by value
42 === 42          // true!

Ini berarti:

jsx
function App() {
  const [count, setCount] = useState(0);
  
  // Object ini BARU setiap render, meskipun isinya sama!
  const config = { tema: 'dark', bahasa: 'id' };
  
  useEffect(() => {
    console.log('Effect jalan!', config);
  }, [config]); // Effect jalan SETIAP render karena config selalu "beda"!
  
  return <button onClick={() => setCount(c => c + 1)}>Render: {count}</button>;
}

Solusi-solusi:

jsx
// Solusi A: Pindahkan ke luar komponen (kalau nggak pakai props/state)
const CONFIG = { tema: 'dark', bahasa: 'id' };

function App() {
  useEffect(() => {
    console.log(CONFIG);
  }, []); // CONFIG stabil
}

// Solusi B: useMemo (kalau depend pada props/state)
function App({ tema, bahasa }) {
  const config = useMemo(() => ({ tema, bahasa }), [tema, bahasa]);
  
  useEffect(() => {
    console.log(config);
  }, [config]); // config cuma berubah kalau tema/bahasa berubah
}

// Solusi C: Depend pada primitive values langsung
function App({ tema, bahasa }) {
  useEffect(() => {
    const config = { tema, bahasa }; // Bikin di dalam Effect
    console.log(config);
  }, [tema, bahasa]); // Depend pada primitives
}

Jebakan: Function sebagai Dependency

Fungsi juga object di JavaScript. Fungsi yang didefinisikan di dalam komponen = fungsi baru setiap render:

jsx
// ❌ fetchData baru setiap render!
function UserProfile({ userId }) {
  async function fetchData() {
    const res = await fetch(`/api/user/${userId}`);
    return res.json();
  }
  
  useEffect(() => {
    fetchData().then(data => setUser(data));
  }, [fetchData]); // fetchData berubah setiap render → infinite loop!
}

// ✅ Solusi A: Pindahkan ke dalam Effect
function UserProfile({ userId }) {
  useEffect(() => {
    async function fetchData() {
      const res = await fetch(`/api/user/${userId}`);
      return res.json();
    }
    
    fetchData().then(data => setUser(data));
  }, [userId]);
}

// ✅ Solusi B: useCallback (kalau fungsi dipakai di tempat lain juga)
function UserProfile({ userId }) {
  const fetchData = useCallback(async () => {
    const res = await fetch(`/api/user/${userId}`);
    return res.json();
  }, [userId]); // fetchData cuma berubah kalau userId berubah
  
  useEffect(() => {
    fetchData().then(data => setUser(data));
  }, [fetchData]); // Aman!
}

Contoh Lengkap: Refactoring Dependencies

Mari kita refactor komponen yang penuh masalah dependency:

jsx
// ❌ SEBELUM: banyak masalah dependency
function SearchResults({ query, filters, onResultClick }) {
  const [results, setResults] = useState([]);
  
  // filters = object, berubah setiap render
  // onResultClick = function, berubah setiap render
  const searchOptions = {
    query,
    ...filters,
    callback: onResultClick
  };
  
  useEffect(() => {
    let aktif = true;
    
    fetch('/api/search', {
      method: 'POST',
      body: JSON.stringify(searchOptions)
    })
      .then(res => res.json())
      .then(data => {
        if (aktif) setResults(data);
      });
    
    return () => { aktif = false; };
  }, [searchOptions]); // Object baru setiap render → fetch terus-terusan!
  
  return (
    <ul>
      {results.map(r => (
        <li key={r.id} onClick={() => onResultClick(r)}>{r.title}</li>
      ))}
    </ul>
  );
}
jsx
// ✅ SESUDAH: dependency bersih
function SearchResults({ query, kategori, minHarga, maxHarga, onResultClick }) {
  const [results, setResults] = useState([]);
  
  // Destructure filters jadi primitive values
  useEffect(() => {
    let aktif = true;
    
    // Bikin object di dalam Effect
    const searchOptions = {
      query,
      kategori,
      minHarga,
      maxHarga
    };
    
    fetch('/api/search', {
      method: 'POST',
      body: JSON.stringify(searchOptions)
    })
      .then(res => res.json())
      .then(data => {
        if (aktif) setResults(data);
      });
    
    return () => { aktif = false; };
  }, [query, kategori, minHarga, maxHarga]); // Semua primitive!
  
  // onResultClick dipakai di event handler, bukan Effect
  return (
    <ul>
      {results.map(r => (
        <li key={r.id} onClick={() => onResultClick(r)}>{r.title}</li>
      ))}
    </ul>
  );
}

Perubahan yang dilakukan:

  1. filters object → destructure jadi kategori, minHarga, maxHarga (primitives)
  2. searchOptions → bikin di dalam Effect
  3. onResultClick → pindah ke event handler (nggak dipakai di Effect)

Menghindari Infinite Loop

Infinite loop terjadi kalau Effect mengubah sesuatu yang jadi dependency-nya sendiri:

jsx
// ❌ INFINITE LOOP!
function Salah() {
  const [items, setItems] = useState([]);
  
  useEffect(() => {
    // setItems bikin items berubah → Effect jalan lagi → setItems lagi → ...
    setItems([...items, 'baru']);
  }, [items]); // items berubah setiap kali Effect jalan!
}

// ✅ Pakai updater function
function Benar() {
  const [items, setItems] = useState([]);
  
  useEffect(() => {
    // Updater function: nggak perlu items di dependency
    setItems(prev => [...prev, 'baru']);
  }, []); // Jalan sekali
}
jsx
// ❌ INFINITE LOOP dengan object!
function Salah() {
  const [data, setData] = useState({ count: 0 });
  
  useEffect(() => {
    // Bikin object baru → data berubah → Effect jalan lagi → ...
    setData({ count: data.count + 1 });
  }, [data]); // data selalu "beda" (object baru)!
}

// ✅ Pakai updater + depend pada primitive
function Benar() {
  const [count, setCount] = useState(0);
  
  useEffect(() => {
    setCount(c => c + 1);
  }, []); // Atau depend pada trigger yang spesifik
}

Tabel Ringkasan Strategi

MasalahStrategiContoh
State di dependencyUpdater functionsetCount(c => c + 1)
Object di dependencyBikin di dalam EffectuseEffect(() => { const obj = {...}; })
Function di dependencyDefinisikan di dalam EffectuseEffect(() => { function fn() {...} })
Props objectDestructure ke primitivesconst { nama } = user;
KonstantaPindah ke luar komponenconst URL = '...' di luar
Callback propPakai di handler, bukan EffectonClick={() => callback(data)}
Computed valueuseMemouseMemo(() => {...}, [deps])

⚠️ Jebakan

Jebakan 1: Suppress Linter = Bug Tersembunyi

jsx
// ❌ "Ah, suppress aja biar nggak warning"
useEffect(() => {
  const id = setInterval(() => {
    setCount(count + 1); // count SELALU 0! (stale closure)
  }, 1000);
  return () => clearInterval(id);
  // eslint-disable-next-line
}, []);

// Hasilnya: count nggak pernah lebih dari 1
// Karena count di closure selalu 0 (nilai saat Effect pertama jalan)

Jebakan 2: Masukin Semua ke Dependency (Tanpa Pikir)

jsx
// ❌ "Masukin aja semua biar linter diem"
function Chat({ roomId, tema, bahasa, user, settings }) {
  useEffect(() => {
    const koneksi = buatKoneksi(roomId);
    koneksi.connect();
    
    // Cuma pakai tema buat log
    console.log(`Connected (tema: ${tema})`);
    
    return () => koneksi.disconnect();
  }, [roomId, tema]); // tema bikin reconnect setiap ganti tema!
  
  // Solusi: pakai ref buat tema (lihat bab sebelumnya)
}

Jebakan 3: useCallback/useMemo di Mana-mana

jsx
// ❌ Overkill: memo semua hal
function App() {
  const handleClick = useCallback(() => {
    console.log('clicked');
  }, []);
  
  const style = useMemo(() => ({
    color: 'red'
  }), []);
  
  const label = useMemo(() => 'Tombol', []);
  // ^ Ini nggak perlu! String itu primitive, selalu stabil.
  
  return <button onClick={handleClick} style={style}>{label}</button>;
}

// ✅ Pakai memo HANYA kalau jadi dependency Effect atau diteruskan ke komponen yang di-memo
function App() {
  return <button onClick={() => console.log('clicked')} style={{ color: 'red' }}>Tombol</button>;
}

Jebakan 4: Dependency Array yang Nggak Sinkron dengan Kode

jsx
// ❌ Dependency array nggak cocok sama yang dipakai di Effect
function Salah({ a, b, c }) {
  useEffect(() => {
    doSomething(a, b); // Pakai a dan b
  }, [a]); // Tapi cuma a di dependency! b bisa stale!
}

// ✅ Semua yang dipakai harus di dependency
function Benar({ a, b, c }) {
  useEffect(() => {
    doSomething(a, b);
  }, [a, b]); // Lengkap!
}

Ringkasan

  1. Jangan suppress linter warning. Ubah kode-nya.
  2. Updater function (setState(prev => ...)) menghilangkan kebutuhan state di dependency
  3. Pindahkan object/function ke dalam Effect kalau cuma dipakai di situ
  4. Destructure object props jadi primitive values
  5. Pindahkan konstanta ke luar komponen
  6. Object/array/function selalu beda setiap render (referensi baru)
  7. Pakai useMemo/useCallback hanya kalau benar-benar dibutuhkan

🏋️ Challenge

Challenge 1: Fix Infinite Loop

Kode berikut menyebabkan infinite loop. Fix tanpa suppress warning:

jsx
function BuggyComponent({ userId }) {
  const [user, setUser] = useState(null);
  const [posts, setPosts] = useState([]);
  
  const fetchOptions = {
    headers: { 'Authorization': `Bearer ${userId}` }
  };
  
  useEffect(() => {
    fetch(`/api/user/${userId}`, fetchOptions)
      .then(res => res.json())
      .then(data => setUser(data));
  }, [fetchOptions, userId]);
  
  useEffect(() => {
    fetch(`/api/posts?author=${userId}`, fetchOptions)
      .then(res => res.json())
      .then(data => setPosts(data));
  }, [fetchOptions, userId]);
}

Hint: fetchOptions adalah object baru setiap render.

Lihat Solusi
jsx
function FixedComponent({ userId }) {
  const [user, setUser] = useState(null);
  const [posts, setPosts] = useState([]);
  
  useEffect(() => {
    // Bikin fetchOptions di dalam Effect
    const fetchOptions = {
      headers: { 'Authorization': `Bearer ${userId}` }
    };
    
    let aktif = true;
    
    fetch(`/api/user/${userId}`, fetchOptions)
      .then(res => res.json())
      .then(data => {
        if (aktif) setUser(data);
      });
    
    return () => { aktif = false; };
  }, [userId]); // Cuma userId (primitive)
  
  useEffect(() => {
    const fetchOptions = {
      headers: { 'Authorization': `Bearer ${userId}` }
    };
    
    let aktif = true;
    
    fetch(`/api/posts?author=${userId}`, fetchOptions)
      .then(res => res.json())
      .then(data => {
        if (aktif) setPosts(data);
      });
    
    return () => { aktif = false; };
  }, [userId]);
  
  return (
    <div>
      {user && <h2>{user.nama}</h2>}
      <ul>
        {posts.map(p => <li key={p.id}>{p.judul}</li>)}
      </ul>
    </div>
  );
}

Penjelasan: fetchOptions dipindahkan ke dalam masing-masing Effect. Sekarang dependency cuma userId (string/number), yang stabil selama nilainya sama.

Challenge 2: Interval dengan Dynamic Delay

Bikin counter yang naik setiap N milidetik. User bisa ubah delay. Tapi setiap kali delay berubah, counter NGGAK boleh reset ke 0.

Hint: Counter pakai updater function. Delay di dependency (Effect restart dengan interval baru, tapi counter tetap lanjut).

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

function DynamicCounter() {
  const [count, setCount] = useState(0);
  const [delay, setDelay] = useState(1000);
  
  useEffect(() => {
    const intervalId = setInterval(() => {
      // Updater function: nggak perlu count di dependency
      setCount(c => c + 1);
    }, delay);
    
    // Cleanup: clear interval lama saat delay berubah
    return () => clearInterval(intervalId);
  }, [delay]); // Cuma delay! Count nggak perlu karena pakai updater
  
  return (
    <div>
      <h2>Count: {count}</h2>
      
      <div>
        <label>Delay: {delay}ms</label>
        <input
          type="range"
          min="100"
          max="3000"
          step="100"
          value={delay}
          onChange={e => setDelay(Number(e.target.value))}
        />
      </div>
      
      <p style={{ fontSize: '12px', color: 'gray' }}>
        Geser slider: interval berubah, tapi counter lanjut (nggak reset)
      </p>
      
      <button onClick={() => setCount(0)}>Reset Counter</button>
    </div>
  );
}

Penjelasan:

  • setCount(c => c + 1) = updater function, nggak perlu count di dependency
  • delay di dependency = saat delay berubah, interval lama di-clear, interval baru dibuat
  • Counter nggak reset karena setCount cuma nambah dari nilai sebelumnya
  • Kalau pakai setCount(count + 1), kita harus masukin count di dependency, dan Effect bakal jalan setiap detik (karena count berubah setiap detik)

Challenge 3: Multi-Filter Search tanpa Unnecessary Refetch

Bikin komponen search dengan 3 filter (keyword, kategori, sort). Fetch cuma jalan kalau salah satu filter berubah, bukan karena render lain.

Hint: Pastikan semua dependency adalah primitive values.

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

function SmartSearch() {
  const [keyword, setKeyword] = useState('');
  const [kategori, setKategori] = useState('semua');
  const [sortBy, setSortBy] = useState('relevansi');
  const [results, setResults] = useState([]);
  const [loading, setLoading] = useState(false);
  const [fetchCount, setFetchCount] = useState(0);
  
  useEffect(() => {
    // Skip kalau keyword kosong
    if (keyword.trim() === '') {
      setResults([]);
      return;
    }
    
    let aktif = true;
    setLoading(true);
    
    // Debounce
    const timeoutId = setTimeout(async () => {
      // Bikin request body di dalam Effect (nggak jadi dependency)
      const body = {
        keyword: keyword.trim(),
        kategori: kategori === 'semua' ? null : kategori,
        sortBy
      };
      
      try {
        // Simulasi fetch
        console.log('🔍 Fetching:', body);
        await new Promise(resolve => setTimeout(resolve, 500));
        
        if (aktif) {
          // Simulasi hasil
          const hasilSimulasi = Array.from({ length: 5 }, (_, i) => ({
            id: i,
            judul: `${keyword} - Hasil ${i + 1} (${kategori}, ${sortBy})`,
            harga: Math.floor(Math.random() * 100000)
          }));
          
          setResults(hasilSimulasi);
          setFetchCount(c => c + 1);
          setLoading(false);
        }
      } catch (err) {
        if (aktif) setLoading(false);
      }
    }, 300);
    
    return () => {
      aktif = false;
      clearTimeout(timeoutId);
    };
  }, [keyword, kategori, sortBy]); // Semua primitive! Nggak ada object/function
  
  return (
    <div>
      <h2>Smart Search</h2>
      
      <div style={{ display: 'flex', gap: '10px', marginBottom: '10px' }}>
        <input
          value={keyword}
          onChange={e => setKeyword(e.target.value)}
          placeholder="Cari..."
        />
        
        <select value={kategori} onChange={e => setKategori(e.target.value)}>
          <option value="semua">Semua</option>
          <option value="elektronik">Elektronik</option>
          <option value="fashion">Fashion</option>
          <option value="makanan">Makanan</option>
        </select>
        
        <select value={sortBy} onChange={e => setSortBy(e.target.value)}>
          <option value="relevansi">Relevansi</option>
          <option value="harga-asc">Harga ↑</option>
          <option value="harga-desc">Harga ↓</option>
          <option value="terbaru">Terbaru</option>
        </select>
      </div>
      
      <p style={{ fontSize: '12px', color: 'gray' }}>
        Total fetch: {fetchCount} (fetch cuma jalan kalau filter berubah)
      </p>
      
      {loading && <p>Loading...</p>}
      
      <ul>
        {results.map(r => (
          <li key={r.id}>{r.judul} - Rp{r.harga.toLocaleString()}</li>
        ))}
      </ul>
    </div>
  );
}

Penjelasan:

  • Semua dependency (keyword, kategori, sortBy) adalah string (primitive)
  • Nggak ada object atau function di dependency array
  • Effect cuma re-run kalau salah satu filter benar-benar berubah nilainya
  • Debounce mencegah fetch terlalu sering saat user ngetik cepat
  • fetchCount pakai updater function, jadi nggak perlu di dependency

Sudah paham materi ini?

Tandai sebagai selesai untuk melacak progress-mu.