diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.js b/packages/react-reconciler/src/ReactFiberCommitWork.js index c98edc7a0bc57..825b814db5103 100644 --- a/packages/react-reconciler/src/ReactFiberCommitWork.js +++ b/packages/react-reconciler/src/ReactFiberCommitWork.js @@ -292,6 +292,9 @@ import type {Flags} from './ReactFiberFlags'; // Allows us to avoid traversing the return path to find the nearest Offscreen ancestor. let offscreenSubtreeIsHidden: boolean = false; let offscreenSubtreeWasHidden: boolean = false; +// Track whether there's a hidden offscreen above with no HostComponent between. If so, +// it overrides the hiddenness of the HostComponent below. +let offscreenDirectParentIsHidden: boolean = false; // Used to track if a form needs to be reset at the end of the mutation phase. let needsFormReset = false; @@ -2141,8 +2144,14 @@ function commitMutationEffectsOnFiber( // Fall through } case HostComponent: { + // We've hit a host component, so it's no longer a direct parent. + const prevOffscreenDirectParentIsHidden = offscreenDirectParentIsHidden; + offscreenDirectParentIsHidden = false; + recursivelyTraverseMutationEffects(root, finishedWork, lanes); + offscreenDirectParentIsHidden = prevOffscreenDirectParentIsHidden; + commitReconciliationEffects(finishedWork, lanes); if (flags & Ref) { @@ -2422,10 +2431,14 @@ function commitMutationEffectsOnFiber( // effects again. const prevOffscreenSubtreeIsHidden = offscreenSubtreeIsHidden; const prevOffscreenSubtreeWasHidden = offscreenSubtreeWasHidden; + const prevOffscreenDirectParentIsHidden = offscreenDirectParentIsHidden; offscreenSubtreeIsHidden = prevOffscreenSubtreeIsHidden || isHidden; + offscreenDirectParentIsHidden = + prevOffscreenDirectParentIsHidden || isHidden; offscreenSubtreeWasHidden = prevOffscreenSubtreeWasHidden || wasHidden; recursivelyTraverseMutationEffects(root, finishedWork, lanes); offscreenSubtreeWasHidden = prevOffscreenSubtreeWasHidden; + offscreenDirectParentIsHidden = prevOffscreenDirectParentIsHidden; offscreenSubtreeIsHidden = prevOffscreenSubtreeIsHidden; if ( @@ -2504,9 +2517,10 @@ function commitMutationEffectsOnFiber( } if (supportsMutation) { - // TODO: This needs to run whenever there's an insertion or update - // inside a hidden Offscreen tree. - hideOrUnhideAllChildren(finishedWork, isHidden); + // If it's trying to unhide but the parent is still hidden, then we should not unhide. + if (isHidden || !offscreenDirectParentIsHidden) { + hideOrUnhideAllChildren(finishedWork, isHidden); + } } } diff --git a/packages/react-reconciler/src/__tests__/Activity-test.js b/packages/react-reconciler/src/__tests__/Activity-test.js index 01311217e2444..2fdcc16701dbc 100644 --- a/packages/react-reconciler/src/__tests__/Activity-test.js +++ b/packages/react-reconciler/src/__tests__/Activity-test.js @@ -14,6 +14,7 @@ let waitForPaint; let waitFor; let assertLog; let assertConsoleErrorDev; +let Suspense; describe('Activity', () => { beforeEach(() => { @@ -25,6 +26,7 @@ describe('Activity', () => { act = require('internal-test-utils').act; LegacyHidden = React.unstable_LegacyHidden; Activity = React.Activity; + Suspense = React.Suspense; useState = React.useState; useInsertionEffect = React.useInsertionEffect; useLayoutEffect = React.useLayoutEffect; @@ -1424,6 +1426,72 @@ describe('Activity', () => { ); }); + // @gate enableActivity + it('reveal an inner Activity boundary without revealing an outer one on the same host child', async () => { + // This ensures that no update is scheduled, which would cover up the bug if the parent + // then re-hides the child on the way up. + const memoizedElement =
; + function App({showOuter, showInner}) { + return ( +