Правила и подводные камни
Хуки просты, но есть нюансы, на которых спотыкаются все.
Правило 1: только на верхнем уровне
Хуки вызываются в теле компонента или другого хука. Не внутри условий, циклов, вложенных функций.
// Плохо
function Component({ condition }) {
if (condition) {
const [value, setValue] = useState(0);
}
}
// Хорошо
function Component({ condition }) {
const [value, setValue] = useState(0);
if (condition) {
// использование
}
}
Правило 2: только из React-функций
Хук нельзя вызвать из обычной функции. Только из компонента или из другого хука.
// Плохо
function helper() {
const [x, setX] = useState(0); // ошибка
}
// Хорошо
function useHelper() {
const [x, setX] = useState(0);
return x;
}
Порядок вызовов
React хранит состояние хуков по порядку вызова. Порядок должен быть одинаковым при каждом рендере.
function Component() {
const [a, setA] = useState(0);
const [b, setB] = useState('');
const [c, setC] = useState(false);
// ...
}
Если между ними поставить условие — порядок собьётся.
Устаревшие значения в замыканиях
Функция захватывает значения из своего рендера. Если стейт изменился после создания функции — функция всё ещё видит старое значение.
function Component() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
console.log(count); // всегда 0
}, 1000);
return () => clearInterval(id);
}, []);
}
Решения:
- Добавить
countв зависимости - Использовать
useRefдля актуального значения - Использовать функциональную форму
setCount((c) => c + 1)
Объекты в зависимостях
Зависимости сравниваются по ссылке. Объект, созданный в теле компонента, меняется каждый рендер:
// Бесконечный цикл
useEffect(() => {
//
}, [{ id: 1 }]);
Решение — useMemo или вынести объект за пределы компонента.
Функции в зависимостях
Аналогично. Функция создаётся заново при каждом рендере:
const handler = () => console.log('click');
useEffect(() => {
//
}, [handler]); // каждый рендер
Решение — useCallback.
useState не обновляется сразу
setCount(count + 1);
console.log(count); // старое значение
Изменение состояния асинхронно. Новое значение доступно на следующем рендере.
useEffect и StrictMode
В dev-режиме React монтирует компоненты дважды. Эффекты выполняются дважды. Это нормально. Если эффект не идемпотентен — нужно исправить.
Тяжёлые вычисления в рендере
function Component({ items }) {
const sorted = items.sort(...); // плохо: мутация + тяжёлое вычисление
return <List items={sorted} />;
}
Решение — useMemo и копия массива:
const sorted = useMemo(() => [...items].sort(...), [items]);
Асинхронная функция в useEffect
useEffect не принимает async-функцию напрямую:
// Плохо
useEffect(async () => {
await load();
}, []);
Решение — определить функцию внутри и вызвать:
useEffect(() => {
async function load() {
await fetchData();
}
load();
}, []);
Итоги
- Только верхний уровень, только React-функции
- Порядок вызовов должен быть неизменным
- Следи за устаревшими значениями
- Объекты и функции в зависимостях — через мемоизацию
- StrictMode монтирует дважды — это норма