Pas pelajaran sebelumnya kamu udah paham useEffect buat side effect. Tapi ada KILLER feature yang sering kelewat: cleanup function. Bayangin user buka 5 modal beruntun, tiap modal pasang event listener keydown (deteksi ESC). Tanpa cleanup, listener nempel terus walaupun modal udah ketutup — 5 modal = 5 listener nyangkut, klik ESC sekali jalan 5 kali. Memory leak. Performance drop. Bug aneh muncul. Cleanup function = janji ke React: "kalau effect ini gak relevan lagi, jalanin instruksi ini buat bersihin diri".
💡 Aturan emas. Tiap effect yang BIKIN sesuatu yang persist di luar component (timer, listener, subscription, animation, koneksi WebSocket) WAJIB punya cleanup. Kalau gak, kamu lagi bikin memory leak yang bom waktu — gak kelihatan di dev, nyebelin di production.
Cleanup Function
Return dari effect = cleanup function. React panggil saat:
- Component unmount (dilepas dari DOM)
- Sebelum effect re-run (deps berubah, effect lama harus cleanup dulu)
function Timer() {
const [seconds, setSeconds] = useState(0)
useEffect(() => {
const id = setInterval(() => {
setSeconds(s => s + 1)
}, 1000)
return () => clearInterval(id) // ← cleanup
}, [])
return <p>{seconds} detik</p>
}
Lifecycle Pattern
useEffect(() => {
// SETUP — jalan saat mount atau deps berubah
console.log("Setup")
const subscription = subscribeToWebSocket()
return () => {
// CLEANUP — jalan saat unmount atau sebelum re-run
console.log("Cleanup")
subscription.close()
}
}, [url]) // re-run kalau url berubah → cleanup lama dulu, baru setup baru
Race Condition di Fetch — Pattern Wajib
Bug klasik: fetch user 1 → user pindah cepat ke user 2 → fetch user 2. Response user 1 dateng BELAKANGAN → setUser(user1) override user2. UI nampilin user 1 padahal harusnya user 2. Solusi: cancellation flag.
function UserProfile({ userId }) {
const [user, setUser] = useState(null)
useEffect(() => {
let cancelled = false
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(data => {
if (!cancelled) setUser(data)
})
return () => { cancelled = true }
}, [userId])
if (!user) return <p>Loading...</p>
return <h2>{user.name}</h2>
}
Modern: AbortController
useEffect(() => {
const controller = new AbortController()
fetch(`/api/users/${userId}`, { signal: controller.signal })
.then(res => res.json())
.then(setUser)
.catch(err => {
if (err.name !== "AbortError") console.error(err)
})
return () => controller.abort()
}, [userId])
Subscribe to Window Event
useEffect(() => {
const handleResize = () => setWidth(window.innerWidth)
window.addEventListener("resize", handleResize)
return () => window.removeEventListener("resize", handleResize)
}, [])
🎭 Analogi sehari-hari
Cleanup itu kayak bayar listrik kamar kost. Tiap bulan kamu nyewa kamar (effect setup) — pasang lampu, AC, kulkas. Pas pindah keluar (component unmount), kamu WAJIB matiin semua, buang sampah, balikin kunci (cleanup). Kalau gak — listrik tetep jalan di kamar kosong, sewa numpuk gak kebayar, pemilik kost-nya rugi. React pemilik kost yang strict — tiap effect yang setup sesuatu, WAJIB ada janji buat cleanup. Kalau lupa, bug-nya gak immediately kelihatan tapi efeknya numpuk: memory bocor, performa drop, listener listener berlapis-lapis.
⚠️ Jebakan yang sering ditemui
- Lupa cleanup di setInterval/setTimeout — interval jalan terus walau component unmount. Wajib
clearInterval(). - Lupa removeEventListener — listener numpuk tiap mount, jadi 10x listener pas re-mount 10x. Wajib remove di cleanup.
- Race condition di fetch — request lambat jawabnya belakangan, override state baru. Pakai
cancelledflag atauAbortController. - Cleanup mengakses STALE state —
return () => console.log(count)log nilai count saat effect setup, bukan saat unmount. Untuk akses state terkini di cleanup, pakaiuseRef. - Async function di useEffect langsung —
useEffect(async () => {})ERROR (effect harus return cleanup, bukan Promise). Bungkus inner:useEffect(() => { (async () => {...})() }, []). - Strict Mode double-invoke — di dev, useEffect jalan 2x dengan cleanup di tengah (deteksi cleanup bug). Bukan bug, fitur. Pastiin cleanup proper.
- Subscribe ke event di setiap render — effect tanpa deps array = re-subscribe tiap render. Add deps
[]atau yang bener.
🎯 Punya cleanup atau enggak?
setInterval/setTimeout→ wajib clearInterval/clearTimeoutaddEventListener(window/document/element) → wajib removeEventListener- WebSocket / EventSource → wajib
.close()- Fetch → AbortController atau cancelled flag
- Subscribe to library (Firebase, Pusher) → call
.unsubscribe()yang library kasih- Update document.title / DOM → biasanya GAK perlu cleanup (browser auto-revert next setup)
- Log/console → gak butuh cleanup
Aturan: kalau effect setup sesuatu yang HIDUP di luar React (browser API, network, library), WAJIB cleanup.
TL;DR: Cleanup function = return dari useEffect, jalan saat unmount atau sebelum effect re-run. Wajib buat setInterval/setTimeout/addEventListener/WebSocket/fetch (race condition). Pakai AbortController modern atau cancelled flag untuk fetch. Strict Mode dev jalanin effect 2x — bukan bug, deteksi cleanup proper. Async langsung di useEffect = ERROR (return cleanup, bukan Promise). Modern alternatif: TanStack Query handle race condition + cancellation otomatis.