diff --git a/packages/react-client/src/__tests__/ReactFlight-test.js b/packages/react-client/src/__tests__/ReactFlight-test.js index 0c482f72cdc5..2661503b1333 100644 --- a/packages/react-client/src/__tests__/ReactFlight-test.js +++ b/packages/react-client/src/__tests__/ReactFlight-test.js @@ -2820,7 +2820,8 @@ describe('ReactFlight', () => { ] : undefined, ); - expect(getDebugInfo(thirdPartyChildren[2])).toEqual( + const fragment = thirdPartyChildren[2]; + expect(getDebugInfo(fragment)).toEqual( __DEV__ ? [ {time: gate(flags => flags.enableAsyncDebugInfo) ? 54 : 22}, @@ -2835,6 +2836,9 @@ describe('ReactFlight', () => { ] : undefined, ); + expect(getDebugInfo(fragment.props.children[0])).toEqual( + __DEV__ ? null : undefined, + ); ReactNoop.render(result); }); @@ -2847,6 +2851,61 @@ describe('ReactFlight', () => { ); }); + it('preserves debug info for keyed Fragment', async () => { + function App() { + return ReactServer.createElement( + ReactServer.Fragment, + {key: 'app'}, + ReactServer.createElement('h1', null, 'App'), + ReactServer.createElement('div', null, 'Child'), + ); + } + + const transport = ReactNoopFlightServer.render( + ReactServer.createElement( + ReactServer.Fragment, + null, + ReactServer.createElement('link', {key: 'styles'}), + ReactServer.createElement(App, null), + ), + ); + + await act(async () => { + const root = await ReactNoopFlightClient.read(transport); + + const fragment = root[1]; + expect(getDebugInfo(fragment)).toEqual( + __DEV__ + ? [ + {time: 12}, + { + name: 'App', + env: 'Server', + key: null, + stack: ' in Object. (at **)', + props: {}, + }, + {time: 13}, + ] + : undefined, + ); + // Making sure debug info doesn't get added multiple times on Fragment children + expect(getDebugInfo(fragment[0])).toEqual(__DEV__ ? null : undefined); + const fragmentChild = fragment[0].props.children[0]; + expect(getDebugInfo(fragmentChild)).toEqual(__DEV__ ? null : undefined); + + ReactNoop.render(root); + }); + + expect(ReactNoop).toMatchRenderedOutput( + <> + +

App

+
Child
+ , + ); + }); + // @gate enableAsyncIterableChildren && enableComponentPerformanceTrack it('preserves debug info for server-to-server pass through of async iterables', async () => { let resolve; diff --git a/packages/react-devtools-shared/src/__tests__/store-test.js b/packages/react-devtools-shared/src/__tests__/store-test.js index 8c9beb185443..5e39a0fe68f9 100644 --- a/packages/react-devtools-shared/src/__tests__/store-test.js +++ b/packages/react-devtools-shared/src/__tests__/store-test.js @@ -2827,6 +2827,40 @@ describe('Store', () => { `); }); + // @reactVersion >= 19.0 + it('does not duplicate Server Component parents in keyed Fragments', async () => { + // TODO: Use an actual Flight renderer. + // See ReactFlight-test for the produced JSX from Flight. + function ClientComponent() { + return null; + } + // This used to be a keyed Fragment on the Server. + const children = []; + children._debugInfo = [ + {time: 12}, + { + name: 'App', + env: 'Server', + key: null, + stack: ' in Object. (at **)', + props: {}, + }, + {time: 13}, + ]; + + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await actAsync(() => { + root.render([children]); + }); + + expect(store).toMatchInlineSnapshot(` + [root] + ▾ [Server] + + `); + }); + // @reactVersion >= 17.0 it('can reconcile Suspense in fallback positions', async () => { let resolveFallback; diff --git a/packages/react-reconciler/src/ReactChildFiber.js b/packages/react-reconciler/src/ReactChildFiber.js index 2a726447263c..42f1b70918d3 100644 --- a/packages/react-reconciler/src/ReactChildFiber.js +++ b/packages/react-reconciler/src/ReactChildFiber.js @@ -789,6 +789,7 @@ function createChildReconciler( // We treat the parent as the owner for stack purposes. created._debugOwner = returnFiber; created._debugTask = returnFiber._debugTask; + // Make sure to not push again when handling the Fragment child. const prevDebugInfo = pushDebugInfo(newChild._debugInfo); created._debugInfo = currentDebugInfo; currentDebugInfo = prevDebugInfo; @@ -1915,26 +1916,26 @@ function createChildReconciler( } if (isArray(newChild)) { - const prevDebugInfo = pushDebugInfo(newChild._debugInfo); + // We created a Fragment for this child with the debug info. + // No need to push again. const firstChild = reconcileChildrenArray( returnFiber, currentFirstChild, newChild, lanes, ); - currentDebugInfo = prevDebugInfo; return firstChild; } if (getIteratorFn(newChild)) { - const prevDebugInfo = pushDebugInfo(newChild._debugInfo); + // We created a Fragment for this child with the debug info. + // No need to push again. const firstChild = reconcileChildrenIteratable( returnFiber, currentFirstChild, newChild, lanes, ); - currentDebugInfo = prevDebugInfo; return firstChild; } @@ -1942,14 +1943,14 @@ function createChildReconciler( enableAsyncIterableChildren && typeof newChild[ASYNC_ITERATOR] === 'function' ) { - const prevDebugInfo = pushDebugInfo(newChild._debugInfo); + // We created a Fragment for this child with the debug info. + // No need to push again. const firstChild = reconcileChildrenAsyncIteratable( returnFiber, currentFirstChild, newChild, lanes, ); - currentDebugInfo = prevDebugInfo; return firstChild; }