Deferred Data
Furin supports deferred data streaming — returning a mix of synchronous values and Promises from a page loader so the HTML shell can be sent immediately while slow data streams in later.
Why defer?
In a standard loader, every await blocks the entire render. If one query takes 600 ms, the user sees a blank page until everything resolves.
With defer(), scalar fields are embedded in the initial HTML shell immediately, and Promise-valued fields are streamed as late <script> chunks that resolve on the client. The shell renders instantly; slow data populates when ready.
Using defer()
defer() is available from @teyik0/furin/client. It wraps the loader return value and tells Furin to split scalar fields from Promise fields.
import { defer } from "@teyik0/furin/client"
import { route } from "./_route"
export default route.page({
loader: async () => {
const fast = { title: "Dashboard" }
const slow = fetchAnalytics()
return defer({
...fast,
stats: slow, // Promise — streamed later
})
},
component: ({ title, stats }) => (
<div>
<h1>{title}</h1>
<Suspense fallback={<Spinner />}>
<Await resolve={stats}>
{(data) => <AnalyticsChart data={data} />}
</Await>
</Suspense>
</div>
),
})
<Await>
<Await> consumes a deferred Promise. It uses React 19's use() to suspend the boundary until the Promise settles, then calls the render-prop children with the resolved value.
import { Await } from "@teyik0/furin/client"
<Await resolve={stats}>
{(data) => <AnalyticsChart data={data} />}
</Await>
<Await> must be wrapped in a <Suspense> boundary so React has a fallback to show while the Promise is pending.
Error handling
When a deferred Promise rejects, <Await> catches it with an internal error boundary. Provide errorElement to render a fallback:
<Await resolve={stats} errorElement={<ErrorCard />}>
{(data) => <AnalyticsChart data={data} />}
</Await>
Inside errorElement, call useAsyncError() to read the rejection reason:
import { useAsyncError } from "@teyik0/furin/client"
function ErrorCard() {
const error = useAsyncError()
return <p>Failed to load stats: {String(error)}</p>
}
If errorElement is omitted, the error propagates to the nearest React error boundary.
useAsyncValue()
As an alternative to the render-prop API, you can use useAsyncValue() inside a component rendered as children of <Await>:
import { Await, useAsyncValue } from "@teyik0/furin/client"
function Chart() {
const data = useAsyncValue<AnalyticsData>()
return <AnalyticsChart data={data} />
}
<Await resolve={stats} errorElement={<ErrorCard />}>
<Chart />
</Await>
Deferring in layouts
defer() works in any loader of the route tree — page and layout/route loaders defined via createRoute({ loader }). A layout can ship its shell (nav, sidebar) immediately while a slow widget streams in the background:
import { createRoute, defer } from "@teyik0/furin/client"
import { Suspense } from "react"
import { Await } from "@teyik0/furin/client"
export const route = createRoute({
loader: () =>
defer({
user: "alice", // sync — embedded in the initial shell
widgets: fetchSidebarWidgets(), // Promise — streamed when it resolves
}),
layout: ({ children, user, widgets }) => (
<div>
<nav>Hello {user}</nav>
<aside>
<Suspense fallback={<WidgetSkeleton />}>
<Await resolve={widgets}>
{(list) => <WidgetList widgets={list} />}
</Await>
</Suspense>
</aside>
<main>{children}</main>
</div>
),
})
The deferred field is exposed to the layout component as a Promise, exactly like a deferred field would be exposed to a page component. Promises from layouts and pages are streamed together — there is no separate transport.
If both a layout and its page defer() a field with the same key, the page wins (last-spread semantics), matching the merge order used for synchronous loader data.
Important restrictions
- Always wrap
<Await>in<Suspense>— without a Suspense boundary, React has no fallback to render during the pending phase. - SPA navigation — deferred Promises are also supported on client-side navigation; Furin fetches them via
/_furin/dataand reconstructs the same Promise-based props.
Full example
import { defer, Await } from "@teyik0/furin/client"
import { route } from "./_route"
import { Suspense } from "react"
export default route.page({
loader: async ({ params }) => {
const board = await getBoard(params.id) // fast — blocks render
const comments = fetchComments(params.id) // slow — deferred
return defer({
board,
comments,
})
},
component: ({ board, comments }) => (
<div>
<h1>{board.name}</h1>
<Suspense fallback={<p>Loading comments…</p>}>
<Await
resolve={comments}
errorElement={<p>Could not load comments.</p>}
>
{(list) => <CommentList comments={list} />}
</Await>
</Suspense>
</div>
),
})
Limitations
defer()is SSR-only. A loader that returnsdefer()on a route rendered inssgorisrmode throws an error: the HTML is pre-rendered and cached, so the deferred fields would be missing from the hydration payload and<Await>would crash on the client. Either return the data directly (await it inside the loader) or switch the route to SSR mode.
Comparison
| Approach | When to use |
|---|---|
| Normal loader | All data is fast (< 50 ms) or required before any paint |
defer() | Some data is slow and can appear after the shell |