Menu

Правила хуков в React: почему важен порядок и как исправить

У хуков два правила: вызывайте их на верхнем уровне компонента, никогда внутри условий, циклов или вложенных функций, и вызывайте только из функциональных компонентов или пользовательских хуков. Почему React нужен одинаковый порядок вызовов при каждом рендере и как исправить ошибки, когда он меняется.

На этой странице есть исполняемые редакторы: меняйте, запускайте и сразу видите результат.

Правил хуков два: вызывайте хуки только на верхнем уровне компонента, никогда внутри условия, цикла, вложенной функции или после раннего return; и вызывайте их только из функциональных компонентов или пользовательских хуков. Они существуют потому, что React различает хуки по порядку вызова, и этот порядок должен быть одинаковым при каждом рендере.

React хранит значения хуков компонента в списке, привязанном к этому компоненту. При каждом рендере он проходит по списку: первый вызов хука получает первую ячейку, второй вызов вторую и так далее. Имён нет, только позиции. Пример ниже это игрушечная версия этой идеи в нескольких строках обычного JavaScript, а не собственный код React, поэтому его можно безопасно ломать.

Первый рендер заполняет три ячейки: Ada, Countess, 36. Нажмите «Render without nickname»: вызов по условию пропускается, поэтому вызов age занимает ячейку 1 и читает Countess. Настоящий React в одних случаях замечает несоответствие и выбрасывает ошибку, а в других передаёт неправильное значение неправильному хуку, что хуже. Перенесите вызов toyUseState('Countess') выше if, и обе кнопки дадут правильный возраст.

Два правила в коде

function Profile({ user }) {
    // Good: always called, always in this order
    const [tab, setTab] = useState('posts');
    const theme = useContext(ThemeContext);
    useEffect(() => {
        document.title = user.name;
    }, [user.name]);

    // Bad: inside a condition, a loop or a nested function
    if (user.isAdmin) {
        const [log, setLog] = useState([]); // breaks the order
    }
}

Вызов хука, до которого выполнение доходит всегда и в одном и том же порядке, допустим в любом месте тела, если он стоит до любого раннего return.

Исправление хука, вызванного по условию

Линтер сообщает об этом как React Hook "useEffect" is called conditionally. React Hooks must be called in the exact same order in every component render. Исправление всегда одно и то же: вызывайте хук каждый раз, а условие помещайте внутрь него.

// Broken: the effect exists on some renders and not others
function Notifications({ user }) {
    if (user) {
        useEffect(() => {
            console.log('subscribe for', user.name);
        }, [user]);
    }
    return null;
}

Эффект объявляется при каждом рендере, а if внутри него решает, делать ли что-нибудь. Несколько раз войдите и выйдите, и Console покажет подписку на каждый вход и отписку на каждый выход.

Хуки после раннего return

Ранний return тоже условие. Этот код даёт "Rendered more hooks than during the previous render" в первый раз, когда loading становится false, потому что этот рендер доходит до хука, который предыдущий никогда не вызывал. Когда рендер вызывает меньше хуков, чем предыдущий, сообщение будет "Rendered fewer hooks than expected".

// Broken: useState is skipped while loading
function Comments({ loading, comments }) {
    if (loading) return <p>Loading...</p>;
    const [expanded, setExpanded] = useState(false);
    return <List items={comments} expanded={expanded} />;
}

Каждый хук стоит выше return, поэтому обе ветки вызывают одни и те же хуки. Нажмите «Toggle loading», раскройте список, затем дважды переключите загрузку: список помнит, что был раскрыт, потому что состояние никогда не пропускалось.

Хуки в цикле

Нельзя вызывать useState по разу на каждый элемент в цикле, потому что число элементов меняется. Дайте каждому элементу собственный компонент и вызывайте хук там; у каждого экземпляра свои ячейки.

«Invalid hook call»

