@samitouri / QOS-React-1 / commits / 6a44f352ec

Consume the RSC stream twice in the Flight fixture (#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. #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.

Sebastian Markbåge committed Feb 16, 2024 at 18:25 UTC 6a44f352ecdfa93949cade1d2e74755e7d7e9aaa
1 file changed +20 -20
fixtures/flight/server/global.js
+20 -20
@@ -37,6 +37,7 @@ const React = require('react');
37
38 const {renderToPipeableStream} = require('react-dom/server');
39 const {createFromNodeStream} = require('react-server-dom-webpack/client');
40 +const {PassThrough} = require('stream');
41
42 const app = express();
43
@@ -146,34 +147,33 @@ app.all('/', async function (req, res, next) {
147 // so we start by consuming the RSC payload. This needs a module
148 // map that reverse engineers the client-side path to the SSR path.
149
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;
165 let Root = () => {
166 if (!cachedResult) {
168 - cachedResult = getRootAndFormState();
167 + // Read this stream inside the render.
168 + cachedResult = createFromNodeStream(rscResponse2, ssrManifest);
169 }
170 - return React.use(cachedResult);
170 + return React.use(cachedResult).root;
171 };
172 // Render it into HTML by resolving the client components
173 res.set('Content-type', 'text/html');
174 const {pipe} = renderToPipeableStream(React.createElement(Root), {
175 bootstrapScripts: mainJSChunks,
176 - formState: lazyFormState,
176 + formState: formState,
177 });
178 pipe(res);
179 } catch (e) {