Custom Hooks
Membuat dan menggunakan custom hooks untuk logika yang reusable
Apa itu Custom Hook?
Custom Hook adalah function JavaScript yang namanya dimulai dengan "use" dan bisa memanggil Hooks lain. Custom Hooks memungkinkan kita mengekstrak logika component ke fungsi yang reusable.
Kenapa Custom Hooks?
Custom Hooks membantu kita:
- Mengurangi duplikasi kode
- Membuat logika kompleks lebih mudah dipahami
- Memisahkan concern dengan lebih baik
- Membuat code lebih testable
Rules untuk Custom Hooks
- Nama harus dimulai dengan "use" - Agar React tahu itu Hook
- Bisa memanggil Hooks lain - useState, useEffect, dll.
- Harus dipanggil di top level - Tidak di dalam loop atau condition
Custom Hook useLocalStorage
Mari buat custom hook untuk mengelola localStorage:
import { useState, useEffect } from 'react';
function useLocalStorage(key, initialValue) {
// State untuk menyimpan value
const [storedValue, setStoredValue] = useState(() => {
try {
// Dapatkan dari localStorage berdasarkan key
const item = window.localStorage.getItem(key);
// Parse JSON jika ada, jika tidak return initialValue
return item ? JSON.parse(item) : initialValue;
} catch (error) {
// Jika error, return initialValue
console.error(error);
return initialValue;
}
});
// Effect untuk update localStorage ketika storedValue berubah
useEffect(() => {
try {
// Simpan ke localStorage
window.localStorage.setItem(key, JSON.stringify(storedValue));
} catch (error) {
console.error(error);
}
}, [key, storedValue]);
return [storedValue, setStoredValue];
}
export default useLocalStorage;Menggunakan useLocalStorage
Update Home component untuk menggunakan custom hook:
import React from 'react';
import Header from '../../components/Header/Header';
import TaskForm from '../../components/TaskForm/TaskForm';
import TaskItem from '../../components/TaskItem/TaskItem';
import useLocalStorage from '../../hooks/useLocalStorage';
import './Home.css';
function Home() {
// Gunakan custom hook untuk tasks
const [tasks, setTasks] = useLocalStorage('tasks', []);
const handleAddTask = (newTask) => {
setTasks([...tasks, newTask]);
};
const handleDeleteTask = (taskId) => {
setTasks(tasks.filter(task => task.id !== taskId));
};
const handleToggleComplete = (taskId) => {
setTasks(tasks.map(task =>
task.id === taskId ? { ...task, completed: !task.completed } : task
));
};
const completedTasks = tasks.filter(task => task.completed).length;
const remainingTasks = tasks.length - completedTasks;
return (
<div className="home">
<Header
title="React Task Manager"
description="Kelola tugas Anda dengan mudah"
/>
<main className="container">
<div className="stats">
<p>Total: {tasks.length} tugas</p>
<p>Selesai: {completedTasks}</p>
<p>Belum selesai: {remainingTasks}</p>
</div>
<TaskForm onAddTask={handleAddTask} />
<div className="task-list">
{tasks.length === 0 ? (
<p className="empty-message">Belum ada tugas. Tambahkan tugas baru!</p>
) : (
tasks.map(task => (
<TaskItem
key={task.id}
task={task}
onDelete={handleDeleteTask}
onToggleComplete={handleToggleComplete}
/>
))
)}
</div>
</main>
</div>
);
}
export default Home;Code Lebih Bersih!
Perhatikan bagaimana custom hook membuat code kita lebih bersih. Semua logika localStorage sekarang tersembunyi di dalam hook yang reusable!
Custom Hook useToggle
Hook sederhana untuk toggle boolean value:
import { useState } from 'react';
function useToggle(initialValue = false) {
const [value, setValue] = useState(initialValue);
const toggle = () => setValue(prev => !prev);
return [value, toggle];
}
export default useToggle;Penggunaan
import useToggle from './hooks/useToggle';
function Modal() {
const [isOpen, toggleOpen] = useToggle(false);
return (
<div>
<button onClick={toggleOpen}>
{isOpen ? 'Close' : 'Open'} Modal
</button>
{isOpen && <div className="modal">Modal Content</div>}
</div>
);
}Custom Hook useFetch
Hook untuk data fetching:
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 fetchData = async () => {
try {
setLoading(true);
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const json = await response.json();
setData(json);
setError(null);
} catch (err) {
setError(err.message);
setData(null);
} finally {
setLoading(false);
}
};
fetchData();
}, [url]);
return { data, loading, error };
}
export default useFetch;Penggunaan
import useFetch from './hooks/useFetch';
function UserList() {
const { data, loading, error } = useFetch('https://api.example.com/users');
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
return (
<ul>
{data.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}Custom Hook useDebounce
Hook untuk debouncing values (berguna untuk search):
import { useState, useEffect } from 'react';
function useDebounce(value, delay = 500) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
// Set timeout untuk update debounced value
const timer = setTimeout(() => {
setDebouncedValue(value);
}, delay);
// Cleanup timeout jika value berubah sebelum delay selesai
return () => {
clearTimeout(timer);
};
}, [value, delay]);
return debouncedValue;
}
export default useDebounce;Penggunaan
import { useState } from 'react';
import useDebounce from './hooks/useDebounce';
function SearchBox() {
const [searchTerm, setSearchTerm] = useState('');
const debouncedSearchTerm = useDebounce(searchTerm, 500);
// Effect ini hanya run 500ms setelah user berhenti mengetik
useEffect(() => {
if (debouncedSearchTerm) {
// Lakukan API call untuk search
console.log('Searching for:', debouncedSearchTerm);
}
}, [debouncedSearchTerm]);
return (
<input
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="Search..."
/>
);
}Custom Hook usePrevious
Hook untuk mengakses nilai sebelumnya dari state/prop:
import { useRef, useEffect } from 'react';
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
}, [value]);
return ref.current;
}
export default usePrevious;Penggunaan
import { useState } from 'react';
import usePrevious from './hooks/usePrevious';
function Counter() {
const [count, setCount] = useState(0);
const prevCount = usePrevious(count);
return (
<div>
<p>Current: {count}</p>
<p>Previous: {prevCount}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}Custom Hook useWindowSize
Hook untuk track ukuran window:
import { useState, useEffect } from 'react';
function useWindowSize() {
const [windowSize, setWindowSize] = useState({
width: window.innerWidth,
height: window.innerHeight,
});
useEffect(() => {
function handleResize() {
setWindowSize({
width: window.innerWidth,
height: window.innerHeight,
});
}
window.addEventListener('resize', handleResize);
// Cleanup
return () => window.removeEventListener('resize', handleResize);
}, []);
return windowSize;
}
export default useWindowSize;Penggunaan
import useWindowSize from './hooks/useWindowSize';
function ResponsiveComponent() {
const { width, height } = useWindowSize();
return (
<div>
<p>Window width: {width}px</p>
<p>Window height: {height}px</p>
{width < 768 && <p>Mobile view</p>}
{width >= 768 && <p>Desktop view</p>}
</div>
);
}Composing Hooks
Custom hooks bisa menggunakan custom hooks lain:
import { useMemo } from 'react';
function useTaskStats(tasks) {
const stats = useMemo(() => {
const total = tasks.length;
const completed = tasks.filter(t => t.completed).length;
const remaining = total - completed;
const completionRate = total > 0 ? (completed / total) * 100 : 0;
return { total, completed, remaining, completionRate };
}, [tasks]);
return stats;
}
export default useTaskStats;Penggunaan
import useLocalStorage from './hooks/useLocalStorage';
import useTaskStats from './hooks/useTaskStats';
function Home() {
const [tasks, setTasks] = useLocalStorage('tasks', []);
const stats = useTaskStats(tasks);
return (
<div>
<p>Total: {stats.total}</p>
<p>Completed: {stats.completed}</p>
<p>Remaining: {stats.remaining}</p>
<p>Completion: {stats.completionRate.toFixed(1)}%</p>
</div>
);
}Best Practices
- Prefix dengan "use" - Agar React bisa enforce rules of Hooks
- Keep it focused - Satu hook untuk satu concern
- Return useful values - Buat API yang mudah digunakan
- Document your hook - Jelaskan parameter dan return value
- Test your hooks - Gunakan @testing-library/react-hooks
Testing Custom Hooks
npm install --save-dev @testing-library/react-hooksimport { renderHook, act } from '@testing-library/react-hooks';
import useToggle from '../useToggle';
describe('useToggle', () => {
it('should initialize with false', () => {
const { result } = renderHook(() => useToggle());
expect(result.current[0]).toBe(false);
});
it('should initialize with provided value', () => {
const { result } = renderHook(() => useToggle(true));
expect(result.current[0]).toBe(true);
});
it('should toggle value', () => {
const { result } = renderHook(() => useToggle());
act(() => {
result.current[1](); // Call toggle
});
expect(result.current[0]).toBe(true);
act(() => {
result.current[1](); // Call toggle again
});
expect(result.current[0]).toBe(false);
});
});Custom Hooks are Powerful!
Custom Hooks adalah salah satu fitur paling powerful di React. Mereka memungkinkan Kalian membuat abstraksi yang reusable dan membuat code lebih maintainable.
Latihan
- Buat
useOnClickOutsidehook untuk detect click di luar element - Buat
useKeyPresshook untuk detect keyboard key press - Buat
useLocalStorageWithExpiryyang auto-expire data - Buat
useAsynchook untuk handle async operations - Test custom hooks yang Kalian buat
Apa Selanjutnya?
Selanjutnya kita akan belajar Context API untuk global state management!
