@samitouri / QOS-React-2 / commits / 5bcade5fcf

[Flight] Support postponing through a serialized promise (#27818)

Postponing in a promise that is being serialized to the client from the server should be possible however prior to this change Flight treated this case like an error rather than a postpone. This fix adds support for postponing in this position and adds a test asserting you can successfully prerender the root if you unwrap this promise inside a suspense boundary.

Josh Story committed Dec 8, 2023 at 11:05 UTC 5bcade5fcf5610e82e7cda05cc6de574bdace0c7
2 files changed +101 -4
packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js
+87
@@ -32,6 +32,7 @@ let ReactDOMClient;
32 let ReactServerDOMServer;
33 let ReactServerDOMClient;
34 let ReactDOMFizzServer;
35 +let ReactDOMStaticServer;
36 let Suspense;
37 let ErrorBoundary;
38 let JSDOM;
@@ -71,6 +72,7 @@ describe('ReactFlightDOM', () => {
72 Suspense = React.Suspense;
73 ReactDOMClient = require('react-dom/client');
74 ReactDOMFizzServer = require('react-dom/server.node');
75 + ReactDOMStaticServer = require('react-dom/static.node');
76 ReactServerDOMClient = require('react-server-dom-webpack/client');
77
78 ErrorBoundary = class extends React.Component {
@@ -1300,6 +1302,91 @@ describe('ReactFlightDOM', () => {
1302 expect(getMeaningfulChildren(container)).toEqual(<p>hello world</p>);
1303 });
1304
1305 + // @gate enablePostpone
1306 + it('should allow postponing in Flight through a serialized promise', async () => {
1307 + const Context = React.createContext();
1308 + const ContextProvider = Context.Provider;
1309 +
1310 + function Foo() {
1311 + const value = React.use(React.useContext(Context));
1312 + return <span>{value}</span>;
1313 + }
1314 +
1315 + const ClientModule = clientExports({
1316 + ContextProvider,
1317 + Foo,
1318 + });
1319 +
1320 + async function getFoo() {
1321 + React.unstable_postpone('foo');
1322 + }
1323 +
1324 + function App() {
1325 + return (
1326 + <ClientModule.ContextProvider value={getFoo()}>
1327 + <div>
1328 + <Suspense fallback="loading...">
1329 + <ClientModule.Foo />
1330 + </Suspense>
1331 + </div>
1332 + </ClientModule.ContextProvider>
1333 + );
1334 + }
1335 +
1336 + const {writable, readable} = getTestStream();
1337 +
1338 + const {pipe} = ReactServerDOMServer.renderToPipeableStream(
1339 + <App />,
1340 + webpackMap,
1341 + );
1342 + pipe(writable);
1343 +
1344 + let response = null;
1345 + function getResponse() {
1346 + if (response === null) {
1347 + response = ReactServerDOMClient.createFromReadableStream(readable);
1348 + }
1349 + return response;
1350 + }
1351 +
1352 + function Response() {
1353 + return getResponse();
1354 + }
1355 +
1356 + const errors = [];
1357 + function onError(error, errorInfo) {
1358 + errors.push(error, errorInfo);
1359 + }
1360 + const result = await ReactDOMStaticServer.prerenderToNodeStream(
1361 + <Response />,
1362 + {
1363 + onError,
1364 + },
1365 + );
1366 +
1367 + const prelude = await new Promise((resolve, reject) => {
1368 + let content = '';
1369 + result.prelude.on('data', chunk => {
1370 + content += Buffer.from(chunk).toString('utf8');
1371 + });
1372 + result.prelude.on('error', error => {
1373 + reject(error);
1374 + });
1375 + result.prelude.on('end', () => resolve(content));
1376 + });
1377 +
1378 + expect(errors).toEqual([]);
1379 + const doc = new JSDOM(prelude).window.document;
1380 + expect(getMeaningfulChildren(doc)).toEqual(
1381 + <html>
1382 + <head />
1383 + <body>
1384 + <div>loading...</div>
1385 + </body>
1386 + </html>,
1387 + );
1388 + });
1389 +
1390 it('should support float methods when rendering in Fizz', async () => {
1391 function Component() {
1392 return <p>hello world</p>;
packages/react-server/src/ReactFlightServer.js
+14 -4
@@ -407,11 +407,21 @@ function serializeThenable(request: Request, thenable: Thenable<any>): number {
407 pingTask(request, newTask);
408 },
409 reason => {
410 - newTask.status = ERRORED;
410 + if (
411 + enablePostpone &&
412 + typeof reason === 'object' &&
413 + reason !== null &&
414 + (reason: any).$$typeof === REACT_POSTPONE_TYPE
415 + ) {
416 + const postponeInstance: Postpone = (reason: any);
417 + logPostpone(request, postponeInstance.message);
418 + emitPostponeChunk(request, newTask.id, postponeInstance);
419 + } else {
420 + newTask.status = ERRORED;
421 + const digest = logRecoverableError(request, reason);
422 + emitErrorChunk(request, newTask.id, digest, reason);
423 + }
424 request.abortableTasks.delete(newTask);
412 - // TODO: We should ideally do this inside performWork so it's scheduled
413 - const digest = logRecoverableError(request, reason);
414 - emitErrorChunk(request, newTask.id, digest, reason);
425 if (request.destination !== null) {
426 flushCompletedChunks(request, request.destination);
427 }