夯HumGuides
Back to home

Experimental streaming

Streaming SSR is an opt-in escape hatch for routes that benefit from sending the page shell before suspended content is ready. Routes without streaming enabled keep Hum’s normal complete-document rendering.

Live resource demo

This message is resolved by a server request started in this route’s handler. The page shell streams first; the resource arrives after a short delay.

Loading server data…

Component render error demo

This resource resolves successfully, then its child deliberately throws while rendering. The boundary should show its error fallback.

Loading server data…

Enable it for one route

Set streaming: true in the route’s config, then use Hum’s Suspense with a resource. It shows the pending fallback first, renders the resource value when ready, or displays the error fallback if loading fails. The code below is the whole route behind the demos above.

TypeScript
// routes/docs/streaming.tsx — the two demos above
import { createResource, define, page, Suspense, type Resource } from "@humjs/core";

type DemoData = { message: string };
type PageData = { demo: Resource<DemoData>; renderError: Resource<void> };

async function loadDemoData(): Promise<DemoData> {
  await new Promise((resolve) => setTimeout(resolve, 1200));
  return { message: "The delayed server data has arrived." };
}

export const config = define.config({ streaming: true });

export const handlers = define.handlers({
  GET: () =>
    page({
      demo: createResource(loadDemoData()),
      renderError: createResource(loadDemoData()),
    }),
});

export default define.page<PageData>(({ data }) => (
  <>
    <Suspense
      resource={data.demo}
      fallback={<p role="status">Loading server data…</p>}
      errorFallback={<p role="alert">Could not load server data.</p>}
    >
      {(demo) => <ResourceDemo data={demo} />}
    </Suspense>

    <Suspense
      resource={data.renderError}
      fallback={<p role="status">Loading server data…</p>}
      errorFallback={(error) => <p role="alert">Error caught: {String(error)}</p>}
    >
      {() => <BrokenRenderDemo />}
    </Suspense>
  </>
));

The handler starts the request and passes a stable resource to the page.resource.read() returns the value when ready or suspends while pending. If it rejects, Hum’s Suspense handles pending and rejected states in one place.

Share requests across route layers

For data owned by a directory, register it in that directory’s _middleware.ts with a typed key. Descendant layouts and pages can retrieve the request-scoped resource without passing it through every route:

TypeScript
        // routes/admin/_components/resources.ts
import { createResourceKey } from "@humjs/core";
export const accountKey = createResourceKey<Account>("admin.account");

// routes/admin/_middleware.ts
import { registerResource, type HumMiddleware } from "@humjs/core";
        import { accountKey } from "./_components/resources.js";
const middleware: HumMiddleware = async (c, next) => {
  registerResource(accountKey, () => loadAccount(c));
  await next();
};
export default middleware;

// routes/admin/_layout.tsx
import { getResource, Suspense } from "@humjs/core";
import type { ComponentChildren } from "preact";
import { accountKey } from "./_components/resources.js";
export default function AdminLayout(
  { children }: { children?: ComponentChildren },
) {
  return (
    <Suspense resource={getResource(accountKey)} fallback={<p>Loading…</p>}>
      {(account) => <section>{account.name}{children}</section>}
    </Suspense>
  );
}

Resources are isolated per request. Registering the same key twice in one request throws instead of silently replacing the first value.

Early-stage limitations

  • This uses Preact’s early-stage Web Streams SSR implementation and is experimental.
  • Once streaming begins, the HTTP status and response headers can no longer be changed.
  • Hum’s Suspense renders its configured error fallback if a resource rejects or a child component throws while rendering inside that boundary.
  • Errors outside a Hum Suspense boundary are not handled by that boundary. If they happen after the shell starts streaming, Hum cannot replace the response or route it to the _error page; the stream may fail. Put potentially failing streamed content inside a boundary with an appropriate error fallback.
  • Use it selectively and verify the behavior with your own suspended components before relying on it.

Next: Partial page updates →