|
| 1 | +--- |
| 2 | +title: resume |
| 3 | +canary: true |
| 4 | +--- |
| 5 | + |
| 6 | +<Canary> |
| 7 | + |
| 8 | +**The `resume` API is currently only available in React’s Canary and Experimental channels.** |
| 9 | + |
| 10 | +[Learn more about React’s release channels here.](/community/versioning-policy#all-release-channels) |
| 11 | + |
| 12 | +</Canary> |
| 13 | + |
| 14 | +<Intro> |
| 15 | + |
| 16 | +`resume` streams a pre-rendered React tree to a [Readable Web Stream.](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream) |
| 17 | + |
| 18 | +```js |
| 19 | +const stream = await resume(reactNode, postponedState, options?) |
| 20 | +``` |
| 21 | +
|
| 22 | +</Intro> |
| 23 | +
|
| 24 | +<InlineToc /> |
| 25 | +
|
| 26 | +<Note> |
| 27 | +
|
| 28 | +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. |
| 29 | +
|
| 30 | +</Note> |
| 31 | +
|
| 32 | +--- |
| 33 | +
|
| 34 | +## Reference {/*reference*/} |
| 35 | +
|
| 36 | +### `resume(node, postponedState, options?)` {/*resume*/} |
| 37 | +
|
| 38 | +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) |
| 39 | +
|
| 40 | +```js |
| 41 | +import { resume } from 'react-dom/server'; |
| 42 | +import {getPostponedState} from './storage'; |
| 43 | + |
| 44 | +async function handler(request, writable) { |
| 45 | + const postponed = await getPostponedState(request); |
| 46 | + const resumeStream = await resume(<App />, postponed); |
| 47 | + return resumeStream.pipeTo(writable) |
| 48 | +} |
| 49 | +``` |
| 50 | +
|
| 51 | +[See more examples below.](#usage) |
| 52 | +
|
| 53 | +#### Parameters {/*parameters*/} |
| 54 | +
|
| 55 | +* `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. |
| 56 | +* `postponedState`: The opaque `postpone` object returned from a [prerender API](/reference/react-dom/static/index), loaded from wherever you stored it (e.g. redis, a file, or S3). |
| 57 | +* **optional** `options`: An object with streaming options. |
| 58 | + * **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). |
| 59 | + * **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. |
| 60 | + * **optional** `onError`: A callback that fires whenever there is a server error, whether [recoverable](/reference/react-dom/server/renderToReadableStream#recovering-from-errors-outside-the-shell) or [not.](/reference/react-dom/server/renderToReadableStream#recovering-from-errors-inside-the-shell) By default, this only calls `console.error`. If you override it to [log crash reports,](/reference/react-dom/server/renderToReadableStream#logging-crashes-on-the-server) make sure that you still call `console.error`. |
| 61 | +
|
| 62 | +
|
| 63 | +#### Returns {/*returns*/} |
| 64 | +
|
| 65 | +`resume` returns a Promise: |
| 66 | +
|
| 67 | +- If `resume` successfully produced a [shell](/reference/react-dom/server/renderToReadableStream#specifying-what-goes-into-the-shell), that Promise will resolve to a [Readable Web Stream.](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream) that can be piped to a [Writable Web Stream.](https://developer.mozilla.org/en-US/docs/Web/API/WritableStream). |
| 68 | +- If an error happens in the shell, the Promise will reject with that error. |
| 69 | +
|
| 70 | +The returned stream has an additional property: |
| 71 | +
|
| 72 | +* `allReady`: A Promise that resolves when all rendering is complete. You can `await stream.allReady` before returning a response [for crawlers and static generation.](/reference/react-dom/server/renderToReadableStream#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. |
| 73 | +
|
| 74 | +#### Caveats {/*caveats*/} |
| 75 | +
|
| 76 | +- `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`. You can also inject bootstrap content into the writable stream manually. |
| 77 | +- `resume` does not accept `identifierPrefix` since the prefix needs to be the same in both `prerender` and `resume`. |
| 78 | +- Since `nonce` cannot be provided to prerender, you should only provide `nonce` to `resume` if you're not providing scripts to prerender. |
| 79 | +- `resume` re-renders from the root until it finds a component that was not fully pre-rendered. Only fully prerendered Components (the Component and its children finished prerendering) are skipped entirely. |
| 80 | +
|
| 81 | +## Usage {/*usage*/} |
| 82 | +
|
| 83 | +### Resuming a prerender {/*resuming-a-prerender*/} |
| 84 | +
|
| 85 | +<Sandpack> |
| 86 | +
|
| 87 | +```js src/App.js hidden |
| 88 | +``` |
| 89 | +
|
| 90 | +```json package.json hidden |
| 91 | +{ |
| 92 | + "dependencies": { |
| 93 | + "react": "experimental", |
| 94 | + "react-dom": "experimental", |
| 95 | + "react-scripts": "latest" |
| 96 | + }, |
| 97 | + "scripts": { |
| 98 | + "start": "react-scripts start", |
| 99 | + "build": "react-scripts build", |
| 100 | + "test": "react-scripts test --env=jsdom", |
| 101 | + "eject": "react-scripts eject" |
| 102 | + } |
| 103 | +} |
| 104 | +``` |
| 105 | +
|
| 106 | +```html public/index.html |
| 107 | +<!DOCTYPE html> |
| 108 | +<html lang="en"> |
| 109 | +<head> |
| 110 | + <meta charset="UTF-8"> |
| 111 | + <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| 112 | + <title>Document</title> |
| 113 | +</head> |
| 114 | +<body> |
| 115 | + <iframe id="container"></iframe> |
| 116 | +</body> |
| 117 | +</html> |
| 118 | +``` |
| 119 | +
|
| 120 | +```js src/index.js |
| 121 | +import { |
| 122 | + flushReadableStreamToFrame, |
| 123 | + getUser, |
| 124 | + Postponed, |
| 125 | + sleep, |
| 126 | +} from "./demo-helpers"; |
| 127 | +import { StrictMode, Suspense, use, useEffect } from "react"; |
| 128 | +import { prerender } from "react-dom/static"; |
| 129 | +import { resume } from "react-dom/server"; |
| 130 | +import { hydrateRoot } from "react-dom/client"; |
| 131 | + |
| 132 | +function Header() { |
| 133 | + return <header>Me and my descendants can be prerendered</header>; |
| 134 | +} |
| 135 | + |
| 136 | +const { promise: cookies, resolve: resolveCookies } = Promise.withResolvers(); |
| 137 | + |
| 138 | +function Main() { |
| 139 | + const { sessionID } = use(cookies); |
| 140 | + const user = getUser(sessionID); |
| 141 | + |
| 142 | + useEffect(() => { |
| 143 | + console.log("reached interactivity!"); |
| 144 | + }, []); |
| 145 | + |
| 146 | + return ( |
| 147 | + <main> |
| 148 | + Hello, {user.name}! |
| 149 | + <button onClick={() => console.log("hydrated!")}> |
| 150 | + Clicking me requires hydration. |
| 151 | + </button> |
| 152 | + </main> |
| 153 | + ); |
| 154 | +} |
| 155 | + |
| 156 | +function Shell({ children }) { |
| 157 | + // In a real app, this is where you would put your html and body. |
| 158 | + // We're just using tags here we can include in an existing body for demonstration purposes |
| 159 | + return ( |
| 160 | + <html> |
| 161 | + <body>{children}</body> |
| 162 | + </html> |
| 163 | + ); |
| 164 | +} |
| 165 | + |
| 166 | +function App() { |
| 167 | + return ( |
| 168 | + <Shell> |
| 169 | + <Suspense fallback="loading header"> |
| 170 | + <Header /> |
| 171 | + </Suspense> |
| 172 | + <Suspense fallback="loading main"> |
| 173 | + <Main /> |
| 174 | + </Suspense> |
| 175 | + </Shell> |
| 176 | + ); |
| 177 | +} |
| 178 | + |
| 179 | +async function main(frame) { |
| 180 | + // Layer 1 |
| 181 | + const controller = new AbortController(); |
| 182 | + const prerenderedApp = prerender(<App />, { |
| 183 | + signal: controller.signal, |
| 184 | + onError(error) { |
| 185 | + if (error instanceof Postponed) { |
| 186 | + } else { |
| 187 | + console.error(error); |
| 188 | + } |
| 189 | + }, |
| 190 | + }); |
| 191 | + // We're immediately aborting in a macrotask. |
| 192 | + // Any data fetching that's not available synchronously, or in a microtask, will not have finished. |
| 193 | + setTimeout(() => { |
| 194 | + controller.abort(new Postponed()); |
| 195 | + }); |
| 196 | + |
| 197 | + const { prelude, postponed } = await prerenderedApp; |
| 198 | + await flushReadableStreamToFrame(prelude, frame); |
| 199 | + |
| 200 | + // Layer 2 |
| 201 | + // Just waiting here for demonstration purposes. |
| 202 | + // In a real app, the prelude and postponed state would've been serialized in Layer 1 and Layer would deserialize them. |
| 203 | + // The prelude content could be flushed immediated as plain HTML while |
| 204 | + // React is continuing to render from where the prerender left off. |
| 205 | + await sleep(2000); |
| 206 | + |
| 207 | + // You would get the cookies from the incoming HTTP request |
| 208 | + resolveCookies({ sessionID: "abc" }); |
| 209 | + |
| 210 | + const stream = await resume(<App />, postponed); |
| 211 | + |
| 212 | + await flushReadableStreamToFrame(stream, frame); |
| 213 | + |
| 214 | + // Layer 3 |
| 215 | + // Just waiting here for demonstration purposes. |
| 216 | + await sleep(2000); |
| 217 | + |
| 218 | + hydrateRoot(frame.contentWindow.document, <App />); |
| 219 | +} |
| 220 | + |
| 221 | +main(document.getElementById("container")); |
| 222 | + |
| 223 | +``` |
| 224 | +
|
| 225 | +```js src/demo-helpers.js |
| 226 | +export async function flushReadableStreamToFrame(readable, frame) { |
| 227 | + const document = frame.contentWindow.document; |
| 228 | + const decoder = new TextDecoder(); |
| 229 | + for await (const chunk of readable) { |
| 230 | + const partialHTML = decoder.decode(chunk); |
| 231 | + document.write(partialHTML); |
| 232 | + } |
| 233 | +} |
| 234 | + |
| 235 | +// This doesn't need to be an error. |
| 236 | +// You can use any other means to check if an error during prerender was |
| 237 | +// from an intentional abort or a real error. |
| 238 | +export class Postponed extends Error {} |
| 239 | + |
| 240 | +// We're just hardcoding a session here. |
| 241 | +export function getUser(sessionID) { |
| 242 | + return { |
| 243 | + name: "Alice", |
| 244 | + }; |
| 245 | +} |
| 246 | + |
| 247 | +export function sleep(timeoutMS) { |
| 248 | + return new Promise((resolve) => { |
| 249 | + setTimeout(() => { |
| 250 | + resolve(); |
| 251 | + }, timeoutMS); |
| 252 | + }); |
| 253 | +} |
| 254 | +``` |
| 255 | +
|
| 256 | +</Sandpack> |
| 257 | +
|
| 258 | +### Further reading {/*further-reading*/} |
| 259 | +
|
| 260 | +Resuming behaves like `renderToReadableStream`. For more examples, check out the [usage section of `renderToReadableStream`](/reference/react-dom/server/renderToReadableStream#usage). |
| 261 | +The [usage section of `prerender`](/reference/react-dom/static/prerender#usage) includes examples of how to use `prerender` specifically. |
0 commit comments