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

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

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

Основной инструмент для отмены асинхронных операций в браузере — AbortController. Он позволяет создать сигнал, который можно передать в функцию fetch или любой другой API, поддерживающий отмену:

import { html, component, useEffect } from 'haunted';

function FetchData({ url }) {
  const [data, setData] = useState(null);
  const [error, setError] = useState(null);

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

    fetch(url, { signal })
      .then(response => response.json())
      .then(setData)
      .catch(err => {
        if (err.name !== 'AbortError') {
          setError(err);
        }
      });

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

  if (error) return html`<p>Ошибка: ${error.message}</p>`;
  if (!data) return html`<p>Загрузка...</p>`;
  return html`<pre>${JSON.stringify(data, null, 2)}</pre>`;
}

customElements.define('fetch-data', component(FetchData));

В этом примере:

  • AbortController создаётся внутри useEffect для каждой загрузки данных.
  • Сигнал передаётся в fetch.
  • Возврат функции из useEffect гарантирует отмену запроса при размонтировании компонента или при изменении url.
  • Исключение AbortError игнорируется, так как оно является нормальным результатом отмены.

Отмена пользовательских асинхронных операций

Не только fetch поддерживает AbortSignal. Можно создавать собственные асинхронные функции, которые реагируют на сигнал:

function delayedTask(ms, signal) {
  return new Promise((resolve, reject) => {
    const timeout = setTimeout(() => resolve('Готово'), ms);
    signal.addEventListener('abort', () => {
      clearTimeout(timeout);
      reject(new DOMException('Операция отменена', 'AbortError'));
    });
  });
}

function TimerComponent({ delay }) {
  const [message, setMessage] = useState('Ожидание...');

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

    delayedTask(delay, controller.signal)
      .then(setMessage)
      .catch(err => {
        if (err.name !== 'AbortError') setMessage(`Ошибка: ${err.message}`);
      });

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

  return html`<p>${message}</p>`;
}

customElements.define('timer-component', component(TimerComponent));

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

  • Любая асинхронная операция может быть связана с AbortSignal.
  • Отмена осуществляется через controller.abort() при размонтировании компонента.
  • Это предотвращает утечки памяти и обновления состояния размонтированных компонентов.

Комбинация нескольких запросов

Если компонент инициирует несколько асинхронных операций одновременно, можно объединять их с помощью одного AbortController:

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

  const urls = ['/api/users', '/api/posts'];
  Promise.all(urls.map(url =>
    fetch(url, { signal }).then(res => res.json())
  ))
  .then(results => setData({ users: results[0], posts: results[1] }))
  .catch(err => {
    if (err.name !== 'AbortError') setError(err);
  });

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

Это гарантирует, что при размонтировании все запросы будут остановлены одновременно.

Практические рекомендации

  • Создавать AbortController внутри useEffect. Никогда не использовать один и тот же контроллер для разных эффектов, чтобы избежать конфликтов.
  • Обрабатывать AbortError отдельно. Он не является ошибкой приложения и должен игнорироваться при отображении сообщений об ошибках.
  • Передавать signal в пользовательские асинхронные функции, если планируется их отмена при размонтировании компонента.
  • Отмена должна быть быстрой и чистой. Любые таймеры, подписки на WebSocket или промисы должны быть отменены в функции очистки эффекта, чтобы избежать утечек памяти.

Заключение по практике использования

Haunted не предоставляет встроенного механизма отмены асинхронных операций, но сочетание useEffect и AbortController позволяет полностью контролировать жизненный цикл запросов. Это критично для динамических приложений, где компоненты часто монтируются и размонтируются, а асинхронные операции могут продолжаться после удаления DOM-узлов. Такой подход повышает производительность, надёжность и предотвращает типичные ошибки обновления состояния после размонтирования.