Кастомные хуки
Кастомный хук — это функция, которая начинается с 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 - Внутри — другие хуки
- Переиспользуются между компонентами
- Упрощают тестирование и поддержку