@samitouri / QOS-React-2 / commits / 8b3b5c3524

Bugfix: Missing mode check in resetChildLanes (#18961)

Deferring a hidden tree is only supported in Concurrent Mode. The missing check leads to an infinite loop when an update is scheduled inside a hidden tree, because the pending work never gets reset. This "accidentally" worked in the old reconciler because the heurstic we used to detect offscreen trees was if `childExpirationTime` was `Never`. In the new reconciler, we check the tag instead. Which means we also need to check the mode, like we do in the begin phase. We should move this check out of the hot path. It shouldn't have been in the hot path of the old reconciler, either. Probably by moving `resetChildLanes` into the switch statement in ReactFiberCompleteWork.

Andrew Clark committed May 19, 2020 at 23:10 UTC 8b3b5c35245b3aa0b2802b51856fe9dad5bcb671
2 files changed +90 -1
packages/react-reconciler/src/ReactFiberWorkLoop.new.js
+2 -1
@@ -1684,7 +1684,8 @@ function resetChildLanes(completedWork: Fiber) {
1684 (completedWork.tag === LegacyHiddenComponent ||
1685 completedWork.tag === OffscreenComponent) &&
1686 completedWork.memoizedState !== null &&
1687 - !includesSomeLane(subtreeRenderLanes, (OffscreenLane: Lane))
1687 + !includesSomeLane(subtreeRenderLanes, (OffscreenLane: Lane)) &&
1688 + (completedWork.mode & ConcurrentMode) !== NoLanes
1689 ) {
1690 // The children of this component are hidden. Don't bubble their
1691 // expiration times.
packages/react-reconciler/src/__tests__/ReactOffscreen-test.js
+88
@@ -2,6 +2,7 @@ let React;
2 let ReactNoop;
3 let Scheduler;
4 let LegacyHidden;
5 +let useState;
6
7 describe('ReactOffscreen', () => {
8 beforeEach(() => {
@@ -11,6 +12,7 @@ describe('ReactOffscreen', () => {
12 ReactNoop = require('react-noop-renderer');
13 Scheduler = require('scheduler');
14 LegacyHidden = React.unstable_LegacyHidden;
15 + useState = React.useState;
16 });
17
18 function Text(props) {
@@ -77,4 +79,90 @@ describe('ReactOffscreen', () => {
79 </>,
80 );
81 });
82 +
83 + // @gate experimental
84 + // @gate new
85 + it('does not defer in legacy mode', async () => {
86 + let setState;
87 + function Foo() {
88 + const [state, _setState] = useState('A');
89 + setState = _setState;
90 + return <Text text={state} />;
91 + }
92 +
93 + const root = ReactNoop.createLegacyRoot();
94 + await ReactNoop.act(async () => {
95 + root.render(
96 + <>
97 + <LegacyHidden mode="hidden">
98 + <Foo />
99 + </LegacyHidden>
100 + <Text text="Outside" />
101 + </>,
102 + );
103 + // Should not defer the hidden tree
104 + expect(Scheduler).toFlushUntilNextPaint(['A', 'Outside']);
105 + });
106 + expect(root).toMatchRenderedOutput(
107 + <>
108 + <span prop="A" />
109 + <span prop="Outside" />
110 + </>,
111 + );
112 +
113 + // Test that the children can be updated
114 + await ReactNoop.act(async () => {
115 + setState('B');
116 + });
117 + expect(Scheduler).toHaveYielded(['B']);
118 + expect(root).toMatchRenderedOutput(
119 + <>
120 + <span prop="B" />
121 + <span prop="Outside" />
122 + </>,
123 + );
124 + });
125 +
126 + // @gate experimental
127 + // @gate new
128 + it('does not defer in blocking mode', async () => {
129 + let setState;
130 + function Foo() {
131 + const [state, _setState] = useState('A');
132 + setState = _setState;
133 + return <Text text={state} />;
134 + }
135 +
136 + const root = ReactNoop.createBlockingRoot();
137 + await ReactNoop.act(async () => {
138 + root.render(
139 + <>
140 + <LegacyHidden mode="hidden">
141 + <Foo />
142 + </LegacyHidden>
143 + <Text text="Outside" />
144 + </>,
145 + );
146 + // Should not defer the hidden tree
147 + expect(Scheduler).toFlushUntilNextPaint(['A', 'Outside']);
148 + });
149 + expect(root).toMatchRenderedOutput(
150 + <>
151 + <span prop="A" />
152 + <span prop="Outside" />
153 + </>,
154 + );
155 +
156 + // Test that the children can be updated
157 + await ReactNoop.act(async () => {
158 + setState('B');
159 + });
160 + expect(Scheduler).toHaveYielded(['B']);
161 + expect(root).toMatchRenderedOutput(
162 + <>
163 + <span prop="B" />
164 + <span prop="Outside" />
165 + </>,
166 + );
167 + });
168 });