Cara Membuat Custom Hooks React yang Reusable
Apa Itu Custom Hooks di React?
Custom hooks adalah fungsi JavaScript yang namanya diawali dengan use dan memanfaatkan hooks bawaan React seperti useState, useEffect, atau useRef. Dengan custom hooks, kamu bisa mengekstrak logika yang berulang dari komponen dan menjadikannya reusable di berbagai tempat.
Bayangkan kamu punya 5 komponen yang sama-sama melakukan fetch data dari API. Tanpa custom hooks, kamu harus menulis logika useEffect dan useState yang hampir identik di setiap komponen. Custom hooks menyelesaikan masalah duplikasi ini dengan elegan.
Kenapa Harus Pakai Custom Hooks?
Ada beberapa alasan kuat kenapa custom hooks penting dalam pengembangan aplikasi React:
- Mengurangi duplikasi kode — logika yang sama cukup ditulis sekali
- Separation of concerns — komponen fokus ke UI, hooks fokus ke logika
- Mudah di-test — logika yang terisolasi lebih mudah diuji
- Komposisi — hooks bisa memanggil hooks lain, memungkinkan komposisi yang fleksibel
Membuat Custom Hook Pertama: useFetch
Mari mulai dengan contoh paling umum — hook untuk melakukan HTTP request.
Versi Dasar
import { useState, useEffect } from 'react';
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const controller = new AbortController();
async function fetchData() {
setLoading(true);
setError(null);
try {
const response = await fetch(url, {
signal: controller.signal,
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const json = await response.json();
setData(json);
} catch (err) {
if (err.name !== 'AbortError') {
setError(err.message);
}
} finally {
setLoading(false);
}
}
fetchData();
return () => controller.abort();
}, [url]);
return { data, loading, error };
}
Perhatikan bahwa kita menggunakan AbortController untuk membatalkan request saat komponen di-unmount atau URL berubah. Ini penting untuk menghindari memory leak — topik yang sudah kita bahas di artikel sebelumnya.
Cara Menggunakannya
function UserList() {
const { data, loading, error } = useFetch('/api/users');
if (loading) return <p>Memuat data...</p>;
if (error) return <p>Error: {error}</p>;
return (
<ul>
{data.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
Bandingkan dengan menulis semua logika fetch langsung di komponen — jauh lebih bersih dan mudah dibaca.
Custom Hook untuk Form: useForm
Mengelola state form adalah kebutuhan yang sangat umum. Daripada install library besar, untuk form sederhana kita bisa buat custom hook sendiri.
import { useState, useCallback } from 'react';
function useForm(initialValues) {
const [values, setValues] = useState(initialValues);
const [errors, setErrors] = useState({});
const handleChange = useCallback((e) => {
const { name, value, type, checked } = e.target;
setValues(prev => ({
...prev,
[name]: type === 'checkbox' ? checked : value,
}));
}, []);
const reset = useCallback(() => {
setValues(initialValues);
setErrors({});
}, [initialValues]);
const setFieldValue = useCallback((name, value) => {
setValues(prev => ({ ...prev, [name]: value }));
}, []);
return {
values,
errors,
setErrors,
handleChange,
reset,
setFieldValue,
};
}
Penggunaan di Komponen
function ContactForm() {
const { values, handleChange, reset } = useForm({
name: '',
email: '',
message: '',
});
const handleSubmit = async (e) => {
e.preventDefault();
await fetch('/api/contact', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(values),
});
reset();
};
return (
<form onSubmit={handleSubmit}>
<input name="name" value={values.name} onChange={handleChange} />
<input name="email" value={values.email} onChange={handleChange} />
<textarea name="message" value={values.message} onChange={handleChange} />
<button type="submit">Kirim</button>
</form>
);
}
Hook useForm ini cukup untuk banyak kasus. Ketika kebutuhan validasi mulai kompleks (validasi async, form wizard, dsb), barulah pertimbangkan library seperti React Hook Form.
Custom Hook untuk LocalStorage: useLocalStorage
Hook ini berguna untuk menyimpan state yang perlu bertahan meskipun halaman di-refresh.
import { useState, useEffect } from 'react';
function useLocalStorage(key, initialValue) {
const [value, setValue] = useState(() => {
try {
const item = localStorage.getItem(key);
return item !== null ? JSON.parse(item) : initialValue;
} catch {
return initialValue;
}
});
useEffect(() => {
try {
localStorage.setItem(key, JSON.stringify(value));
} catch (err) {
console.error('Gagal menyimpan ke localStorage:', err);
}
}, [key, value]);
return [value, setValue];
}
Contoh Penggunaan
function ThemeToggle() {
const [theme, setTheme] = useLocalStorage('theme', 'light');
return (
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
Mode: {theme}
</button>
);
}
State theme akan otomatis tersimpan di localStorage dan di-restore saat halaman dimuat ulang.
Custom Hook untuk Debounce: useDebounce
Saat membuat fitur search, kamu tidak ingin mengirim request ke API setiap kali user mengetik satu karakter. Hook useDebounce menunda eksekusi sampai user berhenti mengetik.
import { useState, useEffect } from 'react';
function useDebounce(value, delay = 500) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const timer = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => clearTimeout(timer);
}, [value, delay]);
return debouncedValue;
}
Implementasi di Search
function SearchProducts() {
const [query, setQuery] = useState('');
const debouncedQuery = useDebounce(query, 300);
const { data } = useFetch(
debouncedQuery ? `/api/products?q=${debouncedQuery}` : null
);
return (
<div>
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Cari produk..."
/>
{data?.map(product => (
<div key={product.id}>{product.name}</div>
))}
</div>
);
}
Perhatikan bagaimana useDebounce dan useFetch dikomposisikan bersama — inilah kekuatan custom hooks.
Aturan Penting Saat Membuat Custom Hooks
Ada beberapa aturan yang wajib diikuti agar custom hooks bekerja dengan benar:
1. Nama Harus Diawali use
React mengandalkan konvensi penamaan ini untuk mendeteksi hooks dan menerapkan rules of hooks. Tanpa prefix use, React tidak akan memberikan warning jika hooks digunakan dengan cara yang salah.
2. Jangan Panggil Hooks Secara Kondisional
// SALAH - jangan lakukan ini
function useBadHook(shouldFetch) {
if (shouldFetch) {
const [data, setData] = useState(null); // Error!
}
}
// BENAR
function useGoodHook(shouldFetch) {
const [data, setData] = useState(null);
useEffect(() => {
if (!shouldFetch) return;
// fetch logic
}, [shouldFetch]);
return data;
}
3. Return Value yang Konsisten
Gunakan objek untuk hooks dengan banyak return value (seperti useFetch), dan array untuk hooks dengan satu value + setter (seperti useLocalStorage). Ini mengikuti pola yang sudah familiar dari hooks bawaan React.
Tips Organisasi File
Untuk proyek skala menengah, simpan custom hooks dalam folder hooks/:
src/
hooks/
useFetch.js
useForm.js
useLocalStorage.js
useDebounce.js
components/
...
Untuk proyek kecil, satu file hooks.js sudah cukup. Jangan over-engineer struktur folder sebelum waktunya.
Kesimpulan
Custom hooks adalah salah satu fitur paling powerful di React. Dengan memahami cara membuatnya, kamu bisa menulis kode yang lebih bersih, reusable, dan mudah di-maintain. Mulai dari yang sederhana seperti useDebounce, lalu kembangkan sesuai kebutuhan proyek.
Kunci utamanya: ekstrak logika yang berulang, bukan UI. Kalau kamu mendapati dirimu copy-paste blok useState + useEffect yang sama di beberapa komponen, itu tandanya waktunya membuat custom hook.