Асинхронные обновления

Haunted — это легковесная библиотека для создания веб-компонентов с реактивным подходом, вдохновлённая React Hooks. Одной из ключевых особенностей является поддержка асинхронных операций и обновлений состояния без блокировки интерфейса, что делает работу с сетевыми запросами, таймерами и другими асинхронными процессами удобной и безопасной.


Асинхронное состояние через useState и useEffect

В Haunted для хранения состояния используется хук useState. Асинхронные операции часто инициируются внутри useEffect, который позволяет реагировать на изменения зависимостей или выполнять код при монтировании компонента.

Пример асинхронной загрузки данных:

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

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

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

    async function fetchData() {
      try {
        const response = await fetch('https://api.example.com/items');
        const result = await response.json();
        if (!canceled) {
          setData(result);
        }
      } catch (error) {
        console.error('Ошибка загрузки данных', error);
      } finally {
        if (!canceled) setLoading(false);
      }
    }

    fetchData();

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

  return html`
    ${loading ? html`<p>Загрузка...</p>` : html`<pre>${JSON.stringify(data, null, 2)}</pre>`}
  `;
}

customElements.define('async-data-component', component(AsyncDataComponent));

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

  • Внутри useEffect можно использовать асинхронные функции, но сам useEffect не может быть async.
  • Переменная canceled предотвращает попытку обновления состояния после размонтирования компонента, что исключает ошибки типа “Cannot update state of unmounted component”.

Асинхронные события и пользовательский ввод

Haunted позволяет обрабатывать асинхронные события без блокировки UI. Например, отправка формы:

function FormComponent() {
  const [status, setStatus] = useState('');

  async function handleSubmit(event) {
    event.preventDefault();
    setStatus('Отправка...');
    try {
      const formData = new FormData(event.target);
      const response = await fetch('/submit', {
        method: 'POST',
        body: formData
      });
      setStatus(response.ok ? 'Успешно отправлено' : 'Ошибка при отправке');
    } catch (error) {
      setStatus('Ошибка сети');
    }
  }

  return html`
    <form @submit=${handleSubmit}>
      <input name="name" placeholder="Имя" />
      <button type="submit">Отправить</button>
    </form>
    <p>${status}</p>
  `;
}

customElements.define('form-component', component(FormComponent));

Особенности:

  • Состояние status обновляется асинхронно после завершения запроса.
  • Использование await внутри обработчиков событий делает код линейным и легко читаемым.
  • Haunted гарантирует реактивное обновление шаблона при изменении состояния.

Асинхронное вычисление с useMemo и useCallback

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

import { useMemo } from 'haunted';

function ComputedAsyncComponent({ input }) {
  const processedData = useMemo(async () => {
    const result = await fetch(`/process?value=${input}`);
    return result.json();
  }, [input]);

  return html`<p>Результат: ${processedData}</p>`;
}

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


Таймеры и периодические обновления

Асинхронные операции часто реализуются через таймеры. В Haunted можно использовать setInterval или setTimeout внутри useEffect:

function ClockComponent() {
  const [time, setTime] = useState(new Date().toLocaleTimeString());

  useEffect(() => {
    const interval = setInterval(() => {
      setTime(new Date().toLocaleTimeString());
    }, 1000);

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

  return html`<p>Текущее время: ${time}</p>`;
}

customElements.define('clock-component', component(ClockComponent));

Обращение внимания:

  • Очистка таймеров в return функции эффекта предотвращает утечки памяти.
  • Использование setTime внутри таймера обеспечивает реактивное обновление шаблона каждую секунду.

Асинхронные состояния и конкурентность

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

const [version, setVersion] = useState(0);

useEffect(() => {
  let current = version;

  async function fetchData() {
    const result = await fetch(`/data?v=${current}`).then(r => r.json());
    if (current === version) {
      setData(result);
    }
  }

  fetchData();
}, [version]);

Пояснение:

  • Переменная version гарантирует, что только результат последнего запроса обновит состояние.
  • Сравнение текущей версии с глобальной предотвращает неконсистентное состояние компонента при медленных сетевых ответах.

Работа с промисами в шаблонах

Haunted позволяет напрямую использовать промисы внутри шаблонов с помощью условной проверки состояния:

const [dataPromise, setDataPromise] = useState(fetch('/items').then(r => r.json()));

return html`
  <div>
    ${dataPromise
      ? html`<pre>${JSON.stringify(await dataPromise, null, 2)}</pre>`
      : html`<p>Загрузка...</p>`}
  </div>
`;

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


Итоговые паттерны асинхронных обновлений

  • Использование useEffect для инициации асинхронных процессов.
  • Защита обновлений состояния после размонтирования компонента.
  • Обработка промисов и ошибок с try/catch.
  • Очистка таймеров и подписок в функции очистки эффекта.
  • Контроль конкурентных запросов с помощью версий или флагов отмены.
  • Реактивное обновление шаблонов через useState и useReducer.

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