Browse Source

[Beta] renderToReadableStream API (#5346)

main
dan 2 years ago
committed by GitHub
parent
commit
b2e8dbf9ef
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      beta/src/content/apis/react-dom/server/index.md
  2. 44
      beta/src/content/apis/react-dom/server/renderToPipeableStream.md
  3. 612
      beta/src/content/apis/react-dom/server/renderToReadableStream.md
  4. 3
      beta/src/sidebarAPIs.json

2
beta/src/content/apis/react-dom/server/index.md

@ -25,7 +25,7 @@ These methods are only available in the environments with [Node.js Streams:](htt
These methods are only available in the environments with [Web Streams](https://developer.mozilla.org/en-US/docs/Web/API/Streams_API), which includes browsers, Deno, and some modern edge runtimes: These methods are only available in the environments with [Web Streams](https://developer.mozilla.org/en-US/docs/Web/API/Streams_API), which includes browsers, Deno, and some modern edge runtimes:
* [`renderToReadableStream`](/apis/react-dom/server/renderToReadableStream) renders a React tree to a [Readable](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream) Web Stream. * [`renderToReadableStream`](/apis/react-dom/server/renderToReadableStream) renders a React tree to a [Readable Web Stream.](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream)
--- ---

44
beta/src/content/apis/react-dom/server/renderToPipeableStream.md

@ -14,6 +14,12 @@ const { pipe, abort } = renderToPipeableStream(reactNode, options?)
<InlineToc /> <InlineToc />
<Note>
This API is specific to Node.js. Environments with [Web Streams,](https://developer.mozilla.org/en-US/docs/Web/API/Streams_API) like Deno and modern edge runtimes, should use [`renderToReadableStream`](/apis/react-dom/server/renderToReadableStream) instead.
</Note>
--- ---
## Usage {/*usage*/} ## Usage {/*usage*/}
@ -30,6 +36,7 @@ app.use('/', (request, response) => {
const { pipe } = renderToPipeableStream(<App />, { const { pipe } = renderToPipeableStream(<App />, {
bootstrapScripts: ['/main.js'], bootstrapScripts: ['/main.js'],
onShellReady() { onShellReady() {
response.setHeader('content-type', 'text/html');
pipe(response); pipe(response);
} }
}); });
@ -43,8 +50,10 @@ export default function App() {
return ( return (
<html> <html>
<head> <head>
<title>My app</title> <meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="stylesheet" href="/styles.css"></link> <link rel="stylesheet" href="/styles.css"></link>
<title>My app</title>
</head> </head>
<body> <body>
<Router /> <Router />
@ -88,8 +97,9 @@ export default function App({ assetMap }) {
return ( return (
<html> <html>
<head> <head>
<title>My app</title> ...
<link rel="stylesheet" href={assetMap['styles.css']}></link> <link rel="stylesheet" href={assetMap['styles.css']}></link>
...
</head> </head>
... ...
</html> </html>
@ -99,7 +109,7 @@ export default function App({ assetMap }) {
On the server, render `<App assetMap={assetMap} />` and pass your `assetMap` with the asset URLs: On the server, render `<App assetMap={assetMap} />` and pass your `assetMap` with the asset URLs:
```js {1-5,8} ```js {1-5,8,9}
// You'd need to get this JSON from your build tooling, e.g. read it from the build output. // You'd need to get this JSON from your build tooling, e.g. read it from the build output.
const assetMap = { const assetMap = {
'styles.css': '/styles.123456.css', 'styles.css': '/styles.123456.css',
@ -110,6 +120,7 @@ app.use('/', (request, response) => {
const { pipe } = renderToPipeableStream(<App assetMap={assetMap} />, { const { pipe } = renderToPipeableStream(<App assetMap={assetMap} />, {
bootstrapScripts: [assetMap['main.js']], bootstrapScripts: [assetMap['main.js']],
onShellReady() { onShellReady() {
response.setHeader('content-type', 'text/html');
pipe(response); pipe(response);
} }
}); });
@ -131,6 +142,7 @@ app.use('/', (request, response) => {
bootstrapScriptContents: `window.assetMap = ${JSON.stringify(assetMap)};`, bootstrapScriptContents: `window.assetMap = ${JSON.stringify(assetMap)};`,
bootstrapScripts: [assetMap['main.js']], bootstrapScripts: [assetMap['main.js']],
onShellReady() { onShellReady() {
response.setHeader('content-type', 'text/html');
pipe(response); pipe(response);
} }
}); });
@ -272,10 +284,11 @@ If you wrap the whole app into a `<Suspense>` boundary at the root, the shell wi
The `onShellReady` callback fires when the entire shell has been rendered. Usually, you'll start streaming then: The `onShellReady` callback fires when the entire shell has been rendered. Usually, you'll start streaming then:
```js {3-5} ```js {3-6}
const { pipe } = renderToPipeableStream(<App />, { const { pipe } = renderToPipeableStream(<App />, {
bootstrapScripts: ['/main.js'], bootstrapScripts: ['/main.js'],
onShellReady() { onShellReady() {
response.setHeader('content-type', 'text/html');
pipe(response); pipe(response);
} }
}); });
@ -289,10 +302,11 @@ By the time `onShellReady` fires, components in nested `<Suspense>` boundaries m
By default, all errors on the server are logged to console. You can override this behavior to log crash reports: By default, all errors on the server are logged to console. You can override this behavior to log crash reports:
```js {6-9} ```js {7-10}
const { pipe } = renderToPipeableStream(<App />, { const { pipe } = renderToPipeableStream(<App />, {
bootstrapScripts: ['/main.js'], bootstrapScripts: ['/main.js'],
onShellReady() { onShellReady() {
response.setHeader('content-type', 'text/html');
pipe(response); pipe(response);
}, },
onError(error) { onError(error) {
@ -325,14 +339,16 @@ function ProfilePage() {
If an error occurs while rendering those components, React won't have any meaningful HTML to send to the client. Override `onShellError` to send a fallback HTML that doesn't rely on server rendering as the last resort: If an error occurs while rendering those components, React won't have any meaningful HTML to send to the client. Override `onShellError` to send a fallback HTML that doesn't rely on server rendering as the last resort:
```js {6-9} ```js {7-11}
const { pipe } = renderToPipeableStream(<App />, { const { pipe } = renderToPipeableStream(<App />, {
bootstrapScripts: ['/main.js'], bootstrapScripts: ['/main.js'],
onShellReady() { onShellReady() {
response.setHeader('content-type', 'text/html');
pipe(response); pipe(response);
}, },
onShellError(error) { onShellError(error) {
response.statusCode = 500; response.statusCode = 500;
response.setHeader('content-type', 'text/html');
response.send('<h1>Something went wrong</h1>'); response.send('<h1>Something went wrong</h1>');
}, },
onError(error) { onError(error) {
@ -386,10 +402,12 @@ const { pipe } = renderToPipeableStream(<App />, {
bootstrapScripts: ['/main.js'], bootstrapScripts: ['/main.js'],
onShellReady() { onShellReady() {
response.statusCode = 200; response.statusCode = 200;
response.setHeader('content-type', 'text/html');
pipe(response); pipe(response);
}, },
onShellError(error) { onShellError(error) {
response.statusCode = 500; response.statusCode = 500;
response.setHeader('content-type', 'text/html');
response.send('<h1>Something went wrong</h1>'); response.send('<h1>Something went wrong</h1>');
}, },
onError(error) { onError(error) {
@ -403,17 +421,19 @@ If a component *outside* the shell (i.e. inside a `<Suspense>` boundary) throws
However, if you'd like, you can use the fact that something has errored to set the status code: However, if you'd like, you can use the fact that something has errored to set the status code:
```js {1,6,14} ```js {1,6,16}
let didError = false; let didError = false;
const { pipe } = renderToPipeableStream(<App />, { const { pipe } = renderToPipeableStream(<App />, {
bootstrapScripts: ['/main.js'], bootstrapScripts: ['/main.js'],
onShellReady() { onShellReady() {
response.statusCode = didError ? 500 : 200; response.statusCode = didError ? 500 : 200;
response.setHeader('content-type', 'text/html');
pipe(response); pipe(response);
}, },
onShellError(error) { onShellError(error) {
response.statusCode = 500; response.statusCode = 500;
response.setHeader('content-type', 'text/html');
response.send('<h1>Something went wrong</h1>'); response.send('<h1>Something went wrong</h1>');
}, },
onError(error) { onError(error) {
@ -432,7 +452,7 @@ This will only catch errors outside the shell that happened while generating the
You can [create your own `Error` subclasses](https://javascript.info/custom-errors) and use the [`instanceof`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/instanceof) operator to check which error is thrown. For example, you can define a custom `NotFoundError` and throw it from your component. Then your `onError`, `onShellReady`, and `onShellError` callbacks can do something different depending on the error type: You can [create your own `Error` subclasses](https://javascript.info/custom-errors) and use the [`instanceof`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/instanceof) operator to check which error is thrown. For example, you can define a custom `NotFoundError` and throw it from your component. Then your `onError`, `onShellReady`, and `onShellError` callbacks can do something different depending on the error type:
```js {2,4-14,19,23,28} ```js {2,4-14,19,24,30}
let didError = false; let didError = false;
let caughtError = null; let caughtError = null;
@ -452,10 +472,12 @@ const { pipe } = renderToPipeableStream(<App />, {
bootstrapScripts: ['/main.js'], bootstrapScripts: ['/main.js'],
onShellReady() { onShellReady() {
response.statusCode = getStatusCode(); response.statusCode = getStatusCode();
response.setHeader('content-type', 'text/html');
pipe(response); pipe(response);
}, },
onShellError(error) { onShellError(error) {
response.statusCode = getStatusCode(); response.statusCode = getStatusCode();
response.setHeader('content-type', 'text/html');
response.send('<h1>Something went wrong</h1>'); response.send('<h1>Something went wrong</h1>');
}, },
onError(error) { onError(error) {
@ -480,7 +502,7 @@ However, when a crawler visits your page, or if you're generating the pages at t
You can wait for all the content to load using the `onAllReady` callback: You can wait for all the content to load using the `onAllReady` callback:
```js {2,7,10,16-21} ```js {2,7,11,18-24}
let didError = false; let didError = false;
let isCrawler = // ... depends on your bot detection strategy ... let isCrawler = // ... depends on your bot detection strategy ...
@ -489,16 +511,19 @@ const { pipe } = renderToPipeableStream(<App />, {
onShellReady() { onShellReady() {
if (!isCrawler) { if (!isCrawler) {
response.statusCode = didError ? 500 : 200; response.statusCode = didError ? 500 : 200;
response.setHeader('content-type', 'text/html');
pipe(response); pipe(response);
} }
}, },
onShellError(error) { onShellError(error) {
response.statusCode = 500; response.statusCode = 500;
response.setHeader('content-type', 'text/html');
response.send('<h1>Something went wrong</h1>'); response.send('<h1>Something went wrong</h1>');
}, },
onAllReady() { onAllReady() {
if (isCrawler) { if (isCrawler) {
response.statusCode = didError ? 500 : 200; response.statusCode = didError ? 500 : 200;
response.setHeader('content-type', 'text/html');
pipe(response); pipe(response);
} }
}, },
@ -544,6 +569,7 @@ import { renderToPipeableStream } from 'react-dom/server';
const { pipe } = renderToPipeableStream(<App />, { const { pipe } = renderToPipeableStream(<App />, {
bootstrapScripts: ['/main.js'], bootstrapScripts: ['/main.js'],
onShellReady() { onShellReady() {
response.setHeader('content-type', 'text/html');
pipe(response); pipe(response);
} }
}); });

612
beta/src/content/apis/react-dom/server/renderToReadableStream.md

@ -2,22 +2,618 @@
title: renderToReadableStream title: renderToReadableStream
--- ---
<Wip> <Intro>
This section is incomplete, please see the old docs for [renderToReadableStream.](https://reactjs.org/docs/react-dom-server.html#rendertoreadablestream) `renderToReadableStream` renders a React tree to a [Readable Web Stream.](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream)
</Wip> ```js
const stream = await renderToReadableStream(reactNode, options?)
```
</Intro>
<Intro> <InlineToc />
<Note>
This API depends on [Web Streams.](https://developer.mozilla.org/en-US/docs/Web/API/Streams_API) For Node.js, use [`renderToPipeableStream`](/apis/react-dom/server/renderToPipeableStream) instead.
</Note>
---
## Usage {/*usage*/}
### Rendering a React tree as HTML to a Readable Web Stream {/*rendering-a-react-tree-as-html-to-a-readable-web-stream*/}
Call `renderToReadableStream` to render your React tree as HTML into a [Readable Web Stream:](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream)
```js [[1, 4, "<App />"], [2, 5, "['/main.js']"]]
import { renderToReadableStream } from 'react-dom/server';
async function handler(request) {
const stream = await renderToReadableStream(<App />, {
bootstrapScripts: ['/main.js']
});
return new Response(stream, {
headers: { 'content-type': 'text/html' },
});
}
```
Along with the <CodeStep step={1}>root component</CodeStep>, you need to provide a list of <CodeStep step={2}>boostrap `<script>` paths</CodeStep>. Your root component should return **the entire document including the root `<html>` tag.** For example, it might look like this:
```js [[1, 1, "App"]]
export default function App() {
return (
<html>
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="stylesheet" href="/styles.css"></link>
<title>My app</title>
</head>
<body>
<Router />
</body>
</html>
);
}
```
React will automatically inject the [doctype](https://developer.mozilla.org/en-US/docs/Glossary/Doctype) and your <CodeStep step={2}>bootstrap `<script>` tags</CodeStep> into the resulting HTML stream:
```html [[2, 5, "/main.js"]]
<!DOCTYPE html>
<html>
<!-- ... HTML from your components ... -->
</html>
<script src="/main.js" async=""></script>
```
On the client, your bootstrap script should [hydrate the entire `document` with a call to `hydrateRoot`:](/apis/react-dom/client/hydrateRoot#hydrating-an-entire-document)
```js [[1, 4, "<App />"]]
import {hydrateRoot} from 'react-dom/client';
import App from './App.js';
hydrateRoot(document, <App />);
```
This will attach event listeners to the server-generated HTML and make it interactive.
<DeepDive>
#### Reading CSS and JS asset paths from the build output {/*reading-css-and-js-asset-paths-from-the-build-output*/}
The final asset URLs (like JavaScript and CSS files) are often hashed after the build. For example, instead of `styles.css` you might end up with `styles.123456.css`. Hashing static asset filenames guarantees that every distinct build of the same asset will have a different filename. This is useful because it lets you safely enable long-term caching for static assets: a file with a certain name would never change content.
However, if you don't know the asset URLs until after the build, there's no way for you to put them in the source code. For example, hardcoding `"/styles.css"` into JSX like earlier wouldn't work. To keep them out of your source code, your root component can read the real filenames from a map passed as a prop:
```js {1,6}
export default function App({ assetMap }) {
return (
<html>
<head>
<title>My app</title>
<link rel="stylesheet" href={assetMap['styles.css']}></link>
</head>
...
</html>
);
}
```
On the server, render `<App assetMap={assetMap} />` and pass your `assetMap` with the asset URLs:
```js {1-5,8,9}
// You'd need to get this JSON from your build tooling, e.g. read it from the build output.
const assetMap = {
'styles.css': '/styles.123456.css',
'main.js': '/main.123456.js'
};
async function handler(request) {
const stream = await renderToReadableStream(<App assetMap={assetMap} />, {
bootstrapScripts: [assets['/main.js']]
});
return new Response(stream, {
headers: { 'content-type': 'text/html' },
});
}
```
Since your server is now rendering `<App assetMap={assetMap} />`, you need to render it with `assetMap` on the client too to avoid hydration errors. You can serialize and pass `assetMap` to the client like this:
```js {9-10}
// You'd need to get this JSON from your build tooling.
const assetMap = {
'styles.css': '/styles.123456.css',
'main.js': '/main.123456.js'
};
async function handler(request) {
const stream = await renderToReadableStream(<App assetMap={assetMap} />, {
// Careful: It's safe to stringify() this because this data isn't user-generated.
bootstrapScriptContents: `window.assetMap = ${JSON.stringify(assetMap)};`,
bootstrapScripts: [assets['/main.js']],
});
return new Response(stream, {
headers: { 'content-type': 'text/html' },
});
}
```
Streams a React element to its initial HTML. Returns a Promise that resolves to a [Readable Stream.](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream) Fully supports Suspense and streaming of HTML. [Read more](https://github.com/reactwg/react-18/discussions/127) In the example above, the `bootstrapScriptContents` option adds an extra inline `<script>` tag that sets the global `window.assetMap` variable on the client. This lets the client code read the same `assetMap`:
```js {4}
import {hydrateRoot} from 'react-dom/client';
import App from './App.js';
hydrateRoot(document, <App assetMap={window.assetMap} />);
```
Both client and server render `App` with the same `assetMap` prop, so there are no hydration errors.
</DeepDive>
---
### Streaming more content as it loads {/*streaming-more-content-as-it-loads*/}
Streaming allows the user to start seeing the content even before all the data has loaded on the server. For example, consider a profile page that shows a cover, a sidebar with friends and photos, and a list of posts:
```js ```js
renderToReadableStream(element, options); function ProfilePage() {
return (
<ProfileLayout>
<ProfileCover />
<Sidebar>
<Friends />
<Photos />
</Sidebar>
<Posts />
</ProfileLayout>
);
}
``` ```
</Intro> Imagine that loading data for `<Posts />` takes some time. Ideally, you'd want to show the rest of the profile page content to the user without waiting for the posts. To do this, [wrap `Posts` in a `<Suspense>` boundary:](/apis/react/Suspense#displaying-a-fallback-while-content-is-loading)
<InlineToc /> ```js {9,11}
function ProfilePage() {
return (
<ProfileLayout>
<ProfileCover />
<Sidebar>
<Friends />
<Photos />
</Sidebar>
<Suspense fallback={<PostsGlimmer />}>
<Posts />
</Suspense>
</ProfileLayout>
);
}
```
This way, you tell React to start streaming the HTML before `Posts` loads its data. React will send the HTML for the loading fallback (`PostsGlimmer`) first, and then, when `Posts` finishes loading its data, React will send the remaining HTML along with an inline `<script>` tag that replaces the loading fallback with that HTML. From the user's perspective, the page will first appear with the `PostsGlimmer`, and then it will be replaced by the `Posts`.
You can further [nest `<Suspense>` boundaries](/apis/react/Suspense#revealing-nested-content-as-it-loads) to create a more granular loading sequence:
```js {5,13}
function ProfilePage() {
return (
<ProfileLayout>
<ProfileCover />
<Suspense fallback={<BigSpinner />}>
<Sidebar>
<Friends />
<Photos />
</Sidebar>
<Suspense fallback={<PostsGlimmer />}>
<Posts />
</Suspense>
</Suspense>
</ProfileLayout>
);
}
```
In this example, React can start streaming the page even earlier. Only `ProfileLayout` and `ProfileCover` must finish rendering first because they are not wrapped in any `<Suspense>` boundary. However, if `Sidebar`, `Friends`, or `Photos` need to load some data, React will send the HTML for the `BigSpinner` fallback instead. Then, as enough data becomes available, more content will continue to be revealed until all of it becomes visible.
Streaming does not need to wait for React itself to load in the browser, or for your app to become interactive. The HTML content from the server will get progressively revealed before any of the `<script>` tags have loaded.
[Read more about how streaming HTML works.](https://github.com/reactwg/react-18/discussions/37)
<Note>
**Only Suspense-enabled data sources will activate the Suspense component.** They include:
- Data fetching with Suspense-enabled frameworks like [Relay](https://relay.dev/docs/guided-tour/rendering/loading-states/) and [Next.js](https://nextjs.org/docs/advanced-features/react-18)
- Lazy-loading component code with [`lazy`](/apis/react/lazy)
Suspense **does not** detect when data is fetched inside an Effect or event handler.
The exact way you would load data in the `Posts` component above depends on your framework. If you use a Suspense-enabled framework, you'll find the details in its data fetching documentation.
Suspense-enabled data fetching without the use of an opinionated framework is not yet supported. The requirements for implementing a Suspense-enabled data source are unstable and undocumented. An official API for integrating data sources with Suspense will be released in a future version of React.
</Note>
---
### Specifying what goes into the shell {/*specifying-what-goes-into-the-shell*/}
The part of your app outside of any `<Suspense>` boundaries is called *the shell:*
```js {3-5,13,14}
function ProfilePage() {
return (
<ProfileLayout>
<ProfileCover />
<Suspense fallback={<BigSpinner />}>
<Sidebar>
<Friends />
<Photos />
</Sidebar>
<Suspense fallback={<PostsGlimmer />}>
<Posts />
</Suspense>
</Suspense>
</ProfileLayout>
);
}
```
It determines the earliest loading state that the user may see:
```js {3-5,13
<ProfileLayout>
<ProfileCover />
<BigSpinner />
</ProfileLayout>
```
If you wrap the whole app into a `<Suspense>` boundary at the root, the shell will only contain that spinner. However, that's not a pleasant user experience because seeing a big spinner on the screen can feel slower and more annoying than waiting a bit more and seeing the real layout. This is why usually you'll want to place the `<Suspense>` boundaries so that the shell feels *minimal but complete*--like a skeleton of the entire page layout.
The async call to `renderToReadableStream` will resolve to a `stream` as soon as the entire shell has been rendered. Usually, you'll start streaming then by creating and returning a response with that `stream:
```js {5}
async function handler(request) {
const stream = await renderToReadableStream(<App />, {
bootstrapScripts: ['/main.js']
});
return new Response(stream, {
headers: { 'content-type': 'text/html' },
});
}
```
By the time the `stream` is returned, components in nested `<Suspense>` boundaries might still be loading data.
---
### Logging crashes on the server {/*logging-crashes-on-the-server*/}
By default, all errors on the server are logged to console. You can override this behavior to log crash reports:
```js {4-7}
async function handler(request) {
const stream = await renderToReadableStream(<App />, {
bootstrapScripts: ['/main.js'],
onError(error) {
console.error(error);
logServerCrashReport(error);
}
});
return new Response(stream, {
headers: { 'content-type': 'text/html' },
});
}
```
If you provide a custom `onError` implementation, don't forget to also log errors to the console like above.
---
### Recovering from errors inside the shell {/*recovering-from-errors-inside-the-shell*/}
In this example, the shell contains `ProfileLayout`, `ProfileCover`, and `PostsGlimmer`:
```js {3-5,7-8}
function ProfilePage() {
return (
<ProfileLayout>
<ProfileCover />
<Suspense fallback={<PostsGlimmer />}>
<Posts />
</Suspense>
</ProfileLayout>
);
}
```
If an error occurs while rendering those components, React won't have any meaningful HTML to send to the client. Wrap your `renderToReadableStream` call in a `try...catch` to send a fallback HTML that doesn't rely on server rendering as the last resort:
```js {2,13-18}
async function handler(request) {
try {
const stream = await renderToReadableStream(<App />, {
bootstrapScripts: ['/main.js'],
onError(error) {
console.error(error);
logServerCrashReport(error);
}
});
return new Response(stream, {
headers: { 'content-type': 'text/html' },
});
} catch (error) {
return new Response('<h1>Something went wrong</h1>', {
status: 500,
headers: { 'content-type': 'text/html' },
});
}
}
```
If there is an error while generating the shell, both `onError` and your `catch` block will run. Use `onError` for error reporting and use the `catch` block to send the fallback HTML document. Your fallback HTML does not have to be an error page. For example, you can include an alternative shell that tries to render your app on the client only.
---
### Recovering from errors outside the shell {/*recovering-from-errors-outside-the-shell*/}
In this example, the `<Posts />` component is wrapped in `<Suspense>` so it is *not* a part of the shell:
```js {6}
function ProfilePage() {
return (
<ProfileLayout>
<ProfileCover />
<Suspense fallback={<PostsGlimmer />}>
<Posts />
</Suspense>
</ProfileLayout>
);
}
```
If an error happens in the `Posts` component or somewhere inside it, React will [try to recover from it:](/apis/react/Suspense#providing-a-fallback-for-server-errors-and-server-only-content)
1. It will emit the loading fallback for the closest `<Suspense>` boundary (`PostsGlimmer`) into the HTML.
2. It will "give up" on trying to render the `Posts` content on the server anymore.
3. When the JavaScript code loads on the client, React will *retry* rendering the `Posts` component on the client.
If retrying rendering `Posts` on the client *also* fails, React will throw the error on the client. As with all the errors thrown during rendering, the [closest parent error boundary](/apis/react/Component#static-getderivedstatefromerror) determines how to present the error to the user. In practice, this means that the user will see a loading indicator until it is certain that the error is not recoverable.
If retrying rendering `Posts` on the client succeeds, the loading fallback from the server will be replaced with the client rendering output. The user will not know that there was a server error. However, the server `onError` callback and the client [`onRecoverableError`](/apis/react-dom/client/hydrateRoot#hydrateroot) callbacks will fire so that you can get notified about the error.
---
### Setting the status code {/*setting-the-status-code*/}
Streaming introduces a tradeoff. You want to start streaming the page as early as possible so that the user can see the content sooner. However, once you start streaming, you can no longer set the response status code.
By [dividing your app](#specifying-what-goes-into-the-shell) into the shell (above all `<Suspense>` boundaries) and the rest of the content, you've already solved a part of this problem. If the shell errors, your `catch` block will run which lets you set the error status code. Otherwise, you know that the app may recover on the client, so the "OK" status code is reasonable.
```js {11}
async function handler(request) {
try {
const stream = await renderToReadableStream(<App />, {
bootstrapScripts: ['/main.js'],
onError(error) {
console.error(error);
logServerCrashReport(error);
}
});
return new Response(stream, {
status: 200,
headers: { 'content-type': 'text/html' },
});
} catch (error) {
return new Response('<h1>Something went wrong</h1>', {
status: 500,
headers: { 'content-type': 'text/html' },
});
}
}
```
If a component *outside* the shell (i.e. inside a `<Suspense>` boundary) throws an error, React will not stop rendering. This means that the `onError` callback will fire, but your code will continue running without getting into the `catch` block. This is because React will try to recover from that error on the client, [as described above.](#recovering-from-errors-outside-the-shell)
However, if you'd like, you can use the fact that something has errored to set the status code:
```js {3,7,13}
async function handler(request) {
try {
let didError = false;
const stream = await renderToReadableStream(<App />, {
bootstrapScripts: ['/main.js'],
onError(error) {
didError = true;
console.error(error);
logServerCrashReport(error);
}
});
return new Response(stream, {
status: didError ? 500 : 200,
headers: { 'content-type': 'text/html' },
});
} catch (error) {
return new Response('<h1>Something went wrong</h1>', {
status: 500,
headers: { 'content-type': 'text/html' },
});
}
}
```
This will only catch errors outside the shell that happened while generating the initial shell content, so it's not exhaustive. If knowing whether an error occurred for some content is critical, you can move it up into the shell.
---
### Handling different errors in different ways {/*handling-different-errors-in-different-ways*/}
You can [create your own `Error` subclasses](https://javascript.info/custom-errors) and use the [`instanceof`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/instanceof) operator to check which error is thrown. For example, you can define a custom `NotFoundError` and throw it from your component. Then you can save the error in `onError` and do something different before returning the response depending on the error type:
```js {2-3,5-15,22,28,33}
async function handler(request) {
let didError = false;
let caughtError = null;
function getStatusCode() {
if (didError) {
if (caughtError instanceof NotFoundError) {
return 404;
} else {
return 500;
}
} else {
return 200;
}
}
try {
const stream = await renderToReadableStream(<App />, {
bootstrapScripts: ['/main.js'],
onError(error) {
didError = true;
caughtError = error;
console.error(error);
logServerCrashReport(error);
}
});
return new Response(stream, {
status: getStatusCode(),
headers: { 'content-type': 'text/html' },
});
} catch (error) {
return new Response('<h1>Something went wrong</h1>', {
status: getStatusCode(),
headers: { 'content-type': 'text/html' },
});
}
}
```
Keep in mind that once you emit the shell and start streaming, you can't change the status code.
---
### Waiting for all content to load for crawlers and static generation {/*waiting-for-all-content-to-load-for-crawlers-and-static-generation*/}
Streaming offers a better user experience because the user can see the content as it becomes available.
However, when a crawler visits your page, or if you're generating the pages at the build time, you might want to let all of the content load first and then produce the final HTML output instead of revealing it progressively.
You can wait for all the content to load by awaiting the `stream.allReady` Promise:
```js {12-15}
async function handler(request) {
try {
let didError = false;
const stream = await renderToReadableStream(<App />, {
bootstrapScripts: ['/main.js'],
onError(error) {
didError = true;
console.error(error);
logServerCrashReport(error);
}
});
let isCrawler = // ... depends on your bot detection strategy ...
if (isCrawler) {
await stream.allReady;
}
return new Response(stream, {
status: didError ? 500 : 200,
headers: { 'content-type': 'text/html' },
});
} catch (error) {
return new Response('<h1>Something went wrong</h1>', {
status: 500,
headers: { 'content-type': 'text/html' },
});
}
}
```
A regular visitor will get a stream of progressively loaded content. A crawler will receive the final HTML output after all the data loads. However, this also means that the crawler will have to wait for *all* data, some of which might be slow to load or error. Depending on your app, you could choose to send the shell to the crawlers too.
---
### Aborting server rendering {/*aborting-server-rendering*/}
You can force the server rendering to "give up" after a timeout:
```js {3,4-6,9}
async function handler(request) {
try {
const controller = new AbortController();
setTimeout(() => {
controller.abort();
}, 10000);
const stream = await renderToReadableStream(<App />, {
signal: controller.signal,
bootstrapScripts: ['/main.js'],
onError(error) {
didError = true;
console.error(error);
logServerCrashReport(error);
}
});
// ...
```
React will flush the remaining loading fallbacks as HTML, and will attempt to render the rest on the client.
---
## Reference {/*reference*/}
### `renderToReadableStream(reactNode, options?)` {/*rendertoreadablestream*/}
Call `renderToReadableStream` to render your React tree as HTML into a [Node.js Stream.](https://nodejs.org/api/stream.html#writable-streams)
```js
import { renderToReadableStream } from 'react-dom/server';
async function handler(request) {
const stream = await renderToReadableStream(<App />, {
bootstrapScripts: ['/main.js']
});
return new Response(stream, {
headers: { 'content-type': 'text/html' },
});
}
```
On the client, call [`hydrateRoot`](/apis/react-dom/client/hydrateRoot) to make the server-generated HTML interactive.
#### Parameters {/*parameters*/}
* `reactNode`: A React node you want to render to HTML. For example, a JSX element like `<App />`. It is expected to represent the entire document, so the `App` component should render the `<html>` tag.
* **optional** `options`: An object with streaming options.
* **optional** `bootstrapScriptContent`: If specified, this string will be placed in an inline `<script>` tag.
* **optional** `bootstrapScripts`: An array of string URLs for the `<script>` tags to emit on the page. Use this to include the `<script>` that calls [`hydrateRoot`.](/apis/react-dom/client/hydrateRoot) Omit it if you don't want to run React on the client at all.
* **optional** `bootstrapModules`: Like `bootstrapScripts`, but emits [`<script type="module">`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules) instead.
* **optional** `identifierPrefix`: A string prefix React uses for IDs generated by [`useId`.](/apis/react/useId) Useful to avoid conflicts when using multiple roots on the same page. Must be the same prefix as passed to [`hydrateRoot`.](/apis/react-dom/client/hydrateRoot#parameters)
* **optional** `namespaceURI`: A string with the root [namespace URI](https://developer.mozilla.org/en-US/docs/Web/API/Document/createElementNS#important_namespace_uris) for the stream. Defaults to regular HTML. Pass `'http://www.w3.org/2000/svg'` for SVG or `'http://www.w3.org/1998/Math/MathML'` for MathML.
* **optional** `nonce`: A [`nonce`](http://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#nonce) string to allow scripts for [`script-src` Content-Security-Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/script-src).
* **optional** `onError`: A callback that fires whenever there is a server error, whether [recoverable](#recovering-from-errors-outside-the-shell) or [not.](#recovering-from-errors-inside-the-shell) By default, this only calls `console.error`. If you override it to [log crash reports,](#logging-crashes-on-the-server) make sure that you still call `console.error`. You can also use it to [adjust the status code](#setting-the-status-code) before the shell is emitted.
* **optional** `progressiveChunkSize`: The number of bytes in a chunk. [Read more about the default heuristic.](https://github.com/facebook/react/blob/14c2be8dac2d5482fda8a0906a31d239df8551fc/packages/react-server/src/ReactFizzServer.js#L210-L225)
* **optional** `signal`: An [abort signal](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal) that lets you [abort server rendering](#aborting-server-rendering) and render the rest on the client.
#### Returns {/*returns*/}
`renderToReadableStream` returns a Promise:
- If rendering the [shell](#specifying-what-goes-into-the-shell) is successful, that Promise will resolve to a [Readable Web Stream.](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream)
- If rendering the shell fails, the Promise will be rejected. [Use this to output a fallback shell.](#recovering-from-errors-inside-the-shell)
The returned stream has an additional property:
* `allReady`: A Promise that resolves when all rendering is complete, including both the [shell](#specifying-what-goes-into-the-shell) and all additional [content.](#streaming-more-content-as-it-loads) You can `await stream.allReady` before returning a response [for crawlers and static generation.](#waiting-for-all-content-to-load-for-crawlers-and-static-generation) If you do that, you won't get any progressive loading. The stream will contain the final HTML.

3
beta/src/sidebarAPIs.json

@ -180,8 +180,7 @@
}, },
{ {
"title": "renderToReadableStream", "title": "renderToReadableStream",
"path": "/apis/react-dom/server/renderToReadableStream", "path": "/apis/react-dom/server/renderToReadableStream"
"wip": true
}, },
{ {
"title": "renderToStaticMarkup", "title": "renderToStaticMarkup",

Loading…
Cancel
Save