← Về danh sách bài họcBài 16/25

🪝 Bài 16: Custom Hooks - Tạo Hooks Riêng

⏱️ Thời gian đọc: 15 phút | 📚 Độ khó: Trung bình

🎯 Sau bài học này, bạn sẽ:

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