-
Notifications
You must be signed in to change notification settings - Fork 7.7k
[wip] Init PPR docs #7869
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
rickhanlonii
wants to merge
1
commit into
reactjs:main
Choose a base branch
from
rickhanlonii:rh/resume
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+520
−9
Open
[wip] Init PPR docs #7869
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,6 @@ | ||
/// <reference types="next" /> | ||
/// <reference types="next/image-types/global" /> | ||
/// <reference types="next/navigation-types/compat/navigation" /> | ||
|
||
// NOTE: This file should not be edited | ||
// see https://nextjs.org/docs/pages/api-reference/config/typescript for more information. | ||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,240 @@ | ||
--- | ||
title: resume | ||
canary: true | ||
--- | ||
|
||
<Intro> | ||
|
||
`resume` streams a pre-rendered React tree to a [Readable Web Stream.](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream) | ||
|
||
```js | ||
const stream = await resume(reactNode, postponedState, options?) | ||
``` | ||
|
||
</Intro> | ||
|
||
<InlineToc /> | ||
|
||
<Note> | ||
|
||
This API depends on [Web Streams.](https://developer.mozilla.org/en-US/docs/Web/API/Streams_API) For Node.js, use [`resumeToNodeStream`](/reference/react-dom/server/renderToPipeableStream) instead. | ||
|
||
</Note> | ||
|
||
--- | ||
|
||
## Reference {/*reference*/} | ||
|
||
### `resume(node, postponed, options?)` {/*resume*/} | ||
|
||
Call `resume` to resume rendering a pre-rendered React tree as HTML into a [Readable Web Stream.](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream) | ||
|
||
```js | ||
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.](#usage) | ||
|
||
#### Parameters {/*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. | ||
* **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** `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. | ||
* **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`. | ||
|
||
|
||
#### Returns {/*returns*/} | ||
|
||
`resume` returns a Promise: | ||
|
||
- If `prerender` successfully produced a [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) whether replaying the shell errors or not. | ||
- If `prerender` failed to produce a [shell](#specifying-what-goes-into-the-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.](#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. | ||
|
||
#### Caveats {/*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 {/*usage*/} | ||
|
||
### Resuming a prerender to a Readable Web Stream {/*resuming-a-prerender-to-a-readable-web-stream*/} | ||
|
||
TODO | ||
|
||
--- | ||
|
||
### 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 {9-10} | ||
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 {/*recovering-from-errors-inside-the-shell*/} | ||
|
||
TODO: this is for when the shell completed. | ||
|
||
In this example, prerender successfully rendered a shell containing `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 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? | ||
|
||
```js {2,13-18} | ||
// TODO | ||
``` | ||
|
||
If there is an error while replaying the shell, it will be logged to `onError`. | ||
|
||
### Recovering from errors re-creating the shell {/*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 {/*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: | ||
|
||
```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:](/reference/react/Suspense#providing-a-fallback-for-server-errors-and-client-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 `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](/reference/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`](/reference/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*/} | ||
|
||
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 {/*handling-different-errors-in-different-ways*/} | ||
|
||
TODO: update this example. | ||
|
||
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' }, | ||
}); | ||
} | ||
} | ||
``` | ||
|
||
--- | ||
|
||
### Waiting for all content to load for crawlers and static generation {/*waiting-for-all-content-to-load-for-crawlers-and-static-generation*/} | ||
|
||
TODO: this doesn't make sense for `resume` right? | ||
|
||
--- | ||
|
||
### Aborting server rendering {/*aborting-server-rendering*/} | ||
|
||
TODO |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this looks like a real npm package, is this meant to be something like
readFile
?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Loading from the filesystem is probably not going to work right? Typically it would need to be like redis or s3 or something like that.
What about
'./your-storge-layer'
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
that seems fine, my main point is just it’s hard to imagine what this is supposed to mean if the format isn’t clear (ie is this just JSON?)
i don’t see why it wouldn’t work via file system though
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah we'll clean it up to make it more clear cc @mattcarrollcode
For the file system to work you'd need to serve both the prerender response and the resume request from the same server, which would be uncommon for prod usecases (you're probably scaled to multiple servers, or running vms that can be destroyed between requests, etc). And if you're serving the prerender result from a CDN, it's basically not possible/advisable because you'd need to sync the postpone state to your resume server somehow. So you probably need some kind of permanent storage layer.