До 2019 года логику в React можно было переиспользовать только через сложные конструкции вроде render-пропсов и компонентов высшего порядка. Это превращало компоненты в трёхуровневые вложения ради одного общего кусочка поведения. Хуки решили эту проблему: теперь любую логику с состоянием можно вынести в обычную функцию и звать её из любого компонента.
Правила хуков
Хуки — это функции, имя которых начинается с use. React знает о них благодаря двум жёстким правилам.
Первое правило: только на верхнем уровне. Хук нельзя вызывать внутри условия, цикла или вложенной функции.
// неверно: порядок вызовов «поплывёт» при каждом рендере
if (isOpen) {
const [value, setValue] = useState("");
}
// верно: хук вызывается всегда, условие — внутри
const [value, setValue] = useState("");
if (isOpen) {
// используем value
}
React запоминает состояние каждого хука по его порядковому номеру в списке вызовов. Если порядок меняется от рендера к рендеру, React теряет, какое состояние принадлежит какому хуку.
Второе правило: только из React-функций. Хуки вызывают из компонентов и из других кастомных хуков — не из обычных вспомогательных функций.
Линтер-плагин eslint-plugin-react-hooks ловит нарушения обоих правил. Его держат включённым и не «затыкают» предупреждения.
Кастомные хуки
Кастомный хук — это обычная функция с именем use..., внутри которой вызываются другие хуки. Она ничем не отличается технически, но позволяет упаковать повторяющуюся логику в одно место.
Например, задержка перед поиском (дебаунс) нужна в нескольких местах приложения. Без хука этот useEffect придётся копировать в каждый компонент. С хуком:
function useDebounced<T>(value: T, delayMs: number): T {
const [debounced, setDebounced] = useState(value);
useEffect(() => {
const id = setTimeout(() => setDebounced(value), delayMs);
return () => clearTimeout(id);
}, [value, delayMs]);
return debounced;
}
Теперь компонент просто пользуется хуком, не зная о деталях:
function Search() {
const [query, setQuery] = useState("");
const debouncedQuery = useDebounced(query, 300);
// debouncedQuery обновится не сразу, а через 300 мс после ввода
}
Кастомные хуки хорошо компонуются: один может вызывать другой. Логика не дублируется, компонент остаётся про отображение.
useEffect: для чего он на самом деле
useEffect позволяет синхронизировать компонент с чем-то внешним: подпиской, таймером, ручной работой с DOM, внешним хранилищем. Вот типичный пример — подписка на событие браузера:
useEffect(() => {
function handleResize() {
setWidth(window.innerWidth);
}
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
}, []); // пустой массив — эффект запустится один раз при монтировании
Функция, которую возвращает эффект — это очистка. Она вызывается перед следующим запуском эффекта и при размонтировании компонента. Без очистки подписки и таймеры накапливаются.
Зависимости: почему массив важен
Второй аргумент useEffect — массив зависимостей. React запускает эффект заново, когда хоть одно значение из массива изменилось.
useEffect(() => {
document.title = `Привет, ${name}`;
}, [name]); // эффект запустится снова, только если name изменился
Если забыть указать переменную в массиве, эффект будет работать с устаревшим значением. Линтер предупреждает об этом — и его правке стоит доверять, а не добавлять // eslint-disable-line вручную.
Гонки при загрузке данных
Если компонент загружает данные в useEffect и пользователь быстро меняет входные данные, может прийти ответ на старый запрос поверх уже полученного свежего. Это называется гонкой запросов.
Стандартная защита — флаг отмены в функции очистки:
useEffect(() => {
let cancelled = false;
fetchUser(userId).then((user) => {
if (!cancelled) setUser(user);
});
return () => {
cancelled = true;
};
}, [userId]);
Когда userId меняется, React запускает очистку предыдущего эффекта (cancelled = true) прежде, чем запустить следующий. Старый ответ придёт, но не попадёт в состояние.
Когда useEffect не нужен
Самая частая ошибка — использовать useEffect там, где он не нужен. Три таких случая:
Производное значение. Если новое значение вычисляется из уже имеющихся данных, просто считайте его при рендере:
// не нужен эффект
const fullName = firstName + " " + lastName;
Эффект с useState сделает то же самое, но с лишним рендером и лишним кодом.
Реакция на событие. Если что-то должно произойти в ответ на нажатие кнопки или отправку формы — поместите логику в обработчик события, а не в эффект, который следит за изменением состояния.
Загрузка серверных данных. Ручной useEffect для запроса данных с сервера — это примерно десять строк кода с неполной защитой от гонок, без кэширования и без состояния загрузки. Библиотеки вроде TanStack Query решают эту задачу надёжно и компактно.
Простой вопрос перед написанием эффекта: «это синхронизация с внешним миром или реакция на собственное состояние?» Если второе — эффект не нужен.
Коротко
- Хуки — функции с именем
use..., которые позволяют использовать состояние и побочные эффекты в функциональных компонентах. - Два правила: вызывать только на верхнем уровне и только из React-функций. Нарушение ломает порядок вызовов.
- Кастомный хук упаковывает повторяющуюся логику в одну функцию — без дублирования по компонентам.
useEffectнужен для синхронизации с внешним миром: подписки, таймеры, DOM. Не для реакции на собственное состояние.- Функция очистки в
useEffectобязательна, если создаёте подписку или таймер — иначе утечки. - Массив зависимостей должен включать всё, что эффект читает. Линтер помогает это контролировать.
- При загрузке данных в эффекте защищайтесь от гонок через флаг отмены.
- Большинство
useEffectу новичков — лишние: производные значения считают при рендере, реакцию на события кладут в обработчики.
Что почитать дальше
- Компоненты и пропсы — как устроена основная единица React.
- Состояние: локальное и серверное — какие данные стоит хранить в
useState, а какие — в библиотеке. - Загрузка данных — TanStack Query как замена ручному
useEffectдля запросов.