Deferred Data

defer() lets an SSR route send its ready fields in the initial shell and settle selected promise fields in later chunks. The API is explicit: only promise values inside a defer() result are streamed.

This is separate from Elysia 2's request-context defer(callback): Elysia queues cleanup work after afterResponse, while Furin's defer(data) streams loader values into React Suspense and reconstructs them during SPA navigation. Neither API can replace the other.

src/pages/dashboard/index.tsx
import { defineRoute } from "@teyik0/furin";
import { Await, defer } from "@teyik0/furin/client";
import { Suspense } from "react";

export const route = defineRoute("/dashboard")
  .config({ mode: "ssr" })
  .loader(() =>
    defer({
      title: "Dashboard",
      analytics: fetchAnalytics(),
    })
  )
  .page(({ analytics, title }) => (
    <main>
      <h1>{title}</h1>
      <Suspense fallback={<ChartSkeleton />}>
        <Await resolve={analytics}>
          {(analytics) => <Chart value={analytics} />}
        </Await>
      </Suspense>
    </main>
  ));

<Await> reads the promise with React use(), so it must be inside <Suspense>. During SPA navigation, the page URL with ?_furin=data streams the same fields as NDJSON and reconstructs the promise-valued contract before React renders the destination.

Only synchronous loader fields are available to head(). Keep titles and other metadata inputs outside deferred promises.

Local Error UI

tsx
import { Await, useAsyncError } from "@teyik0/furin/client";

function AnalyticsError() {
  const error = useAsyncError();
  return <p>Analytics unavailable: {String(error)}</p>;
}

<Suspense fallback={<ChartSkeleton />}>
  <Await errorElement={<AnalyticsError />} resolve={analytics}>
    {(analytics) => <Chart value={analytics} />}
  </Await>
</Suspense>;

Without errorElement, the rejection bubbles to the nearest Furin or React error boundary. Production sanitizes ordinary deferred errors and attaches the same correlation digest used elsewhere. useAsyncValue<T>() is available when the resolved value is easier to consume in a nested component than through the render function.

Layout Loaders

Layouts use the same API. Their synchronous and deferred fields merge with descendant data; if a deeper loader deliberately reuses a key, the deeper value wins.

Restrictions

  • Deferred public data is supported only by mode: "ssr".
  • SSG and ISR fail fast when a public loader returns deferred promise fields.
  • requestLoader() is a different mechanism: it isolates per-request data from a cached public result and exposes each returned field as a promise.
  • Do not call defer() twice on the same object.
  • Keep the shell useful without the deferred result; otherwise a normal awaited loader is simpler.
NeedUse
All public data before renderPlain loader() result
Public SSR data can arrive after the shelldefer() and <Await>
Cookies or headers beside a cached public resultrequestLoader() and Suspense

Comments