Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 60 additions & 1 deletion packages/react-client/src/__tests__/ReactFlight-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand All @@ -2835,6 +2836,9 @@ describe('ReactFlight', () => {
]
: undefined,
);
expect(getDebugInfo(fragment.props.children[0])).toEqual(
__DEV__ ? null : undefined,
);
ReactNoop.render(result);
});

Expand All @@ -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;
Expand Down
34 changes: 34 additions & 0 deletions packages/react-devtools-shared/src/__tests__/store-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
13 changes: 7 additions & 6 deletions packages/react-reconciler/src/ReactChildFiber.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -1915,41 +1916,41 @@ 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;
}

if (
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;
}

Expand Down
Loading