Kamu Pasti Paham useEffect React Hook Setelah Lihat Contoh Ini

Kamu Pasti Paham useEffect React Hook Setelah Lihat Contoh Ini
Photo by Tim Johnson/Unsplash

Pernah nggak sih kamu lagi asyik ngoding React, terus tiba-tiba ketemu masalah yang nggak bisa diselesaikan cuma pakai useState atau props doang? Misalnya, kamu perlu ngambil data dari API pas komponen pertama kali muncul, atau kamu mau update judul halaman tiap kali ada data baru, atau bahkan kamu mau nambahin event listener pas user klik sesuatu, terus pas komponennya ilang, event listener-nya juga ikutan ilang biar nggak memory leak? Nah, di sinilah useEffect React Hook jadi pahlawan super kamu!

Banyak banget developer pemula yang bingung sama useEffect. Konsepnya memang agak sedikit beda dari yang biasa, apalagi kalau kamu sebelumnya cuma kenal class component. Tapi tenang aja, di artikel ini kita bakal kupas tuntas useEffect sampai kamu benar-benar paham, lengkap dengan contoh-contohnya yang gampang dicerna. Anggap aja ini obrolan santai antar teman yang lagi belajar bareng. Siap? Yuk, kita mulai!

useEffect Itu Apa Sih Sebenarnya? Kenapa Penting?

Oke, pertama-tama, apa sih useEffect itu? Simpelnya, useEffect itu salah satu React Hook yang khusus diciptakan buat menangani "side effects" di dalam functional component kamu. Loh, "side effects" itu apa lagi?

Bayangin gini: React itu didesain biar UI kamu jadi fungsi murni (pure function) dari props dan state. Artinya, kalau props atau state-nya sama, output UI-nya juga harusnya sama persis. Ini bagus banget buat bikin kode yang prediktif dan gampang dites.

Tapi, dunia nyata kan nggak se-pure itu. Kadang kamu perlu ngelakuin hal-hal di luar lingkup "render" UI yang murni. Nah, inilah yang disebut "side effects". Contoh side effects itu banyak banget, misalnya:

  • Mengambil data (data fetching) dari API. Ini paling sering.
  • Mengatur subscription (misalnya ke WebSocket atau Redux store).
  • Memanipulasi DOM secara langsung (misalnya mengubah document.title).
  • Mengatur timer (setTimeout, setInterval).
  • Mengakses local storage.

Dulu, di class component, side effects ini diurus pakai lifecycle methods kayak componentDidMount, componentDidUpdate, atau componentWillUnmount. Nah, useEffect ini adalah versi modern dan lebih fleksibel dari gabungan ketiga lifecycle method itu di dunia functional component. Tujuannya satu: menjaga agar komponen kamu tetap bersih dari logika side effect yang berantakan, dan mengumpulkannya di satu tempat yang terorganisir.

Cara Pakai useEffect yang Paling Dasar

Struktur dasar useEffect itu gampang banget, cuma butuh dua argumen:

javascript
useEffect(callbackFungsi, [arrayDependencies]);
  1. callbackFungsi (fungsi pertama): Ini adalah fungsi yang bakal dieksekusi sama React. Di sinilah kamu masukin semua kode side effect kamu. Fungsi ini bisa bersifat sinkronus, tapi kalau kamu mau async (misalnya fetch data), kamu harus panggil fungsi async di dalamnya atau definisikan fungsi async-nya di dalam callback. Kita bahas nanti contohnya.
  2. [arrayDependencies] (array kedua, opsional): Ini yang paling penting dan sering bikin orang bingung. Array ini berisi daftar nilai-nilai (bisa props, state, atau variabel lain) yang "diamati" sama React. useEffect akan menjalankan ulang callbackFungsi-nya cuma kalau ada nilai di dalam array ini yang berubah dari render sebelumnya. Kalau array ini nggak kamu sertakan sama sekali, useEffect bakal jalan setiap kali komponen di-render ulang. Kalau array ini kosong ([]), useEffect cuma jalan sekali pas komponen pertama kali muncul, mirip componentDidMount.

Oke, biar kebayang, kita lihat contoh paling dasar:

