Отмена запросов

Atomico — современная библиотека для построения веб-компонентов на JavaScript с реактивной архитектурой. Одной из важных особенностей работы с асинхронными операциями является корректное управление запросами и возможность их отмены, что повышает производительность и предотвращает утечки памяти.


Реактивные эффекты и асинхронные функции

В Atomico асинхронные операции обычно инициируются внутри эффектов (useEffect) или реактивных сигналов (useSignal). Основной принцип: эффект должен быть чистым и корректно завершать предыдущие асинхронные операции при повторном вызове.

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

function fetchData(url) {
    return fetch(url).then(res => res.json());
}

function Component() {
    const data = useSignal(null);

    useEffect(async (host) => {
        const result = await fetchData("/api/items");
        data.value = result;
    });

    return <host>{data.value && JSON.stringify(data.value)}</host>;
}

export default c(Component);

В этом примере useEffect запускает асинхронный запрос. Однако, если компонент будет размонтирован или URL изменится, предыдущий запрос останется активным, что может привести к нежелательным эффектам.


Использование AbortController для отмены запросов

Для управления жизненным циклом асинхронных запросов применяют AbortController. Он позволяет отменять fetch-запросы при необходимости.

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

    fetch("/api/items", { signal: controller.signal })
        .then(res => res.json())
        .then(result => {
            data.value = result;
        })
        .catch(err => {
            if (err.name === "AbortError") return;
            console.error(err);
        });

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

Ключевые моменты:

  • Создание нового AbortController для каждого запроса.
  • Передача signal в fetch.
  • В cleanup эффекта вызывается controller.abort(), чтобы отменить запрос при размонтировании компонента или повторном запуске эффекта.
  • Ошибки от отмены фильтруются по err.name === "AbortError".

Отмена предыдущих запросов при изменении зависимостей

Если эффект зависит от реактивных сигналов, нужно отменять предыдущий запрос перед запуском нового:

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

    fetch(`/api/items?query=${query.value}`, { signal: controller.signal })
        .then(res => res.json())
        .then(result => data.value = result)
        .catch(err => {
            if (err.name === "AbortError") return;
            console.error(err);
        });

    return () => controller.abort();
}, [query]);
  • Каждый раз при изменении query старый запрос отменяется.
  • Обеспечивается актуальность данных и предотвращается гонка состояний.

Комбинирование с useSignal для реактивного управления

Сигналы в Atomico позволяют хранить состояние запроса и управлять его статусом:

const status = useSignal("idle"); // idle | loading | success | error

useEffect((host) => {
    const controller = new AbortController();
    status.value = "loading";

    fetch("/api/items", { signal: controller.signal })
        .then(res => res.json())
        .then(result => {
            data.value = result;
            status.value = "success";
        })
        .catch(err => {
            if (err.name === "AbortError") return;
            status.value = "error";
            console.error(err);
        });

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

Преимущества такого подхода:

  • Чёткое управление состоянием загрузки.
  • Возможность отображать индикаторы загрузки или ошибки.
  • Исключение некорректных обновлений состояния после отмены запроса.

Практическая рекомендация: отслеживание нескольких запросов

При параллельных запросах можно хранить массив AbortController для массовой отмены:

const controllers = useSignal([]);

useEffect((host) => {
    const controller = new AbortController();
    controllers.value.push(controller);

    fetch("/api/items", { signal: controller.signal })
        .then(res => res.json())
        .then(result => data.value = result)
        .catch(err => {
            if (err.name === "AbortError") return;
            console.error(err);
        });

    return () => {
        controllers.value.forEach(c => c.abort());
        controllers.value = [];
    };
}, []);

Такой подход позволяет контролировать сразу несколько асинхронных операций и корректно завершать их при размонтировании компонента или изменении зависимостей.


Использование кастомных хуков для удобной отмены

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

function useFetch(url) {
    const data = useSignal(null);
    const status = useSignal("idle");

    useEffect(() => {
        const controller = new AbortController();
        status.value = "loading";

        fetch(url, { signal: controller.signal })
            .then(res => res.json())
            .then(result => {
                data.value = result;
                status.value = "success";
            })
            .catch(err => {
                if (err.name === "AbortError") return;
                status.value = "error";
                console.error(err);
            });

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

    return { data, status };
}

Использование хука упрощает повторное применение логики отмены запросов в разных компонентах и делает код более чистым и читаемым.


Особенности и подводные камни

  • Гонка состояний: без отмены предыдущего запроса данные могут перезаписываться результатами старых запросов.
  • Ошибки отмены: важно фильтровать ошибки AbortError, чтобы не нарушить логику обработки других ошибок.
  • Асинхронные эффекты: нельзя просто возвращать промис из useEffect; необходимо использовать cleanup функцию для контроля отмены.
  • Память и ресурсы: активные запросы после размонтирования компонента могут привести к утечкам памяти, поэтому всегда использовать abort.

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