Sejauh ini di series React dari Nol kita sudah bisa bikin komponen, mengatur state, dan menangani event. Tapi ada satu hal yang belum bisa dilakukan aplikasi kita: ngobrol dengan dunia luar. Data cuaca, daftar produk, postingan blog, semuanya tinggal di server orang lain, dan komponen React butuh cara resmi untuk mengambilnya. Di episode #54 ini kita kenalan dengan useEffect, hook yang jadi jembatan antara komponenmu dan segala sesuatu di luar React, terutama API.
Posisinya jelas: ini episode keempat dari lima di series React dari Nol. Episode sebelumnya (#53) kita belajar event handling dan form, dan episode berikutnya (#55) kita tutup series ini dengan proyek aplikasi catatan. Jadi anggap artikel ini sebagai bekal terakhir sebelum kamu bikin aplikasi beneran.
Kenapa Komponen Butuh yang Namanya Efek Samping
Bayangkan kamu punya asisten rumah tangga. Tugas utamanya rapi dan jelas: masak, beres-beres, jaga rumah. Tapi sesekali dia harus keluar rumah: beli gas, bayar listrik, ambil paket. Pekerjaan "keluar rumah" itu beda sifatnya dengan pekerjaan di dalam rumah. Di React, komponen itu seperti asisten tersebut. Tugas utamanya adalah render UI dari props dan state, sesuatu yang murni dan bisa diprediksi. Mengambil data dari API, menyalakan timer, atau subscribe ke WebSocket adalah "pekerjaan keluar rumah": efek samping.
Kenapa harus dipisah? Karena render itu bisa terjadi berkali-kali. React me-render ulang komponen setiap kali state berubah. Kalau kode fetch API kamu taruh langsung di badan komponen, setiap render akan memicu fetch baru. Fetch mengubah state, state memicu render, render memicu fetch lagi. Itu resep infinite loop yang bikin browser hang. useEffect ada untuk memberi tahu React: "kode ini efek samping, jalankan di waktu yang tepat, bukan setiap render".
Jadi aturan berpikirnya begini. Badan komponen = hitung apa yang tampil di layar. useEffect = sinkronisasi dengan sistem di luar React. Kalau kamu bisa menjawab "kapan kode ini harus jalan?" dengan "setiap kali nilai X berubah" atau "sekali saat komponen muncul", itu kandidat useEffect.
Persepsi yang Perlu Diluruskan: useEffect Bukan Tempat Sampah Logika
Mitos yang paling sering menjangkiti pemula: semua logika yang "agak ribet" dilempar ke useEffect. Padahal tidak. Ada tiga hal yang sering salah ditaruh di useEffect.
Pertama, menghitung data turunan. Kalau kamu punya firstName dan lastName di state, lalu bikin useEffect untuk menggabungkannya jadi fullName dan menyimpannya di state lain, itu salah. Hitung saja langsung saat render: const fullName = firstName + ' ' + lastName. Tidak perlu effect, tidak perlu state tambahan.
Kedua, merespons aksi user. Tombol diklik lalu mau kirim data ke server? Taruh di event handler onClick, bukan di useEffect yang mengawasi state. Effect bukan untuk "kalau user melakukan X", tapi untuk "sinkronkan dengan sistem luar".
Ketiga, mengubah state lain setiap kali sebuah state berubah. Itu rantai effect yang rapuh dan susah di-debug. Kalau B bisa dihitung dari A, hitung saat render.
Ingat kalimat kuncinya: useEffect itu untuk sinkronisasi dengan sistem eksternal, DOM, network, timer, subscription. Bukan untuk logika aplikasi biasa.
Anatomi useEffect: Setup, Cleanup, dan Dependency Array
Bentuk dasarnya begini:
useEffect(() => {
// setup: kode efek samping jalan di sini
return () => {
// cleanup: beres-beres di sini
};
}, [nilaiYangDiawasi]);
Ada tiga bagian. Setup adalah kode yang dijalankan. Cleanup adalah fungsi yang kamu return, dijalankan React sebelum effect berjalan ulang dan saat komponen dihapus dari layar. Dependency array adalah daftar nilai yang kalau berubah, effect dijalankan ulang.
Analoginya seperti menyewa kamar kos. Setup = kamu masuk dan menata barang. Dependency array = daftar kondisi yang kalau berubah kamu harus pindah kamar, misalnya pindah kerja ke kota lain. Cleanup = sebelum pindah, kamu bereskan kamar lama supaya tidak berantakan dan tidak dobel bayar. Tanpa cleanup, setiap kali effect jalan ulang, sisa effect lama menumpuk: timer dobel, subscription dobel, request dobel.
Aturan dependency array: kosongkan ([]) artinya jalan sekali saat komponen muncul. Isi dengan nilai ([userId]) artinya jalan ulang setiap nilai itu berubah. Hilangkan arraynya sama sekali artinya jalan setiap render, hampir tidak pernah kamu mau.
Berhenti dan tebak dulu! Kode ini jalan berapa kali saat komponen pertama kali muncul di mode development?
useEffect(() => {
console.log('effect jalan');
}, []);
Praktik: Mengambil Data dari API dengan Fetch
Sekarang bagian intinya. Kita ambil daftar user dari API gratis JSONPlaceholder yang tidak butuh API key. Pola yang benar selalu melacak tiga state: data, loading, dan error. Kenapa tiga? Karena request network punya tiga fase yang harus dirender: sedang menunggu, berhasil, dan gagal. Kalau kamu cuma siapkan state data, user tidak tahu aplikasimu sedang loading atau error.
import { useState, useEffect } from 'react';
function DaftarUser() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const controller = new AbortController();
fetch('https://jsonplaceholder.typicode.com/users', {
signal: controller.signal
})
.then((res) => {
if (!res.ok) throw new Error('Gagal mengambil data');
return res.json();
})
.then((data) => setUsers(data))
.catch((err) => {
if (err.name !== 'AbortError') setError(err.message);
})
.finally(() => setLoading(false));
return () => controller.abort();
}, []);
if (loading) return <p>Memuat data...</p>;
if (error) return <p>Error: {error}</p>;
return (
<ul>
{users.map((u) => <li key={u.id}>{u.name}</li>)}
</ul>
);
}
Mari bedah alur berpikirnya baris per baris. Pertama, kenapa fetch dibungkus AbortController? Bayangkan user membuka halaman daftar user lalu langsung pindah halaman sebelum request selesai. Komponen sudah tidak ada di layar, tapi request masih jalan, dan saat respons datang, setUsers dipanggil untuk komponen yang sudah mati. Itu kebocoran. Cleanup controller.abort() membatalkan request yang tidak dibutuhkan lagi.
Kedua, kenapa ada pengecekan err.name !== 'AbortError'? Karena pembatalan yang kita lakukan sendiri akan me-reject promise dengan error bernama AbortError. Itu bukan kegagalan beneran, jadi jangan tampilkan ke user sebagai error.
Ketiga, kenapa if (!res.ok) throw? Ini jebakan klasik: fetch TIDAK menganggap status 404 atau 500 sebagai error. Selama server merespons, fetch anggap sukses. Jadi kamu harus cek sendiri.
Keempat, kenapa effect tidak boleh async langsung? Karena useEffect mengharapkan return berupa fungsi cleanup atau undefined. Fungsi async me-return promise, bukan fungsi. Solusinya: definisikan fungsi async di dalam effect lalu panggil, atau pakai .then() seperti contoh di atas.
Berhenti dan tebak dulu! Apa yang terjadi kalau dependency array di kode ini diisi [users]?
useEffect(() => {
fetch('https://jsonplaceholder.typicode.com/users')
.then((res) => res.json())
.then((data) => setUsers(data));
}, [users]);
Jebakan Klasik: Race Condition Saat Refetch
Ada bug yang lebih licik dari infinite loop: race condition. Misal kamu bikin fitur pilih kategori produk, dan setiap ganti kategori, effect fetch ulang dengan dependency [kategori]. User mengklik kategori A, lalu cepat mengklik kategori B. Dua request melayang. Request B (yang baru) selesai duluan, request A (yang lama) selesai belakangan dan menimpa state dengan data kategori A. Layar menampilkan kategori B tapi datanya kategori A.
Di sinilah cleanup AbortController bersinar dua kali. Setiap kali kategori berubah, React menjalankan cleanup effect lama dulu sebelum setup yang baru. Cleanup membatalkan request A, sehingga respons basi tidak pernah sampai ke state. Polanya persis sama seperti contoh fetch di atas, hanya dependency array-nya berisi [kategori] dan URL-nya memakai kategori tersebut. Satu pola, menyelesaikan dua masalah: kebocoran saat unmount dan data basi saat refetch.
Analogi sehari-harinya: kamu pesan ojek online ke alamat A, lalu berubah pikiran dan pesan lagi ke alamat B. Driver pertama harus dibatalkan, kalau tidak, dua driver datang dan kamu bingung naik yang mana. Cleanup adalah tombol "batalkan order lama".
Kuis cepat: Kapan kamu WAJIB menulis cleanup function di useEffect?
Kenapa Effect Jalan Dua Kali di Development
Hampir semua pemula React pernah panik melihat console.log di effect tercetak dua kali. Tenang, itu bukan bug kodemu. Di React 18 dan 19, kalau aplikasimu dibungkus <StrictMode> (bawaan template Vite), React sengaja menjalankan effect dengan urutan setup, cleanup, lalu setup lagi, khusus di mode development.
Tujuannya mulia: mensimulasikan apa yang memang terjadi di production saat user navigasi bolak-balik. Komponen di-mount, di-unmount, lalu di-mount lagi. Kalau effect-mu tidak tahan diperlakukan begitu secara simetris, artinya ada bug yang suatu saat muncul di production dalam kondisi timing yang langka dan susah di-debug. StrictMode memaksa bug itu muncul sejak hari pertama, saat masih gampang diperbaiki.
Kesalahan respons yang umum adalah memasang "pengaman" ref supaya effect cuma jalan sekali. Jangan. Itu menyembunyikan bug, bukan memperbaikinya. Respons yang benar: pastikan setiap setup punya cleanup yang benar-benar membatalkan apa yang dilakukan setup. Kalau fetch-mu memakai AbortController di cleanup, double-invoke tidak menimbulkan masalah apa pun: request pertama dibatalkan, request kedua jalan normal.
Latihan dan Langkah Berikutnya
Teori tanpa praktik cepat menguap. Kerjakan latihan ini, semuanya meminta kamu mengubah atau menambah kode, bukan mengetik ulang.
Latihan 1. Ubah contoh DaftarUser di atas menjadi aplikasi daftar produk memakai https://fakestoreapi.com/products. Tambahkan state kategori dan dropdown select berisi beberapa kategori. Fetch ulang setiap kategori berubah (dependency array berisi kategori), dan pastikan cleanup AbortController tetap ada supaya tidak kena race condition.
Latihan 2. Tambahkan fitur pencarian: input teks yang memfilter produk yang sudah di-fetch. Pertanyaan jebakan: apakah filter ini butuh useEffect dan state baru? (Jawaban yang benar: tidak. Hitung langsung saat render dari state produk dan state keyword.)
Latihan 3. Sengaja buat bug: hapus cleanup AbortController, lalu buka-tutup komponen dengan cepat (misal pakai tombol toggle). Perhatikan warning atau perilaku aneh di console. Lalu kembalikan cleanup-nya. Merasakan langsung bug-nya bikin pelajaran menempel sepuluh kali lebih kuat.
Di episode berikutnya, #55, kita menutup series React dari Nol dengan proyek beneran: aplikasi catatan lengkap dengan tambah, hapus, cari, dan penyimpanan permanen memakai localStorage. Semua yang kita pelajari dari #50 sampai #54 akan dipakai di sana. Sampai jumpa di proyek penutup.
Diskusi & Komentar 0
Punya tanggapan, pertanyaan, atau tips seputar tutorial ini? Tulis komentar kamu di bawah!
Jadilah yang pertama membuka obrolan dan berbagi pandangan seputar tutorial resep kode ini.
Tulis Komentar
Komentar menggunakan akun Google. Harap berkomentar dengan santun dan berbobot.