Skip to content

Commit 18fff5d

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 18fff5d

File tree

2 files changed

+101
-4
lines changed

2 files changed

+101
-4
lines changed

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

+84
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,88 @@ describe('ReactFlightDOM', () => {
13001302
expect(getMeaningfulChildren(container)).toEqual(<p>hello world</p>);
13011303
});
13021304

1305+
it('should allow postponing in Flight through a serialized promise', async () => {
1306+
const Context = React.createContext();
1307+
const ContextProvider = Context.Provider;
1308+
1309+
function Foo() {
1310+
const value = React.use(React.useContext(Context));
1311+
return <span>{value}</span>;
1312+
}
1313+
1314+
const ClientModule = clientExports({
1315+
ContextProvider,
1316+
Foo,
1317+
});
1318+
1319+
async function getFoo() {
1320+
await 1;
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+
onError,
1363+
);
1364+
1365+
const prelude = await new Promise((resolve, reject) => {
1366+
let content = '';
1367+
result.prelude.on('data', chunk => {
1368+
content += Buffer.from(chunk).toString('utf8');
1369+
});
1370+
result.prelude.on('error', error => {
1371+
reject(error);
1372+
});
1373+
result.prelude.on('end', () => resolve(content));
1374+
});
1375+
1376+
const doc = new JSDOM(prelude).window.document;
1377+
expect(getMeaningfulChildren(doc)).toEqual(
1378+
<html>
1379+
<head />
1380+
<body>
1381+
<div>loading...</div>
1382+
</body>
1383+
</html>,
1384+
);
1385+
});
1386+
13031387
it('should support float methods when rendering in Fizz', async () => {
13041388
function Component() {
13051389
return <p>hello world</p>;

packages/react-server/src/ReactFlightServer.js

+17-4
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)