@samitouri / QOS-React-2 / commits / 9c9ea94852

flush only on exiting outermost act() (#15682)

Sunil Pai committed May 21, 2019 at 10:41 UTC 9c9ea948520de7e55368b2d1cb58ff39e2ff3c10
4 files changed +76 -33
packages/react-dom/src/__tests__/ReactTestUtilsAct-test.js
+31 -18
@@ -137,6 +137,26 @@ function runActTests(label, render, unmount) {
137 expect(container.innerHTML).toBe('5');
138 });
139
140 + it('should flush effects only on exiting the outermost act', () => {
141 + function App() {
142 + React.useEffect(() => {
143 + Scheduler.yieldValue(0);
144 + });
145 + return null;
146 + }
147 + // let's nest a couple of act() calls
148 + act(() => {
149 + act(() => {
150 + render(<App />, container);
151 + });
152 + // the effect wouldn't have yielded yet because
153 + // we're still inside an act() scope
154 + expect(Scheduler).toHaveYielded([]);
155 + });
156 + // but after exiting the last one, effects get flushed
157 + expect(Scheduler).toHaveYielded([0]);
158 + });
159 +
160 it('warns if a setState is called outside of act(...)', () => {
161 let setValue = null;
162 function App() {
@@ -281,7 +301,7 @@ function runActTests(label, render, unmount) {
301 });
302 });
303 describe('asynchronous tests', () => {
284 - it('can handle timers', async () => {
304 + it('works with timeouts', async () => {
305 function App() {
306 let [ctr, setCtr] = React.useState(0);
307 function doSomething() {
@@ -295,16 +315,17 @@ function runActTests(label, render, unmount) {
315 }, []);
316 return ctr;
317 }
298 - act(() => {
299 - render(<App />, container);
300 - });
318 +
319 await act(async () => {
320 + render(<App />, container);
321 + // flush a little to start the timer
322 + expect(Scheduler).toFlushAndYield([]);
323 await sleep(100);
324 });
325 expect(container.innerHTML).toBe('1');
326 });
327
307 - it('can handle async/await', async () => {
328 + it('flushes microtasks before exiting', async () => {
329 function App() {
330 let [ctr, setCtr] = React.useState(0);
331 async function someAsyncFunction() {
@@ -321,10 +342,7 @@ function runActTests(label, render, unmount) {
342 }
343
344 await act(async () => {
324 - act(() => {
325 - render(<App />, container);
326 - });
327 - // pending promises will close before this ends
345 + render(<App />, container);
346 });
347 expect(container.innerHTML).toEqual('1');
348 });
@@ -361,7 +379,7 @@ function runActTests(label, render, unmount) {
379 }
380 });
381
364 - it('commits and effects are guaranteed to be flushed', async () => {
382 + it('async commits and effects are guaranteed to be flushed', async () => {
383 function App() {
384 let [state, setState] = React.useState(0);
385 async function something() {
@@ -378,17 +396,12 @@ function runActTests(label, render, unmount) {
396 }
397
398 await act(async () => {
381 - act(() => {
382 - render(<App />, container);
383 - });
384 - expect(container.innerHTML).toBe('0');
385 - expect(Scheduler).toHaveYielded([0]);
399 + render(<App />, container);
400 });
387 - // this may seem odd, but it matches user behaviour -
388 - // a flash of "0" followed by "1"
401 + // exiting act() drains effects and microtasks
402
403 + expect(Scheduler).toHaveYielded([0, 1]);
404 expect(container.innerHTML).toBe('1');
391 - expect(Scheduler).toHaveYielded([1]);
405 });
406
407 it('propagates errors', async () => {
packages/react-dom/src/test-utils/ReactTestUtilsAct.js
+15 -5
@@ -84,16 +84,15 @@ function flushWorkAndMicroTasks(onDone: (err: ?Error) => void) {
84 let actingUpdatesScopeDepth = 0;
85
86 function act(callback: () => Thenable) {
87 - let previousActingUpdatesScopeDepth;
87 + let previousActingUpdatesScopeDepth = actingUpdatesScopeDepth;
88 + actingUpdatesScopeDepth++;
89 if (__DEV__) {
89 - previousActingUpdatesScopeDepth = actingUpdatesScopeDepth;
90 - actingUpdatesScopeDepth++;
90 ReactShouldWarnActingUpdates.current = true;
91 }
92
93 function onDone() {
94 + actingUpdatesScopeDepth--;
95 if (__DEV__) {
96 - actingUpdatesScopeDepth--;
96 if (actingUpdatesScopeDepth === 0) {
97 ReactShouldWarnActingUpdates.current = false;
98 }
@@ -143,6 +142,13 @@ function act(callback: () => Thenable) {
142 called = true;
143 result.then(
144 () => {
145 + if (actingUpdatesScopeDepth > 1) {
146 + onDone();
147 + resolve();
148 + return;
149 + }
150 + // we're about to exit the act() scope,
151 + // now's the time to flush tasks/effects
152 flushWorkAndMicroTasks((err: ?Error) => {
153 onDone();
154 if (err) {
@@ -171,7 +177,11 @@ function act(callback: () => Thenable) {
177
178 // flush effects until none remain, and cleanup
179 try {
174 - flushWork();
180 + if (actingUpdatesScopeDepth === 1) {
181 + // we're about to exit the act() scope,
182 + // now's the time to flush effects
183 + flushWork();
184 + }
185 onDone();
186 } catch (err) {
187 onDone();
packages/react-noop-renderer/src/createReactNoop.js
+15 -5
@@ -697,16 +697,15 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
697 let actingUpdatesScopeDepth = 0;
698
699 function act(callback: () => Thenable) {
700 - let previousActingUpdatesScopeDepth;
700 + let previousActingUpdatesScopeDepth = actingUpdatesScopeDepth;
701 + actingUpdatesScopeDepth++;
702 if (__DEV__) {
702 - previousActingUpdatesScopeDepth = actingUpdatesScopeDepth;
703 - actingUpdatesScopeDepth++;
703 ReactShouldWarnActingUpdates.current = true;
704 }
705
706 function onDone() {
707 + actingUpdatesScopeDepth--;
708 if (__DEV__) {
709 - actingUpdatesScopeDepth--;
709 if (actingUpdatesScopeDepth === 0) {
710 ReactShouldWarnActingUpdates.current = false;
711 }
@@ -756,6 +755,13 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
755 called = true;
756 result.then(
757 () => {
758 + if (actingUpdatesScopeDepth > 1) {
759 + onDone();
760 + resolve();
761 + return;
762 + }
763 + // we're about to exit the act() scope,
764 + // now's the time to flush tasks/effects
765 flushWorkAndMicroTasks((err: ?Error) => {
766 onDone();
767 if (err) {
@@ -784,7 +790,11 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
790
791 // flush effects until none remain, and cleanup
792 try {
787 - flushWork();
793 + if (actingUpdatesScopeDepth === 1) {
794 + // we're about to exit the act() scope,
795 + // now's the time to flush effects
796 + flushWork();
797 + }
798 onDone();
799 } catch (err) {
800 onDone();
packages/react-test-renderer/src/ReactTestRendererAct.js
+15 -5
@@ -65,16 +65,15 @@ function flushWorkAndMicroTasks(onDone: (err: ?Error) => void) {
65 let actingUpdatesScopeDepth = 0;
66
67 function act(callback: () => Thenable) {
68 - let previousActingUpdatesScopeDepth;
68 + let previousActingUpdatesScopeDepth = actingUpdatesScopeDepth;
69 + actingUpdatesScopeDepth++;
70 if (__DEV__) {
70 - previousActingUpdatesScopeDepth = actingUpdatesScopeDepth;
71 - actingUpdatesScopeDepth++;
71 ReactShouldWarnActingUpdates.current = true;
72 }
73
74 function onDone() {
75 + actingUpdatesScopeDepth--;
76 if (__DEV__) {
77 - actingUpdatesScopeDepth--;
77 if (actingUpdatesScopeDepth === 0) {
78 ReactShouldWarnActingUpdates.current = false;
79 }
@@ -124,6 +123,13 @@ function act(callback: () => Thenable) {
123 called = true;
124 result.then(
125 () => {
126 + if (actingUpdatesScopeDepth > 1) {
127 + onDone();
128 + resolve();
129 + return;
130 + }
131 + // we're about to exit the act() scope,
132 + // now's the time to flush tasks/effects
133 flushWorkAndMicroTasks((err: ?Error) => {
134 onDone();
135 if (err) {
@@ -152,7 +158,11 @@ function act(callback: () => Thenable) {
158
159 // flush effects until none remain, and cleanup
160 try {
155 - flushWork();
161 + if (actingUpdatesScopeDepth === 1) {
162 + // we're about to exit the act() scope,
163 + // now's the time to flush effects
164 + flushWork();
165 + }
166 onDone();
167 } catch (err) {
168 onDone();