diff --git a/packages/react-devtools-shared/src/__tests__/storeComponentFilters-test.js b/packages/react-devtools-shared/src/__tests__/storeComponentFilters-test.js index 1ef17e4c5475..083a9c0393bd 100644 --- a/packages/react-devtools-shared/src/__tests__/storeComponentFilters-test.js +++ b/packages/react-devtools-shared/src/__tests__/storeComponentFilters-test.js @@ -572,6 +572,78 @@ describe('Store component filters', () => { `); }); + // @reactVersion >= 19.0 + it('stays in sync when a filtered boundary suspends during a sibling restructure', async () => { + const neverResolves = new Promise(() => {}); + + function Reader() { + React.use(neverResolves); + return
read
; + } + + function A({collapsed}) { + if (collapsed) { + return ( + a-fb}> + + + ); + } + return ( + a-outer-fb}> + a-inner-fb}> + + + a-sib-fb}> +
a-sibling
+
+
+ ); + } + + function App({collapsed, extra}) { + return ( +
+ {extra ? side : null} + +
+ ); + } + + store.componentFilters = [ + utils.createElementTypeFilter(Types.ElementTypeSuspense), + ]; + + await actAsync(() => render()); + expect(store).toMatchInlineSnapshot(` + [root] + ▾ + ▾
+ ▾ +
+
+ `); + + await actAsync(() => render()); + expect(store).toMatchInlineSnapshot(` + [root] + ▾ + ▾
+ + ▾ +
+ `); + + await actAsync(() => render()); + expect(store).toMatchInlineSnapshot(` + [root] + ▾ + ▾
+ ▾ +
+ `); + }); + describe('inline errors and warnings', () => { const {render: legacyRender} = getLegacyRenderImplementation(); diff --git a/packages/react-devtools-shared/src/backend/fiber/renderer.js b/packages/react-devtools-shared/src/backend/fiber/renderer.js index b6f64cd48b97..c4f5d61f5a6d 100644 --- a/packages/react-devtools-shared/src/backend/fiber/renderer.js +++ b/packages/react-devtools-shared/src/backend/fiber/renderer.js @@ -3933,6 +3933,7 @@ export function attach( nextFirstChild, nextLastChild, prevFirstChild, + null, traceNearestHostComponentUpdate, virtualLevel + 1, ); @@ -3971,6 +3972,7 @@ export function attach( nextFirstChild: Fiber, nextLastChild: null | Fiber, // non-inclusive prevFirstChild: null | Fiber, + prevLastChild: null | Fiber, // non-inclusive traceNearestHostComponentUpdate: boolean, virtualLevel: number, // the nth level of virtual instances ): UpdateFlags { @@ -4247,7 +4249,10 @@ export function attach( } } // If we have no more children, but used to, they don't line up. - if (prevChildAtSameIndex !== null) { + if ( + prevChildAtSameIndex !== null && + prevChildAtSameIndex !== prevLastChild + ) { updateFlags |= ShouldResetChildren | ShouldResetSuspenseChildren; } return updateFlags; @@ -4266,6 +4271,7 @@ export function attach( nextFirstChild, null, prevFirstChild, + null, traceNearestHostComponentUpdate, 0, ); @@ -4284,10 +4290,14 @@ export function attach( const nextFallbackFiber = nextContentFiber.sibling; // First update only the Offscreen boundary. I.e. the main content. + // The previous set is bounded by the previous fallback fragment since the + // fallback is reconciled separately below. Without the bound, a boundary + // that stays suspended would always report its content set as changed. updateFlags |= updateVirtualChildrenRecursively( nextContentFiber, nextFallbackFiber, prevContentFiber, + prevFallbackFiber, traceNearestHostComponentUpdate, 0, ); @@ -4306,6 +4316,7 @@ export function attach( nextFallbackFiber, null, prevFallbackFiber, + null, traceNearestHostComponentUpdate, 0, );