Урок 03

useEffect: подробно

useEffect выполняет побочные эффекты после рендера. Это запросы к API, подписки, работа с DOM, таймеры.

Синтаксис

useEffect(() => {
    // эффект
    return () => {
        // очистка (опционально)
    };
}, [зависимости]);

Три режима

Без зависимостей

useEffect(() => {
    console.log('после каждого рендера');
});

Выполняется после каждого рендера. Редко нужен.

Пустой массив

useEffect(() => {
    console.log('только при монтировании');
}, []);

Выполняется один раз после первого рендера.

С зависимостями

useEffect(() => {
    console.log('count изменился');
}, [count]);

Выполняется при монтировании и при каждом изменении зависимостей.

Функция очистки

Возвращаемая функция вызывается:

  • перед повторным запуском эффекта
  • при размонтировании компонента
useEffect(() => {
    const id = setInterval(() => console.log('тик'), 1000);

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

Запрос к API

useEffect(() => {
    let cancelled = false;

    async function load() {
        const res = await fetch(`/api/users/${userId}`);
        const data = await res.json();

        if (!cancelled) {
            setUser(data);
        }
    }

    load();

    return () => {
        cancelled = true;
    };
}, [userId]);

cancelled защищает от гонки: если userId изменился, старый запрос не перезапишет состояние.

Подписка на событие

useEffect(() => {
    function handleScroll() {
        console.log(window.scrollY);
    }

    window.addEventListener('scroll', handleScroll);

    return () => window.removeEventListener('scroll', handleScroll);
}, []);

Работа с DOM

useEffect(() => {
    document.title = `Сообщений: ${count}`;
}, [count]);

Зависимости и ссылки

React сравнивает зависимости по ссылке. Объекты и функции — новые при каждом рендере:

// Будет срабатывать каждый рендер
useEffect(() => {
    //
}, [{ id: 1 }]);

Решение:

  • Для объектов — useMemo
  • Для функций — useCallback
  • Или вынести объект за пределы компонента

Пропуск эффекта

Можно пропустить эффект, вернув undefined:

useEffect(() => {
    if (!userId) return;

    // запрос
}, [userId]);

useEffect не для всего

Не используй useEffect для:

  • Вычисляемых значений — считай их прямо в теле компонента
  • Синхронизации между стейтами — используй один стейт
  • Обработки событий — используй обработчики
// Плохо
const [total, setTotal] = useState(0);
useEffect(() => {
    setTotal(price * quantity);
}, [price, quantity]);

// Хорошо
const total = price * quantity;

useEffect и StrictMode

В dev-режиме React монтирует компоненты дважды, чтобы выявить проблемы с очисткой. Это нормально: эффект выполнится дважды, очистка тоже дважды.

Итоги

  • useEffect выполняет эффекты после рендера
  • Зависимости определяют, когда эффект сработает
  • Очистка отменяет подписки и запросы
  • Не используй useEffect для вычислений
  • В dev-режиме эффект может срабатывать дважды