Streaming and async work
Deliver the route shell first, defer secondary server content, and give every async operation an owner.
Defer secondary content
Route loaders should resolve data required for navigation, SEO, and the primary shell. Deferred boundaries are for slower, optional server content such as analytics summaries, recommendations, or below-the-fold panels.
Create a meaningful boundary
Give each boundary a stable ID, a lightweight fallback, and timeout behavior when the content is optional. Keep the promise stable for its component instance and create request-specific work inside that owner. A value created at module scope would be shared by every SSR request handled by that process. Enable ssr.stream in tavo.config.ts to receive progressive server output.
tsximport { Deferred, createDeferredValue, createTavo, TavoController } from "@tavojs/core";
import { Card, Skeleton, Text } from "@tavojs/ui";
async function loadStats(): Promise<{ total: number }> {
await new Promise<void>((resolve) => setTimeout(resolve, 250));
return { total: 12 };
}
class StatsController extends TavoController {
stats = createDeferredValue(loadStats(), {
id: "dashboard-stats",
timeoutMs: 1500
});
}
export const Stats = createTavo({
controller: StatsController,
view: ({ controller }) => controller ? (
<Deferred
value={controller.stats}
fallback={<Skeleton height="6rem" />}
errorFallback={<Text>Project statistics are unavailable.</Text>}
>
{(value) => <Card title="Projects"><Text>{value.total} active projects</Text></Card>}
</Deferred>
) : null
});Give work an owner
Navigation owns loaders and middleware, a resource owns its current load, and deferred work belongs to its signal or render lifecycle. Pass AbortSignal through every supported layer and never publish results after the owner is gone.
Treat
AbortErroras control flow, not a user-facing failure.Use transactions or idempotency keys for side effects; cancellation cannot undo a committed mutation.
Set timeouts for optional remote dependencies that should not hold a stream open.
Deferred API reference
Configure deferred values, fallbacks, timeouts, serialization, and cancellation explicitly.
tsximport {
createDeferredValue,
Deferred,
} from "@tavojs/core";
type Recommendation = { id: string; title: string };
export function Recommendations({
recommendations,
}: {
recommendations: Promise<Recommendation[]>;
}) {
// The caller owns and reuses this promise for the current request or component.
const value = createDeferredValue(recommendations, {
id: "recommendations",
timeoutMs: 1500,
});
return (
<Deferred
value={value}
fallback={<p aria-busy="true">Loading recommendations…</p>}
errorFallback={<p role="alert">Recommendations are unavailable.</p>}
>
{(items) => <ul>{items.map((item) => <li key={item.id}>{item.title}</li>)}</ul>}
</Deferred>
);
}| API / option | Type | Default | Behavior |
|---|---|---|---|
value | T | Promise<T> | DeferredValue<T> | required | The immediate or deferred value rendered by the boundary. |
fallback | Child | null | Initial SSR and pending content. |
errorFallback | Child | (error) => Child | fallback | Replaces the boundary when the promise rejects. |
timeoutFallback | Child | (error) => Child | errorFallback | Used specifically for TAVO_DEFERRED_TIMEOUT. |
id | string | generated | Stable sharing and hydration key. Reusing an ID coordinates one promise across boundaries. |
timeoutMs | number | disabled | Positive finite timeout in milliseconds; other values do not create a timer. |
signal | AbortSignal | unset | Rejects pending work with the signal reason or AbortError. |
serialize / deserialize | functions | identity | Controls the value stored in and restored from the hydration registry. |
SSR, CSR, and hydration behavior
| API / option | Type | Default | Behavior |
|---|---|---|---|
SSR string render | HTML string | fallback | Promise-backed boundaries render fallback content synchronously. |
SSR stream | HTML chunks | stream: false | With streaming enabled, fallback arrives first and patch scripts follow as work settles. |
CSR | DOM render | fallback | In a pure CSR document, the boundary observes its promise and replaces fallback with resolved, rejected, or timed-out output. Keep the promise stable across renders. |
Hydration | DOM hydrate | reuse server state | A stable ID restores the resolved, rejected, or timed-out streamed result. This does not cancel a request already started by application code while creating a browser promise. |
A timeout rejects with code TAVO_DEFERRED_TIMEOUT plus id, timeoutMs, and a safe message. Other rejections are serialized as a generic failure string; application error objects are not copied into the client document.
Production streaming contract
tscreateNodeRequestHandler({
modules,
stream: true,
document: { nonce: requestNonce }
});Streaming is disabled unless stream: true is passed to the handler.
Deferredpatch scripts and serialized state receivedocument.noncefor a strict Content Security Policy.The Node handler waits for drain when response backpressure is signaled.
A disconnected Node request aborts route work and cancels the stream reader.
Redirects are returned as a complete one-chunk document with Location metadata.
Use timeouts for optional dependencies so one remote service cannot hold the response open indefinitely.
Look up exact public types
Follow linked API names to their canonical TypeScript declarations and package boundaries.