Хуки — то, чем компонент остаётся про отображение: логику выносят в хуки, а компонент их вызывает. Большинство хуков просты, а вот useEffect — источник самых частых багов на frontend, потому что его применяют там, где он не нужен. Понять хуки — значит уметь извлекать логику и не злоупотреблять эффектами.

Правила хуков

Два правила, нарушение которых ломает React. Хуки вызывают только (1) на верхнем уровне компонента или другого хука — не в условиях, циклах, вложенных функциях; (2) из React-функций — компонентов и кастомных хуков, не из обычных функций.

// неверно: хук под условием — порядок вызовов «поплывёт»
if (open) { const [x, setX] = useState(0); }

// верно: хук на верхнем уровне, условие внутри
const [x, setX] = useState(0);
if (open) { /* используем x */ }

React опирается на стабильный порядок вызовов хуков между рендерами — отсюда правила. Линтер (eslint-plugin-react-hooks) ловит нарушения; его держат включённым.

Кастомные хуки

Кастомный хук — функция с именем use..., которая собирает логику из других хуков. Это способ переиспользовать логику без дублирования и без композиции компонентов ради одной логики.

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);
  const { data } = useSearch(debouncedQuery);
  // ...
}

Логика дебаунса вынесена и переиспользуема; компонент остаётся про вид. Кастомные хуки компонуются — один зовёт другой, как useSearch мог бы звать TanStack Query.

Ловушки useEffect

useEffect синхронизирует компонент с внешним миром (подписки, таймеры, ручная работа с DOM). Три ловушки.

Зависимости. Массив зависимостей перечисляет всё, что эффект читает; забыл — эффект работает на устаревших значениях. Линтер подсказывает, и его правке стоит доверять, а не «затыкать» массив вручную.

Гонки. Если эффект грузит данные, быстрый повтор даёт устаревший ответ поверх свежего. Защита — флаг отмены в очистке:

useEffect(() => {
  let cancelled = false;
  fetchUser(id).then((u) => { if (!cancelled) setUser(u); });
  return () => { cancelled = true; };
}, [id]);

Очистка. Подписки, таймеры, слушатели закрывают в функции, возвращаемой из эффекта, — иначе утечки.

Когда useEffect НЕ нужен

Самое важное: большинство useEffect, которые пишут новички, не нужны. Признаки лишнего эффекта:

  • Производное значение — не эффект + state, а вычисление при рендере: const fullName = first + " " + last; (не useEffect, обновляющий fullName).
  • Реакция на событие — логика идёт в обработчик события, не в эффект, реагирующий на изменение состояния.
  • Загрузка серверных данных — это TanStack Query, а не useEffect+useState (см. ловушку гонок выше — Query решает её сам).

Правило: прежде чем писать useEffect, спроси, не вычисляется ли это при рендере и не место ли этому в обработчике. Эффект — для синхронизации с внешним миром, не для реакции на собственное состояние.

Где это в UCP

Хуки — это организация логики frontend: правила держат React предсказуемым, кастомные хуки выносят и переиспользуют логику (как Handler выносит сценарий из контроллера), а дисциплина «useEffect только для внешней синхронизации» убирает целый класс багов. Для продукт-инженера это меньше скрытых ошибок и более читаемые компоненты — а то, каким состоянием хуки управляют, разобрано отдельно.