Как реализовать собственный хук (custom hook) и в каких случаях это нужно?
В React custom hook (пользовательский хук) — это функция, имя которой начинается с use, и которая использует внутри себя другие хуки (useState, useEffect, useRef, useMemo и т.д.). Они позволяют выносить повторяющуюся логику из компонентов в переиспользуемые функции, сохраняя при этом доступ к жизненному циклу компонента и реактивности.
Когда нужно создавать custom hook
-
Повторяющаяся логика в разных компонентах
Например, одинаковая логика для подписки на события, работы с формами, API-запросами, debounce и т.п. -
Абстрагирование сложной логики
Вынос сложных и плохо читаемых блоков useEffect, useState, таймеров и т.д. в изолированные функции. -
Упрощение и декомпозиция компонента
Чтобы основной компонент не был перегружен логикой, а лишь отображал результат. -
Инкапсуляция доступа к внешним данным или API
Например, useUser, useProducts, useAuth, useTheme и т.д.
Как создать кастомный хук — базовая структура
import { useState, useEffect } from 'react';
function useWindowWidth() {
const \[width, setWidth\] = useState(window.innerWidth);
useEffect(() => {
const handleResize = () => setWidth(window.innerWidth);
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, \[\]);
return width;
}
Использование в компоненте:
function MyComponent() {
const width = useWindowWidth();
return <p>Ширина окна: {width}px</p>;
}
Примеры полезных custom hooks
1. usePrevious — для хранения предыдущего значения
import { useRef, useEffect } from 'react';
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
}, \[value\]);
return ref.current;
}
2. useDebounce — отложенное обновление значения
import { useState, useEffect } from 'react';
function useDebounce(value, delay = 500) {
const \[debouncedValue, setDebouncedValue\] = useState(value);
useEffect(() => {
const id = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => clearTimeout(id);
}, \[value, delay\]);
return debouncedValue;
}
Использование:
const searchTerm = useDebounce(inputValue, 300);
3. useLocalStorage — хук для работы с localStorage
import { useState } from 'react';
function useLocalStorage(key, initialValue) {
const \[value, setValue\] = useState(() => {
try {
const item = localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch {
return initialValue;
}
});
const setStoredValue = (newValue) => {
try {
const valueToStore = newValue instanceof Function ? newValue(value) : newValue;
setValue(valueToStore);
localStorage.setItem(key, JSON.stringify(valueToStore));
} catch {}
};
return \[value, setStoredValue\];
}
Использование:
const \[theme, setTheme\] = useLocalStorage('theme', 'light');
Особенности и правила custom хуков
-
Имена всегда начинаются с use — это нужно, чтобы React распознал функцию как хук и применил правила работы с хуками.
-
Хуки можно вызывать только на верхнем уровне функции — нельзя вызывать внутри if, for, try/catch, вложенных функций и т.д.
-
Custom hook может возвращать что угодно — массив, объект, примитивы. Главное — контракт между автором и потребителем.
Пример сложного хука: useFetch
import { useState, useEffect } from 'react';
function useFetch(url, options = {}) {
const \[data, setData\] = useState(null);
const \[error, setError\] = useState();
const \[loading, setLoading\] = useState(true);
useEffect(() => {
let isMounted = true;
setLoading(true);
fetch(url, options)
.then((res) => {
if (!res.ok) throw new Error('Ошибка запроса');
return res.json();
})
.then((data) => {
if (isMounted) setData(data);
})
.catch((err) => {
if (isMounted) setError(err);
})
.finally(() => {
if (isMounted) setLoading(false);
});
return () => {
isMounted = false;
};
}, \[url\]);
return { data, error, loading };
}
Использование:
const { data, error, loading } = useFetch('/api/products');
Комбинирование хуков внутри custom hook
В одном custom hook можно использовать любые другие хуки — useState, useEffect, useRef, useMemo, useCallback, а также другие custom hooks.
function useCustomThing() {
const value = useLocalStorage('key', 'default');
const debounced = useDebounce(value, 300);
return debounced;
}
Где хранить custom hooks
-
В каталоге hooks/ или src/hooks
-
Один хук — один файл: useScrollPosition.js, useDarkMode.ts, useClipboard.js
-
Если хук связан с доменом (например, useCart в интернет-магазине), можно хранить в features/cart/hooks/
Польза custom hooks
-
Повышают читаемость компонентов
-
Обеспечивают повторное использование логики
-
Упрощают тестирование (тестируются отдельно от UI)
-
Делают проект более масштабируемым и поддерживаемым
Возможности TypeScript с custom hooks
Типизация хуков позволяет чётко описывать входные и выходные данные:
function useCounter(initial: number = 0): \[number, () => void\] {
const \[count, setCount\] = useState<number>(initial);
const increment = () => setCount(c => c + 1);
return \[count, increment\];
}
Нейминг
-
useForm, useAuth, useTimer, useDarkMode, usePagination, useIntersectionObserver
-
Название должно точно отражать логику или поведение хука
-
Если используется несколько хуков — начинай с глагола: useFetchPosts, useToggleModal, useHandleScroll
Custom hooks — это мощный инструмент React, позволяющий создавать чистую, переиспользуемую и модульную логику приложения без зависимости от UI-компонентов.