diff --git a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js index eebca4710e954..c12c6904b872f 100644 --- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js +++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js @@ -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(

hello world

); }); + // @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 {value}; + } + + const ClientModule = clientExports({ + ContextProvider, + Foo, + }); + + async function getFoo() { + React.unstable_postpone('foo'); + } + + function App() { + return ( + +
+ + + +
+
+ ); + } + + const {writable, readable} = getTestStream(); + + const {pipe} = ReactServerDOMServer.renderToPipeableStream( + , + 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( + , + { + 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( + + + +
loading...
+ + , + ); + }); + it('should support float methods when rendering in Fizz', async () => { function Component() { return

hello world

; diff --git a/packages/react-server/src/ReactFlightServer.js b/packages/react-server/src/ReactFlightServer.js index 479a4a746d5e2..cc65f746f42e8 100644 --- a/packages/react-server/src/ReactFlightServer.js +++ b/packages/react-server/src/ReactFlightServer.js @@ -407,11 +407,21 @@ function serializeThenable(request: Request, thenable: Thenable): 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); }