Skip to content

Commit 7712744

Browse files
sebmarkbageAndyPengc12
authored andcommitted
Consume the RSC stream twice in the Flight fixture (facebook#28353)
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.
1 parent ff046ee commit 7712744

File tree

1 file changed

+20
-20
lines changed

1 file changed

+20
-20
lines changed

fixtures/flight/server/global.js

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ const React = require('react');
3737

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

4142
const app = express();
4243

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

149-
// This is a bad hack to set the form state after SSR has started. It works
150-
// because we block the root component until we have the form state and
151-
// any form that reads it necessarily will come later. It also only works
152-
// because the formstate type is an object which may change in the future
153-
const lazyFormState = [];
154-
155-
let cachedResult = null;
156-
async function getRootAndFormState() {
157-
const {root, formState} = await createFromNodeStream(
158-
rscResponse,
159-
ssrManifest
160-
);
161-
// We shouldn't be assuming formState is an object type but at the moment
162-
// we have no way of setting the form state from within the render
163-
Object.assign(lazyFormState, formState);
164-
return root;
165-
}
150+
// We need to get the formState before we start rendering but we also
151+
// need to run the Flight client inside the render to get all the preloads.
152+
// The API is ambivalent about what's the right one so we need two for now.
153+
154+
// Tee the response into two streams so that we can do both.
155+
const rscResponse1 = new PassThrough();
156+
const rscResponse2 = new PassThrough();
157+
158+
rscResponse.pipe(rscResponse1);
159+
rscResponse.pipe(rscResponse2);
160+
161+
const {formState} = await createFromNodeStream(rscResponse1, ssrManifest);
162+
rscResponse1.end();
163+
164+
let cachedResult;
166165
let Root = () => {
167166
if (!cachedResult) {
168-
cachedResult = getRootAndFormState();
167+
// Read this stream inside the render.
168+
cachedResult = createFromNodeStream(rscResponse2, ssrManifest);
169169
}
170-
return React.use(cachedResult);
170+
return React.use(cachedResult).root;
171171
};
172172
// Render it into HTML by resolving the client components
173173
res.set('Content-type', 'text/html');
174174
const {pipe} = renderToPipeableStream(React.createElement(Root), {
175175
bootstrapScripts: mainJSChunks,
176-
formState: lazyFormState,
176+
formState: formState,
177177
});
178178
pipe(res);
179179
} catch (e) {

0 commit comments

Comments
 (0)