← Về danh sách bài họcBài 16/25
🪝 Bài 16: Custom Hooks - Tạo Hooks Riêng
🎯 Sau bài học này, bạn sẽ:
- Hiểu tại sao và khi nào tạo custom hook
- Xây dựng useFetch, useLocalStorage, useDebounce
- Xây dựng useMediaQuery, useClickOutside
- Biết quy tắc đặt tên và best practices
1. Custom Hook Là Gì?
Custom hook là function bắt đầu bằng use, sử dụng các hooks khác bên trong. Giúp tái sử dụng logic giữa nhiều components.
// useFetch - hook phổ biến nhất
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const controller = new AbortController();
const fetchData = async () => {
try {
setLoading(true);
const res = await fetch(url, { signal: controller.signal });
if (!res.ok) throw new Error(`HTTP ${res.status}`);
setData(await res.json());
} catch (err) {
if (err.name !== 'AbortError') setError(err.message);
} finally { setLoading(false); }
};
fetchData();
return () => controller.abort();
}, [url]);
return { data, loading, error };
}
// Sử dụng
function Users() {
const { data: users, loading, error } = useFetch('/api/users');
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error}</p>;
return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
}2. useLocalStorage
function useLocalStorage(key, initialValue) {
const [value, setValue] = useState(() => {
try {
const saved = localStorage.getItem(key);
return saved ? JSON.parse(saved) : initialValue;
} catch { return initialValue; }
});
useEffect(() => {
localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
return [value, setValue];
}
// Sử dụng
function Settings() {
const [theme, setTheme] = useLocalStorage('theme', 'light');
const [lang, setLang] = useLocalStorage('lang', 'vi');
return (
<div>
<select value={theme} onChange={e => setTheme(e.target.value)}>
<option value="light">Light</option>
<option value="dark">Dark</option>
</select>
</div>
);
}3. useDebounce
function useDebounce(value, delay = 500) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const timer = setTimeout(() => setDebouncedValue(value), delay);
return () => clearTimeout(timer);
}, [value, delay]);
return debouncedValue;
}
// Sử dụng: search không gọi API mỗi keystroke
function Search() {
const [query, setQuery] = useState('');
const debouncedQuery = useDebounce(query, 300);
const { data } = useFetch(
debouncedQuery ? `/api/search?q=${debouncedQuery}` : null
);
return (
<div>
<input value={query} onChange={e => setQuery(e.target.value)} />
{data?.map(item => <p key={item.id}>{item.name}</p>)}
</div>
);
}4. useClickOutside & useMediaQuery
// Detect click outside element
function useClickOutside(ref, handler) {
useEffect(() => {
const listener = (e) => {
if (!ref.current || ref.current.contains(e.target)) return;
handler(e);
};
document.addEventListener('mousedown', listener);
return () => document.removeEventListener('mousedown', listener);
}, [ref, handler]);
}
// Responsive hook
function useMediaQuery(query) {
const [matches, setMatches] = useState(
() => window.matchMedia(query).matches
);
useEffect(() => {
const media = window.matchMedia(query);
const handler = (e) => setMatches(e.matches);
media.addEventListener('change', handler);
return () => media.removeEventListener('change', handler);
}, [query]);
return matches;
}
// Sử dụng
function App() {
const isMobile = useMediaQuery('(max-width: 768px)');
return isMobile ? <MobileLayout /> : <DesktopLayout />;
}📝 Tóm Tắt
- Custom hooks = function bắt đầu bằng
use, tái sử dụng logic useFetch: fetch data + loading/error statesuseLocalStorage: persist state vào localStorageuseDebounce: trì hoãn value cho search/inputuseClickOutside,useMediaQuery: DOM interactions