← назад к разделу

До 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 для запросов.