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 триггерит повторный рендер, когда асинхронные
данные становятся доступными.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 обязательна при работе с
асинхронными операциями в компонентах, чтобы избежать ошибок обновления
размонтированного компонента.Хотя 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 с реактивной моделью, предоставляя гибкие инструменты для работы с промисами, потоками данных и сложными состояниями без необходимости сторонних библиотек.