From 95ea8ed47c8a92dd72d43349cdd2f12e6b48292a Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Tue, 19 May 2020 15:58:02 -0700 Subject: [PATCH] LegacyHidden: mode that defers without hiding (#18958) Need this to unblock www. Not sure yet how we'll support this properly long term. While adding this, I noticed that the normal "hidden" mode of LegacyHidden doesn't work properly because it doesn't toggle the visibility of newly inserted nodes. This is fine for now since we only use it via a userspace abstraction that wraps the children in an additional node. But implementing this correctly is required for us to start using it like a fragment, without the wrapper node. --- .../src/ReactFiberBeginWork.new.js | 5 +- .../src/ReactFiberCompleteWork.new.js | 5 +- .../src/ReactFiberOffscreenComponent.js | 2 +- .../src/__tests__/ReactOffscreen-test.js | 80 +++++++++++++++++++ 4 files changed, 89 insertions(+), 3 deletions(-) create mode 100644 packages/react-reconciler/src/__tests__/ReactOffscreen-test.js diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.new.js b/packages/react-reconciler/src/ReactFiberBeginWork.new.js index 76838eda06..3e4ae4d96f 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.new.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.new.js @@ -566,7 +566,10 @@ function updateOffscreenComponent( const prevState: OffscreenState | null = current !== null ? current.memoizedState : null; - if (nextProps.mode === 'hidden') { + if ( + nextProps.mode === 'hidden' || + nextProps.mode === 'unstable-defer-without-hiding' + ) { if ((workInProgress.mode & ConcurrentMode) === NoMode) { // In legacy sync mode, don't defer the subtree. Render it now. // TODO: Figure out what we should do in Blocking mode. diff --git a/packages/react-reconciler/src/ReactFiberCompleteWork.new.js b/packages/react-reconciler/src/ReactFiberCompleteWork.new.js index 8e78d36e6a..8256e3f783 100644 --- a/packages/react-reconciler/src/ReactFiberCompleteWork.new.js +++ b/packages/react-reconciler/src/ReactFiberCompleteWork.new.js @@ -1308,7 +1308,10 @@ function completeWork( const prevIsHidden = prevState !== null; const nextIsHidden = nextState !== null; - if (prevIsHidden !== nextIsHidden) { + if ( + prevIsHidden !== nextIsHidden && + newProps.mode !== 'unstable-defer-without-hiding' + ) { workInProgress.effectTag |= Update; } } diff --git a/packages/react-reconciler/src/ReactFiberOffscreenComponent.js b/packages/react-reconciler/src/ReactFiberOffscreenComponent.js index 58f2360501..9f3aabcdde 100644 --- a/packages/react-reconciler/src/ReactFiberOffscreenComponent.js +++ b/packages/react-reconciler/src/ReactFiberOffscreenComponent.js @@ -17,7 +17,7 @@ export type OffscreenProps = {| // // Default mode is visible. Kind of a weird default for a component // called "Offscreen." Possible alt: ? - mode?: 'hidden' | 'visible' | null | void, + mode?: 'hidden' | 'unstable-defer-without-hiding' | 'visible' | null | void, children?: ReactNodeList, |}; diff --git a/packages/react-reconciler/src/__tests__/ReactOffscreen-test.js b/packages/react-reconciler/src/__tests__/ReactOffscreen-test.js new file mode 100644 index 0000000000..c85f333067 --- /dev/null +++ b/packages/react-reconciler/src/__tests__/ReactOffscreen-test.js @@ -0,0 +1,80 @@ +let React; +let ReactNoop; +let Scheduler; +let LegacyHidden; + +describe('ReactOffscreen', () => { + beforeEach(() => { + jest.resetModules(); + + React = require('react'); + ReactNoop = require('react-noop-renderer'); + Scheduler = require('scheduler'); + LegacyHidden = React.unstable_LegacyHidden; + }); + + function Text(props) { + Scheduler.unstable_yieldValue(props.text); + return ; + } + + // @gate experimental + // @gate new + it('unstable-defer-without-hiding should never toggle the visibility of its children', async () => { + function App({mode}) { + return ( + <> + + + + + + ); + } + + // Test the initial mount + const root = ReactNoop.createRoot(); + await ReactNoop.act(async () => { + root.render(); + expect(Scheduler).toFlushUntilNextPaint(['Normal']); + expect(root).toMatchRenderedOutput(); + }); + expect(Scheduler).toHaveYielded(['Deferred']); + expect(root).toMatchRenderedOutput( + <> + + + , + ); + + // Now try after an update + await ReactNoop.act(async () => { + root.render(); + }); + expect(Scheduler).toHaveYielded(['Normal', 'Deferred']); + expect(root).toMatchRenderedOutput( + <> + + + , + ); + + await ReactNoop.act(async () => { + root.render(); + expect(Scheduler).toFlushUntilNextPaint(['Normal']); + expect(root).toMatchRenderedOutput( + <> + + + , + ); + }); + expect(Scheduler).toHaveYielded(['Deferred']); + expect(root).toMatchRenderedOutput( + <> + + + , + ); + }); +});