jsx
import React, { useEffect, useState } from 'react';function MyComponent() {
  const [count, setCount] = useState(0);// Contoh 1: Tanpa array dependencies (berjalan setiap render)
  useEffect(() => {
    console.log('Komponen di-render atau state/props berubah!', count);
  }); // Tidak ada array dependencies// Contoh 2: Dengan array dependencies kosong (berjalan sekali saat mount)
  useEffect(() => {
    console.log('Komponen pertama kali muncul di layar!');
    // Biasanya buat fetch data awal
  }, []); // Array dependencies kosong// Contoh 3: Dengan array dependencies berisi nilai (berjalan saat nilai berubah)
  useEffect(() => {
    console.log('Nilai count berubah jadi:', count);
  }, [count]); // Berjalan hanya jika 'count' berubahreturn (
    
      Contoh useEffect
      Count: {count}
       setCount(prevCount => prevCount + 1)}>
        Tambah Count
      
       setCount(0)}>Reset Count
    
  );
}

Dari contoh di atas, kamu bisa lihat bedanya:

  • useEffect tanpa array dependencies: Tiap kamu klik tombol "Tambah Count", atau "Reset Count", bahkan kalau ada props lain yang berubah yang bikin komponen ini di-render ulang, console.log yang pertama bakal muncul. Ini jarang banget dipakai karena bisa bikin performa jadi jelek kalau side effect-nya berat.
  • useEffect dengan []: Kalau kamu refresh browser atau komponen MyComponent ini baru muncul, console.log yang kedua cuma bakal muncul sekali aja. Setelah itu, mau count berubah berapa kalipun, dia nggak akan jalan lagi. Cocok buat inisialisasi awal.
  • useEffect dengan [count]: console.log yang ketiga cuma akan muncul kalau nilai count berubah. Kalau ada state lain di komponen ini yang berubah tapi count nggak, useEffect yang ini nggak akan jalan.

Paham kan bedanya? Ini kunci banget buat menguasai useEffect.

Kapan Array Dependencies Jadi Kritis? (Dan Kenapa [] Itu Penting)

Ini dia bagian yang paling sering bikin pusing tapi justru paling powerful: array dependencies.

1. useEffect tanpa array dependencies (No Dependency Array)

Kalo kamu nggak kasih array dependencies sama sekali, useEffect bakal jalan setiap kali komponen di-render ulang. Ini mirip gabungan componentDidMount dan componentDidUpdate di class component.

javascript
useEffect(() => {
  // Ini akan berjalan SETIAP kali komponen di-render ulang
  // Jadi harus hati-hati, bisa bikin infinite loop kalau kamu update state di sini
  console.log('Saya selalu muncul!');
});

Contoh: Kalau kamu fetch data di sini, bayangin tiap user ngetik sesuatu di input field, data bakal di-fetch ulang terus. Boros resource dan bisa bikin error. Hindari sebisa mungkin, kecuali kamu memang butuh efek yang dieksekusi setiap render.

2. useEffect dengan array dependencies kosong ([])

Ini artinya useEffect bakal jalan hanya sekali saja, setelah render pertama komponen. Mirip banget sama componentDidMount.

javascript
useEffect(() => {
  // Ini hanya akan berjalan SATU KALI setelah komponen pertama kali muncul
  console.log('Saya muncul cuma sekali lho, pas pertama lahir!');
  // Contoh umum: fetching data awal dari API
  fetch('/api/data')
    .then(response => response.json())
    .then(data => console.log(data));
}, []); // Perhatikan array kosong di sini

Kapan ini berguna? Saat kamu perlu inisialisasi sesuatu yang cuma perlu dilakukan sekali aja, kayak:

  • Ngambil data awal dari server.
  • Nambahin event listener global (misalnya untuk keyboard shortcut).
  • Mengatur langganan ke service eksternal yang cuma perlu diatur sekali.

3. useEffect dengan array dependencies berisi nilai ([dep1, dep2, ...])

Ini yang paling sering dipakai dan paling fleksibel. useEffect bakal jalan hanya jika salah satu nilai di dalam array dependencies berubah dari render sebelumnya.

