---
title: renderToStream
---

# renderToStream

Returns a `ReadableStream<string>`: the shell first, as a single chunk, then one
chunk per fragment as each resolves.

## Signature

```tsx
import { renderToStream } from "@vincle/flow";

function renderToStream(
  node: () => JSX.Element,
  adapter: StreamingAdapter,
  opts?: FlowOptions & { mode?: "full" | "fragment" },
): ReadableStream<string>;
```

## Options

| Option           | Type                     | Default  | Description                                 |
| ---------------- | ------------------------ | -------- | ------------------------------------------- |
| `signal`         | `AbortSignal`            | N/A      | Signal to abort the stream                  |
| `onError`        | `FlowOptions["onError"]` | N/A      | Handler for fragment / stream render errors |
| `defaultTimeout` | `number`                 | N/A      | Per-fragment render timeout in ms           |
| `mode`           | `"full" \| "fragment"`   | `"full"` | `"fragment"` emits neither shell nor close  |

`FlowOptions` is `{ signal?, onError?, defaultTimeout? }`. The adapter is a
required second argument, not an option. Options are validated at the call,
before anything renders: see [Error handling](/api/core/error-handling).

## Usage

```tsx
import { Slot, Defer, renderToStream } from "@vincle/flow";
import { NativeAdapter } from "@vincle/flow/adapters";

declare function fetchComments(): Promise<{ text: string }[]>;

async function Comments() {
  const items = await fetchComments();
  return (
    <ul>
      {items.map((c) => (
        <li>{c.text}</li>
      ))}
    </ul>
  );
}

// <Slot> declares the placeholder with fallback content in the shell.
// <Defer> pushes the real content, which replaces it when resolved.
function Page() {
  return (
    <html>
      <body>
        <h1>My page</h1>
        <Slot name="comments">
          <p>Loading comments…</p>
        </Slot>
        <Defer target="comments">{() => <Comments />}</Defer>
      </body>
    </html>
  );
}

const stream = renderToStream(() => <Page />, NativeAdapter);

// e.g. Node: for await (const chunk of stream) res.write(chunk);
```

It returns the stream only: headers are yours to set, or use
[`serve()`](/api/flow/serve), which sets `content-type: text/html; charset=utf-8`.

## The shell is one chunk

The shell is never streamed progressively, `transformShell` included: it is
rendered in full, `<head>` and all, and only then emitted. A fully synchronous
page is a single chunk. Only fragments arrive later. Async siblings in the
shell resolve in document order, not concurrently; for regions that render in
parallel and patch into place, use [`<Defer>` and `<Slot>`](/api/flow/components).

A `<Defer>` whose content is an `AsyncIterable` streams one patch per item,
with its `merge` type, and keeps the stream open until the iterable is
exhausted or the client disconnects.
