[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;