Урок 11

Правила и подводные камни

Хуки просты, но есть нюансы, на которых спотыкаются все.

Правило 1: только на верхнем уровне

Хуки вызываются в теле компонента или другого хука. Не внутри условий, циклов, вложенных функций.

// Плохо
function Component({ condition }) {
    if (condition) {
        const [value, setValue] = useState(0);
    }
}

// Хорошо
function Component({ condition }) {
    const [value, setValue] = useState(0);

    if (condition) {
        // использование
    }
}

Правило 2: только из React-функций

Хук нельзя вызвать из обычной функции. Только из компонента или из другого хука.

// Плохо
function helper() {
    const [x, setX] = useState(0); // ошибка
}

// Хорошо
function useHelper() {
    const [x, setX] = useState(0);
    return x;
}

Порядок вызовов

React хранит состояние хуков по порядку вызова. Порядок должен быть одинаковым при каждом рендере.

function Component() {
    const [a, setA] = useState(0);
    const [b, setB] = useState('');
    const [c, setC] = useState(false);
    // ...
}

Если между ними поставить условие — порядок собьётся.

Устаревшие значения в замыканиях

Функция захватывает значения из своего рендера. Если стейт изменился после создания функции — функция всё ещё видит старое значение.

function Component() {
    const [count, setCount] = useState(0);

    useEffect(() => {
        const id = setInterval(() => {
            console.log(count); // всегда 0
        }, 1000);

        return () => clearInterval(id);
    }, []);
}

Решения:

  • Добавить count в зависимости
  • Использовать useRef для актуального значения
  • Использовать функциональную форму setCount((c) => c + 1)

Объекты в зависимостях

Зависимости сравниваются по ссылке. Объект, созданный в теле компонента, меняется каждый рендер:

// Бесконечный цикл
useEffect(() => {
    //
}, [{ id: 1 }]);

Решение — useMemo или вынести объект за пределы компонента.

Функции в зависимостях

Аналогично. Функция создаётся заново при каждом рендере:

const handler = () => console.log('click');

useEffect(() => {
    //
}, [handler]); // каждый рендер

Решение — useCallback.

useState не обновляется сразу

setCount(count + 1);
console.log(count); // старое значение

Изменение состояния асинхронно. Новое значение доступно на следующем рендере.

useEffect и StrictMode

В dev-режиме React монтирует компоненты дважды. Эффекты выполняются дважды. Это нормально. Если эффект не идемпотентен — нужно исправить.

Тяжёлые вычисления в рендере

function Component({ items }) {
    const sorted = items.sort(...); // плохо: мутация + тяжёлое вычисление
    return <List items={sorted} />;
}

Решение — useMemo и копия массива:

const sorted = useMemo(() => [...items].sort(...), [items]);

Асинхронная функция в useEffect

useEffect не принимает async-функцию напрямую:

// Плохо
useEffect(async () => {
    await load();
}, []);

Решение — определить функцию внутри и вызвать:

useEffect(() => {
    async function load() {
        await fetchData();
    }
    load();
}, []);

Итоги

  • Только верхний уровень, только React-функции
  • Порядок вызовов должен быть неизменным
  • Следи за устаревшими значениями
  • Объекты и функции в зависимостях — через мемоизацию
  • StrictMode монтирует дважды — это норма