resume streams a pre-rendered React tree to a Readable Web Stream.

const stream = await resume(reactNode, postponedState, options?)

Note

This API depends on Web Streams. For Node.js, use resumeToNodeStream instead.


Reference

resume(node, postponed, options?)

Call resume to resume rendering a pre-rendered React tree as HTML into a Readable Web Stream.

import { resume } from 'react-dom/server';
import {getPostponedState} from 'storage';

async function handler(request) {
const postponed = await getPostponedState(request);
const stream = await resume(<App />, postponed, {
bootstrapScripts: ['/main.js']
});
return new Response(stream, {
headers: { 'content-type': 'text/html' },
});
}

TODO: when do you call hydrateRoot? In the shell or when you resume?

See more examples below.

Parameters

  • reactNode: The React node you called prerender with. For example, a JSX element like <App />. It is expected to represent the entire document, so the App component should render the <html> tag.
  • postponedState: The opaque postpone object returned from prerender, loaded from wherever you stored it (e.g. redis, a file, or S3).
  • optional options: An object with streaming options.

Returns

resume returns a Promise:

  • If prerender successfully produced a shell is successful, that Promise will resolve to a Readable Web Stream. whether replaying the shell errors or not.
  • If prerender failed to produce a shell, and resume errors, the Promise will be rejected. TODO: Example?

The returned stream has an additional property:

  • allReady: A Promise that resolves when all rendering is complete. You can await stream.allReady before returning a response for crawlers and static generation. If you do that, you won’t get any progressive loading. The stream will contain the final HTML.

Caveats

  • resume does not accept options for bootstrapScripts, bootstrapScriptContent, or bootstrapModules. Instead, you need to pass these options to the prerender call that generates the postponedState. These may be injected either during pre-render or resume.
  • resume does not accept identifierPrefix since the prefix needs to be the same in both prerender and resume.
  • Since nonce cannot be provided to prerender, you should only provide nonce to resume if you’re not providing scripts to prerender.
  • resume re-renders from the root until it finds a component that was not fully pre-rendered, and skips fully pre-rendered components.

Usage

Resuming a prerender to a Readable Web Stream

TODO


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:

import { resume } from 'react-dom/server';
import { getPostponedState } from 'storage';
import { logServerCrashReport } from 'logging';

async function handler(request) {
const postponed = await getPostponedState(request);
const stream = await resume(<App />, postponed, {
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 replaying the shell

TODO: this is for when the shell completed.

In this example, prerender successfully rendered a shell containing ProfileLayout, ProfileCover, and PostsGlimmer:

function ProfilePage() {
return (
<ProfileLayout>
<ProfileCover />
<Suspense fallback={<PostsGlimmer />}>
<Posts />
</Suspense>
</ProfileLayout>
);
}

If an error occurs while replaying those components, React won’t have any meaningful HTML to send to the client. TODO: how to recover from this, since the promise is resolved. I think it will just encode an error in the stream and trigger an error boundary?

// TODO

If there is an error while replaying the shell, it will be logged to onError.

Recovering from errors re-creating the shell

TODO: this is for when the shell errors, and re-creating the shell fails.


Recovering from errors outside the shell

TODO: confirm this section is correct.

In this example, the <Posts /> component is wrapped in <Suspense> so it is not a part of the shell:

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:

  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 Posts 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 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 callbacks will fire so that you can get notified about the error.


Setting the status code

TODO: you can’t set the status code in resume, unless you’re calling prerender in the same request. If so, set the status code between prerender and resume.


Handling different errors in different ways

TODO: update this example.

You can create your own Error subclasses and use the 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:

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' },
});
}
}

Waiting for all content to load for crawlers and static generation

TODO: this doesn’t make sense for resume right?


Aborting server rendering

TODO