Halo! Kali ini aku mau bahas sesuatu yang super penting banget kalau kamu main React: React Hooks. Kalau kamu masih pakai class component dan agak bingung kenapa orang-orang pada pindah ke functional component, artikel ini bakal jelasin semuanya dari nol.
React Hooks itu fitur yang diintroduksi di React 16.8 (awal 2019) dan sekarang jadi standar de facto di dunia React. Hampir semua project baru pakai hooks. Jadi kalau kamu belum familiar, wajib banget buat dipelajari.
Yuk, langsung gas aja!
Kenapa React Hooks?
Sebelum hooks, kalau kamu mau pakai state atau lifecycle method di React, kamu harus pakai class component. Itu agak ribet karena:
- Syntax class yang lebih verbose
thisbinding yang sering bikin bingung- Logic yang sering terpecah-pecah (contohnya,
componentDidMountdancomponentDidUpdateuntuk data fetching yang sama) - Reuse stateful logic antar komponen susah (harus pakai HOC atau render props)
Hooks menyelesaikan semua masalah ini. Dengan hooks, kamu bisa:
- Pakai state di functional component
- Pakai lifecycle-features tanpa class
- Reuse stateful logic dengan custom hooks
- Bikin kode yang lebih clean dan mudah dibaca
useState — Hook Paling Dasar
useState adalah hook yang paling sering kamu pakai. Hook ini memungkinkan kamu buat nambahin state ke functional component.
import React, { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Kamu sudah klik {count} kali</p>
<button onClick={() => setCount(count + 1)}>
Klik Di Sini
</button>
</div>
);
}
export default Counter;
useState(0) mengembalikan array dengan dua elemen: state saat ini (count) dan function buat update state (setCount). Angka 0 di dalamnya adalah initial value.
Update State dengan Function
Kalau kamu mau update state berdasarkan state sebelumnya, jangan langsung pakai count + 1. Pakai callback function biar lebih aman:
// ❌ Kurang aman (bisa race condition)
setCount(count + 1);
// ✅ Lebih aman
setCount(prevCount => prevCount + 1);
Kenapa? Karena React bisa nge-batch beberapa setState sekaligus. Kalau kamu pakai count + 1 langsung, nilainya bisa outdated.
State dengan Object
Kalau state kamu berupa object, hati-hati jangan lupa spread property sebelumnya:
function UserForm() {
const [user, setUser] = useState({
nama: '',
email: '',
umur: 0,
});
const handleChange = (e) => {
setUser({
...user,
[e.target.name]: e.target.value,
});
};
return (
<form>
<input
name="nama"
value={user.nama}
onChange={handleChange}
placeholder="Nama"
/>
<input
name="email"
value={user.email}
onChange={handleChange}
placeholder="Email"
/>
<input
name="umur"
type="number"
value={user.umur}
onChange={handleChange}
placeholder="Umur"
/>
<p>Halo, {user.nama}! Umur kamu {user.umur} tahun.</p>
</form>
);
}
useEffect — Side Effects di React
useEffect digunakan untuk menjalankan side effects — hal-hal yang dilakukan di luar rendering komponen. Contohnya: fetch data, subscribes, timer, atau manipulasi DOM.
import React, { useState, useEffect } from 'react';
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
async function fetchUser() {
setLoading(true);
const response = await fetch(`https://api.example.com/users/${userId}`);
const data = await response.json();
setUser(data);
setLoading(false);
}
fetchUser();
}, [userId]); // dependency array: hanya jalan kalau userId berubah
if (loading) return <p>Loading...</p>;
return (
<div>
<h2>{user?.nama}</h2>
<p>{user?.email}</p>
</div>
);
}
Dependency Array
Ini bagian yang paling penting di useEffect. Ada tiga kemungkinan:
// 1. Tanpa dependency array — jalan SETIAP render
useEffect(() => {
console.log('Setiap render!');
});
// 2. Dengan array kosong — jalan HANYA sekali (mount)
useEffect(() => {
console.log('Hanya saat mount');
}, []);
// 3. Dengan dependencies — jalan saat dependencies berubah
useEffect(() => {
console.log(`userId berubah: ${userId}`);
}, [userId]);
Cleanup Function
Kalau kamu bikin timer atau subscribe, jangan lupa bersihkan di cleanup function:
function Timer() {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setSeconds(prev => prev + 1);
}, 1000);
// Cleanup function
return () => clearInterval(interval);
}, []);
return <p>Waktu berjalan: {seconds} detik</p>;
}
Tanpa cleanup, interval akan terus jalan walaupun komponen sudah unmount — ini yang sering bikin memory leak.
useContext — Akses Context Tanpa Wrapper
useContext memudahkan kamu buat akses data dari React Context tanpa harus pakai consumer pattern yang verbose.
import React, { createContext, useContext, useState } from 'react';
// 1. Buat context
const ThemeContext = createContext();
// 2. Buat provider
function ThemeProvider({ children }) {
const [darkMode, setDarkMode] = useState(false);
const toggleTheme = () => setDarkMode(prev => !prev);
return (
<ThemeContext.Provider value={{ darkMode, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}
// 3. Pakai di komponen manapun
function Header() {
const { darkMode, toggleTheme } = useContext(ThemeContext);
return (
<header style={{
background: darkMode ? '#333' : '#fff',
color: darkMode ? '#fff' : '#333',
}}>
<h1>My App</h1>
<button onClick={toggleTheme}>
{darkMode ? '☀️ Light' : '🌙 Dark'}
</button>
</header>
);
}
// 4. Wrap aplikasi dengan provider
function App() {
return (
<ThemeProvider>
<Header />
<p>Isi konten aplikasi di sini</p>
</ThemeProvider>
);
}
useContext mengambil context object dan mengembalikan value dari provider terdekat. Keren banget kan? Gak perlu lagi nesting <Context.Consumer> di mana-mana.
useRef — Akses DOM dan Simpan Mutable Value
useRef punya dua kegunaan utama: akses DOM element langsung, dan simpan value yang persisten antar render tanpa trigger re-render.
import React, { useRef, useState } from 'react';
function SearchInput() {
const inputRef = useRef(null);
const [query, setQuery] = useState('');
const handleFocus = () => {
inputRef.current.focus();
inputRef.current.select();
};
const handleSubmit = (e) => {
e.preventDefault();
alert(`Mencari: ${query}`);
};
return (
<form onSubmit={handleSubmit}>
<input
ref={inputRef}
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Ketik sesuatu..."
/>
<button type="button" onClick={handleFocus}>Fokus ke Input</button>
<button type="submit">Cari</button>
</form>
);
}
useRef untuk Simpan Previous Value
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
}, [value]);
return ref.current;
}
function Counter() {
const [count, setCount] = useState(0);
const prevCount = usePrevious(count);
return (
<div>
<p>Sekarang: {count}, Sebelumnya: {prevCount}</p>
<button onClick={() => setCount(count + 1)}>+1</button>
</div>
);
}
Yang keren dari useRef adalah perubahan ref.current tidak menyebabkan re-render, berbeda dengan useState.
useCallback — Optimasi Function References
useCallback mengembalikan memoized version dari function. Ini berguna kalau kamu meneruskan function sebagai props ke child component yang di-memoize.
import React, { useState, useCallback, memo } from 'react';
const TodoItem = memo(({ todo, onToggle }) => {
console.log(`Rendering: ${todo.text}`);
return (
<li onClick={() => onToggle(todo.id)}>
{todo.done ? '✅' : '⬜'} {todo.text}
</li>
);
});
function TodoList() {
const [todos, setTodos] = useState([
{ id: 1, text: 'Belajar React', done: false },
{ id: 2, text: 'Bikin project', done: false },
{ id: 3, text: 'Deploy ke production', done: false },
]);
const handleToggle = useCallback((id) => {
setTodos(prev =>
prev.map(todo =>
todo.id === id ? { ...todo, done: !todo.done } : todo
)
);
}, []); // dependency array kosong, function gak pernah berubah
return (
<ul>
{todos.map(todo => (
<TodoItem key={todo.id} todo={todo} onToggle={handleToggle} />
))}
</ul>
);
}
Tanpa useCallback, function handleToggle akan dibuat ulang setiap render, dan TodoItem (yang dibungkus memo) akan selalu re-render. Dengan useCallback, function-nya stabil dan TodoItem hanya re-render saat datanya berubah.
useMemo — Optimasi Expensive Computation
useMemo meng-cache hasil dari computasi yang mahal. Berbeda dengan useCallback yang cache function, useMemo cache nilai return dari function.
import React, { useState, useMemo } from 'react';
function ExpensiveList({ items }) {
const [filter, setFilter] = useState('');
// ✅ useMemo: hanya hitung ulang kalau items atau filter berubah
const filteredItems = useMemo(() => {
console.log('Menghitung filtered items...');
return items.filter(item =>
item.nama.toLowerCase().includes(filter.toLowerCase())
);
}, [items, filter]);
return (
<div>
<input
type="text"
value={filter}
onChange={(e) => setFilter(e.target.value)}
placeholder="Filter items..."
/>
<p>Jumlah hasil: {filteredItems.length}</p>
<ul>
{filteredItems.map(item => (
<li key={item.id}>{item.nama} — Rp{item.harga.toLocaleString()}</li>
))}
</ul>
</div>
);
}
Tanpa useMemo, filtering akan dihitung ulang setiap kali component re-render (misalnya saat user ketik di input), yang bisa bikin app lambat kalau list-nya besar.
Custom Hooks — Reuse Stateful Logic
Salah satu kekuatan terbesar hooks adalah kemampuan buat bikin custom hooks. Custom hook cuma function biasa yang diawali kata “use” dan di dalamnya memanggil hooks lain.
import { useState, useEffect } from 'react';
// Custom hook: fetch data
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const abortController = new AbortController();
async function fetchData() {
try {
setLoading(true);
setError(null);
const response = await fetch(url, {
signal: abortController.signal,
});
if (!response.ok) throw new Error('Network response not ok');
const json = await response.json();
setData(json);
} catch (err) {
if (err.name !== 'AbortError') {
setError(err.message);
}
} finally {
setLoading(false);
}
}
fetchData();
return () => abortController.abort();
}, [url]);
return { data, loading, error };
}
// Pakai custom hook
function PostList() {
const { data: posts, loading, error } = useFetch(
'https://jsonplaceholder.typicode.com/posts'
);
if (loading) return <p>Loading posts...</p>;
if (error) return <p>Error: {error}</p>;
return (
<ul>
{posts?.slice(0, 10).map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
Contoh Custom Hook Lain: useLocalStorage
import { useState, useEffect } from 'react';
function useLocalStorage(key, initialValue) {
const [value, setValue] = useState(() => {
try {
const stored = localStorage.getItem(key);
return stored ? JSON.parse(stored) : initialValue;
} catch {
return initialValue;
}
});
useEffect(() => {
localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
return [value, setValue];
}
// Pakai
function Settings() {
const [theme, setTheme] = useLocalStorage('theme', 'light');
const [lang, setLang] = useLocalStorage('lang', 'id');
return (
<div>
<select value={theme} onChange={(e) => setTheme(e.target.value)}>
<option value="light">Light</option>
<option value="dark">Dark</option>
</select>
<select value={lang} onChange={(e) => setLang(e.target.value)}>
<option value="id">Indonesia</option>
<option value="en">English</option>
</select>
<p>Theme: {theme}, Language: {lang}</p>
</div>
);
}
Custom hooks bikin kode kamu jauh lebih clean, reusable, dan mudah di-test.
Best Practices React Hooks
Dari pengalaman aku, berikut best practices yang wajib kamu ikuti:
1. Pakai Rules of Hooks
Hooks hanya bisa dipanggil di top level — gak boleh di dalam loops, conditions, atau nested functions. Juga, hanya bisa dipanggil dari React functions (component atau custom hooks).
// ❌ JANGAN
if (someCondition) {
const [value, setValue] = useState(0); // ERROR!
}
// ✅ BENAR
const [value, setValue] = useState(0);
if (someCondition) {
// pakai value di sini
}
2. Selalu Lengkapi Dependency Array
Kalau kamu pakai variable dari scope luar di dalam useEffect, useCallback, atau useMemo, masukkan ke dependency array. Biarkan ESLint plugin react-hooks membantu kamu.
// ❌ Lupa dependency
useEffect(() => {
fetchData(userId);
}, []); // userId gak di-declare!
// ✅ Benar
useEffect(() => {
fetchData(userId);
}, [userId]);
3. Satu Responsibility per Custom Hook
Bikin custom hook yang fokus pada satu hal. Jangan bikin “super hook” yang ngurusin semuanya.
4. Jangan Over-Optimize
useCallback dan useMemo itu ada harganya (memory). Jangan pakai di mana-mana. Pakai hanya kalau kamu punya bukti ada performance issue.
Common Mistakes yang Sering Terjadi
1. Stale Closure
Ini salah satu bug yang paling tricky di hooks:
function StaleCounter() {
const [count, setCount] = useState(0);
// ❌ Bug: count selalu 0 di dalam interval
useEffect(() => {
const interval = setInterval(() => {
setCount(count + 1); // count = 0, selalu!
}, 1000);
return () => clearInterval(interval);
}, []);
// ✅ Fix: pakai functional update
useEffect(() => {
const interval = setInterval(() => {
setCount(prev => prev + 1); // prev = count terbaru
}, 1000);
return () => clearInterval(interval);
}, []);
return <p>{count}</p>;
}
2. Infinite Loop di useEffect
// ❌ Infinite loop: effect set state, state berubah, effect jalan lagi
useEffect(() => {
setData(transformData(data));
}, [data]);
// ✅ Fix: pakai state terpisah atau gunakan useMemo
const transformedData = useMemo(() => transformData(data), [data]);
3. Cleanup yang Ketinggalan
Selalu cleanup subscriptions, timers, dan event listeners. Kalau gak, kamu bakal dapet memory leaks.
4. Mutasi State Langsung
// ❌ Jangan pernah mutasi state langsung
const [items, setItems] = useState([1, 2, 3]);
items.push(4); // state gak update, tapi array-nya berubah!
// ✅ Buat array baru
setItems([...items, 4]);
Hook Lifecycle Mapping
Kalau kamu terbiasa pakai class component, ini cheat sheet mapping lifecycle ke hooks:
| Class Component | Hook Equivalent |
|---|---|
componentDidMount | useEffect(() => {}, []) |
componentDidUpdate | useEffect(() => {}, [deps]) |
componentWillUnmount | useEffect(() => { return () => {} }, []) |
shouldComponentUpdate | React.memo / useMemo |
getDerivedStateFromProps | Hilangkan, pakai logic biasa |
componentDidCatch | ErrorBoundary (bukan hook) |
Mengapa React Team Belum Bikin Semua Hook?
Ada beberapa fitur yang belum bisa di-handle dengan hooks biasa:
- getSnapshotFromUpdate — React belum punya hook equivalent
- Error boundaries — Masih harus pakai class component atau error boundary library
- Static methods dan lifecycle — Beberapa class-only features belum ada hook-nya
Tapi untuk 99% kasus, hooks sudah lebih dari cukup.
Kesimpulan
React Hooks itu game changer yang bikin development di React jadi lebih clean, modular, dan enjoyable. Dengan useState, useEffect, useContext, useRef, useCallback, useMemo, dan custom hooks, kamu bisa handle hampir semua use case tanpa harus pakai class component.
Kalau kamu baru mulai, fokus dulu di useState dan useEffect. Setelah nyaman, pelan-pelan pelajari sisanya. Dan jangan lupa: practice makes perfect!
Kalau ada pertanyaan atau kamu mau request tutorial lain, langsung aja email aku di [email protected]. Jangan ragu buat nyontak ya! 🚀
FAQ
Q: Apakah React Hooks bisa dipakai di class component?
A: Enggak bisa. Hooks dirancang khusus untuk functional component. Kalau kamu masih pakai class component, stick dulu ke lifecycle methods (componentDidMount, dll). Tapi aku saranin pelan-pelan migrasi ke functional component ya, karena React ecosystem sekarang sudah hampir sepenuhnya hooks-based.
Q: Kapan harus pakai useCallback vs useMemo?
A: useCallback dipakai untuk mencache function, sedangkan useMemo untuk mencache nilai return dari function. Kalau kamu mau meneruskan function ke child component yang pakai React.memo, pakai useCallback. Kalau kamu mau cache hasil perhitungan mahal (sorting, filtering data besar), pakai useMemo.
Q: Berapa banyak state yang boleh ada di satu komponen?
A: Sebanyak yang kamu butuhkan! Tapi kalau state kamu udah banyak banget (lebih dari 5-6 useState), itu bisa jadi tanda komponen kamu terlalu besar. Pertimbangkan buat split jadi beberapa komponen atau pakai useReducer / state management library seperti Zustand atau Jotai.
Q: Apakah custom hook harus return array seperti useState?
A: Enggak harus. Custom hook bisa return apa aja: object, array, string, atau kombinasi semuanya. Yang penting, return value harus konsisten dengan cara kamu pakainya. Banyak custom hook yang return object ({ data, loading, error }) supaya lebih fleksibel.
Q: Kenapa useEffect saya jalan terus menerus (infinite loop)?
A: Ini biasanya terjadi karena di dalam useEffect kamu manggil setState yang bikin re-render, dan dependency array-nya include state tersebut. Cek dependency array kamu — pastikan kamu cuma masukin values yang memang perlu trigger ulang effect. Kalau kamu transform data di dalam useEffect, pertimbangkan pakai useMemo sebagai gantinya.