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();