Hooks memungkinkan functional component menyimpan state dan berinteraksi dengan dunia luar, sesuatu yang dulu hanya bisa dilakukan class component. Dua yang paling sering dipakai adalah useState dan useEffect. Keduanya terlihat sederhana, dan justru karena itu banyak bug React bermula dari salah paham soal cara kerjanya.
useState
Hook ini mengembalikan nilai saat ini beserta fungsi untuk memperbaruinya.
import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>+</button>
</div>
);
}
State diperbarui secara asinkron
Ini sumber kebingungan nomor satu. Memanggil setCount tidak langsung mengubah count pada baris berikutnya. React menjadwalkan render ulang, dan nilai baru baru terlihat pada render berikutnya.
function handleClick() {
setCount(count + 1);
setCount(count + 1);
console.log(count); // Masih nilai lama
}
Kode di atas hanya menambah satu, bukan dua. Kedua pemanggilan membaca count yang sama dari render saat ini. Solusinya memakai bentuk fungsional, yang menerima nilai terbaru sebagai argumen:
setCount(prev => prev + 1);
setCount(prev => prev + 1); // Sekarang bertambah dua
Aturan praktisnya: kalau nilai berikutnya dihitung dari nilai sebelumnya, selalu pakai bentuk fungsional.
Jangan mengubah state secara langsung
React membandingkan referensi objek untuk memutuskan perlu render ulang atau tidak. Mengubah isi objek atau array yang sudah ada tidak mengubah referensinya, jadi React tidak melihat apa pun berubah.
// Salah, referensi array tetap sama
todos.push(newTodo);
setTodos(todos);
// Benar, array baru
setTodos([...todos, newTodo]);
Lazy initial state
Argumen useState dievaluasi pada setiap render, meski hasilnya cuma dipakai sekali. Kalau nilainya mahal dihitung, bungkus dalam fungsi:
// readFromLocalStorage() jalan setiap render
const [data, setData] = useState(readFromLocalStorage());
// Hanya jalan sekali saat mount
const [data, setData] = useState(() => readFromLocalStorage());
useEffect
useEffect dipakai untuk menyinkronkan komponen dengan sesuatu di luar React: permintaan jaringan, langganan event, timer, atau API browser.
import { useState, useEffect } from 'react';
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
let ignore = false;
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(data => {
if (!ignore) setUser(data);
});
return () => {
ignore = true;
};
}, [userId]);
return user ? <h1>{user.name}</h1> : <p>Loading...</p>;
}
Perhatikan variabel ignore dan fungsi yang dikembalikan. Tanpa keduanya, contoh ini punya race condition yang nyata. Kalau userId berubah cepat dari 1 ke 2, dua permintaan berjalan bersamaan. Kalau respons untuk pengguna 1 datang terlambat, ia akan menimpa data pengguna 2, dan layar menampilkan orang yang salah. Fungsi cleanup menandai efek lama sebagai kedaluwarsa, sehingga hasilnya diabaikan.
Versi tanpa cleanup yang sering ditemukan di tutorial singkat terlihat lebih rapi, tapi menyimpan bug yang hanya muncul saat jaringan lambat, dan itu justru kondisi yang dialami pengguna sungguhan.
Dependency array
[]berarti efek berjalan sekali setelah mount[userId]berarti efek berjalan ulang setiapuserIdberubah- Tanpa array sama sekali berarti efek berjalan setiap render
Aturannya sederhana: setiap nilai dari dalam komponen yang dipakai di dalam efek harus masuk ke dependency array. Menghapus dependensi supaya efek βtidak jalan terusβ adalah cara memperbaiki gejala, bukan penyebab, dan hasilnya adalah stale closure.
Stale closure terjadi ketika efek menangkap nilai dari render lama dan terus memakainya:
// Bermasalah, interval selalu melihat count bernilai 0
useEffect(() => {
const id = setInterval(() => {
setCount(count + 1);
}, 1000);
return () => clearInterval(id);
}, []);
// Benar, tidak perlu membaca count sama sekali
useEffect(() => {
const id = setInterval(() => {
setCount(prev => prev + 1);
}, 1000);
return () => clearInterval(id);
}, []);
Pasang plugin ESLint react-hooks. Peringatan exhaustive-deps yang sering dianggap mengganggu itu justru menangkap kelas bug ini sebelum sampai ke pengguna.
Cleanup selalu berpasangan
Apa pun yang Anda mulai di dalam efek harus dihentikan di fungsi cleanup: langganan event, timer, koneksi WebSocket, IntersectionObserver.
useEffect(() => {
const onResize = () => setWidth(window.innerWidth);
window.addEventListener('resize', onResize);
return () => window.removeEventListener('resize', onResize);
}, []);
Tanpa cleanup, listener menumpuk setiap kali komponen dipasang ulang, dan aplikasi perlahan melambat. Di mode pengembangan dengan Strict Mode, React sengaja memasang lalu melepas efek dua kali untuk memunculkan kelalaian semacam ini lebih awal. Kalau komponen Anda berperilaku aneh di development tapi normal di production, biasanya itu penyebabnya, dan yang perlu diperbaiki adalah cleanup-nya, bukan mematikan Strict Mode.
Kapan Tidak Perlu useEffect
Ini bagian yang paling banyak menghemat kode. Banyak useEffect di proyek nyata sebenarnya tidak perlu ada.
Untuk nilai turunan, hitung saja saat render. Tidak perlu state tambahan dan tidak perlu efek:
// Berlebihan
const [fullName, setFullName] = useState('');
useEffect(() => {
setFullName(firstName + ' ' + lastName);
}, [firstName, lastName]);
// Cukup begini
const fullName = firstName + ' ' + lastName;
Versi pertama memicu render dua kali dan membuka peluang state jadi tidak sinkron. Versi kedua tidak mungkin salah.
Untuk merespons aksi pengguna, tulis di event handler. Kalau sesuatu harus terjadi karena tombol ditekan, tempatnya di fungsi penanganan klik, bukan di efek yang mengamati perubahan state.
Untuk menyaring atau mengurutkan daftar, lakukan saat render. Kalau perhitungannya benar-benar berat dan terbukti lambat, barulah bungkus dengan useMemo.
Patokan yang berguna: useEffect adalah untuk menyinkronkan dengan sistem di luar React. Kalau tidak ada sistem luar yang terlibat, kemungkinan besar Anda tidak membutuhkannya.
Ringkasan
useState menyimpan nilai yang berubah, dan pembaruannya asinkron serta harus dilakukan tanpa memodifikasi nilai lama. useEffect menghubungkan komponen dengan dunia luar, harus jujur soal dependensinya, dan harus membereskan apa pun yang ia mulai. Sisanya, sebagian besar, cukup dihitung langsung saat render.