Navigated to /docs/core/streaming-and-async

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. Create the deferred value inside request-owned rendering work. A value created at module scope would be shared by every SSR request handled by that process.

TSX
tsximport { Deferred, createDeferredValue } 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 };
}

export function Stats() {
  const stats = createDeferredValue(loadStats(), {
    id: "dashboard-stats",
    timeoutMs: 1500
  });

  return <Deferred value={stats} fallback={<Skeleton height="6rem" />}>
    {(value) => <Card title="Projects"><Text>{value.total} active projects</Text></Card>}
  </Deferred>;
}

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 AbortError as 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.

TSX
tsximport {
  createDeferredValue,
  Deferred,
} from "@tavojs/core";

const recommendations = createDeferredValue(loadRecommendations(), {
  id: "recommendations",
  timeoutMs: 1500,
});

export function Recommendations() {
  return (
    <Deferred
      value={recommendations}
      fallback={<p aria-busy="true">Loading recommendations…</p>}
      errorFallback={<p role="alert">Recommendations are unavailable.</p>}
    >
      {(items) => <RecommendationList items={items} />}
    </Deferred>
  );
}
API / optionTypeDefaultBehavior
valueT | Promise<T> | DeferredValue<T>requiredThe immediate or deferred value rendered by the boundary.
fallbackChildnullInitial SSR and pending content.
errorFallbackChild | (error) => ChildfallbackReplaces the boundary when the promise rejects.
timeoutFallbackChild | (error) => ChilderrorFallbackUsed specifically for TAVO_DEFERRED_TIMEOUT.
idstringgeneratedStable sharing and hydration key. Reusing an ID coordinates one promise across boundaries.
timeoutMsnumberdisabledPositive finite timeout in milliseconds; other values do not create a timer.
signalAbortSignalunsetRejects pending work with the signal reason or AbortError.
serialize / deserializefunctionsidentityControls the value stored in and restored from the hydration registry.

SSR, CSR, and hydration behavior

API / optionTypeDefaultBehavior
SSR string renderHTML stringfallbackPromise-backed boundaries render fallback content synchronously.
SSR streamHTML chunksstream: falseWith streaming enabled, fallback arrives first and patch scripts follow as work settles.
CSRDOM renderfallbackPromise-backed Deferred does not coordinate browser data loading; use loaders, resources, controllers, or stores.
HydrationDOM hydratereuse server stateResolved, rejected, and timed-out streamed state is reused without restarting the client 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

TS
tscreateNodeRequestHandler({
  modules,
  stream: true,
  document: { nonce: requestNonce }
});
  • Streaming is disabled unless stream: true is passed to the handler.

  • Deferred patch scripts and serialized state receive document.nonce for 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.