mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
[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.
This commit is contained in:
@@ -32,6 +32,7 @@ let ReactDOMClient;
|
||||
let ReactServerDOMServer;
|
||||
let ReactServerDOMClient;
|
||||
let ReactDOMFizzServer;
|
||||
let ReactDOMStaticServer;
|
||||
let Suspense;
|
||||
let ErrorBoundary;
|
||||
let JSDOM;
|
||||
@@ -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 {
|
||||
@@ -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>;
|
||||
|
||||
+14
-4
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user