react-streaming
React 18 Streaming. Made fully-fledged & easy.
Contents
Intro
Features (for React users):
- Unlocks
<Suspsense>
for SSR apps. - Unlocks React libraries of tomorrow. (Such as using Telefunc for data fetching.)
- Supports all platforms (Vercel, Cloudflare Workers, AWS, Netlify Edge, Deno, Deploy, ...).
- Two SEO strategies:
conservative
or google-speed
. - Bonus: new
useAsync()
hook.
Features (for library authors):
useSsrData()
: Define isomorphic data.injectToStream()
: Inject chunks to the stream.
Easy:
import { renderToStream } from 'react-streaming/server'
const {
pipe,
readable
} = await renderToStream(<Page />)
Why Streaming
React 18's new SSR streaming architecture unlocks many capabilities:
- Data Fetching:
- Use RPC to fetch data in a seamless way, e.g. with Telefunc or tRPC. (Data fetching SSR hooks will be a thing of the past: no more Next.js's
getServerSideProps()
nor vite-plugin-ssr
's onBeforeRender()
.) - Expect your GraphQL tools to significantly improve, both performance and DX. (Also expect new tools such as Vilay.)
- 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 low-end and/or poorly-connected devices.)
- Progressive Hydration. (Page is interactive before the page/data finished loading.)
The problem: The current React 18 Streaming architecture is low-level and its ergonomics are cumbersome. (E.g. there is no standard way for library authors to take advantage of the new streaming architecture.)
The solution: react-streaming
.
Get Started
-
Install
npm install react-streaming
-
Server-side
import { renderToStream } from 'react-streaming/server'
const {
pipe,
readable
} = await renderToStream(<Page />, options)
-
Client-side
import { ReactStreaming } from 'react-streaming/client'
const page = (
<ReactStreaming>
<Page />
</ReactStreaming>
)
Options
-
options.disable?: boolean
: Disable streaming.
The component is still rendered to a stream, but the promise const promise = await renderTo...
resolves only after the stream has finished. (This effectively disables streaming from a user perspective, while unlocking React 18 Streaming capabilities such as SSR <Supsense>
.)
-
options.seoStrategy?: 'conservative' | 'google-speed'
-
conservative
(default): Disable streaming if the HTTP request originates from a bot. (Ensuring the bot always sees the whole HTML.)
-
google-speed
: Don't disable streaming for the Google Bot.
- Pro: Google ranks your website higher because the initial HTTP response is faster. (To be researched.)
- Con: Google will likely not wait for the whole HTML, and therefore not see it. (To be tested.)
-
Custom SEO strategy: use options.disable
. For example:
const disable = false
const disable =
isBot(userAgent) &&
!['googlebot', 'some-other-bot'].some(n => userAgent.toLowerCase().includes(n))
const stream = await renderToStream(<Page />, { disable })
-
options.userAgent?: string
: The HTTP User-Agent request header. (Needed for options.seoStrategy
.)
-
options.webStream?: boolean
: Use Web Streams instead of Node.js Streams in Node.js. (Node.js 18 released Web Streams support.)
Bonus: useAsync()
import { useAsync } from 'react-streaming'
function StarWarsMovies() {
return (
<div>
<p>List of Star Wars movies:</p>
<Suspense fallback={<p>Loading...</p>}>
<MovieList />
</Suspense>
</div>
)
}
function MovieList() {
const movies = useAsync(async () => {
const response = await fetch('https://star-wars.brillout.com/api/films.json')
return response.json()
})
return (
<ul>
{movies.forEach((movie) => (
<li>
{movie.title} ({movie.release_date})
</li>
))}
</ul>
)
}
Get Started (Library Authors)
react-streaming
enables you to suspend React rendering and await 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 three methods:
useAsync()
: Highest-level & easiest.useSsrData()
: High-level & easy.injectToStream()
: Low-level and highly flexible (both useAsync()
and useSsrData()
are based on it). Easy & recommended for injecting script and style tags. Complex for data fetching (if possible, use useSsrData()
or useAsync()
instead).
useAsync()
For how to use useAsync()
, see example above.
useSsrData()
import { useSsrData } from 'react-streaming'
function SomeComponent() {
const key = 'some-unique-key'
const someAsyncFunc = async function () {
return 'someData'
}
const value = useSsrData(key, someAsyncFunc)
assert(value === 'someData')
}
If <SomeComponent>
is rendered only on the client-side, then useSsrData()
is essentially a
cache that never invalidates. (If you want to re-run someAsyncFunc()
, then change the key.)
If <SomeComponent>
is rendered on the server-side (SSR), it injects the
resolved value into the stream and the client-side picks up the injected value. (So that the
client-side doesn't call someAsyncFunc()
but, instead, re-uses the value resolved on
the server-side.)
This is for example how useAsync()
is implemented:
import { useId } from 'react'
import { useSsrData } from 'react-streaming'
function useAsync(asyncFn) {
const id = useId()
return useSsrData(id, asyncFn)
}
injectToStream()
injectToStream(htmlChunk: string)
allows you to inject strings to the current stream.
There are two ways to access injectToStream()
:
- With
renderToStream()
:
import { renderToStream } from 'react-streaming/server'
const { injectToStream } = await renderToStream(<Page />)
- With
useStream()
:
import { useStream } from 'react-streaming'
function SomeComponent() {
const stream = useStream()
if (stream === null) {
}
const { injectToStream } = stream
}
Usage examples:
injectToStream('<script type="module" src="/main.js"></script>')
injectToStream('<styles>.some-component { color: blue }</styles>')
injectToStream(`<script type="application/json">${JSON.stringify(someData)}</script>`)
For a full example of using injectToStream()
, have a look at useSsrData()
's implementation.