@samitouri / QOS-React-2 / commits / eab523e2a9

[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