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.
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
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.
| Need | Use |
|---|---|
| All public data before render | Plain loader() result |
| Public SSR data can arrive after the shell | defer() and <Await> |
| Cookies or headers beside a cached public result | requestLoader() and Suspense |