Bayangkan Anda sedang menulis kode React yang rapi, semua komponen tampil sebagaimana mestinya, tetapi tiba-tiba muncul kebutuhan “tambahan”: memanggil API, menyimpan data ke localStorage, mengubah judul halaman, atau memasang event listener ke window. Hal-hal inilah yang disebut side effect-logika yang berada di luar proses render murni sebuah komponen.
Di sinilah useEffect berperan sebagai “ahli” pengelola side effect di React. Hook ini menjadi jembatan antara dunia tampilan (UI) dan segala sesuatu yang terjadi di belakang layar, mulai dari fetch data hingga pembersihan (cleanup) efek yang tidak lagi dibutuhkan. Tanpa useEffect, komponen Anda akan kesulitan berkomunikasi dengan dunia luar secara terstruktur.
Artikel ini akan mengajak Anda mengenal useEffect dari dasar hingga praktiknya untuk Fetch API. Kita akan membahas cara kerjanya, pola penggunaan yang tepat, hingga jebakan umum yang sering membuat developer pusing-seperti infinite loop atau permintaan data berulang. Dengan begitu, Anda dapat mengelola side effect secara lebih terkontrol, bersih, dan mudah dipelihara dalam proyek React Anda.
Memahami Hakikat Side Effect di React dan Kapan useEffect Benar Benar Dibutuhkan
Dalam React, side effect itu segala hal yang “menyentuh dunia luar” atau mengubah sesuatu di luar proses render murni: memanggil API, mengubah document.title, menyimpan data ke localStorage, memasang dan membersihkan event listener, sampai mengatur timer dengan setTimeout. Render React sendiri idealnya seperti fungsi murni di JavaScript: sama input, sama output, tanpa efek samping. Begitu kita mulai berurusan dengan hal-hal di luar itu, di situlah efek samping muncul. Biar lebih kebayang, bayangkan komponen sebagai “mesin hitung”: kalau dia tiba-tiba nelpon server atau ngedit DOM sendiri, itu sudah bukan hitung-hitungan murni lagi, tapi efek samping yang harus dikelola dengan rapi.
Masalahnya, banyak yang pakai useEffect buat semua hal, padahal tidak selalu perlu. Ada beberapa kondisi di mana hook ini memang jadi alat yang tepat:
- Butuh sinkronisasi dengan dunia luar, misalnya fetch data dari API tiap kali
idberubah. - Perlu setup & cleanup seperti event listener window, WebSocket, atau interval yang harus dibersihkan saat komponen unmount.
- Meng-update sesuatu di luar React tree, contohnya mengubah
document.titleatau berinteraksi dengan library pihak ketiga.
| Kebutuhan | Perlu useEffect? |
|---|---|
| Hitung nilai dari props/state | Tidak, pakai variable biasa atau useMemo |
| Fetch data dari API | Ya, cocok diletakkan di dalam useEffect |
| Update UI di luar React (title, DOM manual) | Ya, kelola via useEffect |
| Set state setelah event onClick | Tidak, cukup di handler event saja |
Membedah Pola Dasar useEffect dari Dependency Array hingga Cleanup Function yang Rapi
Kalau mau nyaman pakai useEffect, kuncinya ada di cara kita mengatur dependency array. Di sinilah kita ngasih tahu React, “Efek ini perlu jalan ulang kalau apa yang berubah?” Semakin spesifik dan jujur dependensinya, semakin kecil kemungkinan bug “aneh-aneh” kayak data yang nggak sinkron atau efek yang kepanggil berkali-kali. Biasakan cek: state atau props apa saja yang dipakai di dalam efek, lalu pastikan semuanya masuk ke dalam array. Kalau memang efek cuma mau dijalankan sekali saat mount, pakai array kosong, tapi jangan jadikan itu alasan buat mengabaikan warning dari ESLint tanpa tahu risikonya.
- Dependency array sebagai “daftar pemicu” efek
- Cleanup function untuk beres-beres sebelum efek baru jalan
- Menghindari memory leak pada fetch, event listener, dan timer
- Efek yang rapi = component lebih mudah dirawat dan di-debug
| Polanya | Kapan Dipakai |
|---|---|
useEffect(fn, []) |
Efek sekali saat component muncul |
useEffect(fn, [state]) |
Efek tergantung perubahan nilai tertentu |
useEffect(() => { return cleanup }) |
Beresin efek sebelum update/unmount |
Bagian yang sering di-skip tapi justru penting adalah cleanup function. Ibaratnya, setiap kali kita bikin efek yang “buka sesuatu” (misal: bikin interval, pasang event listener, atau masih nunggu respon fetch), kita juga wajib sediain cara buat “nutup” lagi sebelum efek baru dijalankan atau sebelum komponen di-unmount. Dengan begitu, kita nggak ninggalin event listener nyangkut atau timer yang masih hidup di belakang layar. Polanya simpel: kita balikin sebuah fungsi dari dalam useEffect yang isinya tugas-tugas beres-beres itu. Dan kalau fetch API digabung sama useEffect, nggak ada salahnya pakai AbortController supaya request yang nggak kepakai bisa dibatalkan dengan rapi.
“`jsx
useEffect(() => {
const controller = new AbortController();
fetch(‘/api/posts’, { signal: controller.signal })
.then((res) => res.json())
.then((data) => setPosts(data))
.catch((err) => {
if (err.name !== ‘AbortError’) {
console.error(err);
}
});
return () => {
controller.abort();
};
}, [filter]);
“`
Strategi Fetch API dengan useEffect dari Loading State sampai Error Handling yang Andal
Di skenario nyata, pola fetch dengan useEffect yang rapi biasanya selalu punya tiga elemen: loading, data, dan error. Cara paling aman adalah memisahkan state untuk masing-masing kebutuhan ini, lalu mengatur alurnya secara eksplisit. Misalnya, setiap kali efek dijalankan, kita mulai dari reset error dan set loading ke true, baru kemudian lakukan fetch. Kira-kira strukturnya seperti ini:
“`jsx
useEffect(() => {
let isMounted = true;
const controller = new AbortController();
const fetchData = async () => {
setLoading(true);
setError(null);
try {
const res = await fetch(‘/api/posts’, { signal: controller.signal });
if (!res.ok) throw new Error(‘Gagal mengambil data’);
const json = await res.json();
if (isMounted) setData(json);
} catch (err) {
if (isMounted && err.name !== ‘AbortError’) {
setError(err.message || ‘Terjadi kesalahan’);
}
} finally {
if (isMounted) setLoading(false);
}
};
fetchData();
return () => {
isMounted = false;
controller.abort();
};
}, []);
“`
Supaya tampilan dan pengalaman pengguna tetap nyaman, kombinasikan state tadi dengan UI yang jelas dan bersih. Biasanya, pola yang enak dipakai adalah:
- Loading state → tampilkan skeleton atau spinner.
- Success state → render daftar data secara terstruktur.
- Error state → tampilkan pesan singkat, plus opsi retry.
| State | UI yang Disarankan |
|---|---|
| Loading | Skeleton, spinner, atau teks “Mengambil data…” |
| Berhasil | List rapi, bisa dipaginasi atau diurutkan |
| Error | Pesan jelas + tombol “Coba lagi” |
“`jsx
if (loading) return
Sedang memuat…
;
if (error) return
Ups, ada masalah: {error}
;
if (!data || data.length === 0) return
Belum ada data tersedia.
;
return (
-
{data.map(item => (
- {item.title}
))}
);
“`
Rekomendasi Praktik Terbaik Mengoptimalkan useEffect agar Aplikasi Tetap Cepat dan Terukur
Supaya efek tetap terasa “ringan”, kuncinya ada di cara kita menulis dependensi dan memecah logika. Hindari memasukkan objek atau fungsi yang selalu berubah ke dalam array dependensi tanpa memikirkan konsekuensinya, karena itu bisa memicu render ulang terus-menerus. Sebisa mungkin, gunakan memoization dengan useCallback atau useMemo untuk fungsi dan nilai turunan yang dipakai di dalam efek. Selain itu, jangan satukan semua hal ke dalam satu efek besar yang melakukan banyak tugas sekaligus; pecah jadi beberapa useEffect yang masing-masing mengurus satu tanggung jawab, misalnya: satu untuk sinkronisasi URL, satu untuk event listener, dan satu khusus untuk fetch data.
- Pastikan dependensi se-spesifik mungkin, hindari memasukkan state/global yang tidak relevan.
- Batasi efek yang berat (misalnya perhitungan besar atau fetch berantai), pakai debouncing/throttling bila perlu.
- Bersihkan efek dengan rapi menggunakan fungsi cleanup untuk event listener, timer, dan subscription.
- Gunakan library data fetching (seperti React Query atau SWR) ketika kebutuhan mulai kompleks, supaya efek tidak jadi “tempat sampah” semua side effect.
| Kasus | Pola useEffect |
|---|---|
| Fetch data sekali di awal | useEffect dengan dependensi kosong [] |
| Sync dengan props tertentu | Dependensi hanya berisi props terkait |
| Listener scroll/resize | Cleanup wajib untuk remove listener |
| Hitung ulang mahal | Gabungkan dengan useMemo di luar efek |
Untuk aplikasi yang makin besar, cobalah mulai menerapkan pattern ekstraksi efek menjadi custom hook. Alih-alih menulis logic fetch di tiap komponen, misalnya, buat useFetchPosts atau useUserDetail sehingga komponen tetap ramping dan enak dipahami. Cara ini bukan cuma bikin kinerja lebih mudah diawasi, tapi juga mempermudah pengukuran performa; kalau ada lag, kamu tinggal cek di hook tertentu, bukan bongkar satu komponen raksasa. Di sisi lain, biasakan memantau efek yang aktif dengan bantuan DevTools dan warning ESLint khusus hooks, karena sering kali “pelakunya” adalah efek yang tidak sengaja terpanggil berkali-kali akibat dependensi yang keliru.
The Way Forward
Pada akhirnya, useEffect bukanlah “mantra ajaib” yang harus selalu dipakai, tetapi lebih seperti remote control yang mengatur kapan dan bagaimana efek samping dijalankan. Mulai dari sekadar mengubah judul halaman, menyetel event listener, hingga melakukan fetch data dari API-semua bisa diorkestrasi rapi jika kita memahami pola kerjanya.
Ketika Anda sudah nyaman dengan tiga pola dasarnya-tanpa dependency array, dengan dependency tertentu, dan hanya sekali jalan-maka mengelola side effect bukan lagi terasa kacau, melainkan terprediksi. Dari situ, langkah berikutnya adalah mulai bereksperimen: memecah efek yang terlalu gemuk, menambahkan cleanup yang rapi, dan menggabungkannya dengan custom hook agar logika fetching bisa dipakai ulang.
Jadi, lain kali Anda butuh mengambil data dari API, menyinkronkan state dengan dunia luar, atau sekadar “ngoprek” perilaku komponen setelah render, ingatlah: useEffect adalah pusat kendalinya. Semakin Anda paham karakter dan batasannya, semakin luwes pula Anda membangun aplikasi React yang tangguh, rapi, dan mudah dirawat-tanpa tenggelam dalam lautan side effect yang tak terkontrol.

