diff --git a/packages/react-dom/src/__tests__/ReactTestUtilsAct-test.js b/packages/react-dom/src/__tests__/ReactTestUtilsAct-test.js
index fd32092584d06..838936a49949d 100644
--- a/packages/react-dom/src/__tests__/ReactTestUtilsAct-test.js
+++ b/packages/react-dom/src/__tests__/ReactTestUtilsAct-test.js
@@ -137,6 +137,26 @@ function runActTests(label, render, unmount) {
expect(container.innerHTML).toBe('5');
});
+ it('should flush effects only on exiting the outermost act', () => {
+ function App() {
+ React.useEffect(() => {
+ Scheduler.yieldValue(0);
+ });
+ return null;
+ }
+ // let's nest a couple of act() calls
+ act(() => {
+ act(() => {
+ render(, container);
+ });
+ // the effect wouldn't have yielded yet because
+ // we're still inside an act() scope
+ expect(Scheduler).toHaveYielded([]);
+ });
+ // but after exiting the last one, effects get flushed
+ expect(Scheduler).toHaveYielded([0]);
+ });
+
it('warns if a setState is called outside of act(...)', () => {
let setValue = null;
function App() {
@@ -281,7 +301,7 @@ function runActTests(label, render, unmount) {
});
});
describe('asynchronous tests', () => {
- it('can handle timers', async () => {
+ it('works with timeouts', async () => {
function App() {
let [ctr, setCtr] = React.useState(0);
function doSomething() {
@@ -295,16 +315,17 @@ function runActTests(label, render, unmount) {
}, []);
return ctr;
}
- act(() => {
- render(, container);
- });
+
await act(async () => {
+ render(, container);
+ // flush a little to start the timer
+ expect(Scheduler).toFlushAndYield([]);
await sleep(100);
});
expect(container.innerHTML).toBe('1');
});
- it('can handle async/await', async () => {
+ it('flushes microtasks before exiting', async () => {
function App() {
let [ctr, setCtr] = React.useState(0);
async function someAsyncFunction() {
@@ -321,10 +342,7 @@ function runActTests(label, render, unmount) {
}
await act(async () => {
- act(() => {
- render(, container);
- });
- // pending promises will close before this ends
+ render(, container);
});
expect(container.innerHTML).toEqual('1');
});
@@ -361,7 +379,7 @@ function runActTests(label, render, unmount) {
}
});
- it('commits and effects are guaranteed to be flushed', async () => {
+ it('async commits and effects are guaranteed to be flushed', async () => {
function App() {
let [state, setState] = React.useState(0);
async function something() {
@@ -378,17 +396,12 @@ function runActTests(label, render, unmount) {
}
await act(async () => {
- act(() => {
- render(, container);
- });
- expect(container.innerHTML).toBe('0');
- expect(Scheduler).toHaveYielded([0]);
+ render(, container);
});
- // this may seem odd, but it matches user behaviour -
- // a flash of "0" followed by "1"
+ // exiting act() drains effects and microtasks
+ expect(Scheduler).toHaveYielded([0, 1]);
expect(container.innerHTML).toBe('1');
- expect(Scheduler).toHaveYielded([1]);
});
it('propagates errors', async () => {
diff --git a/packages/react-dom/src/test-utils/ReactTestUtilsAct.js b/packages/react-dom/src/test-utils/ReactTestUtilsAct.js
index 9667c1fbd5bbe..2ec5029233e65 100644
--- a/packages/react-dom/src/test-utils/ReactTestUtilsAct.js
+++ b/packages/react-dom/src/test-utils/ReactTestUtilsAct.js
@@ -84,16 +84,15 @@ function flushWorkAndMicroTasks(onDone: (err: ?Error) => void) {
let actingUpdatesScopeDepth = 0;
function act(callback: () => Thenable) {
- let previousActingUpdatesScopeDepth;
+ let previousActingUpdatesScopeDepth = actingUpdatesScopeDepth;
+ actingUpdatesScopeDepth++;
if (__DEV__) {
- previousActingUpdatesScopeDepth = actingUpdatesScopeDepth;
- actingUpdatesScopeDepth++;
ReactShouldWarnActingUpdates.current = true;
}
function onDone() {
+ actingUpdatesScopeDepth--;
if (__DEV__) {
- actingUpdatesScopeDepth--;
if (actingUpdatesScopeDepth === 0) {
ReactShouldWarnActingUpdates.current = false;
}
@@ -143,6 +142,13 @@ function act(callback: () => Thenable) {
called = true;
result.then(
() => {
+ if (actingUpdatesScopeDepth > 1) {
+ onDone();
+ resolve();
+ return;
+ }
+ // we're about to exit the act() scope,
+ // now's the time to flush tasks/effects
flushWorkAndMicroTasks((err: ?Error) => {
onDone();
if (err) {
@@ -171,7 +177,11 @@ function act(callback: () => Thenable) {
// flush effects until none remain, and cleanup
try {
- flushWork();
+ if (actingUpdatesScopeDepth === 1) {
+ // we're about to exit the act() scope,
+ // now's the time to flush effects
+ flushWork();
+ }
onDone();
} catch (err) {
onDone();
diff --git a/packages/react-noop-renderer/src/createReactNoop.js b/packages/react-noop-renderer/src/createReactNoop.js
index e7bd09890a7bc..6cbaa5a659966 100644
--- a/packages/react-noop-renderer/src/createReactNoop.js
+++ b/packages/react-noop-renderer/src/createReactNoop.js
@@ -697,16 +697,15 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
let actingUpdatesScopeDepth = 0;
function act(callback: () => Thenable) {
- let previousActingUpdatesScopeDepth;
+ let previousActingUpdatesScopeDepth = actingUpdatesScopeDepth;
+ actingUpdatesScopeDepth++;
if (__DEV__) {
- previousActingUpdatesScopeDepth = actingUpdatesScopeDepth;
- actingUpdatesScopeDepth++;
ReactShouldWarnActingUpdates.current = true;
}
function onDone() {
+ actingUpdatesScopeDepth--;
if (__DEV__) {
- actingUpdatesScopeDepth--;
if (actingUpdatesScopeDepth === 0) {
ReactShouldWarnActingUpdates.current = false;
}
@@ -756,6 +755,13 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
called = true;
result.then(
() => {
+ if (actingUpdatesScopeDepth > 1) {
+ onDone();
+ resolve();
+ return;
+ }
+ // we're about to exit the act() scope,
+ // now's the time to flush tasks/effects
flushWorkAndMicroTasks((err: ?Error) => {
onDone();
if (err) {
@@ -784,7 +790,11 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
// flush effects until none remain, and cleanup
try {
- flushWork();
+ if (actingUpdatesScopeDepth === 1) {
+ // we're about to exit the act() scope,
+ // now's the time to flush effects
+ flushWork();
+ }
onDone();
} catch (err) {
onDone();
diff --git a/packages/react-test-renderer/src/ReactTestRendererAct.js b/packages/react-test-renderer/src/ReactTestRendererAct.js
index a3d1e4e9cc460..db9f5d50b4a65 100644
--- a/packages/react-test-renderer/src/ReactTestRendererAct.js
+++ b/packages/react-test-renderer/src/ReactTestRendererAct.js
@@ -65,16 +65,15 @@ function flushWorkAndMicroTasks(onDone: (err: ?Error) => void) {
let actingUpdatesScopeDepth = 0;
function act(callback: () => Thenable) {
- let previousActingUpdatesScopeDepth;
+ let previousActingUpdatesScopeDepth = actingUpdatesScopeDepth;
+ actingUpdatesScopeDepth++;
if (__DEV__) {
- previousActingUpdatesScopeDepth = actingUpdatesScopeDepth;
- actingUpdatesScopeDepth++;
ReactShouldWarnActingUpdates.current = true;
}
function onDone() {
+ actingUpdatesScopeDepth--;
if (__DEV__) {
- actingUpdatesScopeDepth--;
if (actingUpdatesScopeDepth === 0) {
ReactShouldWarnActingUpdates.current = false;
}
@@ -124,6 +123,13 @@ function act(callback: () => Thenable) {
called = true;
result.then(
() => {
+ if (actingUpdatesScopeDepth > 1) {
+ onDone();
+ resolve();
+ return;
+ }
+ // we're about to exit the act() scope,
+ // now's the time to flush tasks/effects
flushWorkAndMicroTasks((err: ?Error) => {
onDone();
if (err) {
@@ -152,7 +158,11 @@ function act(callback: () => Thenable) {
// flush effects until none remain, and cleanup
try {
- flushWork();
+ if (actingUpdatesScopeDepth === 1) {
+ // we're about to exit the act() scope,
+ // now's the time to flush effects
+ flushWork();
+ }
onDone();
} catch (err) {
onDone();