Using Suspense

Estimated reading time: 3 minutes

Import Suspense and renderToStream from @kitajs/html/suspense.

Basic usage

Wrap async components in a Suspense element with a rid, a fallback, and optionally a catch handler. Then pass the tree to renderToStream, which returns a Readable stream.


async function 
function UserStats({ id }: {
    id: string;
}): Promise<string>
UserStats
({
id: string
id
}: {
id: string
id
: string }) {
const
const stats: {
    totalPosts: number;
}
stats
= await
let api: {
    getStats(id: string): Promise<{
        totalPosts: number;
    }>;
}
api
.
function getStats(id: string): Promise<{
    totalPosts: number;
}>
getStats
(
id: string
id
)
return <
JSX.IntrinsicElements.div: JSX.HtmlTag
div
>{
const stats: {
    totalPosts: number;
}
stats
.
totalPosts: number
totalPosts
} posts</
JSX.IntrinsicElements.div: JSX.HtmlTag
div
>
} const
const stream: Readable
stream
=
function renderToStream(html: JSX.Element | ((rid: number | string) => JSX.Element), rid?: number | string): Readable

Transforms a component tree who may contain Suspense components into a stream of HTML.

@example
import { text} from 'node:stream/consumers';

// Prints out the rendered stream (2nd example shows with a custom id)
const stream = renderToStream(r => <AppWithSuspense rid={r} />)
const stream = renderToStream(<AppWithSuspense rid={myCustomId} />, myCustomId)

// You can consume it as a stream
for await (const html of stream) {
 console.log(html.toString())
}

// Or join it all together (Wastes ALL Suspense benefits, but useful for testing)
console.log(await text(stream))
@paramhtml The component tree to render or a function that returns the component tree.@paramrid The request id to identify the request, if not provided, a new incrementing id will be used.@see{@linkcode Suspense}
renderToStream
((
rid: string | number
rid
) => (
<
JSX.IntrinsicElements.html: JSX.HtmlHtmlTag
html
>
<
JSX.IntrinsicElements.body: JSX.HtmlBodyTag
body
>
<
JSX.IntrinsicElements.h1: JSX.HtmlTag
h1
>Dashboard</
JSX.IntrinsicElements.h1: JSX.HtmlTag
h1
>
<
function Suspense(props: SuspenseProps): JSX.Element

A component that returns a fallback while the async children are loading.

The rid prop is the one {@linkcode renderToStream } returns, this way the suspense knows which request it belongs to.

Warning: Using Suspense without any type of runtime support will LEAK memory and not work. Always use with renderToStream or within a framework that supports it.

Suspense
SuspenseProps.rid: string | number

The request id is used to identify the request for this suspense.

rid
={
rid: string | number
rid
}
SuspenseProps.fallback: JSX.Element

The fallback to render while the async children are loading.

fallback
={<
JSX.IntrinsicElements.div: JSX.HtmlTag
div
>Loading stats...</
JSX.IntrinsicElements.div: JSX.HtmlTag
div
>}>
<
function UserStats({ id }: {
    id: string;
}): Promise<string>
UserStats
id: string
id
="123" />
</
function Suspense(props: SuspenseProps): JSX.Element

A component that returns a fallback while the async children are loading.

The rid prop is the one {@linkcode renderToStream } returns, this way the suspense knows which request it belongs to.

Warning: Using Suspense without any type of runtime support will LEAK memory and not work. Always use with renderToStream or within a framework that supports it.

Suspense
>
</
JSX.IntrinsicElements.body: JSX.HtmlBodyTag
body
>
</
JSX.IntrinsicElements.html: JSX.HtmlHtmlTag
html
>
))
<html>
  <body>
    <h1>Dashboard</h1>
    <div id="B:1:1" data-sf><div>Loading stats...</div></div>
  </body>
</html>
<!-- SuspenseScript omitted for brevity -->
<template id="N:1:1" data-sr><div>123 posts</div></template>
<script id="S:1:1" data-ss>
  $KITA_RC("1:1");
</script>

The renderToStream callback receives the rid automatically. Pipe the returned stream to your HTTP response.

import { text } from 'node:stream/consumers'

// As a stream
stream.pipe(response)

