В каком порядке вызываются хуки в компоненте?

В 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 проходит по хукам строго в порядке, порядок сам по себе не влияет на производительность. Однако, ошибки в порядке вызова могут вызывать повторные перерендеры, лишние вызовы эффектов, утечки памяти, и даже падения приложения. Поэтому важно не просто соблюдать порядок, а делать его предсказуемым и стабильным при любом рендере.