Skip to content

Commit

Permalink
Postponing in a promise that is being serialized to the client from t…
Browse files Browse the repository at this point in the history
…he 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 committed Dec 8, 2023
1 parent d3ed07b commit b36305c
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 b36305c

Please sign in to comment.