@samitouri / QOS-React-2 / commits / 95ea8ed47c

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.

Andrew Clark committed May 19, 2020 at 15:58 UTC 95ea8ed47c8a92dd72d43349cdd2f12e6b48292a
4 files changed +89 -3
packages/react-reconciler/src/ReactFiberBeginWork.new.js
+4 -1
@@ -566,7 +566,10 @@ function updateOffscreenComponent(
566 const prevState: OffscreenState | null =
567 current !== null ? current.memoizedState : null;
568
569 - if (nextProps.mode === 'hidden') {
569 + if (
570 + nextProps.mode === 'hidden' ||
571 + nextProps.mode === 'unstable-defer-without-hiding'
572 + ) {
573 if ((workInProgress.mode & ConcurrentMode) === NoMode) {
574 // In legacy sync mode, don't defer the subtree. Render it now.
575 // TODO: Figure out what we should do in Blocking mode.
packages/react-reconciler/src/ReactFiberCompleteWork.new.js
+4 -1
@@ -1308,7 +1308,10 @@ function completeWork(
1308
1309 const prevIsHidden = prevState !== null;
1310 const nextIsHidden = nextState !== null;
1311 - if (prevIsHidden !== nextIsHidden) {
1311 + if (
1312 + prevIsHidden !== nextIsHidden &&
1313 + newProps.mode !== 'unstable-defer-without-hiding'
1314 + ) {
1315 workInProgress.effectTag |= Update;
1316 }
1317 }
packages/react-reconciler/src/ReactFiberOffscreenComponent.js
+1 -1
@@ -17,7 +17,7 @@ export type OffscreenProps = {|
17 //
18 // Default mode is visible. Kind of a weird default for a component
19 // called "Offscreen." Possible alt: <Visibility />?
20 - mode?: 'hidden' | 'visible' | null | void,
20 + mode?: 'hidden' | 'unstable-defer-without-hiding' | 'visible' | null | void,
21 children?: ReactNodeList,
22 |};
23
packages/react-reconciler/src/__tests__/ReactOffscreen-test.js new
+80
@@ -0,0 +1,80 @@
1 +let React;
2 +let ReactNoop;
3 +let Scheduler;
4 +let LegacyHidden;
5 +
6 +describe('ReactOffscreen', () => {
7 + beforeEach(() => {
8 + jest.resetModules();
9 +
10 + React = require('react');
11 + ReactNoop = require('react-noop-renderer');
12 + Scheduler = require('scheduler');
13 + LegacyHidden = React.unstable_LegacyHidden;
14 + });
15 +
16 + function Text(props) {
17 + Scheduler.unstable_yieldValue(props.text);
18 + return <span prop={props.text} />;
19 + }
20 +
21 + // @gate experimental
22 + // @gate new
23 + it('unstable-defer-without-hiding should never toggle the visibility of its children', async () => {
24 + function App({mode}) {
25 + return (
26 + <>
27 + <Text text="Normal" />
28 + <LegacyHidden mode={mode}>
29 + <Text text="Deferred" />
30 + </LegacyHidden>
31 + </>
32 + );
33 + }
34 +
35 + // Test the initial mount
36 + const root = ReactNoop.createRoot();
37 + await ReactNoop.act(async () => {
38 + root.render(<App mode="unstable-defer-without-hiding" />);
39 + expect(Scheduler).toFlushUntilNextPaint(['Normal']);
40 + expect(root).toMatchRenderedOutput(<span prop="Normal" />);
41 + });
42 + expect(Scheduler).toHaveYielded(['Deferred']);
43 + expect(root).toMatchRenderedOutput(
44 + <>
45 + <span prop="Normal" />
46 + <span prop="Deferred" />
47 + </>,
48 + );
49 +
50 + // Now try after an update
51 + await ReactNoop.act(async () => {
52 + root.render(<App mode="visible" />);
53 + });
54 + expect(Scheduler).toHaveYielded(['Normal', 'Deferred']);
55 + expect(root).toMatchRenderedOutput(
56 + <>
57 + <span prop="Normal" />
58 + <span prop="Deferred" />
59 + </>,
60 + );
61 +
62 + await ReactNoop.act(async () => {
63 + root.render(<App mode="unstable-defer-without-hiding" />);
64 + expect(Scheduler).toFlushUntilNextPaint(['Normal']);
65 + expect(root).toMatchRenderedOutput(
66 + <>
67 + <span prop="Normal" />
68 + <span prop="Deferred" />
69 + </>,
70 + );
71 + });
72 + expect(Scheduler).toHaveYielded(['Deferred']);
73 + expect(root).toMatchRenderedOutput(
74 + <>
75 + <span prop="Normal" />
76 + <span prop="Deferred" />
77 + </>,
78 + );
79 + });
80 +});