// Or consume entirely (loses streaming benefits, useful for testing)
const html = await text(stream)

Custom request IDs

If you need to control the request ID, pass it as the second argument and use it directly in your JSX.

const 
const stream: Readable
stream
=
function renderToStream(html: JSX.Element | ((rid: number | string) => JSX.Element), rid?: number | string): Readable

Transforms a component tree who may contain Suspense components into a stream of HTML.

@example
import { text} from 'node:stream/consumers';

// Prints out the rendered stream (2nd example shows with a custom id)
const stream = renderToStream(r => <AppWithSuspense rid={r} />)
const stream = renderToStream(<AppWithSuspense rid={myCustomId} />, myCustomId)

// You can consume it as a stream
for await (const html of stream) {
 console.log(html.toString())
}

// Or join it all together (Wastes ALL Suspense benefits, but useful for testing)
console.log(await text(stream))
@paramhtml The component tree to render or a function that returns the component tree.@paramrid The request id to identify the request, if not provided, a new incrementing id will be used.@see{@linkcode Suspense}
renderToStream
(
(
rid: string | number
rid
) => (
<
function Suspense(props: SuspenseProps): JSX.Element

A component that returns a fallback while the async children are loading.

The rid prop is the one {@linkcode renderToStream } returns, this way the suspense knows which request it belongs to.

Warning: Using Suspense without any type of runtime support will LEAK memory and not work. Always use with renderToStream or within a framework that supports it.

Suspense
SuspenseProps.rid: string | number

The request id is used to identify the request for this suspense.

rid
={
rid: string | number
rid
}
SuspenseProps.fallback: JSX.Element

The fallback to render while the async children are loading.

fallback
={<
JSX.IntrinsicElements.div: JSX.HtmlTag
div
>Loading...</
JSX.IntrinsicElements.div: JSX.HtmlTag
div
>}>
<
function AsyncContent(): Promise<string>
AsyncContent
/>
</
function Suspense(props: SuspenseProps): JSX.Element

A component that returns a fallback while the async children are loading.

The rid prop is the one {@linkcode renderToStream } returns, this way the suspense knows which request it belongs to.

Warning: Using Suspense without any type of runtime support will LEAK memory and not work. Always use with renderToStream or within a framework that supports it.

Suspense
>
),
const myCustomId: string
myCustomId
)
<div id="B:req-psyka8qt0hm:1" data-sf><div>Loading...</div></div>
<!-- SuspenseScript omitted for brevity -->
<template id="N:req-psyka8qt0hm:1" data-sr><div>Loaded content</div></template>
<script id="S:req-psyka8qt0hm:1" data-ss>
  $KITA_RC("req-psyka8qt0hm:1");
</script>

Error handling

The catch prop handles errors thrown by async children. It accepts a JSX element or a function that receives the error and returns JSX.

const 
const html: JSX.Element
html
= (
<
function Suspense(props: SuspenseProps): JSX.Element

A component that returns a fallback while the async children are loading.

The rid prop is the one {@linkcode renderToStream } returns, this way the suspense knows which request it belongs to.

Warning: Using Suspense without any type of runtime support will LEAK memory and not work. Always use with renderToStream or within a framework that supports it.

Suspense
SuspenseProps.rid: string | number

The request id is used to identify the request for this suspense.

rid
={
const rid: "req-1"
rid
}
SuspenseProps.fallback: JSX.Element

The fallback to render while the async children are loading.

fallback
={<
JSX.IntrinsicElements.div: JSX.HtmlTag
div
>Loading...</
JSX.IntrinsicElements.div: JSX.HtmlTag
div
>}
SuspenseProps.catch?: JSX.Element | ((error: unknown) => JSX.Element) | undefined

This error boundary is used to catch any error thrown by an async component and streams its fallback instead.

Undefined behaviors happens on each browser kind when the html stream is unexpected closed by the server if an error is thrown. You should always define an error boundary to catch errors.

This does not catches for errors thrown by the suspense itself or async fallback components. Please use {@linkcode ErrorBoundary } to catch them instead.

catch
={(
error: unknown
error
) => <
JSX.IntrinsicElements.div: JSX.HtmlTag
div
>Failed: {
var String: StringConstructor
(value?: any) => string

Allows manipulation and formatting of text strings and determination and location of substrings within strings.

String
(
error: unknown
error
)}</
JSX.IntrinsicElements.div: JSX.HtmlTag
div
>}
> <
function AsyncContent(): Promise<string>
AsyncContent
/>
</
function Suspense(props: SuspenseProps): JSX.Element

A component that returns a fallback while the async children are loading.

The rid prop is the one {@linkcode renderToStream } returns, this way the suspense knows which request it belongs to.

Warning: Using Suspense without any type of runtime support will LEAK memory and not work. Always use with renderToStream or within a framework that supports it.

Suspense
>
)
<div id="B:req-1:1" data-sf><div>Loading...</div></div>

