Асинхронный рендеринг

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


Основы асинхронного рендеринга

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

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

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

  useEffect(() => {
    fetch('https://api.example.com/data')
      .then(res => res.json())
      .then(json => setData(json));
  }, []);

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

customElements.define('async-component', component(AsyncComponent));

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

  • useEffect с пустым массивом зависимостей ([]) выполняется один раз при монтировании компонента.
  • setData триггерит повторный рендер, когда асинхронные данные становятся доступными.
  • JSX-подобная разметка через html позволяет вставлять данные напрямую в DOM без лишних обёрток.

Асинхронные функции внутри хуков

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

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

  async function loadData() {
    try {
      const response = await fetch('https://api.example.com/data');
      const json = await response.json();
      if (!cancelled) setData(json);
    } catch (err) {
      if (!cancelled) setData({ error: err.message });
    }
  }

  loadData();

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

Пояснения:

  • Использование переменной cancelled предотвращает попытку обновления состояния после размонтирования компонента.
  • Хук useEffect может возвращать функцию очистки, которая вызывается при размонтировании или изменении зависимостей.

Интеграция с useState для асинхронных данных

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

const [state, setState] = useState({ loading: true, data: null, error: null });

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

  fetch('https://api.example.com/data')
    .then(res => res.json())
    .then(json => !cancelled && setState({ loading: false, data: json, error: null }))
    .catch(err => !cancelled && setState({ loading: false, data: null, error: err.message }));

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

Выводы:

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

Паттерн “Suspense-like”

Хотя Haunted не имеет встроенного Suspense, можно реализовать аналогичный паттерн для асинхронного рендеринга:

function Suspense({ promise, fallback, children }) {
  const [resolved, setResolved] = useState(null);

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

    promise.then(result => !cancelled && setResolved(result));
    return () => cancelled = true;
  }, [promise]);

  return resolved ? children(resolved) : fallback;
}

customElements.define('suspense-component', component(Suspense));

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

<Suspense
  promise={fetch('https://api.example.com/data').then(res => res.json())}
  fallback=${html`<p>Загрузка...</p>`}
>
  ${data => html`<pre>${JSON.stringify(data, null, 2)}</pre>`}
</Suspense>

Асинхронный рендеринг с использованием useReducer

Для сложных состояний, особенно с множественными асинхронными запросами, эффективнее использовать useReducer. Это позволяет управлять состоянием через действия, аналогично Redux.

const initialState = { loading: false, data: null, error: null };

function reducer(state, action) {
  switch(action.type) {
    case 'FETCH_START': return { loading: true, data: null, error: null };
    case 'FETCH_SUCCESS': return { loading: false, data: action.payload, error: null };
    case 'FETCH_ERROR': return { loading: false, data: null, error: action.payload };
    default: return state;
  }
}

function AsyncWithReducer() {
  const [state, dispatch] = useReducer(reducer, initialState);

  useEffect(() => {
    dispatch({ type: 'FETCH_START' });
    fetch('https://api.example.com/data')
      .then(res => res.json())
      .then(json => dispatch({ type: 'FETCH_SUCCESS', payload: json }))
      .catch(err => dispatch({ type: 'FETCH_ERROR', payload: err.message }));
  }, []);

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

customElements.define('async-reducer', component(AsyncWithReducer));

Преимущества:

  • Чёткая структура действий и состояний.
  • Лёгкая масштабируемость при добавлении новых асинхронных операций.
  • Явное управление состоянием упрощает отладку.

Асинхронные рендеры с подписками и потоками данных

Haunted позволяет интегрироваться с Observable и любыми потоками данных, предоставляя реактивное обновление интерфейса:

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

function TimerComponent() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const subscription = interval(1000).subscribe(setCount);
    return () => subscription.unsubscribe();
  }, []);

  return html`<p>Секунд прошло: ${count}</p>`;
}

customElements.define('timer-component', component(TimerComponent));
  • Любой поток данных может быть подключён через useEffect и setState.
  • Отписка в функции очистки предотвращает утечки памяти.

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