Skip to content

Async component data and SSR ​

Async work can happen while Gluon renders on the server, but the async boundary must be explicit. Put the request in Suspense({ source }); do not start a request from a module-global variable or an untracked render side effect.

One component, two server policies ​

ts
import { html, Suspense } from '@gluonjs/core';
import { prepareForHydration, renderProgressively } from '@gluonjs/ssr';

interface Product { readonly id: string; readonly name: string; }

async function loadProduct(id: string, signal: AbortSignal): Promise<Product> {
  const response = await fetch(`/api/products/${id}`, { signal });
  if (!response.ok) throw new Error(`Product request failed: ${response.status}`);
  return response.json() as Promise<Product>;
}

export function productBoundary(id: string) {
  return html`
    ${Suspense({
      source: ({ signal }) => loadProduct(id, signal),
      fallback: html`<p aria-busy="true">Loading product…</p>`,
      children: (product) => html`<h1>${product.name}</h1>`,
      error: (error, retry) => html`<p>${String(error)} <button @click=${retry}>Retry</button></p>`,
    })}
  `;
}

export async function renderBlockingProduct(id: string): Promise<string> {
  const prepared = await prepareForHydration(productBoundary(id));
  return prepared.html;
}

export async function renderStreamingProduct(id: string) {
  return renderProgressively(productBoundary(id));
}

prepareForHydration() resolves the declared boundary, then serializes the same prepared value for the browser to hydrate. renderProgressively() emits a fallback shell first and later boundary chunks. Both APIs propagate an abort signal to the source. The browser-only Suspense path can instead show its fallback immediately and resolve after the page is interactive.

NeedBoundaryWhat the user receives
Browser-only loadingSuspense() in the browser appFallback first, then resolved content.
Blocking SSRprepareForHydration()Server waits, sends resolved HTML, browser hydrates it.
Progressive SSRrenderProgressively()Server sends fallback shell, then ordered boundary chunks.
Page prefetchrenderRequest({ load })Request-local data is loaded before app creation.
Component-owned loadingSuspense({ source, fallback, error })The component owns pending, error, retry, and cancellation UI.

The source receives { signal, attempt }. A timeout or disconnect aborts the request. A retry starts a new attempt, and a cancelled result cannot update the old render part.

Common mistakes ​

  • Do not fetch at module scope: concurrent requests would share the result.
  • Do not pass an already-settled promise when retry or cancellation is needed; pass a loader function.
  • Do not call browser APIs in the server source. Keep browser-only work in the hydrated application or behind a browser check.
  • Do not confuse page prefetching with component-owned loading. renderRequest owns request data; Suspense owns a visible component boundary.

The async rendering reference contains the complete built-in contract. The universal rendering guide shows how the request result reaches the browser.

Version-matched documentation. Examples are checked against the public package contracts.