Closed
Description
Action items
- Before waiting for microtasks to flush, React should call
Scheduler.flushAll
to flush pending Scheduler work.act()
- s / flushPassiveEffects / Scheduler.unstable_flushWithoutYielding #15591 -
act
should not flush anything until the outermostact
call exits (except for the updates that always flush early likeflushSync
and serial events). flush only on exiting outermost act() #15682 - Add
act
warning to React DOM'sroot.update()
(sincecreateRoot
is a new API). using the wrong renderer's act() should warn #15756 - React should warn if an update is scheduled but is nested inside the wrong renderer's
act
(e.g. a DOM update nested inside Test Renderer'sact
), regardless of whether the update was triggered by a legacy API. using the wrong renderer's act() should warn #15756 - React should warn if a passive effect is scheduled by an update outside of
act
, regardless of whether the update was triggered by a legacy API (e.g.this.setState
orReactDOM.render
) warn if passive effects get queued outside of an act() call. #15763 - nested
act
s from different renderers should work (eg - a react-art update inside a react-dom tree shouldn't warn allow nestedact()
s from different renderers #15816 -
act
should force pending fallbacks to commit at the end, ignoring how much time has passed, without affecting unrelated timers. -
act
should warn if it's called from inside a React event handler or React effect/lifecycle. -
act
should have the same behavior regardless of whether the result is awaited.
Discussion
- In Legacy Mode, updates that happen after the first
await
will not be batched, but they shouldn't fire the warning. We should still wait to flush passive effects, Scheduler, and microtasks until the end. - Because passive effects are scheduled with Scheduler, they are flushed by
Scheduler.flushAll
. That means we don't need to callflushPassiveEffects
separately in order to flush them. However, we currently use the return value offlushPassiveEffects
to determine if additional passive effects were scheduled. So perhaps we should export a method likehasPendingEffects
instead. - The recommendation is to await the result of
act
even if the handler is synchronous, because that ensures that any dangling microtasks are flushed before the test proceeds. However, it's hard to fire a warning if the user neglects to do this, because such a warning needs to happen in an async task, and the test could exit before the async task fires. The warning is also controversial because of the additional boilerplate. But regardless of whether we fire a warning, we should stick to our recommendation to always awaitact
. - The API is designed primarily for Batched/Concurrent Mode. That's why we wait until the outermost
act
exits before flushing anything.- The behavior is slightly different in Legacy Mode, but they are the same in the simple case of a single event handler inside a single
act
. For the remaining cases, our suggestion is to switch to the Batched Mode API.
- The behavior is slightly different in Legacy Mode, but they are the same in the simple case of a single event handler inside a single
- No longer need to count the
act
"depth" because nestedact
s are a no-op in Batched Mode.
Idiomatic examples
Single event handler
await act(() => setState());
Using a testing framework
await simulate('click', domElement);
where simulate
is imported from a testing framework and looks something like:
async function simulate(eventType, domElement) {
const event = new Event(eventType);
await act(() => domElement.dispatchEvent(event));
}
Advanced: Multiple events that occur in sequence
In Batched Mode, these would all be flushed in a single batch, so we group them together with an outer act
.
await act(async () => {
await simulate(domElement, 'mousedown');
await simulate(domElement, 'mouseup');
});