From d1e35c70398a3341d2e090d09a0863c7fe6c3325 Mon Sep 17 00:00:00 2001 From: Samuel Susla Date: Thu, 10 Nov 2022 14:49:30 +0000 Subject: [PATCH] Don't disappear layout effects unnecessarily (#25660) Nested Offscreens can run into a case where outer Offscreen is revealed while inner one is hidden in a single commit. This is an edge case that was previously missed. We need to prevent call to disappear layout effects. When we go from state: ```jsx // outer offscreen // inner offscreen {children} ``` To following. Notice that visibility of each offscreen flips. ```jsx // outer offscreen // inner offscreen {children} ``` Inner offscreen must not call `recursivelyTraverseDisappearLayoutEffects`. Check unit tests for an example of this. --- .../src/ReactFiberCommitWork.new.js | 7 +- .../src/ReactFiberCommitWork.old.js | 7 +- .../src/__tests__/ReactOffscreen-test.js | 92 ++++++++++++++----- 3 files changed, 75 insertions(+), 31 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.new.js b/packages/react-reconciler/src/ReactFiberCommitWork.new.js index 0b885bf13488b..1a9b72c8aeb0b 100644 --- a/packages/react-reconciler/src/ReactFiberCommitWork.new.js +++ b/packages/react-reconciler/src/ReactFiberCommitWork.new.js @@ -2880,12 +2880,13 @@ function commitMutationEffectsOnFiber( if (isHidden) { const isUpdate = current !== null; - const isAncestorOffscreenHidden = offscreenSubtreeIsHidden; + const wasHiddenByAncestorOffscreen = + offscreenSubtreeIsHidden || offscreenSubtreeWasHidden; // Only trigger disapper layout effects if: // - This is an update, not first mount. // - This Offscreen was not hidden before. - // - No ancestor Offscreen is hidden. - if (isUpdate && !wasHidden && !isAncestorOffscreenHidden) { + // - Ancestor Offscreen was not hidden in previous commit. + if (isUpdate && !wasHidden && !wasHiddenByAncestorOffscreen) { if ((finishedWork.mode & ConcurrentMode) !== NoMode) { // Disappear the layout effects of all the children recursivelyTraverseDisappearLayoutEffects(finishedWork); diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.old.js b/packages/react-reconciler/src/ReactFiberCommitWork.old.js index cbbca982127f0..a873d23c3d729 100644 --- a/packages/react-reconciler/src/ReactFiberCommitWork.old.js +++ b/packages/react-reconciler/src/ReactFiberCommitWork.old.js @@ -2880,12 +2880,13 @@ function commitMutationEffectsOnFiber( if (isHidden) { const isUpdate = current !== null; - const isAncestorOffscreenHidden = offscreenSubtreeIsHidden; + const wasHiddenByAncestorOffscreen = + offscreenSubtreeIsHidden || offscreenSubtreeWasHidden; // Only trigger disapper layout effects if: // - This is an update, not first mount. // - This Offscreen was not hidden before. - // - No ancestor Offscreen is hidden. - if (isUpdate && !wasHidden && !isAncestorOffscreenHidden) { + // - Ancestor Offscreen was not hidden in previous commit. + if (isUpdate && !wasHidden && !wasHiddenByAncestorOffscreen) { if ((finishedWork.mode & ConcurrentMode) !== NoMode) { // Disappear the layout effects of all the children recursivelyTraverseDisappearLayoutEffects(finishedWork); diff --git a/packages/react-reconciler/src/__tests__/ReactOffscreen-test.js b/packages/react-reconciler/src/__tests__/ReactOffscreen-test.js index 417f851ad45b7..d830265610823 100644 --- a/packages/react-reconciler/src/__tests__/ReactOffscreen-test.js +++ b/packages/react-reconciler/src/__tests__/ReactOffscreen-test.js @@ -275,7 +275,7 @@ describe('ReactOffscreen', () => { // goes from visible to hidden in synchronous update. class ClassComponent extends React.Component { render() { - return ; + return ; } componentWillUnmount() { @@ -287,47 +287,89 @@ describe('ReactOffscreen', () => { } } - let _setIsVisible; - let isFirstRender = true; + const root = ReactNoop.createRoot(); + await act(async () => { + // Outer and inner offscreen are hidden. + root.render( + + + + + , + ); + }); - function App() { - const [isVisible, setIsVisible] = React.useState(true); - _setIsVisible = setIsVisible; - if (isFirstRender === true) { - setIsVisible(false); - isFirstRender = false; - } + expect(Scheduler).toHaveYielded(['child']); + expect(root).toMatchRenderedOutput(