javascript
import React, { useEffect, useState } from 'react';function UserProfile({ userId }) {
  const [userData, setUserData] = useState(null);
  const [loading, setLoading] = useState(true);useEffect(() => {
    setLoading(true);
    // Kita anggap ini simulasi fetch data dari API
    console.log(Mengambil data untuk user ID: ${userId}...);
    fetch(https://api.example.com/users/${userId})
      .then(res => res.json())
      .then(data => {
        setUserData(data);
        setLoading(false);
      })
      .catch(error => {
        console.error('Error fetching user data:', error);
        setLoading(false);
      });
  }, [userId]); // Efek ini akan berjalan setiap kali 'userId' berubahif (loading) {
    return Loading user data...;
  }if (!userData) {
    return Tidak ada data user.;
  }return (
    
      {userData.name}
      Email: {userData.email}
      ID: {userData.id}
    
  );
}

Di contoh UserProfile di atas:

  • Ketika komponen UserProfile pertama kali di-render, useEffect akan jalan dan mengambil data untuk userId yang pertama.
  • Jika userId yang dikirim sebagai prop dari komponen induk berubah (misalnya user klik tombol untuk melihat profil user lain), useEffect akan mendeteksi perubahan userId dan akan menjalankan ulang fungsi fetch data-nya. Ini otomatis dan efisien!

Penting: Selalu pastikan semua nilai yang digunakan di dalam callbackFungsi useEffect yang bisa berubah dan membuat efek perlu dijalankan ulang dimasukkan ke dalam array dependencies. Kalau tidak, kamu akan menghadapi masalah yang namanya "stale closure", di mana fungsi useEffect kamu "melihat" nilai state atau props yang lama. ESLint biasanya akan ngasih peringatan kalau kamu lupa nambahin dependencies. Dengarkan dia!

Fungsi Cleanup: Jangan Lupa Bersih-Bersih!

Bayangin kamu pakai kamar hotel. Abis pakai, kamu pasti beresin barang bawaan kamu kan? Nggak dibiarkan berantakan gitu aja. Nah, useEffect juga punya konsep bersih-bersih yang sama, namanya fungsi cleanup.

Terkadang, side effects yang kamu buat perlu "dibersihkan" saat komponen nggak lagi dipakai, atau sebelum useEffect dieksekusi ulang. Kalau nggak dibersihin, bisa terjadi memory leak atau perilaku aplikasi yang aneh.

useEffect memungkinkan kamu mengembalikan sebuah fungsi dari callbackFungsi-nya. Fungsi yang dikembalikan inilah yang disebut fungsi cleanup. Kapan fungsi cleanup ini dieksekusi?

  1. Sebelum useEffect dieksekusi ulang (jika dependencies berubah).
  2. Ketika komponen di-unmount (dihilangkan dari DOM).

Contoh yang paling sering: setInterval dan addEventListener.

jsx
import React, { useEffect, useState } from 'react';function Timer() {
  const [seconds, setSeconds] = useState(0);useEffect(() => {
    // Setiap 1 detik, tambahin 'seconds'
    const intervalId = setInterval(() => {
      setSeconds(prevSeconds => prevSeconds + 1);
    }, 1000);// Fungsi cleanup: Ini akan jalan pas komponen di-unmount atau sebelum efek dieksekusi ulang
    return () => {
      console.log('Membersihkan interval...');
      clearInterval(intervalId); // Penting! Hentikan interval
    };
  }, []); // Array kosong, artinya efek ini jalan sekali dan cleanup juga sekali pas unmountreturn (
    
      Timer: {seconds} detik
      Lihat console untuk pesan cleanup.
    
  );
}function App() {
  const [showTimer, setShowTimer] = useState(true);return (
    
      Aplikasi Utama
       setShowTimer(!showTimer)}>
        {showTimer ? 'Sembunyikan Timer' : 'Tampilkan Timer'}
      
      {showTimer && }
    
  );
}

Coba jalankan kode di atas. Ketika kamu klik "Sembunyikan Timer", komponen Timer akan di-unmount, dan kamu akan melihat pesan "Membersihkan interval..." di console. Ini penting banget karena kalau clearInterval nggak dipanggil, setInterval akan terus berjalan di background meskipun komponen Timer sudah nggak ada, yang bisa menyebabkan memory leak dan error.

Contoh lain dengan addEventListener:

jsx
import React, { useEffect } from 'react';function MouseTracker() {
  useEffect(() => {
    const handleMouseMove = (event) => {
      console.log(Mouse bergerak di X: ${event.clientX}, Y: ${event.clientY});
    };window.addEventListener('mousemove', handleMouseMove);return () => {
      // Pastikan listener dihapus saat komponen di-unmount
      window.removeEventListener('mousemove', handleMouseMove);
      console.log('Event listener mousemove dihapus.');
    };
  }, []); // [] agar listener cuma ditambah sekali
  
  return Gerakkan mouse di atas halaman dan lihat console.;
}

