В каком порядке вызываются хуки в компоненте?
В React хуки вызываются в строгом порядке сверху вниз, в точном соответствии с их расположением внутри тела функционального компонента. Этот порядок важен, потому что React связывает внутренние структуры состояния и эффектов по порядковому индексу в списке вызовов хуков. Нарушение порядка приводит к ошибкам и нестабильной работе компонента.
1. Общий принцип: одинаковый порядок хуков при каждом рендере
React использует внутренний массив хуков, где каждый вызов useState, useEffect и других регистрируется под определённым индексом. При следующем рендере он снова проходит по хукам в том же порядке и «знает», какие значения соответствуют какому хуку. Именно поэтому:
-
Нельзя вызывать хуки внутри условий, циклов, вложенных функций.
-
Нельзя вызывать хуки динамически, например, внутри if, for, switch.
Пример корректного порядка вызовов:
function MyComponent() {
const \[count, setCount\] = useState(0); // хук №1
const \[name, setName\] = useState(''); // хук №2
const memoizedValue = useMemo(() => count \* 2, \[count\]); // хук №3
useEffect(() => {
document.title = name;
}, \[name\]); // хук №4
return <div>{name}: {count}</div>;
}
Этот порядок должен сохраняться при любом рендере. Нарушение вызывает ошибку:
"Rendered fewer hooks than expected" или
"Rendered more hooks than during the previous render."
2. Что происходит при рендере
Во время рендера React:
-
Запускает компонент и начинает выполнение его тела.
-
Последовательно регистрирует хуки (useState, useEffect, useRef, и т.д.).
-
При этом он не «запоминает» имена переменных — только порядок вызовов.
-
Хранилище значений хуков (называется hook state list) индексируется по порядку вызова.
3. Почему порядок критичен
Рассмотрим некорректный пример:
function MyComponent({ show }) {
const \[count, setCount\] = useState(0); // хук №1
if (show) {
const \[name, setName\] = useState(''); // ❌ хук может быть №2 или не вызван
}
const \[age, setAge\] = useState(0); // хук №2 или №3?
return <div>{count}</div>;
}
В этом случае при show = true хук useState('') вызовется, а при show = false — нет. Это нарушает стабильность индексирования хуков и приводит к непредсказуемому поведению или ошибке во время рендера.
4. Разрешённый порядок вызова хуков
Хуки можно вызывать:
-
На верхнем уровне тела компонента
-
В порядке, не изменяющемся от рендера к рендеру
-
В пользовательских хуках (custom hooks), при условии, что они тоже соблюдают правила
Пример пользовательского хука:
function useUserData(id) {
const \[user, setUser\] = useState(null); // хук №1 внутри хука
useEffect(() => {
fetch(\`/api/user/${id}\`)
.then(res => res.json())
.then(setUser);
}, \[id\]); // хук №2 внутри хука
return user;
}
function MyComponent({ id }) {
const user = useUserData(id); // хук №1 в компоненте (вызов другого хука)
const \[theme, setTheme\] = useState('light'); // хук №2 в компоненте
return <div>{user?.name}</div>;
}
Порядок вызовов:
-
MyComponent вызывает useUserData — считается одним хуком.
-
Внутри useUserData вызываются свои хуки — они отслеживаются в отдельном контексте, не конфликтуют с внешними.
5. Влияние StrictMode на вызов хуков
В режиме разработки React StrictMode может двойной раз вызывать компонент при первом маунте, чтобы обнаружить побочные эффекты. Это касается только useEffect, useLayoutEffect, и не влияет на порядок хуков, но важно понимать, почему useEffect иногда вызывается дважды.
<React.StrictMode>
<App />
</React.StrictMode>
6. Взаимное расположение хуков
Все хуки (не только useState, но и useReducer, useEffect, useMemo, useCallback, useRef, useContext, useLayoutEffect и др.) учитываются в общем списке вызовов. То есть:
function MyComponent() {
const \[count, setCount\] = useState(0); // хук №1
const ref = useRef(null); // хук №2
const theme = useContext(ThemeContext); // хук №3
const memo = useMemo(() => count \* 2, \[count\]); // хук №4
useEffect(() => {
console.log(count);
}, \[count\]); // хук №5
}
React отслеживает все 5 хуков строго по позиции вызова. Если в каком-либо рендере один из них пропущен, остальные сдвигаются, и React начинает использовать неправильные значения — отсюда критическая важность последовательности.
7. Отладка порядка хуков
Для анализа порядка хуков можно использовать:
-
React DevTools — показывает дерево компонентов и состояние хуков
-
eslint-plugin-react-hooks — линтер, который проверяет:
-
Нарушение правил вызова хуков
-
Отсутствующие зависимости в useEffect, useCallback, useMemo
-
Настройка .eslintrc:
{
"plugins": \["react-hooks"\],
"rules": {
"react-hooks/rules-of-hooks": "error", // проверяет правильный вызов хуков
"react-hooks/exhaustive-deps": "warn" // проверяет зависимости эффектов
}
}
8. Что происходит при изменении количества хуков
Любое добавление или удаление вызова хука внутри компонента должно быть сделано так, чтобы не нарушался порядок. Нельзя оборачивать хуки в условия:
// ❌
if (someCondition) {
useEffect(() => {}, \[\]);
}
Допустимые альтернативы:
- Всегда вызывать хук, но условно выполнять действие внутри:
useEffect(() => {
if (!someCondition) return;
// ...
}, \[someCondition\]);
- Вынос логики в отдельный хук:
function useConditionalEffect(condition) {
useEffect(() => {
if (!condition) return;
// ...
}, \[condition\]);
}
- Вынос логики в другой компонент
9. Порядок при useMemo и useCallback
Эти хуки, хоть и возвращают мемоизированные значения/функции, тоже считаются хуками и регистрируются в общем списке. Их вызовы также должны быть стабильны по порядку. Даже если они возвращают что-то простое:
const expensiveCalc = useMemo(() => a + b, \[a, b\]);
Если вызвать useMemo в условии, нарушится порядок всех последующих хуков.
10. Влияние на производительность
Поскольку React проходит по хукам строго в порядке, порядок сам по себе не влияет на производительность. Однако, ошибки в порядке вызова могут вызывать повторные перерендеры, лишние вызовы эффектов, утечки памяти, и даже падения приложения. Поэтому важно не просто соблюдать порядок, а делать его предсказуемым и стабильным при любом рендере.