renderToPipeableStream renders a React tree to a pipeableNode.js Stream.
const{pipe,abort} =renderToPipeableStream(reactNode,options?)- Reference
- Usage
- Rendering a React tree as HTML to a Node.js Stream
- Streaming more content as it loads
- Specifying what goes into the shell
- Logging crashes on the server
- Recovering from errors inside the shell
- Recovering from errors outside the shell
- Setting the status code
- Handling different errors in different ways
- Waiting for all content to load for crawlers and static generation
- Aborting server rendering
Note
This API is specific to Node.js. Environments withWeb Streams, like Deno and modern edge runtimes, should userenderToReadableStream instead.
Reference
renderToPipeableStream(reactNode, options?)
CallrenderToPipeableStream to render your React tree as HTML into aNode.js Stream.
import{renderToPipeableStream}from'react-dom/server';
const{pipe} =renderToPipeableStream(<App/>,{
bootstrapScripts:['/main.js'],
onShellReady(){
response.setHeader('content-type','text/html');
pipe(response);
}
});On the client, callhydrateRoot to make the server-generated HTML interactive.
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 theAppcomponent 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 callshydrateRoot. Omit it if you don’t want to run React on the client at all. - optional
bootstrapModules: LikebootstrapScripts, but emits<script type="module">instead. - optional
identifierPrefix: A string prefix React uses for IDs generated byuseId. Useful to avoid conflicts when using multiple roots on the same page. Must be the same prefix as passed tohydrateRoot. - optional
namespaceURI: A string with the rootnamespace URI 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: Anoncestring to allow scripts forscript-srcContent-Security-Policy. - optional
onAllReady: A callback that fires when all rendering is complete, including both theshell and all additionalcontent. You can use this instead ofonShellReadyfor crawlers and static generation. If you start streaming here, you won’t get any progressive loading. The stream will contain the final HTML. - optional
onError: A callback that fires whenever there is a server error, whetherrecoverable ornot. By default, this only callsconsole.error. If you override it tolog crash reports, make sure that you still callconsole.error. You can also use it toadjust the status code before the shell is emitted. - optional
onShellReady: A callback that fires right after theinitial shell has been rendered. You canset the status code and callpipehere to start streaming. React willstream the additional content after the shell along with the inline<script>tags that replace the HTML loading fallbacks with the content. - optional
onShellError: A callback that fires if there was an error rendering the initial shell. It receives the error as an argument. No bytes were emitted from the stream yet, and neitheronShellReadynoronAllReadywill get called, so you canoutput a fallback HTML shell. - optional
progressiveChunkSize: The number of bytes in a chunk.Read more about the default heuristic.
- optional
Returns
renderToPipeableStream returns an object with two methods:
pipeoutputs the HTML into the providedWritable Node.js Stream. CallpipeinonShellReadyif you want to enable streaming, or inonAllReadyfor crawlers and static generation.abortlets youabort server rendering and render the rest on the client.
Usage
Rendering a React tree as HTML to a Node.js Stream
CallrenderToPipeableStream to render your React tree as HTML into aNode.js Stream:
import{renderToPipeableStream}from'react-dom/server';
// The route handler syntax depends on your backend framework
app.use('/',(request,response)=>{
const{pipe} =renderToPipeableStream(<App />,{
bootstrapScripts:['/main.js'],
onShellReady(){
response.setHeader('content-type','text/html');
pipe(response);
}
});
});Along with theroot component, you need to provide a list ofbootstrap<script> paths. Your root component should returnthe entire document including the root<html> tag.
For example, it might look like this:
exportdefaultfunctionApp(){
return(
<html>
<head>
<metacharSet="utf-8"/>
<metaname="viewport"content="width=device-width, initial-scale=1"/>
<linkrel="stylesheet"href="/styles.css"></link>
<title>My app</title>
</head>
<body>
<Router/>
</body>
</html>
);
}React will inject thedoctype and yourbootstrap<script> tags into the resulting HTML stream:
<!DOCTYPE html>
<html>
<!-- ... HTML from your components ... -->
</html>
<scriptsrc="/main.js"async=""></script>On the client, your bootstrap script shouldhydrate the entiredocument with a call tohydrateRoot:
import{hydrateRoot}from'react-dom/client';
importAppfrom'./App.js';
hydrateRoot(document,<App />);This will attach event listeners to the server-generated HTML and make it interactive.
Deep Dive
The final asset URLs (like JavaScript and CSS files) are often hashed after the build. For example, instead ofstyles.css you might end up withstyles.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:
exportdefaultfunctionApp({assetMap}){
return(
<html>
<head>
...
<linkrel="stylesheet"href={assetMap['styles.css']}></link>
...
</head>
...
</html>
);
}On the server, render<App assetMap={assetMap} /> and pass yourassetMap with the asset URLs:
// You'd need to get this JSON from your build tooling, e.g. read it from the build output.
constassetMap ={
'styles.css':'/styles.123456.css',
'main.js':'/main.123456.js'
};
app.use('/',(request,response)=>{
const{pipe} =renderToPipeableStream(<AppassetMap={assetMap}/>,{
bootstrapScripts:[assetMap['main.js']],
onShellReady(){
response.setHeader('content-type','text/html');
pipe(response);
}
});
});Since your server is now rendering<App assetMap={assetMap} />, you need to render it withassetMap on the client too to avoid hydration errors. You can serialize and passassetMap to the client like this:
// You'd need to get this JSON from your build tooling.
constassetMap ={
'styles.css':'/styles.123456.css',
'main.js':'/main.123456.js'
};
app.use('/',(request,response)=>{
const{pipe} =renderToPipeableStream(<AppassetMap={assetMap}/>,{
// Careful: It's safe to stringify() this because this data isn't user-generated.
bootstrapScriptContent:`window.assetMap =${JSON.stringify(assetMap)};`,
bootstrapScripts:[assetMap['main.js']],
onShellReady(){
response.setHeader('content-type','text/html');
pipe(response);
}
});
});In the example above, thebootstrapScriptContent option adds an extra inline<script> tag that sets the globalwindow.assetMap variable on the client. This lets the client code read the sameassetMap:
import{hydrateRoot}from'react-dom/client';
importAppfrom'./App.js';
hydrateRoot(document,<AppassetMap={window.assetMap}/>);Both client and server renderApp with the sameassetMap prop, so there are no hydration errors.
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:
functionProfilePage(){
return(
<ProfileLayout>
<ProfileCover/>
<Sidebar>
<Friends/>
<Photos/>
</Sidebar>
<Posts/>
</ProfileLayout>
);
}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,wrapPosts in a<Suspense> boundary:
functionProfilePage(){
return(
<ProfileLayout>
<ProfileCover/>
<Sidebar>
<Friends/>
<Photos/>
</Sidebar>
<Suspensefallback={<PostsGlimmer/>}>
<Posts/>
</Suspense>
</ProfileLayout>
);
}This tells React to start streaming the HTML beforePosts loads its data. React will send the HTML for the loading fallback (PostsGlimmer) first, and then, whenPosts 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 thePostsGlimmer, later replaced by thePosts.
You can furthernest<Suspense> boundaries to create a more granular loading sequence:
functionProfilePage(){
return(
<ProfileLayout>
<ProfileCover/>
<Suspensefallback={<BigSpinner/>}>
<Sidebar>
<Friends/>
<Photos/>
</Sidebar>
<Suspensefallback={<PostsGlimmer/>}>
<Posts/>
</Suspense>
</Suspense>
</ProfileLayout>
);
}In this example, React can start streaming the page even earlier. OnlyProfileLayout andProfileCover must finish rendering first because they are not wrapped in any<Suspense> boundary. However, ifSidebar,Friends, orPhotos need to load some data, React will send the HTML for theBigSpinner fallback instead. Then, as more 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 load.
Read more about how streaming HTML works.
Note
Only Suspense-enabled data sources will activate the Suspense component. They include:
- Data fetching with Suspense-enabled frameworks likeRelay andNext.js
- Lazy-loading component code with
lazy - Reading the value of a Promise with
use
Suspensedoes not detect when data is fetched inside an Effect or event handler.
The exact way you would load data in thePosts 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.
Specifying what goes into the shell
The part of your app outside of any<Suspense> boundaries is calledthe shell:
functionProfilePage(){
return(
<ProfileLayout>
<ProfileCover/>
<Suspensefallback={<BigSpinner/>}>
<Sidebar>
<Friends/>
<Photos/>
</Sidebar>
<Suspensefallback={<PostsGlimmer/>}>
<Posts/>
</Suspense>
</Suspense>
</ProfileLayout>
);
}It determines the earliest loading state that the user may see:
<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 feelsminimal but complete—like a skeleton of the entire page layout.
TheonShellReady callback fires when the entire shell has been rendered. Usually, you’ll start streaming then:
const{pipe} =renderToPipeableStream(<App/>,{
bootstrapScripts:['/main.js'],
onShellReady(){
response.setHeader('content-type','text/html');
pipe(response);
}
});By the timeonShellReady fires, components in nested<Suspense> boundaries might still be loading data.
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:
const{pipe} =renderToPipeableStream(<App/>,{
bootstrapScripts:['/main.js'],
onShellReady(){
response.setHeader('content-type','text/html');
pipe(response);
},
onError(error){
console.error(error);
logServerCrashReport(error);
}
});If you provide a customonError implementation, don’t forget to also log errors to the console like above.
Recovering from errors inside the shell
In this example, the shell containsProfileLayout,ProfileCover, andPostsGlimmer:
functionProfilePage(){
return(
<ProfileLayout>
<ProfileCover/>
<Suspensefallback={<PostsGlimmer/>}>
<Posts/>
</Suspense>
</ProfileLayout>
);
}If an error occurs while rendering those components, React won’t have any meaningful HTML to send to the client. OverrideonShellError to send a fallback HTML that doesn’t rely on server rendering as the last resort:
const{pipe} =renderToPipeableStream(<App/>,{
bootstrapScripts:['/main.js'],
onShellReady(){
response.setHeader('content-type','text/html');
pipe(response);
},
onShellError(error){
response.statusCode =500;
response.setHeader('content-type','text/html');
response.send('<h1>Something went wrong</h1>');
},
onError(error){
console.error(error);
logServerCrashReport(error);
}
});If there is an error while generating the shell, bothonError andonShellError will fire. UseonError for error reporting and useonShellError to send the fallback HTML document. Your fallback HTML does not have to be an error page. Instead, you may include an alternative shell that renders your app on the client only.
Recovering from errors outside the shell
In this example, the<Posts /> component is wrapped in<Suspense> so it isnot a part of the shell:
functionProfilePage(){
return(
<ProfileLayout>
<ProfileCover/>
<Suspensefallback={<PostsGlimmer/>}>
<Posts/>
</Suspense>
</ProfileLayout>
);
}If an error happens in thePosts component or somewhere inside it, React willtry to recover from it:
- It will emit the loading fallback for the closest
<Suspense>boundary (PostsGlimmer) into the HTML. - It will “give up” on trying to render the
Postscontent on the server anymore. - When the JavaScript code loads on the client, React willretry rendering
Postson the client.
If retrying renderingPosts on the clientalso fails, React will throw the error on the client. As with all the errors thrown during rendering, theclosest parent error boundary 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 renderingPosts 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 serveronError callback and the clientonRecoverableError callbacks will fire so that you can get notified about the error.
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.
Bydividing your app 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, you’ll get theonShellError callback which lets you set the error status code. Otherwise, you know that the app may recover on the client, so you can send “OK”.
const{pipe} =renderToPipeableStream(<App/>,{
bootstrapScripts:['/main.js'],
onShellReady(){
response.statusCode =200;
response.setHeader('content-type','text/html');
pipe(response);
},
onShellError(error){
response.statusCode =500;
response.setHeader('content-type','text/html');
response.send('<h1>Something went wrong</h1>');
},
onError(error){
console.error(error);
logServerCrashReport(error);
}
});If a componentoutside the shell (i.e. inside a<Suspense> boundary) throws an error, React will not stop rendering. This means that theonError callback will fire, but you will still getonShellReady instead ofonShellError. This is because React will try to recover from that error on the client,as described above.
However, if you’d like, you can use the fact that something has errored to set the status code:
letdidError =false;
const{pipe} =renderToPipeableStream(<App/>,{
bootstrapScripts:['/main.js'],
onShellReady(){
response.statusCode =didError ?500 :200;
response.setHeader('content-type','text/html');
pipe(response);
},
onShellError(error){
response.statusCode =500;
response.setHeader('content-type','text/html');
response.send('<h1>Something went wrong</h1>');
},
onError(error){
didError =true;
console.error(error);
logServerCrashReport(error);
}
});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
You cancreate your ownError subclasses and use theinstanceof operator to check which error is thrown. For example, you can define a customNotFoundError and throw it from your component. Then youronError,onShellReady, andonShellError callbacks can do something different depending on the error type:
letdidError =false;
letcaughtError =null;
functiongetStatusCode(){
if(didError){
if(caughtErrorinstanceofNotFoundError){
return404;
}else{
return500;
}
}else{
return200;
}
}
const{pipe} =renderToPipeableStream(<App/>,{
bootstrapScripts:['/main.js'],
onShellReady(){
response.statusCode =getStatusCode();
response.setHeader('content-type','text/html');
pipe(response);
},
onShellError(error){
response.statusCode =getStatusCode();
response.setHeader('content-type','text/html');
response.send('<h1>Something went wrong</h1>');
},
onError(error){
didError =true;
caughtError =error;
console.error(error);
logServerCrashReport(error);
}
});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
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 using theonAllReady callback:
letdidError =false;
letisCrawler =// ... depends on your bot detection strategy ...
const{pipe} =renderToPipeableStream(<App/>,{
bootstrapScripts:['/main.js'],
onShellReady(){
if(!isCrawler){
response.statusCode =didError ?500 :200;
response.setHeader('content-type','text/html');
pipe(response);
}
},
onShellError(error){
response.statusCode =500;
response.setHeader('content-type','text/html');
response.send('<h1>Something went wrong</h1>');
},
onAllReady(){
if(isCrawler){
response.statusCode =didError ?500 :200;
response.setHeader('content-type','text/html');
pipe(response);
}
},
onError(error){
didError =true;
console.error(error);
logServerCrashReport(error);
}
});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 forall 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
You can force the server rendering to “give up” after a timeout:
const{pipe,abort} =renderToPipeableStream(<App/>,{
// ...
});
setTimeout(()=>{
abort();
},10000);React will flush the remaining loading fallbacks as HTML, and will attempt to render the rest on the client.