[DevTools] Handle LegacyHidden Fibers like Offscreen Fibers. (#34564)

This commit is contained in:
Sebastian "Sebbie" Silbermann 2025-09-23 20:14:53 +02:00 committed by GitHub
parent 83c88ad470
commit 012b371cde
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 45 additions and 74 deletions

View File

@ -725,14 +725,14 @@ describe('ProfilingCache', () => {
const commitData = store.profilerStore.getDataForRoot(rootID).commitData;
expect(commitData).toHaveLength(2);
const isLegacySuspense = React.version.startsWith('17');
if (isLegacySuspense) {
if (React.version.startsWith('17')) {
// React 17 will mount all children until it suspends in a LegacyHidden
// The ID gap is from the Fiber for <Async> that's in the disconnected tree.
expect(commitData[0].fiberActualDurations).toMatchInlineSnapshot(`
Map {
1 => 15,
2 => 15,
3 => 5,
4 => 3,
5 => 2,
}
`);
@ -741,7 +741,6 @@ describe('ProfilingCache', () => {
1 => 0,
2 => 10,
3 => 3,
4 => 3,
5 => 2,
}
`);

View File

@ -19,8 +19,6 @@ describe('commit tree', () => {
let Scheduler;
let store: Store;
let utils;
const isLegacySuspense =
React.version.startsWith('16') || React.version.startsWith('17');
beforeEach(() => {
utils = require('./utils');
@ -186,24 +184,13 @@ describe('commit tree', () => {
utils.act(() => store.profilerStore.startProfiling());
utils.act(() => legacyRender(<App renderChildren={true} />));
await Promise.resolve();
if (isLegacySuspense) {
expect(store).toMatchInlineSnapshot(`
[root]
<App>
<Suspense>
<Lazy>
[suspense-root] rects={null}
<Suspense name="App" rects={null}>
`);
} else {
expect(store).toMatchInlineSnapshot(`
[root]
<App>
<Suspense>
[suspense-root] rects={null}
<Suspense name="App" rects={null}>
`);
}
expect(store).toMatchInlineSnapshot(`
[root]
<App>
<Suspense>
[suspense-root] rects={null}
<Suspense name="App" rects={null}>
`);
utils.act(() => legacyRender(<App renderChildren={true} />));
expect(store).toMatchInlineSnapshot(`
[root]
@ -231,13 +218,7 @@ describe('commit tree', () => {
);
}
expect(commitTrees[0].nodes.size).toBe(
isLegacySuspense
? // <Root> + <App> + <Suspense> + <Lazy>
4
: // <Root> + <App> + <Suspense>
3,
);
expect(commitTrees[0].nodes.size).toBe(3);
expect(commitTrees[1].nodes.size).toBe(4); // <Root> + <App> + <Suspense> + <LazyInnerComponent>
expect(commitTrees[2].nodes.size).toBe(2); // <Root> + <App>
});
@ -291,24 +272,13 @@ describe('commit tree', () => {
it('should support Lazy components that are unmounted before resolving (legacy render)', async () => {
utils.act(() => store.profilerStore.startProfiling());
utils.act(() => legacyRender(<App renderChildren={true} />));
if (isLegacySuspense) {
expect(store).toMatchInlineSnapshot(`
[root]
<App>
<Suspense>
<Lazy>
[suspense-root] rects={null}
<Suspense name="App" rects={null}>
`);
} else {
expect(store).toMatchInlineSnapshot(`
[root]
<App>
<Suspense>
[suspense-root] rects={null}
<Suspense name="App" rects={null}>
`);
}
expect(store).toMatchInlineSnapshot(`
[root]
<App>
<Suspense>
[suspense-root] rects={null}
<Suspense name="App" rects={null}>
`);
utils.act(() => legacyRender(<App renderChildren={false} />));
expect(store).toMatchInlineSnapshot(`
[root]
@ -327,13 +297,7 @@ describe('commit tree', () => {
);
}
expect(commitTrees[0].nodes.size).toBe(
isLegacySuspense
? // <Root> + <App> + <Suspense> + <Lazy>
4
: // <Root> + <App> + <Suspense>
3,
);
expect(commitTrees[0].nodes.size).toBe(3);
expect(commitTrees[1].nodes.size).toBe(2); // <Root> + <App>
});

View File

@ -2828,7 +2828,7 @@ describe('Store', () => {
`);
});
// @reactVersion >= 18.0
// @reactVersion >= 17.0
it('can reconcile Suspense in fallback positions', async () => {
let resolveFallback;
const fallbackPromise = new Promise(resolve => {
@ -2907,7 +2907,7 @@ describe('Store', () => {
`);
});
// @reactVersion >= 18.0
// @reactVersion >= 17.0
it('can reconcile resuspended Suspense with Suspense in fallback positions', async () => {
let resolveHeadFallback;
let resolveHeadContent;

View File

@ -460,10 +460,10 @@ export function getInternalReactConstants(version: string): {
IncompleteFunctionComponent: 28,
IndeterminateComponent: 2, // removed in 19.0.0
LazyComponent: 16,
LegacyHiddenComponent: 23,
LegacyHiddenComponent: 23, // Does not exist in 18+ OSS but exists in fb builds
MemoComponent: 14,
Mode: 8,
OffscreenComponent: 22, // Experimental
OffscreenComponent: 22, // Experimental in 17. Stable in 18+
Profiler: 12,
ScopeComponent: 21, // Experimental
SimpleMemoComponent: 15,
@ -3057,13 +3057,23 @@ export function attach(
}
}
function isHiddenOffscreen(fiber: Fiber): boolean {
switch (fiber.tag) {
case LegacyHiddenComponent:
// fallthrough since all published implementations currently implement the same state as Offscreen.
case OffscreenComponent:
return fiber.memoizedState !== null;
default:
return false;
}
}
function unmountRemainingChildren() {
if (
reconcilingParent !== null &&
(reconcilingParent.kind === FIBER_INSTANCE ||
reconcilingParent.kind === FILTERED_FIBER_INSTANCE) &&
reconcilingParent.data.tag === OffscreenComponent &&
reconcilingParent.data.memoizedState !== null &&
isHiddenOffscreen(reconcilingParent.data) &&
!isInDisconnectedSubtree
) {
// This is a hidden offscreen, we need to execute this in the context of a disconnected subtree.
@ -3170,8 +3180,7 @@ export function attach(
if (
(parent.kind === FIBER_INSTANCE ||
parent.kind === FILTERED_FIBER_INSTANCE) &&
parent.data.tag === OffscreenComponent &&
parent.data.memoizedState !== null
isHiddenOffscreen(parent.data)
) {
// We're inside a hidden offscreen Fiber. We're in a disconnected tree.
return;
@ -3819,7 +3828,9 @@ export function attach(
(reconcilingParent !== null &&
reconcilingParent.kind === VIRTUAL_INSTANCE) ||
fiber.tag === SuspenseComponent ||
fiber.tag === OffscreenComponent // Use to keep resuspended instances alive inside a SuspenseComponent.
// Use to keep resuspended instances alive inside a SuspenseComponent.
fiber.tag === OffscreenComponent ||
fiber.tag === LegacyHiddenComponent
) {
// If the parent is a Virtual Instance and we filtered this Fiber we include a
// hidden node. We also include this if it's a Suspense boundary so we can track those
@ -3939,7 +3950,7 @@ export function attach(
trackDebugInfoFromHostComponent(nearestInstance, fiber);
}
if (fiber.tag === OffscreenComponent && fiber.memoizedState !== null) {
if (isHiddenOffscreen(fiber)) {
// If an Offscreen component is hidden, mount its children as disconnected.
const stashedDisconnected = isInDisconnectedSubtree;
isInDisconnectedSubtree = true;
@ -4261,7 +4272,7 @@ export function attach(
while (child !== null) {
if (child.kind === FILTERED_FIBER_INSTANCE) {
const fiber = child.data;
if (fiber.tag === OffscreenComponent && fiber.memoizedState !== null) {
if (isHiddenOffscreen(fiber)) {
// The children of this Offscreen are hidden so they don't get added.
} else {
addUnfilteredChildrenIDs(child, nextChildren);
@ -4888,9 +4899,8 @@ export function attach(
const nextDidTimeOut =
isLegacySuspense && nextFiber.memoizedState !== null;
const isOffscreen = nextFiber.tag === OffscreenComponent;
const prevWasHidden = isOffscreen && prevFiber.memoizedState !== null;
const nextIsHidden = isOffscreen && nextFiber.memoizedState !== null;
const prevWasHidden = isHiddenOffscreen(prevFiber);
const nextIsHidden = isHiddenOffscreen(nextFiber);
if (isLegacySuspense) {
if (
@ -5245,8 +5255,7 @@ export function attach(
if (
(child.kind === FIBER_INSTANCE ||
child.kind === FILTERED_FIBER_INSTANCE) &&
child.data.tag === OffscreenComponent &&
child.data.memoizedState !== null
isHiddenOffscreen(child.data)
) {
// This instance's children are already disconnected.
} else {
@ -5275,8 +5284,7 @@ export function attach(
if (
(child.kind === FIBER_INSTANCE ||
child.kind === FILTERED_FIBER_INSTANCE) &&
child.data.tag === OffscreenComponent &&
child.data.memoizedState !== null
isHiddenOffscreen(child.data)
) {
// This instance's children should remain disconnected.
} else {