Sama seperti setInterval, removeEventListener itu krusial. Tanpa itu, handleMouseMove akan terus dipanggil meskipun MouseTracker sudah nggak ada di layar, yang lagi-lagi bisa bikin memory leak dan masalah lainnya.

Contoh Nyata useEffect yang Sering Kamu Temui

Oke, biar makin mantap, kita bahas beberapa skenario useEffect yang paling sering kamu temuin di proyek React:

1. Mengambil Data dari API (Data Fetching)

Ini juara satu! Hampir semua aplikasi modern butuh ngambil data dari backend.

jsx
import React, { useEffect, useState } from 'react';function PostDetail({ postId }) {
  const [post, setPost] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);useEffect(() => {
    const fetchPost = async () => {
      setLoading(true);
      setError(null);
      try {
        const response = await fetch(https://jsonplaceholder.typicode.com/posts/${postId});
        if (!response.ok) {
          throw new Error(HTTP error! status: ${response.status});
        }
        const data = await response.json();
        setPost(data);
      } catch (err) {
        setError(err);
      } finally {
        setLoading(false);
      }
    };if (postId) { // Pastikan postId ada sebelum fetch
      fetchPost();
    }// Tidak ada cleanup spesifik untuk fetch API, karena HTTP request biasanya sekali jalan
    // kecuali kamu pakai WebSocket atau long polling
  }, [postId]); // Efek akan jalan ulang setiap kali postId berubahif (loading) return Loading post...;
  if (error) return Error: {error.message};
  if (!post) return Pilih Post ID untuk menampilkan detail.;return (
    
      {post.title}
      {post.body}
    
  );
}// Cara pakainya di komponen lain:
function App() {
  const [currentPostId, setCurrentPostId] = useState(1);return (
    
      Contoh Fetch Data dengan useEffect
       setCurrentPostId(1)}>Lihat Post 1
       setCurrentPostId(2)}>Lihat Post 2
       setCurrentPostId(3)}>Lihat Post 3
      
      
    
  );
}

Perhatikan:

  • Kita menggunakan async/await di dalam fungsi fetchPost yang kita definisikan di dalam useEffect. Kenapa nggak langsung async useEffect(...)? Karena useEffect callback tidak boleh langsung async.
  • [postId] di dependencies memastikan fetch ulang terjadi hanya ketika postId berubah. Keren kan?

2. Mengubah Judul Dokumen (Document Title)

Contoh simpel DOM manipulation.

jsx
import React, { useEffect, useState } from 'react';function PageTitleUpdater() {
  const [pageName, setPageName] = useState('Home');useEffect(() => {
    document.title = Selamat Datang di ${pageName};
    console.log('Judul halaman diubah menjadi:', document.title);
  }, [pageName]); // Efek ini akan jalan setiap kali 'pageName' berubahreturn (
    
      Update Judul Halaman
      Judul halaman saat ini: {document.title}
       setPageName('About Us')}>Ke Halaman About Us
       setPageName('Produk')}>Ke Halaman Produk
    
  );
}

Setiap kali pageName berubah, document.title akan langsung diupdate.

Jebakan Batman (Pitfalls) dan Tips Terbaik (Best Practices)

Meskipun powerful, useEffect punya beberapa jebakan yang sering bikin pusing kalau kamu nggak hati-hati.

1. Masalah Dependencies: Si Stale Closure dan Infinite Loop

  • Stale Closures: Ini terjadi kalau kamu lupa masukin suatu variabel (state, props, atau fungsi) yang dipakai di dalam useEffect ke dalam array dependencies. Akibatnya, useEffect akan "melihat" nilai lama dari variabel tersebut, bukan nilai yang paling baru. ESLint dengan eslint-plugin-react-hooks punya aturan exhaustive-deps yang sangat membantu untuk mendeteksi ini. Dengarkan peringatan ESLint! Hampir selalu benar.
javascript
    const [count, setCount] = useState(0);
    useEffect(() => {
      // Ini salah kalau kamu cuma mau console.log pas mount.
      // Kalau kamu lupa [count] di dependencies, maka kalau count berubah,
      // console.log ini akan tetap menampilkan nilai count lama saat efek ini pertama kali dipanggil!
      console.log('Nilai count lama:', count); 
    }, []); // <-- ERROR: ESLint akan bilang "React Hook useEffect has a missing dependency: 'count'."

