MCPcopy Index your code
hub / github.com/brillout/react-streaming

github.com/brillout/react-streaming @v0.4.20

Chat with this repo
repository ↗ · DeepWiki ↗ · release v0.4.20 ↗ · + Follow
182 symbols 485 edges 100 files 2 documented · 1% 1 cross-repo links
What it actually does AI analysis from the code graph — generated when you open this
loading…
README

React Streaming

react-streaming

HTML Streaming with React — batteries-included, easy, and rock-solid (powers Vike's official React integration).

[!NOTE] Unfamiliar with HTML Streaming? Check out Dan's article about SSR and Streaming.

[!NOTE] The React team is working on high-level APIs that will eventually make parts of react-streaming obsolete, see @sebmarkbage comment at "RFC: injectToStream".

Contents

Intro

Features (for React users)

  • Unlocks <Suspense> for SSR apps.
  • useAsync(): easily fetch data for SSR apps.
  • Two SEO strategies: conservative or google-speed.
  • Seamless support for Node.js (serverless) platforms (Vercel, AWS EC2, ...) and Edge platforms (Cloudflare Workers, Deno Deploy, Netlify Edge, Vercel Edge, ...).
  • Easy error handling.

Features (for library authors)

  • useAsync(): add data fetching capabilities to your library. High-level and easy to use.
  • injectToStream(): inject chunks to the stream for your library. Low-level and difficult to use, but highly flexible.

Easy

import { renderToStream } from 'react-streaming/server'
const {
  pipe, // Node.js (Vercel, AWS EC2, ...)
  readable // Edge (Cloudflare Workers, Deno Deploy, Netlify Edge, Vercel Edge, ...)
} = await renderToStream(<Page />)

Why Streaming

React's SSR streaming architecture unlocks many capabilities:

  • Easily fetch data for SSR apps.
  • Fundamentally improved mobile performance. (Mobile users can progressively load the page as data is fetched, before even a single line of JavaScript is loaded. Especially important for users with a low-end device and poor internet connection.)
  • Progressive Hydration. (Page is interactive before even the page has finished loading.)

Problem: the current React Streaming architecture is low-level and difficult to use.

Solution: react-streaming.

react-streaming makes it easy to build the libraries of tomorrow, for example: - Use Telefunc to fetch data for your Next.js or Vike app. (Instead of Next.js's getServerSideProps() / Vike's data().) - Better GraphQL tools, e.g. Vilay.

Usage

Get Started

  1. Install

shell npm install react-streaming

  1. Server-side

jsx import { renderToStream } from 'react-streaming/server' const { pipe, // Defined if running in Node.js, otherwise `null` readable // Defined if running on Edge (e.g. Cloudflare Workers), otherwise `null` } = await renderToStream(<Page />)

That's it.

Options

const options = {
  // ...
}
await renderToStream(<Page />, options)
  • options.disable?: boolean: Disable streaming.

    <Page> is still rendered to a stream, but the promise const promise = renderToStream() resolves only after the stream has finished. (This effectively disables streaming from a user perspective, while unlocking capabilities such as server-side <Supsense>.)

  • options.seoStrategy?: 'conservative' | 'google-speed'

  • conservative (default): Disable streaming if the HTTP request originates from a bot. (Ensuring bots to always see the whole HTML.)

  • google-speed: Don't disable streaming for the Google Bot.
  • Custom SEO strategy: use options.disable. For example:

    ```jsx // Always stream, even for bots: const disable = false

    // Disable streaming for bots, except for the Google Bot and some other bot: const disable = isBot(userAgent) && !['googlebot', 'some-other-bot'].some(n => userAgent.toLowerCase().includes(n))

    await renderToStream(, { disable }) ```

  • options.userAgent?: string: The HTTP User-Agent request header. (Needed for options.seoStrategy.)

  • options.webStream?: boolean: In Node.js, use a Web Stream instead of a Node.js Stream. (Node.js 18 released Web Streams support.)
  • options.streamOptions: Options passed to React's renderToReadableStream() and renderToPipeableStream(). Use this to pass nonce, bootstrap scripts, etc. It excludes error handling options, use Error Handling instead.
  • options.timeout?: number | null (seconds): Timeout after which the rendering stream is aborted, see Abort. Defaults to 20 seconds. Set to null to disable automatic timeout (we recommend to then implement a manual timeout as explained at Abort).
  • options.onTimeout?: () => void: Callback when the timeout is reached.
  • options.onBoundaryError?: (err: unknown) => void: Called when a <Suspense> boundary fails. See Error Handling.
  • tsx const { streamEnd } = await renderToStream(<Page />) // ✅ Page Shell succesfully rendered. const success: boolean = await streamEnd // Stream ended. if (success) { // ✅ <Page> succesfully rendered } else { // ❌ A <Suspense> boundary failed. } Note that streamEnd never rejects.

    ⚠️ Read Error Handling before using streamEnd. In particular, do not use success to change the behavior of your app/stream (because React automatically takes care of gracefully handling <Suspense> failures).

Bots

By default, react-streaming disables streaming for bots and crawlers, such as: - The Google Bot, which crawls the HTML of your pages to be able to show a preview of your website on Google's result pages. - The bot of social sites (Twitter/Instagram/WhatsApp...), which crawl the HTML of your pages to be able to show a preview of your website when it's shared on Twitter/Instagram/WhatsApp/... - AI bots

[!NOTE] These bots explore your website by navigating the HTML of your pages. It isn't clear how bots handle HTML streams. It's therefore safer to provide bots with a fully rendered HTML at once that contains all the content of your page (i.e. disable HTML streaming) instead of hoping that bots will await the HTML stream.

For react-streaming to be able to determine whether a request comes from a bot or a real user, you need to provide options.userAgent.

[!NOTE] If you use Vike with vike-react, you can simply set renderPage({ headersOriginal }) instead. (The User-Agent request header will then automatically be passed to react-streaming).

You can implement a custom strategy, see options.seoStrategy.

You can use $ curl to see the HTML response that bots and crawlers receive:

# What bots and crawls get: no HTML Streaming, just "classic SSR"
$ curl http://localhost:3000/star-wars
# What human users get: HTML Streaming
$ curl http://localhost:3000/star-wars -N -H "User-Agent: chrome"

[!NOTE] By default curl sets User-Agent: curl/8.5.0, which react-streaming interprets as bot.

Error Handling

The promise await renderToStream() resolves after the page shell is rendered. This means that if an error occurs while rendering the page shell, then the promise rejects with that error.

:book: The page shell is the set of all components before <Suspense> boundaries.

let stream
try {
  stream = await renderToStream(<Page />)
  // ✅ Page shell succesfully rendered and is ready in the stream buffer.
} catch(err) {
  // ❌ Something went wrong while rendering the page shell.
}

The stream never emits an error. (If it does emit an error then it's a Bug in React or react-streaming.)

:book: If an error occurs during the stream, then it means that a <Suspense> boundary failed. Instead of emiting a stream error, React swallows the error on the server-side and retries the <Suspense> boundary on the client-side. If the <Suspense> fails again on the client-side, then the client-side throws the error.

This means that errors occuring during the stream are handled by React and there is nothing for you to do on the server-side. That said, you may want to gracefully handle the error on the client-side e.g. with react-error-boundary.

You can use options.onBoundaryError() for error tracking purposes.

See also: - React Docs > renderToReadableStream > Recovering from errors outside the shell - React Docs > renderToReadableStream > Recovering from errors inside the shell - React Docs > <Suspense> > Providing a fallback for server errors and client-only content

Abort

After a default timeout of 20 seconds react-streaming aborts the rendering stream, as recommended by React here and there.

When the timeout is reached react-streaming ends the stream and tells React to stop rendering. Note that there isn't any thrown error: React merely stops server-side rendering and continues on the client-side, see explanation at Error Handling.

You can also manually abort:

const { abort } = await renderToStream(<Page />, { timeout: null })
abort()

SSR

With react-streaming, all your page content is included in the HTML stream, giving you all the benefits of SSR.

[!NOTE] It isn't clear how bots handle HTML streams. For now, react-streaming takes a conservative approach and automatically disables HTML streaming for crawlers, falling back to classic SSR — see Bots.

[!NOTE] The order in which the content of your page is included in the HTML stream depends on which data comes first. For example, if you use a loading fallback component, the content of the loading component appears first, followed by the content of the main component after the <Suspense> boundary resolves.

useAsync()

import { useAsync } from 'react-streaming'

function Page({ movieId }) {
  return (
    <Suspense fallback={

Loading...

}>
      <Movie id={movieId}/>
    </Suspense>
  )
}

async function fetchMovie(id) {
  const response = await fetch(`https://star-wars.brillout.com/api/films/${id}.json`)
  return response.json()
}

// This component is isomorphic: it works on both the client-side and server-side. The
// data fetched during SSR is automatically passed and re-used on the client-side.
function Movie({ id }) {
  const key = [
    'star-wars-movies',
    id // Re-run `fetchMovie()` if `id` changes
  ]
  const movie = useAsync(key, () => fetchMovie(id))
  return (
    <ul>
      <li>
        Title: {movie.title}
      </li>
      <li>
        Release Date: {movie.release_date}
      </li>
    </ul>
  )
}

See useAsync() (Library Authors) for more information.

Usage (Library Authors)

Overview

react-streaming enables you to suspend the React rendering and await for something to happen. (Usually data fetching.) The novelty here is that it's isomorphic:

  • It works on the client-side as well as on the server-side (while Serve-Side Rendering).
  • For hydration, data is passed from the server to the client. (So that data isn't loaded twice.)

You have the choice between:

  • useAsync(): High-level and easy.
  • injectToStream(): Low-level and highly flexible (useAsync() is based on it). Easy & recommended for injecting script and style tags. Complex for data fetching (if possible, use useAsync() instead).

useAsync() (Library Authors)

This section is a low-level description of useAsync(). For a high-level descrip

Core symbols most depended-on inside this repo

assert
called by 31
src/utils/assert.ts
assertUsage
called by 13
src/utils/assert.ts
debug
called by 11
src/utils/debug.ts
render
called by 8
test/render.tsx
testCounter
called by 8
examples/vike-react/.testRun.ts
getGlobalObject
called by 6
src/utils/getGlobalObject.ts
injectToStream
called by 5
src/server/renderToStream.ts
ensureWasClientSideRouted
called by 5
examples/vike-react/.testRun.ts

Shape

Function 182

Languages

TypeScript100%

Modules by API surface

examples/vike-react/.testRun.ts15 symbols
src/utils/debug.ts11 symbols
src/server/renderToStream.ts10 symbols
src/server/renderToStream/common.ts9 symbols
src/server/renderToStream/createPipeWrapper.ts8 symbols
src/server/renderToStream/orchestrateChunks.ts7 symbols
src/utils/assert.ts6 symbols
test/render.tsx5 symbols
src/server/renderToStream/createReadableWrapper.ts5 symbols
test/Page.tsx4 symbols
src/utils/createErrorWithCleanStackTrace.ts4 symbols
src/shared/useSuspense.ts4 symbols

Used by 1 indexed graphs manifest dependencies, hub-wide

For agents

$ claude mcp add react-streaming \
  -- python -m otcore.mcp_server <graph>

⬇ download graph artifact