Kenapa ini penting
Kamu sudah sering menulis useEffect, tapi mungkin masih menganggapnya sebagai "kode yang jalan setelah render". Untuk menulis effect yang benar, apalagi yang terhubung ke sistem luar seperti WebSocket, timer, library peta, atau API browser, kamu perlu gambaran utuh siklus hidup komponen: kapan ia muncul (mount), berubah (update), dan hilang (unmount). Kamu juga perlu tahu kenapa di development effect-mu terlihat berjalan dua kali, dan kapan harus memakai useLayoutEffect ketimbang useEffect. Banyak bug sulit di aplikasi React (memory leak, koneksi ganda, tampilan berkedip) berasal dari pemahaman siklus yang kurang tepat.
Konsep inti
Mount, update, unmount dengan hooks
| Fase | Artinya | Dengan hooks |
|---|---|---|
| Mount | Komponen pertama kali muncul di layar | Effect dijalankan setelah render pertama |
| Update | Props/state berubah, komponen render ulang | Jika dependency berubah: cleanup effect lama dijalankan, lalu effect baru |
| Unmount | Komponen dihapus dari layar | Cleanup effect terakhir dijalankan |
Dokumentasi React menyarankan cara berpikir yang lebih tepat: jangan memikirkan effect dari sudut pandang siklus komponen, tetapi dari sudut pandang sinkronisasi. Setiap effect menjawab dua pertanyaan: bagaimana mulai sinkron (setup) dan bagaimana berhenti sinkron (cleanup). React akan menjalankan setup dan cleanup sesering yang diperlukan agar sistem luar selalu cocok dengan props/state terbaru.
Contoh: effect koneksi chat dengan dependency [roomId]. Saat mount, terhubung ke room "umum". Saat roomId berubah ke "kerja", React memutus koneksi "umum" (cleanup) lalu terhubung ke "kerja" (setup). Saat unmount, putuskan koneksi "kerja".
Analogi: effect itu seperti menyalakan keran air untuk mengisi ember tertentu. Cleanup adalah mematikan keran. Kalau ember diganti, kamu matikan keran ke ember lama sebelum mengarahkan ke ember baru. Lupa mematikan = air terbuang (memory leak).
StrictMode: render dan effect dua kali di development
Template Vite membungkus aplikasi dengan <StrictMode>. Menurut dokumentasi React, di development saja, StrictMode:
- Me-render komponen satu kali ekstra untuk menemukan render yang tidak murni (misalnya mengubah array props).
- Menjalankan satu siklus setup + cleanup ekstra untuk setiap effect, untuk menemukan effect yang lupa cleanup.
- Menjalankan siklus ekstra serupa untuk ref callback.
- Memeriksa pemakaian API yang sudah usang.
Semua pemeriksaan ini tidak berlaku di build production. Jadi kalau kamu melihat console.log di effect muncul dua kali atau request terkirim dua kali di development, itu wajar. Jangan "memperbaikinya" dengan menghapus StrictMode atau membuat flag sudahJalan di useRef. Perbaiki dengan menulis cleanup yang benar: kalau setup → cleanup → setup menghasilkan kondisi sama dengan sekali setup, effect kamu benar.
useEffect vs useLayoutEffect
Urutan kerja React setelah state berubah: render (memanggil komponen) → commit (mengubah DOM) → browser paint (menggambar ke layar).
useEffectberjalan setelah browser menggambar. Tidak memblokir tampilan. Pilihan default untuk hampir semua kasus: fetch, subscription, timer, logging.useLayoutEffectberjalan setelah DOM diubah tapi sebelum browser menggambar. Kode di dalamnya memblokir paint. Gunakan hanya ketika kamu perlu mengukur DOM (ukuran, posisi) lalu langsung mengubah tampilan berdasarkan hasilnya, supaya user tidak melihat "kedipan" posisi yang salah. Contoh klasik: tooltip yang harus pindah ke bawah jika tidak muat di atas.
Karena useLayoutEffect memblokir paint, pemakaian berlebihan membuat aplikasi terasa lambat. Mulai dengan useEffect, pindah ke useLayoutEffect hanya jika ada kedipan yang terlihat.
Sinkronisasi dengan sistem luar
"Sistem luar" adalah apa pun yang tidak dikendalikan React: window event, setInterval, WebSocket, document.title, widget library non-React (peta, chart), elemen <video>. Polanya selalu sama:
- Setup di effect (pasang listener, buat koneksi, mulai timer).
- Kembalikan cleanup yang membalik setup (lepas listener, tutup koneksi, hentikan timer).
- Isi dependency array dengan semua nilai reaktif (props/state) yang dipakai.
Sebaliknya, jangan pakai effect untuk hal yang bukan sinkronisasi dengan sistem luar: menghitung data turunan (hitung saja saat render, modul 3) atau merespons klik user (taruh di event handler).
Contoh kode
Melihat urutan mount, update, unmount di console:
import { useEffect, useState } from 'react';
function Jam({ zona }) {
const [waktu, setWaktu] = useState(() => new Date());
useEffect(() => {
console.log(`setup: mulai timer (${zona})`);
const id = setInterval(() => setWaktu(new Date()), 1000);
return () => {
console.log(`cleanup: hentikan timer (${zona})`);
clearInterval(id);
};
}, [zona]);
return <p>{waktu.toLocaleTimeString('id-ID', { timeZone: zona })} ({zona})</p>;
}
export default function App() {
const [tampil, setTampil] = useState(true);
const [zona, setZona] = useState('Asia/Jakarta');
return (
<>
<button onClick={() => setTampil(!tampil)}>{tampil ? 'Sembunyikan' : 'Tampilkan'}</button>
<button onClick={() => setZona(zona === 'Asia/Jakarta' ? 'Asia/Jayapura' : 'Asia/Jakarta')}>
Ganti zona
</button>
{tampil && <Jam zona={zona} />}
</>
);
}
// Mount (development + StrictMode):
// → setup: mulai timer (Asia/Jakarta)
// → cleanup: hentikan timer (Asia/Jakarta) ← siklus ekstra StrictMode
// → setup: mulai timer (Asia/Jakarta)
// Klik "Ganti zona" (update):
// → cleanup: hentikan timer (Asia/Jakarta)
// → setup: mulai timer (Asia/Jayapura)
// Klik "Sembunyikan" (unmount):
// → cleanup: hentikan timer (Asia/Jayapura)
Sinkronisasi dengan API browser dan document.title:
import { useEffect, useState } from 'react';
export function StatusOnline() {
const [online, setOnline] = useState(() => navigator.onLine);
useEffect(() => {
const naik = () => setOnline(true);
const turun = () => setOnline(false);
window.addEventListener('online', naik);
window.addEventListener('offline', turun);
return () => {
window.removeEventListener('online', naik);
window.removeEventListener('offline', turun);
};
}, []);
useEffect(() => {
document.title = online ? 'Toko Saya' : '(Offline) Toko Saya';
}, [online]);
return <p>{online ? '🟢 Online' : '🔴 Offline'}</p>;
}
// matikan Wi-Fi → teks "🔴 Offline" dan judul tab "(Offline) Toko Saya"
useLayoutEffect untuk mengukur DOM sebelum paint (tooltip):
import { useLayoutEffect, useRef, useState } from 'react';
export function Tooltip({ teks, children }) {
const ref = useRef(null);
const [posisi, setPosisi] = useState('atas');
useLayoutEffect(() => {
const kotak = ref.current.getBoundingClientRect();
// kalau tooltip keluar dari atas layar, pindah ke bawah SEBELUM user melihatnya
if (kotak.top < 0) setPosisi('bawah');
}, []);
return (
<span style={{ position: 'relative' }}>
{children}
<span
ref={ref}
style={{
position: 'absolute',
left: 0,
[posisi === 'atas' ? 'bottom' : 'top']: '100%',
background: '#111',
color: '#fff',
padding: '4px 8px',
borderRadius: 4,
whiteSpace: 'nowrap',
}}
>
{teks}
</span>
</span>
);
}
// dengan useEffect, tooltip sempat terlihat "melompat" dari atas ke bawah; dengan useLayoutEffect tidak
Membungkus library non-React (contoh dengan API <dialog> bawaan browser):
import { useEffect, useRef } from 'react';
export function Dialog({ terbuka, onClose, children }) {
const ref = useRef(null);
// sinkronkan props "terbuka" (React) dengan API imperatif dialog (sistem luar)
useEffect(() => {
const dialog = ref.current;
if (terbuka) dialog.showModal();
else dialog.close();
}, [terbuka]);
return (
<dialog ref={ref} onClose={onClose}>
{children}
<button onClick={onClose}>Tutup</button>
</dialog>
);
}
Kesalahan umum
- Mematikan StrictMode karena effect jalan dua kali. Itu justru menyembunyikan bug cleanup. Tulis cleanup yang benar; di production effect hanya berjalan sekali per mount.
- Lupa cleanup. Listener, interval, dan koneksi yang tidak dilepas tetap hidup setelah komponen hilang, menyebabkan memory leak dan error "update state pada komponen yang sudah unmount".
- Memakai
useLayoutEffectsebagai default. Ia memblokir paint sehingga UI terasa lebih lambat. PakaiuseEffect, kecuali kamu perlu mengukur DOM sebelum tampil. - Memakai effect untuk data turunan atau event user.
useEffect(() => setTotal(a + b), [a, b])menambah render ekstra. Hitungconst total = a + bsaat render; respons klik taruh di handler. - Dependency array tidak jujur. Menghapus dependency agar effect "tidak berjalan ulang" membuat effect memakai nilai basi. Kalau effect berjalan terlalu sering, ubah kodenya (misalnya pakai updater function
setX((x) => ...)), bukan berbohong ke dependency array.
Latihan
- Mudah, Buat komponen
Stopwatchdengan tombol Mulai/Berhenti yang memakaisetIntervaldan cleanup yang benar. Pastikan di StrictMode angka tidak bertambah dua kali lebih cepat. - Sedang, Buat hook
useDocumentTitle(judul)yang mengubah judul tab dan mengembalikan judul sebelumnya saat komponen unmount. - Menantang, Buat komponen
KotakUkuranyang menampilkan lebar dan tinggi elemen<div>miliknya sendiri, dan ikut berubah saat jendela di-resize. GunakanResizeObserver(sistem luar) dengan cleanupdisconnect().
Lihat solusi
// 1.
import { useEffect, useState } from 'react';
export function Stopwatch() {
const [detik, setDetik] = useState(0);
const [jalan, setJalan] = useState(false);
useEffect(() => {
if (!jalan) return;
const id = setInterval(() => setDetik((d) => d + 1), 1000);
return () => clearInterval(id); // StrictMode: interval ekstra langsung dibersihkan
}, [jalan]);
return (
<>
<p>{detik} detik</p>
<button onClick={() => setJalan(!jalan)}>{jalan ? 'Berhenti' : 'Mulai'}</button>
</>
);
}
// 2.
import { useEffect } from 'react';
export function useDocumentTitle(judul) {
useEffect(() => {
const sebelumnya = document.title;
document.title = judul;
return () => {
document.title = sebelumnya;
};
}, [judul]);
}
// useDocumentTitle('Keranjang (3)') → tab berjudul "Keranjang (3)"; unmount → judul kembali
// 3.
import { useEffect, useRef, useState } from 'react';
export function KotakUkuran() {
const ref = useRef(null);
const [ukuran, setUkuran] = useState({ w: 0, h: 0 });
useEffect(() => {
const observer = new ResizeObserver(([entry]) => {
const { width, height } = entry.contentRect;
setUkuran({ w: Math.round(width), h: Math.round(height) });
});
observer.observe(ref.current);
return () => observer.disconnect();
}, []);
return (
<div ref={ref} style={{ width: '50%', padding: 16, border: '1px solid #ccc' }}>
{ukuran.w} × {ukuran.h} px
</div>
);
}
// resize jendela → angka lebar berubah mengikuti
Ringkasan
- Mount menjalankan setup effect, update menjalankan cleanup lama lalu setup baru, unmount menjalankan cleanup terakhir.
- Pikirkan effect sebagai sinkronisasi dengan sistem luar: setup untuk mulai, cleanup untuk berhenti.
- StrictMode di development me-render ekstra dan menjalankan satu siklus setup+cleanup ekstra; perbaiki dengan cleanup yang benar, bukan dengan mematikannya.
useEffectberjalan setelah paint (default),useLayoutEffectsebelum paint untuk mengukur DOM tanpa kedipan.- Jangan memakai effect untuk data turunan atau respons event user.