Memahami React Hooks dari Nol
React Hooks membantu kita menulis komponen React berbasis fungsi yang tetap bisa memiliki state, membaca context, menjalankan side effect, dan membungkus logika agar dapat digunakan ulang. Bagi pemula, hooks sering terasa membingungkan karena bentuknya sederhana, tetapi perilakunya sangat terkait dengan cara React melakukan render ulang.
Tutorial ini membahas hooks dengan pendekatan bertahap. Kita tidak hanya melihat sintaks, tetapi juga alasan kapan sebuah hook dipakai, apa konsekuensinya terhadap render, dan kesalahan umum yang perlu dihindari saat membangun aplikasi.
Tujuan Belajar
Setelah mengikuti tutorial ini, kamu diharapkan mampu:
- Menjelaskan perbedaan data biasa, state, dan props.
- Menggunakan
useStateuntuk menyimpan data yang memengaruhi tampilan. - Menggunakan
useEffectuntuk menjalankan side effect dengan dependency yang tepat. - Menggunakan
useContextuntuk membaca data global sederhana. - Membuat custom hook untuk membungkus logika yang berulang.
- Membaca gejala bug umum seperti infinite render, stale state, dan dependency yang hilang.
Prasyarat
Kamu tidak harus mahir React, tetapi sebaiknya sudah paham dasar berikut:
| Prasyarat | Mengapa penting |
|---|---|
| JavaScript function | Komponen React modern biasanya ditulis sebagai function. |
| Array dan object | State sering berbentuk array atau object. |
| Callback | Event handler seperti onClick menerima function. |
| Promise dasar | Banyak side effect berkaitan dengan fetch data. |
Jika beberapa bagian belum kuat, tetap lanjutkan. Fokus utama tutorial ini adalah pola berpikir hooks, bukan menghafal seluruh API React.
Mental Model: Render Adalah Snapshot
Ketika React menjalankan komponen, React menghasilkan sebuah snapshot UI berdasarkan props dan state pada saat itu. Jika state berubah, React menjalankan ulang komponen untuk menghasilkan snapshot baru.
Perhatikan contoh sederhana berikut.
function Greeting({ name }) {
const message = `Halo, ${name}`;
return <p>{message}</p>;
}message bukan state. Nilainya dihitung setiap render. Jika name berubah, komponen dirender ulang dan message dihitung ulang. State baru dibutuhkan ketika sebuah nilai perlu bertahan antar-render dan perubahannya harus membuat UI diperbarui.
useState: Menyimpan Nilai yang Mengubah UI
useState adalah hook paling dasar. Ia mengembalikan dua hal: nilai saat ini dan function untuk meminta React memperbarui nilai tersebut.
import { useState } from "react";
export function Counter() {
const [count, setCount] = useState(0);
return (
<section>
<p>Jumlah klik: {count}</p>
<button onClick={() => setCount(count + 1)}>
Tambah
</button>
</section>
);
}Ada tiga hal penting dari contoh ini. Pertama, count adalah nilai untuk render saat ini. Kedua, setCount tidak mengubah variabel count secara langsung. Ketiga, setelah setCount dipanggil, React menjadwalkan render baru dengan nilai terbaru.
State Berdasarkan Nilai Sebelumnya
Jika nilai baru bergantung pada nilai lama, gunakan bentuk callback. Ini menghindari bug saat beberapa update terjadi berdekatan.
function CounterActions() {
const [count, setCount] = useState(0);
function incrementThreeTimes() {
setCount((current) => current + 1);
setCount((current) => current + 1);
setCount((current) => current + 1);
}
return (
<button onClick={incrementThreeTimes}>
Tambah 3 dari {count}
</button>
);
}Callback menerima nilai state paling mutakhir yang diketahui React. Pola ini cocok untuk counter, toggle, antrean, keranjang belanja, dan data yang diperbarui beberapa kali dari interaksi yang sama.
State Object dan Array Harus Immutable
React mendeteksi perubahan berdasarkan referensi. Karena itu, jangan mengubah object atau array lama secara langsung. Buat salinan baru.
function ProfileForm() {
const [profile, setProfile] = useState({
name: "",
role: "student",
});
function updateName(event) {
setProfile((current) => ({
...current,
name: event.target.value,
}));
}
return (
<label>
Nama
<input value={profile.name} onChange={updateName} />
</label>
);
}Untuk array, gunakan map, filter, atau spread, bukan push langsung pada state lama.
function TodoList() {
const [todos, setTodos] = useState([]);
function addTodo(title) {
setTodos((current) => [
...current,
{ id: crypto.randomUUID(), title, done: false },
]);
}
function toggleTodo(id) {
setTodos((current) =>
current.map((todo) =>
todo.id === id ? { ...todo, done: !todo.done } : todo
)
);
}
return null;
}useEffect: Menjalankan Side Effect
Render sebaiknya tetap murni: menerima input, menghasilkan UI. Aktivitas seperti fetch data, membaca storage, memasang event listener, atau mengubah title dokumen termasuk side effect. Untuk kebutuhan seperti itu, gunakan useEffect.
import { useEffect, useState } from "react";
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [status, setStatus] = useState("idle");
useEffect(() => {
let cancelled = false;
async function loadUser() {
setStatus("loading");
const response = await fetch(`/api/users/${userId}`);
const data = await response.json();
if (!cancelled) {
setUser(data);
setStatus("success");
}
}
loadUser().catch(() => {
if (!cancelled) setStatus("error");
});
return () => {
cancelled = true;
};
}, [userId]);
if (status === "loading") return <p>Memuat data...</p>;
if (status === "error") return <p>Gagal memuat data.</p>;
return <pre>{JSON.stringify(user, null, 2)}</pre>;
}Dependency array [userId] berarti effect berjalan saat komponen pertama muncul dan setiap kali userId berubah. Cleanup function digunakan agar hasil request lama tidak menimpa state ketika komponen sudah berganti konteks.
Memahami Dependency Array
Dependency array sering menjadi sumber bug. Aturan praktisnya: semua nilai dari luar effect yang dipakai di dalam effect perlu masuk dependency.
| Bentuk | Kapan berjalan | Catatan |
|---|---|---|
| Tanpa array | Setelah setiap render | Jarang dibutuhkan karena mudah membuat loop. |
[] | Setelah mount | Cocok untuk setup yang tidak bergantung props/state. |
[value] | Setelah mount dan saat value berubah | Ini pola paling umum. |
Contoh bug umum:
function BadSearch({ query }) {
const [result, setResult] = useState(null);
useEffect(() => {
fetch(`/api/search?q=${query}`)
.then((response) => response.json())
.then(setResult);
}, []);
return <pre>{JSON.stringify(result)}</pre>;
}Kode di atas hanya mencari berdasarkan query awal. Jika query berubah, effect tidak berjalan lagi. Dependency yang benar adalah [query].
useContext: Membaca Nilai Lintas Komponen
Context berguna ketika beberapa komponen perlu membaca data yang sama tanpa mengoper props terlalu jauh. Contoh ringan adalah theme, user session, atau preferensi bahasa.
import { createContext, useContext } from "react";
const ThemeContext = createContext("light");
function ThemedButton() {
const theme = useContext(ThemeContext);
return (
<button className={theme === "dark" ? "button-dark" : "button-light"}>
Simpan
</button>
);
}
export function App() {
return (
<ThemeContext.Provider value="dark">
<ThemedButton />
</ThemeContext.Provider>
);
}Context bukan pengganti semua state management. Jika setiap perubahan context membuat banyak komponen render ulang, pertimbangkan memecah context atau memakai state management yang lebih sesuai.
Custom Hook: Membungkus Logika Berulang
Custom hook adalah function yang namanya diawali use dan boleh memanggil hooks lain. Tujuannya bukan membuat kode terlihat keren, melainkan memindahkan pola yang berulang ke tempat yang lebih mudah diuji dan dibaca.
import { useEffect, useState } from "react";
function useLocalStorage(key, initialValue) {
const [value, setValue] = useState(() => {
if (typeof window === "undefined") return initialValue;
const stored = window.localStorage.getItem(key);
return stored ? JSON.parse(stored) : initialValue;
});
useEffect(() => {
window.localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
return [value, setValue];
}
function NameForm() {
const [name, setName] = useLocalStorage("display-name", "");
return (
<input
value={name}
onChange={(event) => setName(event.target.value)}
placeholder="Nama tampilan"
/>
);
}Custom hook membuat komponen fokus pada UI, sedangkan detail sinkronisasi dengan storage berada di satu tempat.
Rules of Hooks
Ada dua aturan inti:
- Panggil hooks hanya di level teratas komponen atau custom hook.
- Panggil hooks hanya dari React component atau custom hook.
Jangan menaruh hooks di dalam kondisi, loop, atau function biasa.
function BadComponent({ enabled }) {
if (enabled) {
const [value, setValue] = useState("");
}
return null;
}React mengandalkan urutan pemanggilan hooks yang sama pada setiap render. Jika hooks dipanggil secara kondisional, urutan itu berubah dan React tidak bisa memasangkan state dengan benar.
Kesalahan Umum
| Kesalahan | Dampak | Perbaikan |
|---|---|---|
| Mutasi state langsung | UI tidak selalu update | Buat object atau array baru. |
| Dependency effect kosong padahal memakai props | Data tidak sinkron | Masukkan dependency yang dipakai. |
| Menaruh fetch langsung di render | Request berulang tiap render | Pindahkan ke effect atau framework data layer. |
| Terlalu cepat membuat custom hook | Abstraksi sulit dipahami | Ekstrak setelah pola berulang terlihat. |
Coba Sendiri
Buat komponen ReadingTracker dengan fitur berikut:
- Menampilkan daftar materi yang sedang dibaca.
- Tombol untuk menandai materi selesai.
- Progress berupa jumlah materi selesai dari total materi.
- Simpan progress ke
localStoragemenggunakan custom hook.
Contoh data awal:
const lessons = [
{ id: "state", title: "State dan Props" },
{ id: "effect", title: "Side Effect" },
{ id: "context", title: "Context" },
];Mulai dari useState, lalu baru pindahkan logika storage ke custom hook setelah komponen berjalan.
Cek Pemahaman
Kuis
Kapan nilai sebaiknya disimpan sebagai state?
Kuis
Apa tujuan dependency array pada useEffect?
Ringkasan
React Hooks bukan sekadar API baru untuk mengganti class component. Hooks adalah cara menyusun logika UI berdasarkan render, state, dan side effect. Mulailah dari mental model bahwa render adalah snapshot. Gunakan useState untuk data yang memengaruhi UI, useEffect untuk side effect, useContext untuk data lintas komponen yang sederhana, dan custom hook saat ada pola logika yang benar-benar berulang.