Suspense-подобные паттерны

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


Использование useResource для асинхронных данных

В Atomico базовым инструментом для работы с асинхронными ресурсами является хук useResource. Он позволяет описывать источник данных и автоматически управлять его состоянием: загрузкой, ошибкой и успешным результатом.

import { component, useResource, html } from "atomico";

async function fetchUser(id) {
    const response = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`);
    if (!response.ok) throw new Error("Ошибка загрузки данных");
    return response.json();
}

function UserComponent({ id }) {
    const user = useResource(() => fetchUser(id), [id]);

    return html`
        <div>
            ${user.loading && html`<p>Загрузка...</p>`}
            ${user.error && html`<p>Ошибка: ${user.error.message}</p>`}
            ${user.value && html`<p>Имя пользователя: ${user.value.name}</p>`}
        </div>
    `;
}

export const User = component(UserComponent);

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

  • useResource автоматически отслеживает зависимости ([id]) и повторно запускает загрузку при их изменении.
  • user.loading, user.error и user.value позволяют управлять различными состояниями асинхронного запроса без отдельного state-хука.
  • Ошибки ловятся и становятся частью реактивного объекта, что упрощает обработку состояния.

Ленивая загрузка компонентов

Atomico поддерживает ленивую загрузку компонентов с помощью динамического импорта. Этот подход тесно связан с Suspense-подобной концепцией — компонент отображается только после загрузки всех необходимых ресурсов.

import { lazy, component, html } from "atomico";

const LazyProfile = lazy(() => import("./ProfileComponent.js"));

function App() {
    return html`
        <div>
            <h1>Пример ленивой загрузки</h1>
            <${LazyProfile} />
        </div>
    `;
}

export const MainApp = component(App);

Особенности ленивой загрузки:

  • lazy возвращает компонент-заглушку, который рендерится после завершения импорта.
  • Позволяет разбивать приложение на чанки, улучшая производительность.
  • Можно комбинировать с useResource для управления загрузкой данных внутри ленивого компонента.

Оборачивание асинхронных блоков через Suspense

Atomico предоставляет возможность создания собственных обёрток для асинхронного рендеринга, аналогичных Suspense. Основная идея — использовать реактивный state для отслеживания загрузки ресурсов и отображать fallback-контент до завершения загрузки.

import { component, html, useState, useEffect } from "atomico";

function Suspense({ fallback, children }) {
    const [loaded, setLoaded] = useState(false);

    useEffect(() => {
        Promise.resolve(children.props.resource).then(() => setLoaded(true));
    }, [children.props.resource]);

    return html`${loaded ? children : fallback}`;
}

function DataDisplay({ resource }) {
    return html`<p>Данные: ${resource.value}</p>`;
}

// Пример использования
function Example() {
    const resource = useResource(() => fetchUser(1), []);

    return html`
        <${Suspense} fallback=${html`<p>Загрузка...</p>`} resource=${resource}>
            <${DataDisplay} resource=${resource} />
        <//>
    `;
}

export const Demo = component(Example);

Выделенные идеи:

  • Компонент Suspense отделяет состояние ожидания от основной логики рендеринга.
  • Используется Promise.resolve, чтобы унифицировать работу с синхронными и асинхронными ресурсами.
  • Позволяет создавать каскадные цепочки загрузки, где каждый уровень асинхронности обрабатывается отдельно.

Комбинирование с мемоизацией и оптимизация

При работе с Suspense-подобными паттернами важно контролировать повторные запросы данных. Atomico предоставляет useMemo и возможность кэширования ресурсов.

import { useResource, useMemo, component, html } from "atomico";

function CachedUser({ id }) {
    const resource = useMemo(() => useResource(() => fetchUser(id), [id]), [id]);

    return html`
        <div>
            ${resource.loading && html`<p>Загрузка...</p>`}
            ${resource.value && html`<p>Имя: ${resource.value.name}</p>`}
        </div>
    `;
}

export const UserCached = component(CachedUser);
  • Мемоизация предотвращает ненужные повторные запросы при частых ререндерингах.
  • Позволяет легко интегрировать Suspense-подобные блоки в сложные интерфейсы без потери производительности.

Поддержка каскадных асинхронных зависимостей

Atomico позволяет строить иерархию компонентов с асинхронными зависимостями, где каждый компонент загружает свои данные и рендерится только после завершения загрузки:

function ParentComponent() {
    const posts = useResource(() => fetch("/posts").then(res => res.json()), []);

    return html`
        <div>
            ${posts.loading && html`<p>Загрузка постов...</p>`}
            ${posts.value &&
            posts.value.map(
                post => html`<${ChildComponent} postId=${post.id} />`
            )}
        </div>
    `;
}

function ChildComponent({ postId }) {
    const comments = useResource(() => fetch(`/posts/${postId}/comments`).then(r => r.json()), [postId]);

    return html`
        <div>
            ${comments.loading && html`<p>Загрузка комментариев...</p>`}
            ${comments.value &&
            comments.value.map(comment => html`<p>${comment.body}</p>`)}
        </div>
    `;
}

Суть каскадного паттерна:

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

Suspense-подобные паттерны в Atomico позволяют создавать реактивные интерфейсы, где асинхронные данные интегрированы напрямую в поток рендеринга, минимизируя boilerplate и обеспечивая гибкость управления состояниями загрузки и ошибок.