[Fiber] Avoid duplicate debug info for array children (#35733)

This commit is contained in:
Sebastian "Sebbie" Silbermann
2026-02-09 20:36:56 +01:00
committed by GitHub
parent 272441a9ad
commit eab523e2a9
3 changed files with 101 additions and 7 deletions

View File

@@ -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.<anonymous> (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(
<>
<link />
<h1>App</h1>
<div>Child</div>
</>,
);
});
// @gate enableAsyncIterableChildren && enableComponentPerformanceTrack
it('preserves debug info for server-to-server pass through of async iterables', async () => {
let resolve;

View File

@@ -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 = [<ClientComponent key="app" />];
children._debugInfo = [
{time: 12},
{
name: 'App',
env: 'Server',
key: null,
stack: ' in Object.<anonymous> (at **)',
props: {},
},
{time: 13},
];
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await actAsync(() => {
root.render([children]);
});
expect(store).toMatchInlineSnapshot(`
[root]
▾ <App> [Server]
<ClientComponent key="app">
`);
});
// @reactVersion >= 17.0
it('can reconcile Suspense in fallback positions', async () => {
let resolveFallback;

View File

@@ -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;
}