Kenapa ini penting
Sejauh ini komponen kita murni: menerima props dan state, lalu mengembalikan JSX. Tapi aplikasi nyata harus "berbicara" dengan dunia di luar React: mengambil data dari API, mengubah judul tab browser, memasang timer, mendengarkan ukuran layar, atau menyambung ke server chat. Pekerjaan semacam ini disebut side effect (efek samping) karena terjadi di luar proses menghitung tampilan. Tempatnya bukan di badan komponen, melainkan di dalam hook useEffect. Sama pentingnya: tahu kapan tidak perlu effect, karena effect yang berlebihan adalah sumber bug dan render boros paling umum di proyek React.
Konsep inti
Apa itu useEffect
useEffect(() => {
// kode efek: berjalan SETELAH React menampilkan hasil render ke layar
return () => {
// cleanup (opsional): membersihkan efek sebelumnya
};
}, [dependensi]);
Dokumentasi react.dev menggambarkan effect sebagai cara menyinkronkan komponen dengan sistem eksternal. Analogi: AC dengan termostat. Kamu menyetel suhu (state); AC (effect) menyesuaikan diri dengan kondisi ruangan. Kalau setelan berubah, AC berhenti menyesuaikan setelan lama dan mulai menyesuaikan setelan baru.
Dependency array: kapan effect berjalan
| Bentuk | Kapan berjalan |
|---|---|
useEffect(fn) tanpa array |
Setelah setiap render |
useEffect(fn, []) |
Hanya setelah render pertama (saat komponen muncul/mount) |
useEffect(fn, [a, b]) |
Setelah render pertama, lalu setiap kali a atau b berubah |
Aturan penting: semua nilai reaktif (props, state, dan variabel/fungsi yang dihitung di dalam komponen) yang dipakai di dalam effect harus dicantumkan di dependency array. Kamu tidak "memilih" dependensi; kode effect-mu yang menentukannya. Linter React akan memperingatkan kalau ada yang terlewat.
React membandingkan setiap dependensi dengan nilai sebelumnya memakai Object.is. Objek dan array yang dibuat ulang setiap render selalu dianggap "berubah", hal yang akan kita bahas di modul berikutnya.
Cleanup
Fungsi yang di-return dari effect adalah cleanup. React memanggilnya:
- Sebelum menjalankan effect berikutnya (ketika dependensi berubah).
- Ketika komponen dilepas dari layar (unmount).
Cleanup wajib untuk effect yang "memasang" sesuatu: setInterval → clearInterval, addEventListener → removeEventListener, koneksi → putus koneksi. Tanpa cleanup, timer dan listener terus menumpuk (memory leak).
Ingat StrictMode: di development, React menjalankan effect → cleanup → effect lagi sekali saat mount. Kalau aplikasimu berperilaku aneh karena itu (misalnya interval berjalan dobel), artinya cleanup-mu belum benar. Itulah gunanya pemeriksaan tersebut.
Fetch di dalam effect
Mengambil data saat komponen muncul adalah penggunaan effect yang umum. Polanya: buat fungsi async di dalam effect lalu panggil, karena fungsi effect sendiri tidak boleh async (ia harus mengembalikan cleanup atau tidak sama sekali, bukan Promise). Kelola tiga keadaan: loading, error, dan data. Perlindungan dari race condition akan dibahas tuntas di modul berikutnya; di sini kita sudah memasang flag ignore sesuai pola di dokumentasi react.dev.
Catatan dari dokumentasi React: untuk aplikasi produksi, fetching langsung di effect punya keterbatasan (tidak ada cache, rawan "waterfall"). Framework seperti Next.js atau library seperti TanStack Query menyelesaikannya. Tapi memahami pola dasarnya tetap wajib.
Kapan TIDAK perlu effect
Dokumentasi react.dev punya halaman khusus "You Might Not Need an Effect". Intinya:
| Situasi | ❌ Pakai effect | ✅ Cara yang benar |
|---|---|---|
| Menghitung nilai dari state/props (nama lengkap, total harga, hasil filter) | useEffect(() => setTotal(...), [items]) |
Hitung langsung saat render: const total = items.reduce(...) |
| Merespons aksi pengguna (klik beli → kirim ke API) | Set state "harusKirim" lalu effect mengirim | Kirim langsung di event handler |
| Reset state saat prop berubah | Effect yang memanggil setX('') |
Beri komponen key yang berbeda |
Pertanyaan kunci: "Apakah kode ini berjalan karena komponen tampil, atau karena pengguna melakukan sesuatu?" Kalau karena aksi pengguna, tempatnya di event handler.
Contoh kode
// src/App.jsx, judul tab, timer dengan cleanup, listener ukuran layar, fetch data
import { useEffect, useState } from 'react';
function JudulTab({ jumlahPesan }) {
useEffect(() => {
// Sinkronkan judul tab dengan state
document.title = jumlahPesan > 0 ? `(${jumlahPesan}) Kotak Masuk` : 'Kotak Masuk';
}, [jumlahPesan]); // berjalan ulang setiap jumlahPesan berubah
return <p>Pesan belum dibaca: {jumlahPesan}</p>;
}
function Stopwatch() {
const [detik, setDetik] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setDetik(d => d + 1); // updater: tidak perlu detik di dependensi
}, 1000);
return () => clearInterval(id); // cleanup: hentikan timer saat unmount
}, []);
return <p>⏱️ {detik} detik</p>;
}
function LebarLayar() {
const [lebar, setLebar] = useState(window.innerWidth);
useEffect(() => {
function handleResize() {
setLebar(window.innerWidth);
}
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
return <p>Lebar layar: {lebar}px {lebar < 768 ? '(mobile)' : '(desktop)'}</p>;
}
function DaftarUser() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let ignore = false;
async function ambilData() {
try {
const res = await fetch('https://jsonplaceholder.typicode.com/users');
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
if (!ignore) setUsers(data);
} catch (err) {
if (!ignore) setError(err.message);
} finally {
if (!ignore) setLoading(false);
}
}
ambilData();
return () => {
ignore = true;
};
}, []);
if (loading) return <p>Memuat pengguna...</p>;
if (error) return <p style={{ color: 'crimson' }}>Gagal: {error}</p>;
return (
<ul>
{users.map(u => <li key={u.id}>{u.name} - {u.email}</li>)}
</ul>
);
}
// → Memuat pengguna... lalu 10 nama dari API
export default function App() {
const [tampilStopwatch, setTampilStopwatch] = useState(true);
const [pesan, setPesan] = useState(3);
return (
<>
<JudulTab jumlahPesan={pesan} />
<button onClick={() => setPesan(p => p + 1)}>Pesan masuk</button>
<button onClick={() => setTampilStopwatch(t => !t)}>Toggle stopwatch</button>
{tampilStopwatch && <Stopwatch />}
<LebarLayar />
<DaftarUser />
</>
);
}
Contoh effect yang tidak perlu dan perbaikannya:
import { useState } from 'react';
const KERANJANG = [
{ id: 1, nama: 'Kopi', harga: 85000, qty: 2 },
{ id: 2, nama: 'Teh', harga: 30000, qty: 1 },
];
// ❌ Effect berlebihan: render dua kali, state ganda
// const [total, setTotal] = useState(0);
// useEffect(() => { setTotal(items.reduce(...)); }, [items]);
// ✅ Hitung langsung saat render
export function Ringkasan() {
const [items] = useState(KERANJANG);
const total = items.reduce((sum, i) => sum + i.harga * i.qty, 0);
function handleCheckout() {
// ✅ Aksi pengguna → langsung di event handler, bukan di effect
console.log('Kirim pesanan senilai', total);
}
return (
<>
<p>Total: Rp {total.toLocaleString('id-ID')}</p> {/* → Total: Rp 200.000 */}
<button onClick={handleCheckout}>Checkout</button>
</>
);
}
Kesalahan umum
- Membuat fungsi effect
async.useEffect(async () => {...})mengembalikan Promise, bukan cleanup. Buat fungsi async di dalamnya lalu panggil. - Lupa cleanup untuk timer/listener. Interval berjalan dobel dan listener menumpuk. Selalu
returnfungsi pembersih. - Dependency array tidak lengkap. Effect membaca state lama (stale). Cantumkan semua nilai reaktif yang dipakai; jangan bungkam peringatan linter.
- Memakai effect untuk data turunan. Menghitung total, filter, atau format di effect + setState menyebabkan render ganda. Hitung saat render.
- Memakai effect untuk merespons klik. Logika karena aksi pengguna tempatnya di event handler.
Latihan
- Mudah, Buat komponen yang mengubah
document.titlemenjadi "Halo, {nama}" setiap kali input nama berubah. - Sedang, Buat komponen
JamDigitalyang menampilkan jam sekarang (toLocaleTimeString('id-ID')) dan diperbarui setiap detik, dengan cleanup yang benar. Tambahkan tombol untuk menyembunyikan komponen dan pastikan timer berhenti. - Menantang, Fetch daftar post dari
https://jsonplaceholder.typicode.com/posts?_limit=5dan tampilkan judulnya, lengkap dengan state loading dan error. Lalu ubah URL menjadi salah (misalnya/postsxx) untuk menguji pesan error.
Lihat solusi
1.
import { useEffect, useState } from 'react';
export default function JudulDinamis() {
const [nama, setNama] = useState('');
useEffect(() => {
document.title = nama ? `Halo, ${nama}` : 'Kodingup Camp';
}, [nama]);
return <input value={nama} onChange={e => setNama(e.target.value)} placeholder="Nama kamu" />;
}
2.
import { useEffect, useState } from 'react';
function JamDigital() {
const [waktu, setWaktu] = useState(() => new Date());
useEffect(() => {
const id = setInterval(() => setWaktu(new Date()), 1000);
return () => {
clearInterval(id);
console.log('Timer dihentikan'); // → muncul saat komponen disembunyikan
};
}, []);
return <h2>{waktu.toLocaleTimeString('id-ID')}</h2>;
}
export default function App() {
const [tampil, setTampil] = useState(true);
return (
<>
<button onClick={() => setTampil(t => !t)}>{tampil ? 'Sembunyikan' : 'Tampilkan'} jam</button>
{tampil && <JamDigital />}
</>
);
}
3.
import { useEffect, useState } from 'react';
export default function DaftarPost() {
const [posts, setPosts] = useState([]);
const [status, setStatus] = useState('loading'); // 'loading' | 'error' | 'sukses'
const [pesanError, setPesanError] = useState('');
useEffect(() => {
let ignore = false;
async function ambil() {
try {
const res = await fetch('https://jsonplaceholder.typicode.com/posts?_limit=5');
if (!res.ok) throw new Error(`Server membalas ${res.status}`);
const data = await res.json();
if (!ignore) {
setPosts(data);
setStatus('sukses');
}
} catch (err) {
if (!ignore) {
setPesanError(err.message);
setStatus('error');
}
}
}
ambil();
return () => {
ignore = true;
};
}, []);
if (status === 'loading') return <p>Memuat...</p>;
if (status === 'error') return <p>❌ {pesanError}</p>; // URL salah → "Server membalas 404"
return (
<ol>
{posts.map(p => <li key={p.id}>{p.title}</li>)}
</ol>
);
}
Ringkasan
useEffectmenyinkronkan komponen dengan sistem eksternal dan berjalan setelah render ditampilkan.- Dependency array menentukan kapan effect berjalan: tanpa array (setiap render),
[](sekali saat mount),[a, b](saat a/b berubah). - Fungsi yang di-return adalah cleanup, dipanggil sebelum effect berikutnya dan saat unmount; wajib untuk timer dan listener.
- Fungsi effect tidak boleh
async; buat fungsi async di dalamnya dan kelola loading/error/data. - Jangan pakai effect untuk data turunan atau respons aksi pengguna; hitung saat render atau tulis di event handler.