Урок 10

Кастомные хуки

Кастомный хук — это функция, которая начинается с use и вызывает другие хуки. Позволяет вынести логику из компонента и переиспользовать её.

Простой хук

function useToggle(initial = false) {
    const [value, setValue] = useState(initial);

    const toggle = useCallback(() => setValue((v) => !v), []);
    const setTrue = useCallback(() => setValue(true), []);
    const setFalse = useCallback(() => setValue(false), []);

    return { value, toggle, setTrue, setFalse };
}

Использование:

function Modal() {
    const { value: isOpen, toggle, setFalse: close } = useToggle();

    return (
        <>
            <button onClick={toggle}>Открыть</button>
            {isOpen && (
                <div className="modal">
                    <button onClick={close}>Закрыть</button>
                </div>
            )}
        </>
    );
}

useLocalStorage

function useLocalStorage<T>(key: string, initialValue: T) {
    const [value, setValue] = useState<T>(() => {
        const stored = localStorage.getItem(key);
        return stored ? JSON.parse(stored) : initialValue;
    });

    useEffect(() => {
        localStorage.setItem(key, JSON.stringify(value));
    }, [key, value]);

    return [value, setValue] as const;
}

useDebounce

function useDebounce<T>(value: T, delay: number): T {
    const [debounced, setDebounced] = useState(value);

    useEffect(() => {
        const id = setTimeout(() => setDebounced(value), delay);
        return () => clearTimeout(id);
    }, [value, delay]);

    return debounced;
}

Использование:

function Search() {
    const [query, setQuery] = useState('');
    const debouncedQuery = useDebounce(query, 500);

    useEffect(() => {
        if (debouncedQuery) {
            // поиск
        }
    }, [debouncedQuery]);

    return <input value={query} onChange={(e) => setQuery(e.target.value)} />;
}

useFetch

function useFetch<T>(url: string) {
    const [data, setData] = useState<T | null>(null);
    const [loading, setLoading] = useState(true);
    const [error, setError] = useState<Error | null>(null);

    useEffect(() => {
        let cancelled = false;

        async function load() {
            setLoading(true);
            try {
                const res = await fetch(url);
                if (!res.ok) throw new Error(`HTTP ${res.status}`);
                const json = await res.json();
                if (!cancelled) setData(json);
            } catch (e) {
                if (!cancelled) setError(e as Error);
            } finally {
                if (!cancelled) setLoading(false);
            }
        }

        load();

        return () => {
            cancelled = true;
        };
    }, [url]);

    return { data, loading, error };
}

useMediaQuery

function useMediaQuery(query: string): boolean {
    const [matches, setMatches] = useState(() =>
        window.matchMedia(query).matches
    );

    useEffect(() => {
        const media = window.matchMedia(query);
        const handler = (e: MediaQueryListEvent) => setMatches(e.matches);

        media.addEventListener('change', handler);
        return () => media.removeEventListener('change', handler);
    }, [query]);

    return matches;
}

useClickOutside

function useClickOutside<T extends HTMLElement>(handler: () => void) {
    const ref = useRef<T>(null);

    useEffect(() => {
        function listener(e: MouseEvent) {
            if (ref.current && !ref.current.contains(e.target as Node)) {
                handler();
            }
        }

        document.addEventListener('mousedown', listener);
        return () => document.removeEventListener('mousedown', listener);
    }, [handler]);

    return ref;
}

Правила

  • Имя начинается с use
  • Внутри вызываются только хуки
  • Хук — не компонент, он не возвращает JSX
  • Хук можно вызывать только из компонента или другого хука
  • Логика хука не зависит от конкретного компонента

Итоги

  • Кастомные хуки выносят логику из компонентов
  • Имя начинается с use
  • Внутри — другие хуки
  • Переиспользуются между компонентами
  • Упрощают тестирование и поддержку