Skip to content

Commit

Permalink
[Flight] Support postponing through a serialized promise (#27818)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
gnoff authored Dec 8, 2023
1 parent 8ff2c23 commit 5bcade5
Show file tree
Hide file tree
Showing 2 changed files with 101 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ let ReactDOMClient;
let ReactServerDOMServer;
let ReactServerDOMClient;
let ReactDOMFizzServer;
let ReactDOMStaticServer;
let Suspense;
let ErrorBoundary;
let JSDOM;
Expand Down Expand Up @@ -71,6 +72,7 @@ describe('ReactFlightDOM', () => {
Suspense = React.Suspense;
ReactDOMClient = require('react-dom/client');
ReactDOMFizzServer = require('react-dom/server.node');
ReactDOMStaticServer = require('react-dom/static.node');
ReactServerDOMClient = require('react-server-dom-webpack/client');

ErrorBoundary = class extends React.Component {
Expand Down Expand Up @@ -1300,6 +1302,91 @@ describe('ReactFlightDOM', () => {
expect(getMeaningfulChildren(container)).toEqual(<p>hello world</p>);
});

// @gate enablePostpone
it('should allow postponing in Flight through a serialized promise', async () => {
const Context = React.createContext();
const ContextProvider = Context.Provider;

function Foo() {
const value = React.use(React.useContext(Context));
return <span>{value}</span>;
}

const ClientModule = clientExports({
ContextProvider,
Foo,
});

async function getFoo() {
React.unstable_postpone('foo');
}

function App() {
return (
<ClientModule.ContextProvider value={getFoo()}>
<div>
<Suspense fallback="loading...">
<ClientModule.Foo />
</Suspense>
</div>
</ClientModule.ContextProvider>
);
}

const {writable, readable} = getTestStream();

const {pipe} = ReactServerDOMServer.renderToPipeableStream(
<App />,
webpackMap,
);
pipe(writable);

let response = null;
function getResponse() {
if (response === null) {
response = ReactServerDOMClient.createFromReadableStream(readable);
}
return response;
}

function Response() {
return getResponse();
}

const errors = [];
function onError(error, errorInfo) {
errors.push(error, errorInfo);
}
const result = await ReactDOMStaticServer.prerenderToNodeStream(
<Response />,
{
onError,
},
);

const prelude = await new Promise((resolve, reject) => {
let content = '';
result.prelude.on('data', chunk => {
content += Buffer.from(chunk).toString('utf8');
});
result.prelude.on('error', error => {
reject(error);
});
result.prelude.on('end', () => resolve(content));
});

expect(errors).toEqual([]);
const doc = new JSDOM(prelude).window.document;
expect(getMeaningfulChildren(doc)).toEqual(
<html>
<head />
<body>
<div>loading...</div>
</body>
</html>,
);
});

it('should support float methods when rendering in Fizz', async () => {
function Component() {
return <p>hello world</p>;
Expand Down
18 changes: 14 additions & 4 deletions packages/react-server/src/ReactFlightServer.js
Original file line number Diff line number Diff line change
Expand Up @@ -407,11 +407,21 @@ function serializeThenable(request: Request, thenable: Thenable<any>): number {
pingTask(request, newTask);
},
reason => {
newTask.status = ERRORED;
if (
enablePostpone &&
typeof reason === 'object' &&
reason !== null &&
(reason: any).$$typeof === REACT_POSTPONE_TYPE
) {
const postponeInstance: Postpone = (reason: any);
logPostpone(request, postponeInstance.message);
emitPostponeChunk(request, newTask.id, postponeInstance);
} else {
newTask.status = ERRORED;
const digest = logRecoverableError(request, reason);
emitErrorChunk(request, newTask.id, digest, reason);
}
request.abortableTasks.delete(newTask);
// TODO: We should ideally do this inside performWork so it's scheduled
const digest = logRecoverableError(request, reason);
emitErrorChunk(request, newTask.id, digest, reason);
if (request.destination !== null) {
flushCompletedChunks(request, request.destination);
}
Expand Down

0 comments on commit 5bcade5

Please sign in to comment.