useEffect с async функциями

useEffect в библиотеке Atomico выполняет роль жизненного цикла компонента, позволяя реагировать на изменения состояния или пропсов. Часто возникает необходимость выполнять асинхронные операции — запросы к API, задержки, работу с IndexedDB или другими внешними источниками данных. Важно понимать, что функция, передаваемая в useEffect, не может быть асинхронной напрямую, то есть нельзя писать async () => { ... }.

import { c, useState, useEffect } from "atomico";

function MyComponent() {
    const [data, setData] = useState(null);

    useEffect(() => {
        async function fetchData() {
            const response = await fetch("https://api.example.com/data");
            const json = await response.json();
            setData(json);
        }
        fetchData();
    }, []);

    return <host>{data ? JSON.stringify(data) : "Загрузка..."}</host>;
}

В этом примере создается внутренняя асинхронная функция fetchData и вызывается внутри эффекта. Такой подход позволяет корректно использовать await и обрабатывать промисы.


Отмена асинхронных операций

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

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

    async function fetchData() {
        const response = await fetch("https://api.example.com/data");
        if (!cancelled) {
            const json = await response.json();
            setData(json);
        }
    }

    fetchData();

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

Флаг cancelled предотвращает вызов setData после размонтирования компонента, что избавляет от предупреждений о “setState on unmounted component”.


Использование AbortController

Для более строгого контроля над асинхронными запросами можно применять AbortController. Это особенно полезно при работе с fetch:

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

    async function fetchData() {
        try {
            const response = await fetch("https://api.example.com/data", {
                signal: controller.signal
            });
            const json = await response.json();
            setData(json);
        } catch (error) {
            if (error.name !== "AbortError") {
                console.error(error);
            }
        }
    }

    fetchData();

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

Здесь при размонтировании компонента запрос автоматически прерывается, а ошибки отмены (AbortError) игнорируются.


Зависимости эффекта и асинхронные операции

Асинхронные функции в useEffect тесно связаны с массивом зависимостей. Любое изменение зависимостей приводит к повторному вызову эффекта. При этом важно не создавать функцию внутри массива зависимостей, иначе эффект будет срабатывать на каждое изменение ссылки на функцию.

const [query, setQuery] = useState("atomico");

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

    async function fetchData() {
        const response = await fetch(`https://api.example.com/search?q=${query}`);
        if (!cancelled) {
            const json = await response.json();
            setData(json);
        }
    }

    fetchData();

    return () => {
        cancelled = true;
    };
}, [query]); // эффект повторяется только при изменении query

Таким образом, асинхронные операции остаются контролируемыми и предсказуемыми, предотвращая лишние запросы и утечки памяти.


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

Для повторного использования логики асинхронных операций удобно создавать кастомные хуки:

function useFetch(url) {
    const [data, setData] = useState(null);
    const [loading, setLoading] = useState(true);

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

        async function load() {
            setLoading(true);
            try {
                const response = await fetch(url, { signal: controller.signal });
                const json = await response.json();
                setData(json);
            } catch (error) {
                if (error.name !== "AbortError") console.error(error);
            } finally {
                setLoading(false);
            }
        }

        load();

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

    return { data, loading };
}

Использование такого хука упрощает компонент:

function App() {
    const { data, loading } = useFetch("https://api.example.com/data");

    return <host>{loading ? "Загрузка..." : JSON.stringify(data)}</host>;
}

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


Важные рекомендации

  • Нельзя делать сам эффект асинхронным (async), необходимо создавать внутреннюю функцию.
  • Всегда обрабатывать размонтирование компонентов при долгих асинхронных операциях.
  • Использовать AbortController для прерывания fetch-запросов.
  • Контролировать массив зависимостей, чтобы избежать лишних вызовов эффекта.
  • Для повторного использования логики создавать кастомные хуки.

Эти практики позволяют безопасно интегрировать асинхронные операции в useEffect без риска утечек памяти и непредсказуемого поведения компонентов.