Solusi: Masukkan count ke dependencies: }, [count]);

  • Infinite Loop: Ini bahaya! Terjadi kalau kamu update state di dalam useEffect, dan update state itu sendiri memicu useEffect untuk jalan lagi, dan seterusnya.
javascript
    useEffect(() => {
      // JANGAN LAKUKAN INI!
      setCount(count + 1); // Ini akan memicu render ulang, lalu useEffect jalan lagi, dst.
    }, [count]); // Karena count di dependencies, dia akan terus jalan!

Untuk menghindarinya: * Pastikan update state di dalam useEffect punya kondisi yang jelas kapan harus dijalankan (misalnya, hanya jika data benar-benar berubah). * Gunakan useRef untuk menyimpan nilai yang tidak ingin memicu useEffect untuk jalan ulang. * Jika kamu hanya ingin mengubah state berdasarkan state sebelumnya, gunakan functional update dari setState (misalnya setCount(prevCount => prevCount + 1)).

2. Kapan TIDAK Memakai useEffect?

useEffect itu buat side effects. Jangan pakai useEffect untuk hal-hal yang bukan side effect, misalnya:

  • Menghitung nilai baru dari props atau state yang sudah ada: Kalau kamu cuma mau menghitung fullName dari firstName dan lastName, cukup lakukan di dalam body komponen di luar useEffect.
javascript
    // JANGAN LAKUKAN INI
    // useEffect(() => {
    //   setFullName(${firstName} ${lastName});
    // }, [firstName, lastName]);
  • Melakukan validasi atau mengatur state secara langsung setelah render: Kalau kamu mau set state, tapi tidak ada efek samping yang terjadi, langsung saja di dalam body komponen.
javascript
    // JANGAN LAKUKAN INI (kalau tujuannya cuma set default value)
    // useEffect(() => {
    //   if (value === null) setValue(defaultValue);
    // }, [value, defaultValue]);

3. Memecah useEffect (Separate Concerns)

Kalau kamu punya banyak side effect yang tidak saling berkaitan, pecah saja menjadi beberapa useEffect terpisah. Ini membuat kode lebih mudah dibaca, dipahami, dan di-debug.

jsx
function MyComplexComponent() {
  const [userId, setUserId] = useState(1);
  const [theme, setTheme] = useState('light');// Efek 1: Fetch user data (tergantung userId)
  useEffect(() => {
    console.log(Fetching user with ID: ${userId});
    // ... fetch logic ...
  }, [userId]);// Efek 2: Update document title (tergantung theme)
  useEffect(() => {
    document.title = Aplikasi - ${theme} mode;
  }, [theme]);// Efek 3: Log setiap kali komponen di-render
  useEffect(() => {
    console.log('Komponen MyComplexComponent di-render.');
  }); // Tanpa dependencies, jalan setiap renderreturn (
    // ... UI ...
    
        User ID: {userId}
         setUserId(userId + 1)}>Next User
        Theme: {theme}
         setTheme(theme === 'light' ? 'dark' : 'light')}>Toggle Theme
    
  );
}

Ini jauh lebih bersih daripada menyatukan semua logika di satu useEffect yang besar.

4. useCallback dan useMemo dengan useEffect (Optimasi Tingkat Lanjut)

Kadang, kamu mungkin perlu memasukkan fungsi atau objek ke dalam array dependencies useEffect. Masalahnya, setiap kali komponen di-render ulang, fungsi atau objek yang didefinisikan di dalam komponen akan dibuat ulang (memiliki referensi memori yang berbeda). Ini bisa menyebabkan useEffect jalan ulang padahal isinya sama.

Di sinilah useCallback dan useMemo berperan:

  • useCallback: Digunakan untuk "memoize" fungsi. Artinya, fungsi yang dibuat hanya akan berubah referensinya jika dependencies dari useCallback itu sendiri berubah.
  • useMemo: Digunakan untuk "memoize" nilai atau objek. Artinya, nilai/objek yang dihasilkan hanya akan dihitung ulang jika dependencies dari useMemo itu sendiri berubah.