Without a catch prop, errors propagate to the stream as an 'error' event. Always provide a catch handler in production to prevent the stream from closing unexpectedly.

Multiple boundaries

Each Suspense boundary resolves independently. Place separate boundaries around sections that fetch different data so they can appear as soon as their data is ready.

const 
const stream: Readable
stream
=
function renderToStream(html: JSX.Element | ((rid: number | string) => JSX.Element), rid?: number | string): Readable

Transforms a component tree who may contain Suspense components into a stream of HTML.

@example
import { text} from 'node:stream/consumers';

// Prints out the rendered stream (2nd example shows with a custom id)
const stream = renderToStream(r => <AppWithSuspense rid={r} />)
const stream = renderToStream(<AppWithSuspense rid={myCustomId} />, myCustomId)

// You can consume it as a stream
for await (const html of stream) {
 console.log(html.toString())
}

// Or join it all together (Wastes ALL Suspense benefits, but useful for testing)
console.log(await text(stream))
@paramhtml The component tree to render or a function that returns the component tree.@paramrid The request id to identify the request, if not provided, a new incrementing id will be used.@see{@linkcode Suspense}
renderToStream
((
rid: string | number
rid
) => (
<
JSX.IntrinsicElements.html: JSX.HtmlHtmlTag
html
>
<
JSX.IntrinsicElements.body: JSX.HtmlBodyTag
body
>
<
function Suspense(props: SuspenseProps): JSX.Element

A component that returns a fallback while the async children are loading.

The rid prop is the one {@linkcode renderToStream } returns, this way the suspense knows which request it belongs to.

Warning: Using Suspense without any type of runtime support will LEAK memory and not work. Always use with renderToStream or within a framework that supports it.

Suspense
SuspenseProps.rid: string | number

The request id is used to identify the request for this suspense.

rid
={
rid: string | number
rid
}
SuspenseProps.fallback: JSX.Element

The fallback to render while the async children are loading.

fallback
={<
JSX.IntrinsicElements.div: JSX.HtmlTag
div
>Loading user...</
JSX.IntrinsicElements.div: JSX.HtmlTag
div
>}>
<
function UserProfile({ id }: {
    id: string;
}): Promise<string>
UserProfile
id: string
id
="123" />
</
function Suspense(props: SuspenseProps): JSX.Element

A component that returns a fallback while the async children are loading.

The rid prop is the one {@linkcode renderToStream } returns, this way the suspense knows which request it belongs to.

Warning: Using Suspense without any type of runtime support will LEAK memory and not work. Always use with renderToStream or within a framework that supports it.

Suspense
>
<
function Suspense(props: SuspenseProps): JSX.Element

A component that returns a fallback while the async children are loading.

The rid prop is the one {@linkcode renderToStream } returns, this way the suspense knows which request it belongs to.

Warning: Using Suspense without any type of runtime support will LEAK memory and not work. Always use with renderToStream or within a framework that supports it.

Suspense
SuspenseProps.rid: string | number

The request id is used to identify the request for this suspense.

rid
={
rid: string | number
rid
}
SuspenseProps.fallback: JSX.Element

The fallback to render while the async children are loading.

fallback
={<
JSX.IntrinsicElements.div: JSX.HtmlTag
div
>Loading feed...</
JSX.IntrinsicElements.div: JSX.HtmlTag
div
>}>
<
function ActivityFeed({ id }: {
    id: string;
}): Promise<string>
ActivityFeed
id: string
id
="123" />
</
function Suspense(props: SuspenseProps): JSX.Element

A component that returns a fallback while the async children are loading.

The rid prop is the one {@linkcode renderToStream } returns, this way the suspense knows which request it belongs to.

Warning: Using Suspense without any type of runtime support will LEAK memory and not work. Always use with renderToStream or within a framework that supports it.

Suspense
>
</
JSX.IntrinsicElements.body: JSX.HtmlBodyTag
body
>
</
JSX.IntrinsicElements.html: JSX.HtmlHtmlTag
html
>
))
<html>
  <body>
    <div id="B:2:1" data-sf><div>Loading user...</div></div>
    <div id="B:2:2" data-sf><div>Loading feed...</div></div>
  </body>
