Skip to content

Commit 009eef4

Browse files
committed
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.
1 parent b36ae8d commit 009eef4

File tree

2 files changed

+102
-4
lines changed

2 files changed

+102
-4
lines changed

packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ let ReactDOMClient;
3232
let ReactServerDOMServer;
3333
let ReactServerDOMClient;
3434
let ReactDOMFizzServer;
35+
let ReactDOMStaticServer;
3536
let Suspense;
3637
let ErrorBoundary;
3738
let JSDOM;
@@ -71,6 +72,7 @@ describe('ReactFlightDOM', () => {
7172
Suspense = React.Suspense;
7273
ReactDOMClient = require('react-dom/client');
7374
ReactDOMFizzServer = require('react-dom/server.node');
75+
ReactDOMStaticServer = require('react-dom/static.node');
7476
ReactServerDOMClient = require('react-server-dom-webpack/client');
7577

7678
ErrorBoundary = class extends React.Component {
@@ -1300,6 +1302,89 @@ describe('ReactFlightDOM', () => {
13001302
expect(getMeaningfulChildren(container)).toEqual(<p>hello world</p>);
13011303
});
13021304

1305+
// @gate experimental
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+
await 1;
1322+
React.unstable_postpone('foo');
1323+
}
1324+
1325+
function App() {
1326+
return (
1327+
<ClientModule.ContextProvider value={getFoo()}>
1328+
<div>
1329+
<Suspense fallback="loading...">
1330+
<ClientModule.Foo />
1331+
</Suspense>
1332+
</div>
1333+
</ClientModule.ContextProvider>
1334+
);
1335+
}
1336+
1337+
const {writable, readable} = getTestStream();
1338+
1339+
const {pipe} = ReactServerDOMServer.renderToPipeableStream(
1340+
<App />,
1341+
webpackMap,
1342+
);
1343+
pipe(writable);
1344+
1345+
let response = null;
1346+
function getResponse() {
1347+
if (response === null) {
1348+
response = ReactServerDOMClient.createFromReadableStream(readable);
1349+
}
1350+
return response;
1351+
}
1352+
1353+
function Response() {
1354+
return getResponse();
1355+
}
1356+
1357+
const errors = [];
1358+
function onError(error, errorInfo) {
1359+
errors.push(error, errorInfo);
1360+
}
1361+
const result = await ReactDOMStaticServer.prerenderToNodeStream(
1362+
<Response />,
1363+
onError,
1364+
);
1365+
1366+
const prelude = await new Promise((resolve, reject) => {
1367+
let content = '';
1368+
result.prelude.on('data', chunk => {
1369+
content += Buffer.from(chunk).toString('utf8');
1370+
});
1371+
result.prelude.on('error', error => {
1372+
reject(error);
1373+
});
1374+
result.prelude.on('end', () => resolve(content));
1375+
});
1376+
1377+
const doc = new JSDOM(prelude).window.document;
1378+
expect(getMeaningfulChildren(doc)).toEqual(
1379+
<html>
1380+
<head />
1381+
<body>
1382+
<div>loading...</div>
1383+
</body>
1384+
</html>,
1385+
);
1386+
});
1387+
13031388
it('should support float methods when rendering in Fizz', async () => {
13041389
function Component() {
13051390
return <p>hello world</p>;

packages/react-server/src/ReactFlightServer.js

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -407,11 +407,24 @@ function serializeThenable(request: Request, thenable: Thenable<any>): number {
407407
pingTask(request, newTask);
408408
},
409409
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+
if (request.destination !== null) {
425+
flushCompletedChunks(request, request.destination);
426+
}
411427
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);
415428
if (request.destination !== null) {
416429
flushCompletedChunks(request, request.destination);
417430
}

0 commit comments

Comments
 (0)