Cara Menghindari Memory Leak di React useEffect
Cara menghindari memory leak di React useEffect adalah pengetahuan krusial yang sering diabaikan oleh developer frontend. Kamu mungkin pernah melihat warning kuning di console: “Can’t perform a React state update on an unmounted component.” Pesan ini bukan sekadar peringatan kosong - ini sinyal bahwa aplikasimu punya memory leak yang bisa bikin performa makin lambat seiring waktu.
Artikel ini akan membahas secara praktis kenapa memory leak terjadi di useEffect, bagaimana cara mendeteksinya, dan pattern-pattern cleanup yang wajib kamu terapkan di setiap project React.
Kenapa Memory Leak Terjadi di useEffect?
React component punya lifecycle: mount, update, dan unmount. Masalah muncul ketika kamu memulai operasi asynchronous di useEffect - misalnya fetch API, setTimeout, atau subscribe ke event - tapi component keburu unmount sebelum operasi itu selesai.
Ketika operasi async akhirnya selesai dan mencoba setState, React bingung karena component-nya sudah tidak ada di DOM. State update tetap terjadi di memori, tapi tidak punya tujuan. Ini yang disebut memory leak.
Bayangkan skenario nyata: user buka halaman detail produk, data sedang di-fetch dari API, tapi user langsung klik tombol back. Fetch masih jalan di background, dan ketika response datang, dia mencoba update state component yang sudah unmount.
Contoh Kasus Memory Leak yang Sering Terjadi
Fetch API Tanpa Cleanup
Ini adalah penyebab memory leak paling umum di React:
// ❌ Bermasalah: tidak ada cleanup
useEffect(() => {
fetch(`/api/products/${id}`)
.then(res => res.json())
.then(data => setProduct(data));
}, [id]);
Kalau component unmount sebelum fetch selesai, setProduct tetap dipanggil. Tidak ada mekanisme untuk membatalkan request.
setTimeout dan setInterval
// ❌ Bermasalah: timer tetap jalan setelah unmount
useEffect(() => {
const timer = setTimeout(() => {
setShowBanner(true);
}, 5000);
}, []);
Timer 5 detik tetap berjalan meskipun user sudah pindah halaman. Ketika timer selesai, dia akan mencoba update state component yang sudah tidak ada.
Event Listener
// ❌ Bermasalah: listener menumpuk
useEffect(() => {
window.addEventListener('resize', handleResize);
}, []);
Setiap kali component re-mount, listener baru ditambahkan tanpa menghapus yang lama. Hasilnya: listener menumpuk dan handleResize dipanggil berkali-kali untuk satu event resize.
Cara Cleanup yang Benar
Pattern 1: Cleanup Function di useEffect
useEffect bisa return sebuah function yang akan dipanggil React saat component unmount atau sebelum effect dijalankan ulang. Ini adalah mekanisme utama untuk mencegah memory leak.
// ✅ Timer dengan cleanup
useEffect(() => {
const timer = setTimeout(() => {
setShowBanner(true);
}, 5000);
return () => clearTimeout(timer);
}, []);
// ✅ Event listener dengan cleanup
useEffect(() => {
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
Sederhana dan efektif. Ketika component unmount, React otomatis memanggil cleanup function, timer dibatalkan, dan listener dihapus.
Pattern 2: AbortController untuk Fetch
Untuk membatalkan HTTP request, gunakan AbortController - API bawaan browser yang didukung semua browser modern:
useEffect(() => {
const controller = new AbortController();
fetch(`/api/products/${id}`, { signal: controller.signal })
.then(res => res.json())
.then(data => setProduct(data))
.catch(err => {
if (err.name !== 'AbortError') {
setError(err.message);
}
});
return () => controller.abort();
}, [id]);
Ketika component unmount atau id berubah, controller.abort() dipanggil. Fetch request langsung dibatalkan, dan error yang muncul bertipe AbortError - kita filter supaya tidak dianggap error sungguhan.
Pattern 3: Flag Boolean untuk Async Function
Kalau kamu tidak bisa pakai AbortController (misalnya operasi async bukan fetch), gunakan flag boolean:
useEffect(() => {
let isMounted = true;
async function loadData() {
const result = await someAsyncOperation();
if (isMounted) {
setData(result);
}
}
loadData();
return () => {
isMounted = false;
};
}, []);
Flag isMounted dicek sebelum setState. Kalau component sudah unmount, state update di-skip. Ini bukan cara paling ideal karena operasi async-nya tetap jalan sampai selesai, tapi setidaknya mencegah state update pada component yang sudah unmount.
Pattern 4: Cleanup Subscription (WebSocket, Firebase, dll)
Untuk real-time subscription, pattern cleanup sangat penting:
useEffect(() => {
const unsubscribe = onSnapshot(
doc(db, 'orders', orderId),
(snapshot) => {
setOrder(snapshot.data());
}
);
return () => unsubscribe();
}, [orderId]);
useEffect(() => {
const ws = new WebSocket('wss://api.example.com/live');
ws.onmessage = (event) => {
setMessages(prev => [...prev, JSON.parse(event.data)]);
};
return () => ws.close();
}, []);
Tanpa cleanup, setiap kali component re-mount akan membuat koneksi WebSocket baru tanpa menutup yang lama. Dalam hitungan menit, kamu bisa punya puluhan koneksi zombie yang memakan resource.
Mendeteksi Memory Leak
React StrictMode
Di React 18+, StrictMode sengaja mount-unmount-mount component di development mode. Ini membantu mendeteksi effect yang tidak punya cleanup:
// main.jsx
createRoot(document.getElementById('root')).render(
<StrictMode>
<App />
</StrictMode>
);
Kalau kamu lihat fetch dipanggil dua kali di development, itu bukan bug - itu StrictMode membantu memverifikasi cleanup kamu berfungsi.
Chrome DevTools Memory Tab
Untuk kasus yang lebih kompleks, gunakan Memory tab di Chrome DevTools:
- Buka DevTools → Memory tab
- Ambil heap snapshot sebelum navigasi
- Navigasi ke halaman lain, lalu kembali
- Ambil heap snapshot lagi
- Bandingkan kedua snapshot - cari object yang seharusnya sudah di-garbage-collect
Console Warning
React akan menampilkan warning di console kalau ada state update pada unmounted component. Jangan abaikan warning ini - setiap warning adalah potensi memory leak.
Custom Hook untuk Reusability
Kalau kamu sering fetch data, buat custom hook yang sudah include cleanup:
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const controller = new AbortController();
setLoading(true);
fetch(url, { signal: controller.signal })
.then(res => {
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
})
.then(data => {
setData(data);
setLoading(false);
})
.catch(err => {
if (err.name !== 'AbortError') {
setError(err.message);
setLoading(false);
}
});
return () => controller.abort();
}, [url]);
return { data, loading, error };
}
Penggunaan:
function ProductPage({ id }) {
const { data, loading, error } = useFetch(`/api/products/${id}`);
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error}</p>;
return <h1>{data.name}</h1>;
}
Semua logic cleanup sudah ter-encapsulate di dalam hook. Setiap component yang pakai useFetch otomatis terlindungi dari memory leak.
Checklist Anti Memory Leak
Sebelum push code, pastikan setiap useEffect yang kamu tulis sudah melewati checklist ini:
- Ada operasi async? Tambahkan AbortController atau flag boolean
- Ada setTimeout/setInterval? Return
clearTimeoutatauclearInterval - Ada addEventListener? Return
removeEventListenerdengan referensi function yang sama - Ada subscription? Return unsubscribe function
- Ada WebSocket? Return
ws.close()
Kalau useEffect kamu tidak return apa-apa dan di dalamnya ada salah satu dari operasi di atas, hampir pasti ada memory leak.
Kesimpulan
Memory leak di React useEffect bukan masalah yang bisa diabaikan. Di aplikasi kecil mungkin tidak terasa, tapi di aplikasi enterprise dengan puluhan component dan real-time data, efeknya bisa sangat terasa - halaman makin lambat, RAM makin tinggi, dan user experience menurun.
Kabar baiknya, solusinya konsisten dan mudah diingat: selalu return cleanup function di useEffect. Pakai AbortController untuk fetch, clearTimeout untuk timer, removeEventListener untuk event, dan unsubscribe untuk subscription.
Terapkan pattern-pattern di atas, aktifkan StrictMode, dan jadikan checklist anti memory leak sebagai bagian dari code review. Dengan begitu, aplikasi React kamu akan tetap ringan dan responsif meskipun dipakai berjam-jam.