</html>
<!-- SuspenseScript omitted for brevity -->
<template id="N:2:1" data-sr><div>User 123</div></template>
<script id="S:2:1" data-ss>
  $KITA_RC("2:1");
</script>
<template id="N:2:2" data-sr><div>Feed 123</div></template>
<script id="S:2:2" data-ss>
  $KITA_RC("2:2");
</script>

The same rid is shared across all Suspense boundaries in the same request. The stream closes automatically when the last boundary resolves.

Async fallbacks

The fallback prop can be an async component, but this blocks the entire stream until the fallback resolves. The server will not send any HTML until the async fallback is ready.

async function 
function LoadingMessage(): Promise<string>
LoadingMessage
() {
await
function loadTranslations(): Promise<void>
loadTranslations
()
return <
JSX.IntrinsicElements.div: JSX.HtmlTag
div
>Loading user data...</
JSX.IntrinsicElements.div: JSX.HtmlTag
div
>
} const
const stream: Readable
stream
=
function renderToStream(html: JSX.Element | ((rid: number | string) => JSX.Element), rid?: number | string): Readable

Transforms a component tree who may contain Suspense components into a stream of HTML.

@example
import { text} from 'node:stream/consumers';

// Prints out the rendered stream (2nd example shows with a custom id)
const stream = renderToStream(r => <AppWithSuspense rid={r} />)
const stream = renderToStream(<AppWithSuspense rid={myCustomId} />, myCustomId)

// You can consume it as a stream
for await (const html of stream) {
 console.log(html.toString())
}

// Or join it all together (Wastes ALL Suspense benefits, but useful for testing)
console.log(await text(stream))
@paramhtml The component tree to render or a function that returns the component tree.@paramrid The request id to identify the request, if not provided, a new incrementing id will be used.@see{@linkcode Suspense}
renderToStream
((
rid: string | number
rid
) => (
<
function Suspense(props: SuspenseProps): JSX.Element

A component that returns a fallback while the async children are loading.

The rid prop is the one {@linkcode renderToStream } returns, this way the suspense knows which request it belongs to.

Warning: Using Suspense without any type of runtime support will LEAK memory and not work. Always use with renderToStream or within a framework that supports it.

Suspense
SuspenseProps.rid: string | number

The request id is used to identify the request for this suspense.

rid
={
rid: string | number
rid
}
SuspenseProps.fallback: JSX.Element

The fallback to render while the async children are loading.

fallback
={<
function LoadingMessage(): Promise<string>
LoadingMessage
/>}>
<
function UserProfile({ id }: {
    id: string;
}): Promise<string>
UserProfile
id: string
id
="123" />
</
function Suspense(props: SuspenseProps): JSX.Element

A component that returns a fallback while the async children are loading.

The rid prop is the one {@linkcode renderToStream } returns, this way the suspense knows which request it belongs to.

Warning: Using Suspense without any type of runtime support will LEAK memory and not work. Always use with renderToStream or within a framework that supports it.

Suspense
>
))
<div id="B:3:1" data-sf><div>Loading user data...</div></div>
<!-- SuspenseScript omitted for brevity -->
<template id="N:3:1" data-sr><div>User 123</div></template>
<script id="S:3:1" data-ss>
  $KITA_RC("3:1");
</script>

The stream waits for LoadingMessage to resolve before sending anything to the client. This defeats the purpose of streaming. Use synchronous fallbacks whenever possible.

If an async fallback throws an error, wrap the Suspense in an ErrorBoundary to handle it. The catch prop only handles errors from the async children, not from the fallback itself.