Урок 09

useEffect: побочные эффекты

Побочный эффект — это действие, которое выходит за пределы рендера: запрос к API, подписка на событие, изменение заголовка страницы, работа с localStorage.

useEffect выполняет такие действия после рендера.

Синтаксис

import { useEffect } from 'react';

useEffect(() => {
    // эффект
}, [зависимости]);

Первый аргумент — функция с эффектом. Второй — массив зависимостей.

Эффект при монтировании

Пустой массив зависимостей — эффект выполнится один раз:

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

Эффект при изменении

Если указать зависимости — эффект срабатывает при их изменении:

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

Эффект после каждого рендера

Без массива — после каждого рендера:

useEffect(() => {
    console.log('рендер');
});

Используется редко.

Очистка

Если эффект создаёт подписку — нужна функция очистки:

useEffect(() => {
    const id = setInterval(() => {
        console.log('тик');
    }, 1000);

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

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

  • при размонтировании компонента
  • перед повторным запуском эффекта

Запрос к API

function UserProfile({ userId }: { userId: number }) {
    const [user, setUser] = useState<User | null>(null);
    const [loading, setLoading] = useState(true);

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

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

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

        load();

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

    if (loading) return <p>Загрузка...</p>;
    return <p>{user?.name}</p>;
}

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

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

useEffect(() => {
    function handleResize() {
        console.log(window.innerWidth);
    }

    window.addEventListener('resize', handleResize);

    return () => window.removeEventListener('resize', handleResize);
}, []);

Изменение заголовка

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

Зависимости

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

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

Решение — мемоизация через useMemo и useCallback.

useEffect не для вычислений

Если значение можно вычислить из пропсов или стейта — не используй useEffect:

// Плохо
const [total, setTotal] = useState(0);
useEffect(() => {
    setTotal(price * quantity);
}, [price, quantity]);

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

Итоги

  • useEffect выполняет побочные эффекты после рендера
  • Пустой массив — один раз при монтировании
  • Зависимости — массив значений, при изменении которых эффект сработает
  • Функция очистки отменяет подписки и запросы
  • Не используй useEffect для вычисляемых значений