ФАЗА 36. REACT Хуки

Обязательно

Хуки связывают состояние и синхронизацию эффектов

Rules · useEffect · Dependencies

Порядок хуков стабилен, кастомный хук переиспользует логику, useEffect синхронизирует внешние системы и очищает подписки.

1. Хуки вызываются в одном порядке

1 / 5

Только верхний уровень компонента или кастомного хука, без условий, циклов и обычных функций.

  • Условный return идёт после хуков.
  • Число вызовов стабильно.
  • React сопоставляет состояние по порядку.
useStateuseEffectСтабильный следующий render

2. Кастомный хук переиспользует поведение

2 / 5

Логика состояния и эффектов выносится в функцию с префиксом use, а UI остаётся в компоненте.

  • Каждый вызов имеет своё состояние.
  • Правила хуков сохраняются.
  • Возврат имеет ясный контракт.
Хук состоянияКастомный хукКомпонент

3. Effect синхронизирует внешнюю систему

3 / 5

Подписка, таймер или ручное DOM-взаимодействие создаётся после commit и очищается перед повтором и размонтированием.

  • Cleanup закрывает подписку.
  • Вычисление props не является эффектом.
  • Серверный рендер не выполняет effect.
SetupВнешняя системаCleanup

4. Зависимости описывают используемые значения

4 / 5

Эффект перезапускается при изменении реактивных значений. Пустой массив не делает устаревшее состояние актуальным.

  • Функции стабилизируются при необходимости.
  • Functional update берёт предыдущее state.
  • Линтер помогает найти пропуск.
EffectProps и stateСтабильные функцииМассив зависимостей

5. Асинхронная загрузка требует защиты от гонки

5 / 5

Старый запрос может завершиться позже нового. AbortController или флаг ignore не даёт устаревшему ответу заменить актуальное состояние.

  • Новый ввод начинает новый эффект.
  • Cleanup отменяет старую работу.
  • Многие вычисления не требуют useEffect.
Запрос AЗапрос BТолько актуальный результат

Хуки в React: правила, кастомные хуки и useEffect

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

До 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 решают эту задачу надёжно и компактно.

Простой вопрос перед написанием эффекта: «это синхронизация с внешним миром или реакция на собственное состояние?» Если второе — эффект не нужен.

Дополнительно: при первом чтении можно пропустить

Глубже: жизненный цикл, useLayoutEffect и редкие хукирасширенное

Вопрос про жизненный цикл компонента остался в собеседованиях со времён классов, и на него есть короткий перевод в хуки. Монтирование это первый рендер плюс эффекты с пустым массивом зависимостей, обновление это повторный рендер плюс эффекты, чьи зависимости изменились, размонтирование это функции очистки всех эффектов. componentDidMount и componentDidUpdate стали одним useEffect, componentWillUnmount стал его возвращаемой функцией, а shouldComponentUpdate превратился в React.memo.

useLayoutEffect отличается от useEffect только моментом запуска: он выполняется синхронно после изменения DOM и до того, как браузер нарисует кадр. Поэтому в нём измеряют размеры и позиции и сразу двигают элемент, чтобы пользователь не увидел скачок; всё остальное, запросы, подписки, логирование, живёт в useEffect, который кадр не задерживает. На сервере useLayoutEffect не выполняется и предупреждает об этом.

Три хука, о которых спрашивают отдельно. useRef хранит значение между рендерами, не вызывая перерисовку при изменении: ссылка на DOM-узел, идентификатор таймера, предыдущее значение пропса; обращаться к DOM через ref правильнее, чем через querySelector, потому что ссылка привязана к конкретному экземпляру компонента и исчезает вместе с ним. useReducer заменяет useState, когда следующее состояние зависит от предыдущего и от типа действия, например в форме с десятком связанных полей. useId даёт стабильный идентификатор для связки label и input, одинаковый на сервере и клиенте, чтобы не ломать гидрацию. Про useTransition и useDeferredValue, которые помечают обновление несрочным, рассказано в статье как React рисует.

Глубже: React 19 и компиляторрасширенное

Хуки не стоят на месте. Три вещи из React 19 и его окружения, которые меняют повседневный код:

  • ref как обычный проп. В функциональных компонентах ref приходит в props, и forwardRef больше не нужен:
function Input({ ref, ...props }: React.ComponentProps<"input">) {
  return <input ref={ref} {...props} />;
}
  • use(promise) читает промис прямо во время рендера и приостанавливает компонент до ответа, а ближайший Suspense показывает заглушку. В отличие от хуков, use можно звать внутри условий. Это не замена библиотекам запросов: кэш и инвалидация по-прежнему на них.
  • React Compiler мемоизирует компоненты и вычисления сам: ручные useMemo и useCallback «на всякий случай» перестают быть нормой и остаются точечной оптимизацией там, где компилятор не справился. Компилятор полагается на правила React: код, который их нарушает (мутация пропсов, хук в условии), он пропускает без оптимизации, а свежий eslint-plugin-react-hooks такие места подсвечивает.

Проверочный вопрос к существующему коду: сколько в нём useCallback, пользу которых никто не измерял? После компилятора их убирают по мере того, как трогают файл.

Коротко

  • Хуки — функции с именем use..., которые позволяют использовать состояние и побочные эффекты в функциональных компонентах.
  • Два правила: вызывать только на верхнем уровне и только из React-функций. Нарушение ломает порядок вызовов.
  • Кастомный хук упаковывает повторяющуюся логику в одну функцию — без дублирования по компонентам.
  • useEffect нужен для синхронизации с внешним миром: подписки, таймеры, DOM. Не для реакции на собственное состояние.
  • Функция очистки в useEffect обязательна, если создаёте подписку или таймер — иначе утечки.
  • Массив зависимостей должен включать всё, что эффект читает. Линтер помогает это контролировать.
  • При загрузке данных в эффекте защищайтесь от гонок через флаг отмены.
  • Большинство useEffect у новичков — лишние: производные значения считают при рендере, реакцию на события кладут в обработчики.
  • Жизненный цикл в хуках: монтирование это эффекты с пустым массивом, обновление это эффекты с изменившимися зависимостями, размонтирование это очистка; useLayoutEffect до кадра для измерений, useRef для значения без перерисовки, useReducer для связанных переходов, useId для идентификаторов, одинаковых на сервере и клиенте.

Что почитать дальше