[Fiber] Avoid duplicate debug info for array children (#35733)
Sebastian "Sebbie" Silbermann committed
Feb 9, 2026 at 20:36 UTC
eab523e2a99583703b13536670dfdd8a3b1e26e0
3 files changed
+101
-7
packages/react-client/src/__tests__/ReactFlight-test.js
+60
-1
@@ -2820,7 +2820,8 @@ describe('ReactFlight', () => {
2820
]
2821
: undefined,
2822
);
2823
- expect(getDebugInfo(thirdPartyChildren[2])).toEqual(
2823
+ const fragment = thirdPartyChildren[2];
2824
+ expect(getDebugInfo(fragment)).toEqual(
2825
__DEV__
2826
? [
2827
{time: gate(flags => flags.enableAsyncDebugInfo) ? 54 : 22},
@@ -2835,6 +2836,9 @@ describe('ReactFlight', () => {
2836
]
2837
: undefined,
2838
);
2839
+ expect(getDebugInfo(fragment.props.children[0])).toEqual(
2840
+ __DEV__ ? null : undefined,
2841
+ );
2842
ReactNoop.render(result);
2843
});
2844
@@ -2847,6 +2851,61 @@ describe('ReactFlight', () => {
2851
);
2852
});
2853
2854
+ it('preserves debug info for keyed Fragment', async () => {
2855
+ function App() {
2856
+ return ReactServer.createElement(
2857
+ ReactServer.Fragment,
2858
+ {key: 'app'},
2859
+ ReactServer.createElement('h1', null, 'App'),
2860
+ ReactServer.createElement('div', null, 'Child'),
2861
+ );
2862
+ }
2863
+
2864
+ const transport = ReactNoopFlightServer.render(
2865
+ ReactServer.createElement(
2866
+ ReactServer.Fragment,
2867
+ null,
2868
+ ReactServer.createElement('link', {key: 'styles'}),
2869
+ ReactServer.createElement(App, null),
2870
+ ),
2871
+ );
2872
+
2873
+ await act(async () => {
2874
+ const root = await ReactNoopFlightClient.read(transport);
2875
+
2876
+ const fragment = root[1];
2877
+ expect(getDebugInfo(fragment)).toEqual(
2878
+ __DEV__
2879
+ ? [
2880
+ {time: 12},
2881
+ {
2882
+ name: 'App',
2883
+ env: 'Server',
2884
+ key: null,
2885
+ stack: ' in Object.<anonymous> (at **)',
2886
+ props: {},
2887
+ },
2888
+ {time: 13},
2889
+ ]
2890
+ : undefined,
2891
+ );
2892
+ // Making sure debug info doesn't get added multiple times on Fragment children
2893
+ expect(getDebugInfo(fragment[0])).toEqual(__DEV__ ? null : undefined);
2894
+ const fragmentChild = fragment[0].props.children[0];
2895
+ expect(getDebugInfo(fragmentChild)).toEqual(__DEV__ ? null : undefined);
2896
+
2897
+ ReactNoop.render(root);
2898
+ });
2899
+
2900
+ expect(ReactNoop).toMatchRenderedOutput(
2901
+ <>
2902
+ <link />
2903
+ <h1>App</h1>
2904
+ <div>Child</div>
2905
+ </>,
2906
+ );
2907
+ });
2908
+
2909
// @gate enableAsyncIterableChildren && enableComponentPerformanceTrack
2910
it('preserves debug info for server-to-server pass through of async iterables', async () => {
2911
let resolve;
packages/react-devtools-shared/src/__tests__/store-test.js
+34
@@ -2827,6 +2827,40 @@ describe('Store', () => {
2827
`);
2828
});
2829
2830
+ // @reactVersion >= 19.0
2831
+ it('does not duplicate Server Component parents in keyed Fragments', async () => {
2832
+ // TODO: Use an actual Flight renderer.
2833
+ // See ReactFlight-test for the produced JSX from Flight.
2834
+ function ClientComponent() {
2835
+ return null;
2836
+ }
2837
+ // This used to be a keyed Fragment on the Server.
2838
+ const children = [<ClientComponent key="app" />];
2839
+ children._debugInfo = [
2840
+ {time: 12},
2841
+ {
2842
+ name: 'App',
2843
+ env: 'Server',
2844
+ key: null,
2845
+ stack: ' in Object.<anonymous> (at **)',
2846
+ props: {},
2847
+ },
2848
+ {time: 13},
2849
+ ];
2850
+
2851
+ const container = document.createElement('div');
2852
+ const root = ReactDOMClient.createRoot(container);
2853
+ await actAsync(() => {
2854
+ root.render([children]);
2855
+ });
2856
+
2857
+ expect(store).toMatchInlineSnapshot(`
2858
+ [root]
2859
+ ▾ <App> [Server]
2860
+ <ClientComponent key="app">
2861
+ `);
2862
+ });
2863
+
2864
// @reactVersion >= 17.0
2865
it('can reconcile Suspense in fallback positions', async () => {
2866
let resolveFallback;
packages/react-reconciler/src/ReactChildFiber.js
+7
-6
@@ -789,6 +789,7 @@ function createChildReconciler(
789
// We treat the parent as the owner for stack purposes.
790
created._debugOwner = returnFiber;
791
created._debugTask = returnFiber._debugTask;
792
+ // Make sure to not push again when handling the Fragment child.
793
const prevDebugInfo = pushDebugInfo(newChild._debugInfo);
794
created._debugInfo = currentDebugInfo;
795
currentDebugInfo = prevDebugInfo;
@@ -1915,26 +1916,26 @@ function createChildReconciler(
1916
}
1917
1918
if (isArray(newChild)) {
1918
- const prevDebugInfo = pushDebugInfo(newChild._debugInfo);
1919
+ // We created a Fragment for this child with the debug info.
1920
+ // No need to push again.
1921
const firstChild = reconcileChildrenArray(
1922
returnFiber,
1923
currentFirstChild,
1924
newChild,
1925
lanes,
1926
);
1925
- currentDebugInfo = prevDebugInfo;
1927
return firstChild;
1928
}
1929
1930
if (getIteratorFn(newChild)) {
1930
- const prevDebugInfo = pushDebugInfo(newChild._debugInfo);
1931
+ // We created a Fragment for this child with the debug info.
1932
+ // No need to push again.
1933
const firstChild = reconcileChildrenIteratable(
1934
returnFiber,
1935
currentFirstChild,
1936
newChild,
1937
lanes,
1938
);
1937
- currentDebugInfo = prevDebugInfo;
1939
return firstChild;
1940
}
1941
@@ -1942,14 +1943,14 @@ function createChildReconciler(
1943
enableAsyncIterableChildren &&
1944
typeof newChild[ASYNC_ITERATOR] === 'function'
1945
) {
1945
- const prevDebugInfo = pushDebugInfo(newChild._debugInfo);
1946
+ // We created a Fragment for this child with the debug info.
1947
+ // No need to push again.
1948
const firstChild = reconcileChildrenAsyncIteratable(
1949
returnFiber,
1950
currentFirstChild,
1951
newChild,
1952
lanes,
1953
);
1952
- currentDebugInfo = prevDebugInfo;
1954
return firstChild;
1955
}
1956