ReactJuniorTechnical

Объясните правила хуков. Почему хуки нельзя вызывать условно?

Хуки нельзя вызывать условно или в циклах, потому что 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.

Sources

Related topics