Сообщение начинается с "Invalid hook call. Hooks can only be called inside of the body of a function component." У него три обычные причины:

  1. Хук выполняется, когда React не рендерит функциональный компонент. В обработчике события, внутри колбэка эффекта, в классовом компоненте или на уровне модуля. Перенесите его в тело компонента или в пользовательский хук и вызывайте пользовательский хук из компонента.
  2. Версии react и react-dom не совпадают. Установите одну и ту же версию обоих.
  3. Есть две копии React. Связанный локальный пакет или библиотека, которая включает собственный React, приносят вторую копию, а хуки из одной копии не работают с рендерером другой. Выполните npm ls react и убедитесь, что появляется только одна версия.
npm ls react

Превью здесь работает как продакшен-сборка, которая не выводит эти сообщения: хук, вызванный из обработчика события, падает с обычным TypeError вроде "Cannot read properties of null", а собственные ошибки React приходят как короткие коды "Minified React error". Чтобы прочитать полный текст, запускайте приложение в режиме разработки.

Пусть правила проверяет линтер

eslint-plugin-react-hooks проверяет оба правила по мере ввода, а также проверяет массивы зависимостей эффектов. Его включают шаблон React для Vite и конфигурация ESLint в Next.js. В плоской конфигурации ESLint включите его рекомендуемый пресет:

// eslint.config.js
import reactHooks from 'eslint-plugin-react-hooks';
import { defineConfig } from 'eslint/config';

export default defineConfig([reactHooks.configs.flat.recommended]);

Два основных правила это react-hooks/rules-of-hooks (ошибка) и react-hooks/exhaustive-deps (предупреждение). Начиная с версии 7 пресет также включает правила, пришедшие из React Compiler, например react-hooks/purity и react-hooks/refs, которые отмечают нечистые рендеры и чтение рефов во время рендера. Чтобы выбирать правила по одному, зарегистрируйте плагин как plugins: { 'react-hooks': reactHooks } и перечислите нужные правила.

Плагин находит хуки по имени: любая функция, имя которой начинается с use, за которым идёт заглавная буква, считается хуком, и вызывать её могут только компоненты (имена с заглавной буквы) и другие функции use. Поэтому пользовательские хуки должны использовать этот префикс.

Исключение: use

use, добавленный в React 19, читает промис или контекст и, в отличие от всех остальных хуков, может вызываться внутри условия или цикла. Но вызывать его всё равно нужно во время рендера, внутри компонента или хука.

При смене темы цвет меняет только заголовок с темой. Попробуйте то же самое с useContext внутри if, и линтер это отметит. Чтение промисов через него разбирает страница о хуке use.

Часто задаваемые вопросы

Каковы правила хуков?

Их два. Вызывайте хуки только на верхнем уровне функционального компонента или пользовательского хука, никогда внутри условий, циклов, вложенных функций или после раннего return. И вызывайте хуки только из функциональных компонентов или пользовательских хуков, а не из обычных функций или классовых компонентов.

Почему хуки нельзя вызывать по условию?

React не знает ваши хуки по имени. Он хранит их значения в списке и возвращает их в порядке вызовов. Если условие пропускает вызов, каждый следующий хук читает значение, которое принадлежало хуку перед ним.

Что означает "Rendered more hooks than during the previous render"?

Рендер вызвал больше хуков, чем предыдущий, обычно потому, что хук стоит внутри if или после раннего return, который на этот раз сработал иначе. Перенесите каждый хук выше условия, а условие поместите внутрь хука.

Как исправить "Invalid hook call"?

Проверьте три вещи: хук выполняется, пока React рендерит функциональный компонент (не в обработчике события, колбэке эффекта, классовом компоненте или на уровне модуля), версии react и react-dom совпадают, и в приложении только одна копия React (npm ls react их перечисляет).

Можно ли вызывать хук use по условию?

Да. use (React 19) единственное исключение: его можно вызывать внутри блоков if и циклов. Но вызывать его всё равно нужно внутри компонента или хука, а не в обработчике события.

Иллюстрация языков программирования Coddy

Учитесь программировать с Coddy

НАЧАТЬ