@samitouri / QOS-React-1 / commits / 9c2a8dd5f8

[Flight] Ensure we dedupe references if we later discover that it's the model root (#33666)

I noticed we weren't deduping these cases.

Sebastian Markbåge committed Jun 29, 2025 at 10:47 UTC 9c2a8dd5f87266308a8a8e55fb7f029ba530d36a
2 files changed +55
packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js
+50
@@ -2574,4 +2574,54 @@ describe('ReactFlightDOMBrowser', () => {
2574 expect(errors).toEqual([new Error('Connection closed.')]);
2575 expect(container.innerHTML).toBe('');
2576 });
2577 +
2578 + it('can dedupe references inside promises', async () => {
2579 + const foo = {};
2580 + const bar = {
2581 + foo: foo,
2582 + };
2583 + foo.bar = bar;
2584 +
2585 + const object = {
2586 + foo: Promise.resolve(foo),
2587 + bar: Promise.resolve(bar),
2588 + };
2589 +
2590 + const stream = await serverAct(() =>
2591 + ReactServerDOMServer.renderToReadableStream(object, webpackMap),
2592 + );
2593 +
2594 + const response = await ReactServerDOMClient.createFromReadableStream(
2595 + passThrough(stream),
2596 + );
2597 +
2598 + const responseFoo = await response.foo;
2599 + const responseBar = await response.bar;
2600 + expect(responseFoo.bar).toBe(responseBar);
2601 + expect(responseBar.foo).toBe(responseFoo);
2602 + });
2603 +
2604 + it('can deduped outlined references inside promises', async () => {
2605 + const foo = {};
2606 + const bar = new Set([foo]); // This will be outlined which can create a future reference
2607 + foo.bar = bar;
2608 +
2609 + const object = {
2610 + foo: Promise.resolve(foo),
2611 + bar: Promise.resolve(bar),
2612 + };
2613 +
2614 + const stream = await serverAct(() =>
2615 + ReactServerDOMServer.renderToReadableStream(object, webpackMap),
2616 + );
2617 +
2618 + const response = await ReactServerDOMClient.createFromReadableStream(
2619 + passThrough(stream),
2620 + );
2621 +
2622 + const responseFoo = await response.foo;
2623 + const responseBar = await response.bar;
2624 + expect(responseFoo.bar).toBe(responseBar);
2625 + expect(Array.from(responseBar)[0]).toBe(responseFoo);
2626 + });
2627 });
packages/react-server/src/ReactFlightServer.js
+5
@@ -3220,6 +3220,11 @@ function renderModelDestructive(
3220
3221 if (existingReference !== undefined) {
3222 if (modelRoot === value) {
3223 + if (existingReference !== serializeByValueID(task.id)) {
3224 + // Turns out that we already have this root at a different reference.
3225 + // Use that after all.
3226 + return existingReference;
3227 + }
3228 // This is the ID we're currently emitting so we need to write it
3229 // once but if we discover it again, we refer to it by id.
3230 modelRoot = null;