jsx
import React, { useEffect, useState, useCallback } from 'react';function SearchComponent() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);// Fungsi fetchData ini akan dibuat ulang setiap kali SearchComponent di-render
  // Tapi dengan useCallback, referensinya akan stabil kecuali query berubah
  const fetchData = useCallback(async () => {
    if (!query) {
      setResults([]);
      return;
    }
    console.log(Mencari data untuk: ${query});
    // Simulasi API call
    const response = await new Promise(resolve => setTimeout(() => {
      resolve([Result for ${query} 1, Result for ${query} 2]);
    }, 500));
    setResults(response);
  }, [query]); // fetchData hanya akan dibuat ulang jika 'query' berubahuseEffect(() => {
    fetchData();
  }, [fetchData]); // Karena fetchData di-memoize, useEffect hanya akan jalan jika query berubahreturn (
    
       setQuery(e.target.value)}
        placeholder="Cari..."
      />
      
        {results.map((r, index) => (
          {r}
        ))}
      
    
  );
}

Dengan useCallback, fetchData akan memiliki referensi yang stabil selama query tidak berubah. Ini membuat useEffect di bawahnya hanya akan jalan jika fetchData (yang bergantung pada query) benar-benar berubah, atau dengan kata lain, ketika query berubah. Ini penting untuk optimasi agar useEffect tidak terlalu sering jalan.

useEffect vs. Class Lifecycle Methods (Sedikit Perbandingan)

Buat kamu yang pernah ngoding React dengan class component, mungkin penasaran gimana useEffect ini sama dengan lifecycle methods yang lama.

  • componentDidMount: Mirip dengan useEffect(callback, []).
  • componentDidUpdate: Mirip dengan useEffect(callback, [dependencies]) atau useEffect(callback) (tanpa dependencies, tapi ini jarang dipakai).
  • componentWillUnmount: Mirip dengan return cleanupFunction dari useEffect(callback, []).

Kelebihan useEffect adalah dia menyatukan ketiga konsep ini menjadi satu API yang lebih konsisten dan fleksibel, dan juga memungkinkan kita untuk mengelompokkan logika side effect berdasarkan "apa yang dilakukan" bukan "kapan dilakukan".

Sedikit Tentang useLayoutEffect

Ada juga useLayoutEffect, yang sangat mirip dengan useEffect tapi ada satu perbedaan kunci:

useEffect: Berjalan secara asynchronous* setelah browser selesai mengecat (paint) ulang DOM. Ini adalah yang paling umum dan disarankan. useLayoutEffect: Berjalan secara synchronous setelah semua DOM mutations, tapi sebelum* browser mengecat ulang layar.

Kapan pakai useLayoutEffect? Kalau kamu perlu melakukan sesuatu yang memanipulasi DOM dan perlu diukur (misalnya, mendapatkan lebar atau posisi elemen) sebelum browser menampilkan hasilnya ke user. Kalau kamu pakai useEffect untuk ini, bisa jadi ada "flicker" atau kedipan di layar karena browser sudah sempat menggambar UI sebelum perubahanmu diaplikasikan. Tapi ini kasus yang jarang. Untuk sebagian besar kasus, useEffect sudah cukup.

Kesimpulan

Gimana, sekarang sudah lebih paham kan sama useEffect? Memang butuh sedikit latihan buat terbiasa dengan konsep dependencies dan fungsi cleanup-nya. Tapi begitu kamu menguasainya, useEffect bakal jadi salah satu senjata paling ampuh di kotak perkakas React kamu.

Ingat kuncinya:

  1. useEffect(callback) (tanpa array): Jalan setiap render. Jarang dipakai.
  2. useEffect(callback, []) (array kosong): Jalan sekali saat mount. Cocok buat inisialisasi.
  3. useEffect(callback, [dep1, dep2]) (array dengan dependencies): Jalan saat dependencies berubah. Paling sering dipakai.
  4. return cleanupFunction: Penting banget buat membersihkan efek (timers, event listeners, subscriptions) agar tidak ada memory leak.
  5. Perhatikan ESLint: Dia sahabat terbaikmu dalam menemukan missing dependencies.

Jangan takut untuk bereksperimen dan coba sendiri contoh-contoh di atas. Semakin sering kamu praktik, semakin natural kamu akan memahami kapan dan bagaimana menggunakan useEffect dengan benar. Selamat ngoding, ya!