[Flight] don't overwrite existing chunk listeners in 'wakeChunkIfInitialized' (#29204)
Follow up to https://github.com/facebook/react/pull/29201. If a chunk had listeners attached already (e.g. because `.then` was called on the chunk returned from `createFromReadableStream`), `wakeChunkIfInitialized` would overwrite any listeners added during chunk initialization. This caused cyclic [path references](https://github.com/facebook/react/pull/28996) within that chunk to never resolve. Fixed by merging the two arrays of listeners.
Janka Uryga committed
May 21, 2024 at 23:04 UTC
9b3f909cc19fd848dcf8cba2cc3750f32053c056
2 files changed
+70
-2
packages/react-client/src/ReactFlightClient.js
+18
-2
@@ -320,8 +320,24 @@ function wakeChunkIfInitialized<T>(
320
case PENDING:
321
case BLOCKED:
322
case CYCLIC:
323
- chunk.value = resolveListeners;
324
- chunk.reason = rejectListeners;
323
+ if (chunk.value) {
324
+ for (let i = 0; i < resolveListeners.length; i++) {
325
+ chunk.value.push(resolveListeners[i]);
326
+ }
327
+ } else {
328
+ chunk.value = resolveListeners;
329
+ }
330
+
331
+ if (chunk.reason) {
332
+ if (rejectListeners) {
333
+ for (let i = 0; i < rejectListeners.length; i++) {
334
+ chunk.reason.push(rejectListeners[i]);
335
+ }
336
+ }
337
+ } else {
338
+ chunk.reason = rejectListeners;
339
+ }
340
+
341
break;
342
case ERRORED:
343
if (rejectListeners) {
packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js
+52
@@ -296,6 +296,58 @@ describe('ReactFlightDOMBrowser', () => {
296
expect(container.innerHTML).toBe('<pre>[[1,2,3],[1,2,3]]</pre>');
297
});
298
299
+ it('should resolve deduped objects within the same model root when it is blocked and there is a listener attached to the root', async () => {
300
+ let resolveClientComponentChunk;
301
+
302
+ const ClientOuter = clientExports(function ClientOuter({Component, value}) {
303
+ return <Component value={value} />;
304
+ });
305
+
306
+ const ClientInner = clientExports(
307
+ function ClientInner({value}) {
308
+ return <pre>{JSON.stringify(value)}</pre>;
309
+ },
310
+ '42',
311
+ '/test.js',
312
+ new Promise(resolve => (resolveClientComponentChunk = resolve)),
313
+ );
314
+
315
+ function Server({value}) {
316
+ return <ClientOuter Component={ClientInner} value={value} />;
317
+ }
318
+
319
+ const shared = [1, 2, 3];
320
+ const value = [shared, shared];
321
+
322
+ const stream = ReactServerDOMServer.renderToReadableStream(
323
+ <Server value={value} />,
324
+ webpackMap,
325
+ );
326
+
327
+ function ClientRoot({response}) {
328
+ return use(response);
329
+ }
330
+
331
+ const response = ReactServerDOMClient.createFromReadableStream(stream);
332
+ // make sure we have a listener so that `resolveModelChunk` initializes the chunk eagerly
333
+ response.then(() => {});
334
+
335
+ const container = document.createElement('div');
336
+ const root = ReactDOMClient.createRoot(container);
337
+
338
+ await act(() => {
339
+ root.render(<ClientRoot response={response} />);
340
+ });
341
+
342
+ expect(container.innerHTML).toBe('');
343
+
344
+ await act(() => {
345
+ resolveClientComponentChunk();
346
+ });
347
+
348
+ expect(container.innerHTML).toBe('<pre>[[1,2,3],[1,2,3]]</pre>');
349
+ });
350
+
351
it('should progressively reveal server components', async () => {
352
let reportedErrors = [];
353