Урок 06

Типизация useEffect

useEffect редко требует явной типизации — типы выводятся. Но есть нюансы.

Базовый эффект

useEffect(() => {
    console.log('mount');
}, []);

Тип не нужен.

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

useEffect(() => {
    const id = setInterval(() => {}, 1000);
    return () => clearInterval(id);
}, []);

Возвращаемый тип — void | (() => void). Можно вернуть undefined или функцию.

Async-эффект

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

// Ошибка: возвращает Promise
useEffect(async () => {
    const data = await fetchData();
}, []);

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

useEffect(() => {
    async function load() {
        const data = await fetchData();
        setData(data);
    }

    load();
}, []);

С AbortController

useEffect(() => {
    const controller = new AbortController();

    async function load() {
        try {
            const res = await fetch(url, { signal: controller.signal });
            setData(await res.json());
        } catch (e) {
            if (e instanceof Error && e.name !== 'AbortError') {
                setError(e);
            }
        }
    }

    load();

    return () => controller.abort();
}, [url]);

Тип ошибки

В catch ошибка имеет тип unknown:

try {
    await fetchData();
} catch (e: unknown) {
    if (e instanceof Error) {
        setError(e.message);
    } else {
        setError('Неизвестная ошибка');
    }
}

С useUnknownInCatchVariables: false (не рекомендуется) — any.

Зависимости

useEffect(() => {
    //
}, [userId, filters]);

filters — объект. Если создаётся в рендере — эффект срабатывает каждый рендер. Решение — useMemo.

Event listener

useEffect(() => {
    function handleKey(e: KeyboardEvent) {
        if (e.key === 'Escape') close();
    }

    document.addEventListener('keydown', handleKey);
    return () => document.removeEventListener('keydown', handleKey);
}, [close]);

KeyboardEvent — нативный DOM-тип, не React-тип.

IntersectionObserver

useEffect(() => {
    const observer = new IntersectionObserver((entries) => {
        entries.forEach((entry) => {
            if (entry.isIntersecting) setVisible(true);
        });
    });

    if (ref.current) observer.observe(ref.current);
    return () => observer.disconnect();
}, []);

ResizeObserver

useEffect(() => {
    const observer = new ResizeObserver((entries) => {
        const { width, height } = entries[0].contentRect;
        setSize({ width, height });
    });

    if (ref.current) observer.observe(ref.current);
    return () => observer.disconnect();
}, []);

local storage

useEffect(() => {
    localStorage.setItem('theme', theme);
}, [theme]);

Итоги

  • Типизация useEffect обычно не нужна
  • Async-функция — внутри
  • AbortController для отмены
  • catch (e: unknown) — проверяй
  • Нативные типы для DOM-событий
  • useMemo для объектов в зависимостях