Skip to content

Commit

Permalink
Consume the RSC stream twice in the Flight fixture (facebook#28353)
Browse files Browse the repository at this point in the history
We have an unresolved conflict where the Flight client wants to execute
inside Fizz to emit side-effects like preloads (which can be early) into
that stream. However, the FormState API requires the state to be passed
at the root, so if you're getting that through the RSC payload it's a
Catch 22.

facebook#27314 used a hack to mutate the form state array to fill it in later,
but that doesn't actually work because it's not always an array. It's
sometimes null like if there wasn't a POST. This lead to a bunch of
hydration errors - which doesn't have the best error message for this
case neither. It probably should error with something that specifies
that it's form state.

This fixes it by teeing the stream into two streams and consuming it
with two Flight clients. One to read the form state and one to emit
side-effects and read the root.
  • Loading branch information
sebmarkbage authored and AndyPengc12 committed Apr 15, 2024
1 parent ff046ee commit 7712744
Showing 1 changed file with 20 additions and 20 deletions.
40 changes: 20 additions & 20 deletions fixtures/flight/server/global.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ const React = require('react');

const {renderToPipeableStream} = require('react-dom/server');
const {createFromNodeStream} = require('react-server-dom-webpack/client');
const {PassThrough} = require('stream');

const app = express();

Expand Down Expand Up @@ -146,34 +147,33 @@ app.all('/', async function (req, res, next) {
// so we start by consuming the RSC payload. This needs a module
// map that reverse engineers the client-side path to the SSR path.

// This is a bad hack to set the form state after SSR has started. It works
// because we block the root component until we have the form state and
// any form that reads it necessarily will come later. It also only works
// because the formstate type is an object which may change in the future
const lazyFormState = [];

let cachedResult = null;
async function getRootAndFormState() {
const {root, formState} = await createFromNodeStream(
rscResponse,
ssrManifest
);
// We shouldn't be assuming formState is an object type but at the moment
// we have no way of setting the form state from within the render
Object.assign(lazyFormState, formState);
return root;
}
// We need to get the formState before we start rendering but we also
// need to run the Flight client inside the render to get all the preloads.
// The API is ambivalent about what's the right one so we need two for now.

// Tee the response into two streams so that we can do both.
const rscResponse1 = new PassThrough();
const rscResponse2 = new PassThrough();

rscResponse.pipe(rscResponse1);
rscResponse.pipe(rscResponse2);

const {formState} = await createFromNodeStream(rscResponse1, ssrManifest);
rscResponse1.end();

let cachedResult;
let Root = () => {
if (!cachedResult) {
cachedResult = getRootAndFormState();
// Read this stream inside the render.
cachedResult = createFromNodeStream(rscResponse2, ssrManifest);
}
return React.use(cachedResult);
return React.use(cachedResult).root;
};
// Render it into HTML by resolving the client components
res.set('Content-type', 'text/html');
const {pipe} = renderToPipeableStream(React.createElement(Root), {
bootstrapScripts: mainJSChunks,
formState: lazyFormState,
formState: formState,
});
pipe(res);
} catch (e) {
Expand Down

0 comments on commit 7712744

Please sign in to comment.