Зачем кастомные хуки
▸Переиспользование логики
Кастомные хуки позволяют извлекать логику с состоянием и побочными эффектами из компонентов. Это делает код чище и переиспользуемым.
▸Правила хуков
Кастомные хуки начинаются с 'use'. Они могут вызывать другие хуки. Не вызывайте хуки условно.
useLocalStorage
▸Реализация
1function useLocalStorage<T>(key: string, initialValue: T): [T, (value: T | ((val: T) => T)) => void] {2 const [storedValue, setStoredValue] = useState<T>(() => {3 try {4 const item = window.localStorage.getItem(key);5 return item ? JSON.parse(item) : initialValue;6 } catch (error) {7 return initialValue;8 }9 });1011 const setValue = (value: T | ((val: T) => T)) => {12 try {13 const valueToStore = value instanceof Function ? value(storedValue) : value;14 setStoredValue(valueToStore);15 window.localStorage.setItem(key, JSON.stringify(valueToStore));16 } catch (error) {17 console.error(error);18 }19 };2021 return [storedValue, setValue];22}
▸Использование
1function Settings() {2 const [theme, setTheme] = useLocalStorage('theme', 'light');3 return (4 <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>5 Тема: {theme}6 </button>7 );8}
useDebounce
▸Реализация
1function useDebounce<T>(value: T, delay: number): T {2 const [debouncedValue, setDebouncedValue] = useState<T>(value);34 useEffect(() => {5 const handler = setTimeout(() => {6 setDebouncedValue(value);7 }, delay);89 return () => clearTimeout(handler);10 }, [value, delay]);1112 return debouncedValue;13}
▸Использование
1function Search() {2 const [query, setQuery] = useState('');3 const debouncedQuery = useDebounce(query, 300);45 useEffect(() => {6 if (debouncedQuery) {7 search(debouncedQuery);8 }9 }, [debouncedQuery]);1011 return <input value={query} onChange={e => setQuery(e.target.value)} />;12}
useFetch
▸Реализация
1function useFetch<T>(url: string) {2 const [data, setData] = useState<T | null>(null);3 const [loading, setLoading] = useState(true);4 const [error, setError] = useState<Error | null>(null);56 useEffect(() => {7 const controller = new AbortController();89 const fetchData = async () => {10 try {11 setLoading(true);12 const response = await fetch(url, { signal: controller.signal });13 if (!response.ok) throw new Error('Network error');14 const json = await response.json();15 setData(json);16 } catch (err) {17 if (err instanceof Error && err.name !== 'AbortError') {18 setError(err);19 }20 } finally {21 setLoading(false);22 }23 };2425 fetchData();26 return () => controller.abort();27 }, [url]);2829 return { data, loading, error };30}
▸Использование
1function UserProfile({ userId }) {2 const { data: user, loading, error } = useFetch(`/api/users/${userId}`);34 if (loading) return <Spinner />;5 if (error) return <Error message={error.message} />;6 return <div>{user.name}</div>;7}
useIntersectionObserver
▸Реализация
1function useIntersectionObserver(2 ref: RefObject<Element>,3 options?: IntersectionObserverInit4): boolean {5 const [isIntersecting, setIsIntersecting] = useState(false);67 useEffect(() => {8 const element = ref.current;9 if (!element) return;1011 const observer = new IntersectionObserver(12 ([entry]) => setIsIntersecting(entry.isIntersecting),13 options14 );1516 observer.observe(element);17 return () => observer.disconnect();18 }, [ref, options]);1920 return isIntersecting;21}
▸Использование
1function LazyImage({ src, alt }) {2 const ref = useRef<HTMLDivElement>(null);3 const isVisible = useIntersectionObserver(ref, { rootMargin: '100px' });45 return (6 <div ref={ref}>7 {isVisible && <img src={src} alt={alt} />}8 </div>9 );10}
useMediaQuery
▸Реализация
1function useMediaQuery(query: string): boolean {2 const [matches, setMatches] = useState(false);34 useEffect(() => {5 const media = window.matchMedia(query);6 setMatches(media.matches);78 const listener = (e: MediaQueryListEvent) => setMatches(e.matches);9 media.addEventListener('change', listener);10 return () => media.removeEventListener('change', listener);11 }, [query]);1213 return matches;14}
▸Использование
1function ResponsiveLayout() {2 const isMobile = useMediaQuery('(max-width: 768px)');3 return isMobile ? <MobileLayout /> : <DesktopLayout />;4}
useToggle
▸Реализация
1function useToggle(initialValue = false): [boolean, () => void, (value: boolean) => void] {2 const [value, setValue] = useState(initialValue);3 const toggle = useCallback(() => setValue(v => !v), []);4 return [value, toggle, setValue];5}
▸Использование
1function Modal() {2 const [isOpen, toggleOpen] = useToggle(false);3 return (4 <>5 <button onClick={toggleOpen}>Открыть</button>6 {isOpen && <Modal onClose={toggleOpen} />}7 </>8 );9}
usePrevious
▸Реализация
1function usePrevious<T>(value: T): T | undefined {2 const ref = useRef<T>();34 useEffect(() => {5 ref.current = value;6 }, [value]);78 return ref.current;9}
▸Использование
1function Counter() {2 const [count, setCount] = useState(0);3 const prevCount = usePrevious(count);45 return (6 <div>7 <p>Сейчас: {count}, Было: {prevCount}</p>8 <button onClick={() => setCount(c => c + 1)}>+</button>9 </div>10 );11}
useWindowSize
▸Реализация
1function useWindowSize() {2 const [size, setSize] = useState({3 width: window.innerWidth,4 height: window.innerHeight,5 });67 useEffect(() => {8 const handleResize = () => {9 setSize({ width: window.innerWidth, height: window.innerHeight });10 };1112 window.addEventListener('resize', handleResize);13 return () => window.removeEventListener('resize', handleResize);14 }, []);1516 return size;17}
Best Practices
▸Именование
Называйте хуки с префиксом 'use'. Имя должно описывать что хук делает: useFetch, useDebounce, useLocalStorage.
▸Единая ответственность
Каждый хук должен решать одну задачу. Не создавайте монстров-хуков.
▸Тестирование
1import { renderHook, act } from '@testing-library/react';23test('useCounter increments', () => {4 const { result } = renderHook(() => useCounter(0));5 act(() => result.current.increment());6 expect(result.current.count).toBe(1);7});
Заключение
Кастомные хуки — мощный механизм переиспользования логики в React. Выносите общую логику в хуки, делайте их переиспользуемыми и тестируемыми. Библиотека хуков — ценный актив вашего проекта.