Объясните правила хуков. Почему хуки нельзя вызывать условно?
Хуки нельзя вызывать условно или в циклах, потому что React идентифицирует их по порядку вызовов в Fiber-узле. Нарушение порядка приводит к тому, что React возвращает данные не того хука.
Правила хуков в React
React Hooks подчиняются двум жёстким правилам, нарушение которых приводит к труднодиагностируемым багам или ошибке времени выполнения.
Правило 1: вызывать хуки только на верхнем уровне
Хуки нельзя вызывать внутри условий, циклов, вложенных функций или после оператора return. Они должны вызываться всегда в одном и том же порядке при каждом рендере.
// НЕПРАВИЛЬНО
function BadComponent({ isLoggedIn }: { isLoggedIn: boolean }) {
if (isLoggedIn) {
const [user, setUser] = useState(null); // условный вызов — ошибка
}
for (let i = 0; i < 3; i++) {
useEffect(() => {}); // хук в цикле — ошибка
}
}
// ПРАВИЛЬНО
function GoodComponent({ isLoggedIn }: { isLoggedIn: boolean }) {
const [user, setUser] = useState(null); // всегда вызывается
useEffect(() => {
if (isLoggedIn) {
fetchUser().then(setUser); // условие ВНУТРИ хука
}
}, [isLoggedIn]);
return isLoggedIn ? <Profile user={user} /> : <Login />;
}
Правило 2: вызывать хуки только из функциональных компонентов или кастомных хуков
Нельзя вызывать хуки из обычных JavaScript-функций, классовых компонентов, обработчиков событий или таймеров. Кастомный хук — это функция, имя которой начинается с use, и внутри неё хуки разрешены.
// Кастомный хук — правильное место для хуков вне компонента
function useWindowWidth() {
const [width, setWidth] = useState(window.innerWidth);
useEffect(() => {
const handler = () => setWidth(window.innerWidth);
window.addEventListener('resize', handler);
return () => window.removeEventListener('resize', handler);
}, []);
return width;
}
// Использование в компоненте
function ResponsiveLayout() {
const width = useWindowWidth(); // OK: вызов в компоненте
return <div>Ширина: {width}px</div>;
}
// НЕПРАВИЛЬНО: вызов хука вне компонента / кастомного хука
function regularFunction() {
const [value, setValue] = useState(0); // ошибка
}
Почему нельзя вызывать хуки условно — механика React
React хранит state и эффекты каждого компонента в связном списке («hooks list» внутри Fiber-узла). Каждый вызов хука при рендере ставится в очередь по порядку вызова. React не хранит имена хуков — только их порядковые номера (слоты).
При следующем рендере React ожидает получить хуки в точно таком же порядке, чтобы вернуть каждому хуку его данные из предыдущего рендера. Если порядок нарушается — React достаёт не тот state и выдаёт ошибку.
// Представим внутреннее состояние React (упрощённо):
// Рендер 1 (isLoggedIn = true):
// slot[0] = useState(null) → user state
// slot[1] = useEffect(...) → fetch effect
// Рендер 2 (isLoggedIn = false, если useState в if):
// slot[0] = useEffect(...) → React думает, что это user state! БАГИ!
// React явно бросает ошибку:
// "React has detected a change in the order of Hooks called by BadComponent."
Именно поэтому порядок должен быть детерминированным: не «может быть 2 хука», а «всегда ровно 2 хука», независимо от условий.
ESLint-плагин для автоматической проверки
npm install eslint-plugin-react-hooks --save-dev
// .eslintrc.js
module.exports = {
plugins: ['react-hooks'],
rules: {
'react-hooks/rules-of-hooks': 'error', // нарушение правил — ошибка
'react-hooks/exhaustive-deps': 'warn', // пропущенные зависимости — предупреждение
},
};
Подводные камни
- Ранний return до вызова хуков — даже ранний выход из компонента нарушает правило, если хуки объявлены после него. Все хуки должны быть до первого return.
- Хук в try/catch — если хук бросает исключение при первом рендере, следующий рендер получит сдвинутый список. Хуки внутри try-блоков неявно нарушают порядок вызовов при ошибках.
- Имя кастомного хука не начинается с use — ESLint-плагин и React не смогут отличить его от обычной функции и не будут проверять правила внутри неё. Всегда называйте
useSomething. - Вызов хука в обработчике события — частая ошибка новичков:
onClick={() => useState(0)}. Хуки нельзя вызывать внутри event handlers, только в теле компонента. - Хуки в классовых компонентах — технически невозможно (бросает ошибку), но при миграции разработчики иногда пытаются импортировать хук и вызвать его в методе. Используйте HOC или render prop для интеграции хуков с классами.
- Динамическое количество кастомных хуков — если вы генерируете список хуков через map (например,
fields.map(f => useField(f))), это тоже нарушение. Вместо этого хук должен принимать массив и управлять им внутри себя.
Common mistakes
- Звать хук внутри if/for/early-return.
- Звать хук из обычной функции, не из компонента или кастомного хука.
- Полагать, что
use(...)отличается от других хуков и его можно условно.
What the interviewer is testing
- Может ли объяснить устройство связанного списка хуков.
- Знает ли, что
use()— тоже хук. - Применяет ли ESLint-правило
react-hooks.