@samitouri / QOS-React / commits / d49123f73f

Expose prerender() for SSG in stable (#31298)

When we added `renderToReadableStream` we added the `allReady` helper to make it easier to do SSG rendering but it's kind of awkward to wire up that way. Since we're also discouraging `renderToString` in React 19 the cliff is kind of awkward. ([As noted by Docusaurus.](https://github.com/facebook/react/pull/24752#issuecomment-2178309299)) The idea of the `react-dom/static` `prerender` API was that this would be the replacement for SSG rendering. Awkwardly this entry point actually already exists in stable but it has only `undefined` exports. Since then we've also added other useful heuristics into the `prerender` branch that makes this really the favored and easiest to use API for the prerender (SSG/ISR) use case. `prerender` is also used for Partial Prerendering but that part is still experimental. However, we can expose only the `prerender` API on `react-dom/static` without it returning the `postponeState`. Instead the stream is on `prelude`. The naming is a bit awkward if you don't consider resuming but it's the same thing. It's really just `renderToReadable` stream with automatic `allReady` and better heuristics for prerendering.

Sebastian Markbåge committed Oct 19, 2024 at 22:33 UTC d49123f73f12564223c890bfa36be537de2c571d
8 files changed +49 -39
packages/react-dom/src/__tests__/ReactDOMFizzStatic-test.js
+7 -9
@@ -32,9 +32,7 @@ describe('ReactDOMFizzStatic', () => {
32 React = require('react');
33 ReactDOM = require('react-dom');
34 ReactDOMClient = require('react-dom/client');
35 - if (__EXPERIMENTAL__) {
36 - ReactDOMFizzStatic = require('react-dom/static');
37 - }
35 + ReactDOMFizzStatic = require('react-dom/static');
36 Stream = require('stream');
37 Suspense = React.Suspense;
38
@@ -212,7 +210,6 @@ describe('ReactDOMFizzStatic', () => {
210 return readText(text);
211 }
212
215 - // @gate experimental
213 it('should render a fully static document, send it and then hydrate it', async () => {
214 function App() {
215 return (
@@ -230,7 +227,11 @@ describe('ReactDOMFizzStatic', () => {
227
228 const result = await promise;
229
233 - expect(result.postponed).toBe(null);
230 + expect(result.postponed).toBe(
231 + gate(flags => flags.enableHalt || flags.enablePostpone)
232 + ? null
233 + : undefined,
234 + );
235
236 await act(async () => {
237 result.prelude.pipe(writable);
@@ -244,7 +245,6 @@ describe('ReactDOMFizzStatic', () => {
245 expect(getVisibleChildren(container)).toEqual(<div>Hello</div>);
246 });
247
247 - // @gate experimental
248 it('should support importMap option', async () => {
249 const importMap = {
250 foo: 'path/to/foo.js',
@@ -265,7 +265,6 @@ describe('ReactDOMFizzStatic', () => {
265 ]);
266 });
267
268 - // @gate experimental
268 it('supports onHeaders', async () => {
269 let headers;
270 function onHeaders(x) {
@@ -300,7 +299,7 @@ describe('ReactDOMFizzStatic', () => {
299 expect(getVisibleChildren(container)).toEqual('hello');
300 });
301
303 - // @gate experimental && enablePostpone
302 + // @gate enablePostpone
303 it('includes stylesheet preloads in onHeaders when postponing in the Shell', async () => {
304 let headers;
305 function onHeaders(x) {
@@ -336,7 +335,6 @@ describe('ReactDOMFizzStatic', () => {
335 expect(getVisibleChildren(container)).toEqual(undefined);
336 });
337
339 - // @gate experimental
338 it('will prerender Suspense fallbacks before children', async () => {
339 const values = [];
340 function Indirection({children}) {
packages/react-dom/src/__tests__/ReactDOMFizzStaticBrowser-test.js
+6 -18
@@ -42,9 +42,7 @@ describe('ReactDOMFizzStaticBrowser', () => {
42 React = require('react');
43 ReactDOM = require('react-dom');
44 ReactDOMFizzServer = require('react-dom/server.browser');
45 - if (__EXPERIMENTAL__) {
46 - ReactDOMFizzStatic = require('react-dom/static.browser');
47 - }
45 + ReactDOMFizzStatic = require('react-dom/static.browser');
46 Suspense = React.Suspense;
47 container = document.createElement('div');
48 document.body.appendChild(container);
@@ -131,7 +129,6 @@ describe('ReactDOMFizzStaticBrowser', () => {
129 await insertNodesAndExecuteScripts(temp, container, null);
130 }
131
134 - // @gate experimental
132 it('should call prerender', async () => {
133 const result = await serverAct(() =>
134 ReactDOMFizzStatic.prerender(<div>hello world</div>),
@@ -140,7 +137,6 @@ describe('ReactDOMFizzStaticBrowser', () => {
137 expect(prelude).toMatchInlineSnapshot(`"<div>hello world</div>"`);
138 });
139
143 - // @gate experimental
140 it('should emit DOCTYPE at the root of the document', async () => {
141 const result = await serverAct(() =>
142 ReactDOMFizzStatic.prerender(
@@ -155,7 +151,6 @@ describe('ReactDOMFizzStaticBrowser', () => {
151 );
152 });
153
158 - // @gate experimental
154 it('should emit bootstrap script src at the end', async () => {
155 const result = await serverAct(() =>
156 ReactDOMFizzStatic.prerender(<div>hello world</div>, {
@@ -170,7 +165,6 @@ describe('ReactDOMFizzStaticBrowser', () => {
165 );
166 });
167
173 - // @gate experimental
168 it('emits all HTML as one unit', async () => {
169 let hasLoaded = false;
170 let resolve;
@@ -202,7 +196,6 @@ describe('ReactDOMFizzStaticBrowser', () => {
196 expect(prelude).toMatchInlineSnapshot(`"<div><!--$-->Done<!--/$--></div>"`);
197 });
198
205 - // @gate experimental
199 it('should reject the promise when an error is thrown at the root', async () => {
200 const reportedErrors = [];
201 let caughtError = null;
@@ -226,7 +219,6 @@ describe('ReactDOMFizzStaticBrowser', () => {
219 expect(reportedErrors).toEqual([theError]);
220 });
221
229 - // @gate experimental
222 it('should reject the promise when an error is thrown inside a fallback', async () => {
223 const reportedErrors = [];
224 let caughtError = null;
@@ -252,7 +244,6 @@ describe('ReactDOMFizzStaticBrowser', () => {
244 expect(reportedErrors).toEqual([theError]);
245 });
246
255 - // @gate experimental
247 it('should not error the stream when an error is thrown inside suspense boundary', async () => {
248 const reportedErrors = [];
249 const result = await serverAct(() =>
@@ -275,7 +266,6 @@ describe('ReactDOMFizzStaticBrowser', () => {
266 expect(reportedErrors).toEqual([theError]);
267 });
268
278 - // @gate experimental
269 it('should be able to complete by aborting even if the promise never resolves', async () => {
270 const errors = [];
271 const controller = new AbortController();
@@ -306,7 +296,6 @@ describe('ReactDOMFizzStaticBrowser', () => {
296 expect(errors).toEqual(['The operation was aborted.']);
297 });
298
309 - // @gate experimental
299 // @gate !enableHalt
300 it('should reject if aborting before the shell is complete and enableHalt is disabled', async () => {
301 const errors = [];
@@ -376,7 +365,6 @@ describe('ReactDOMFizzStaticBrowser', () => {
365 expect(content).toBe('');
366 });
367
379 - // @gate experimental
368 it('should be able to abort before something suspends', async () => {
369 const errors = [];
370 const controller = new AbortController();
@@ -419,7 +407,6 @@ describe('ReactDOMFizzStaticBrowser', () => {
407 }
408 });
409
422 - // @gate experimental
410 // @gate !enableHalt
411 it('should reject if passing an already aborted signal and enableHalt is disabled', async () => {
412 const errors = [];
@@ -493,7 +480,6 @@ describe('ReactDOMFizzStaticBrowser', () => {
480 expect(content).toBe('');
481 });
482
496 - // @gate experimental
483 it('supports custom abort reasons with a string', async () => {
484 const promise = new Promise(r => {});
485 function Wait() {
@@ -536,7 +522,6 @@ describe('ReactDOMFizzStaticBrowser', () => {
522 expect(errors).toEqual(['foobar', 'foobar']);
523 });
524
539 - // @gate experimental
525 it('supports custom abort reasons with an Error', async () => {
526 const promise = new Promise(r => {});
527 function Wait() {
@@ -1610,7 +1595,6 @@ describe('ReactDOMFizzStaticBrowser', () => {
1595 );
1596 });
1597
1613 - // @gate experimental
1598 it('logs an error if onHeaders throws but continues the prerender', async () => {
1599 const errors = [];
1600 function onError(error) {
@@ -1627,7 +1611,11 @@ describe('ReactDOMFizzStaticBrowser', () => {
1611 onError,
1612 }),
1613 );
1630 - expect(prerendered.postponed).toBe(null);
1614 + expect(prerendered.postponed).toBe(
1615 + gate(flags => flags.enableHalt || flags.enablePostpone)
1616 + ? null
1617 + : undefined,
1618 + );
1619 expect(errors).toEqual(['bad onHeaders']);
1620
1621 await readIntoContainer(prerendered.prelude);
packages/react-dom/src/server/ReactDOMFizzStaticBrowser.js
+11 -4
@@ -38,6 +38,8 @@ import {
38 createRootFormatContext,
39 } from 'react-dom-bindings/src/server/ReactFizzConfigDOM';
40
41 +import {enablePostpone, enableHalt} from 'shared/ReactFeatureFlags';
42 +
43 import {ensureCorrectIsomorphicReactVersion} from '../shared/ensureCorrectIsomorphicReactVersion';
44 ensureCorrectIsomorphicReactVersion();
45
@@ -85,10 +87,15 @@ function prerender(
87 {highWaterMark: 0},
88 );
89
88 - const result = {
89 - postponed: getPostponedState(request),
90 - prelude: stream,
91 - };
90 + const result: StaticResult =
91 + enablePostpone || enableHalt
92 + ? {
93 + postponed: getPostponedState(request),
94 + prelude: stream,
95 + }
96 + : ({
97 + prelude: stream,
98 + }: any);
99 resolve(result);
100 }
101
packages/react-dom/src/server/ReactDOMFizzStaticEdge.js
+11 -4
@@ -38,6 +38,8 @@ import {
38 createRootFormatContext,
39 } from 'react-dom-bindings/src/server/ReactFizzConfigDOM';
40
41 +import {enablePostpone, enableHalt} from 'shared/ReactFeatureFlags';
42 +
43 import {ensureCorrectIsomorphicReactVersion} from '../shared/ensureCorrectIsomorphicReactVersion';
44 ensureCorrectIsomorphicReactVersion();
45
@@ -85,10 +87,15 @@ function prerender(
87 {highWaterMark: 0},
88 );
89
88 - const result = {
89 - postponed: getPostponedState(request),
90 - prelude: stream,
91 - };
90 + const result: StaticResult =
91 + enablePostpone || enableHalt
92 + ? {
93 + postponed: getPostponedState(request),
94 + prelude: stream,
95 + }
96 + : ({
97 + prelude: stream,
98 + }: any);
99 resolve(result);
100 }
101
packages/react-dom/src/server/ReactDOMFizzStaticNode.js
+11 -4
@@ -39,6 +39,8 @@ import {
39 createRootFormatContext,
40 } from 'react-dom-bindings/src/server/ReactFizzConfigDOM';
41
42 +import {enablePostpone, enableHalt} from 'shared/ReactFeatureFlags';
43 +
44 import {ensureCorrectIsomorphicReactVersion} from '../shared/ensureCorrectIsomorphicReactVersion';
45 ensureCorrectIsomorphicReactVersion();
46
@@ -94,10 +96,15 @@ function prerenderToNodeStream(
96 });
97 const writable = createFakeWritable(readable);
98
97 - const result = {
98 - postponed: getPostponedState(request),
99 - prelude: readable,
100 - };
99 + const result: StaticResult =
100 + enablePostpone || enableHalt
101 + ? {
102 + postponed: getPostponedState(request),
103 + prelude: readable,
104 + }
105 + : ({
106 + prelude: readable,
107 + }: any);
108 resolve(result);
109 }
110 const resumableState = createResumableState(
packages/react-dom/src/server/react-dom-server.browser.stable.js
+1
@@ -8,3 +8,4 @@
8 */
9
10 export {renderToReadableStream, version} from './ReactDOMFizzServerBrowser.js';
11 +export {prerender} from './ReactDOMFizzStaticBrowser.js';
packages/react-dom/src/server/react-dom-server.edge.stable.js
+1
@@ -8,3 +8,4 @@
8 */
9
10 export {renderToReadableStream, version} from './ReactDOMFizzServerEdge.js';
11 +export {prerender} from './ReactDOMFizzStaticEdge.js';
packages/react-dom/src/server/react-dom-server.node.stable.js
+1
@@ -8,3 +8,4 @@
8 */
9
10 export {renderToPipeableStream, version} from './ReactDOMFizzServerNode.js';
11 +export {prerenderToNodeStream} from './ReactDOMFizzStaticNode.js';