Skip to content

Commit 32a0522

Browse files
committed
Use async act
1 parent befd7ce commit 32a0522

File tree

6 files changed

+156
-169
lines changed

6 files changed

+156
-169
lines changed

src/act-compat.js

Lines changed: 13 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -33,55 +33,22 @@ function getIsReactActEnvironment() {
3333
return getGlobalThis().IS_REACT_ACT_ENVIRONMENT
3434
}
3535

36-
function withGlobalActEnvironment(actImplementation) {
37-
return callback => {
38-
const previousActEnvironment = getIsReactActEnvironment()
39-
setIsReactActEnvironment(true)
40-
try {
41-
// The return value of `act` is always a thenable.
42-
let callbackNeedsToBeAwaited = false
43-
const actResult = actImplementation(() => {
44-
const result = callback()
45-
if (
46-
result !== null &&
47-
typeof result === 'object' &&
48-
typeof result.then === 'function'
49-
) {
50-
callbackNeedsToBeAwaited = true
51-
}
52-
return result
53-
})
54-
if (callbackNeedsToBeAwaited) {
55-
const thenable = actResult
56-
return {
57-
then: (resolve, reject) => {
58-
thenable.then(
59-
returnValue => {
60-
setIsReactActEnvironment(previousActEnvironment)
61-
resolve(returnValue)
62-
},
63-
error => {
64-
setIsReactActEnvironment(previousActEnvironment)
65-
reject(error)
66-
},
67-
)
68-
},
69-
}
70-
} else {
71-
setIsReactActEnvironment(previousActEnvironment)
72-
return actResult
73-
}
74-
} catch (error) {
75-
// Can't be a `finally {}` block since we don't know if we have to immediately restore IS_REACT_ACT_ENVIRONMENT
76-
// or if we have to await the callback first.
77-
setIsReactActEnvironment(previousActEnvironment)
78-
throw error
79-
}
36+
async function act(scope) {
37+
const previousActEnvironment = getIsReactActEnvironment()
38+
setIsReactActEnvironment(true)
39+
try {
40+
// scope passed to domAct needs to be `async` until React.act treats every scope as async.
41+
// We already enforce `await act()` (regardless of scope) to flush microtasks
42+
// inside the act scope.
43+
const result = await reactAct(async () => {
44+
return scope()
45+
})
46+
return result
47+
} finally {
48+
setIsReactActEnvironment(previousActEnvironment)
8049
}
8150
}
8251

83-
const act = withGlobalActEnvironment(reactAct)
84-
8552
export default act
8653
export {
8754
setIsReactActEnvironment as setReactActEnvironment,

src/fire-event.js

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -15,29 +15,29 @@ Object.keys(dtlFireEvent).forEach(key => {
1515
// @link https://github.com/facebook/react/blob/b87aabdfe1b7461e7331abb3601d9e6bb27544bc/packages/react-dom/src/events/EnterLeaveEventPlugin.js#L24-L31
1616
const mouseEnter = fireEvent.mouseEnter
1717
const mouseLeave = fireEvent.mouseLeave
18-
fireEvent.mouseEnter = (...args) => {
19-
mouseEnter(...args)
18+
fireEvent.mouseEnter = async (...args) => {
19+
await mouseEnter(...args)
2020
return fireEvent.mouseOver(...args)
2121
}
22-
fireEvent.mouseLeave = (...args) => {
23-
mouseLeave(...args)
22+
fireEvent.mouseLeave = async (...args) => {
23+
await mouseLeave(...args)
2424
return fireEvent.mouseOut(...args)
2525
}
2626

2727
const pointerEnter = fireEvent.pointerEnter
2828
const pointerLeave = fireEvent.pointerLeave
29-
fireEvent.pointerEnter = (...args) => {
30-
pointerEnter(...args)
29+
fireEvent.pointerEnter = async (...args) => {
30+
await pointerEnter(...args)
3131
return fireEvent.pointerOver(...args)
3232
}
33-
fireEvent.pointerLeave = (...args) => {
34-
pointerLeave(...args)
33+
fireEvent.pointerLeave = async (...args) => {
34+
await pointerLeave(...args)
3535
return fireEvent.pointerOut(...args)
3636
}
3737

3838
const select = fireEvent.select
39-
fireEvent.select = (node, init) => {
40-
select(node, init)
39+
fireEvent.select = async (node, init) => {
40+
await select(node, init)
4141
// React tracks this event only on focused inputs
4242
node.focus()
4343

@@ -49,20 +49,20 @@ fireEvent.select = (node, init) => {
4949
// - keyDown
5050
// so we can use any here
5151
// @link https://github.com/facebook/react/blob/b87aabdfe1b7461e7331abb3601d9e6bb27544bc/packages/react-dom/src/events/SelectEventPlugin.js#L203-L224
52-
fireEvent.keyUp(node, init)
52+
await fireEvent.keyUp(node, init)
5353
}
5454

5555
// React event system tracks native focusout/focusin events for
5656
// running blur/focus handlers
5757
// @link https://github.com/facebook/react/pull/19186
5858
const blur = fireEvent.blur
5959
const focus = fireEvent.focus
60-
fireEvent.blur = (...args) => {
61-
fireEvent.focusOut(...args)
60+
fireEvent.blur = async (...args) => {
61+
await fireEvent.focusOut(...args)
6262
return blur(...args)
6363
}
64-
fireEvent.focus = (...args) => {
65-
fireEvent.focusIn(...args)
64+
fireEvent.focus = async (...args) => {
65+
await fireEvent.focusIn(...args)
6666
return focus(...args)
6767
}
6868

src/index.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,15 @@ if (typeof process === 'undefined' || !process.env?.RTL_SKIP_AUTO_CLEANUP) {
1010
// ignore teardown() in code coverage because Jest does not support it
1111
/* istanbul ignore else */
1212
if (typeof afterEach === 'function') {
13-
afterEach(() => {
14-
cleanup()
13+
afterEach(async () => {
14+
await cleanup()
1515
})
1616
} else if (typeof teardown === 'function') {
1717
// Block is guarded by `typeof` check.
1818
// eslint does not support `typeof` guards.
1919
// eslint-disable-next-line no-undef
20-
teardown(() => {
21-
cleanup()
20+
teardown(async () => {
21+
await cleanup()
2222
})
2323
}
2424

src/pure.js

Lines changed: 49 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -58,11 +58,7 @@ configureDTL({
5858
}
5959
},
6060
eventWrapper: cb => {
61-
let result
62-
act(() => {
63-
result = cb()
64-
})
65-
return result
61+
return act(cb)
6662
},
6763
})
6864

@@ -89,13 +85,13 @@ function wrapUiIfNeeded(innerElement, wrapperComponent) {
8985
: innerElement
9086
}
9187

92-
function createConcurrentRoot(
88+
async function createConcurrentRoot(
9389
container,
9490
{hydrate, onCaughtError, onRecoverableError, ui, wrapper: WrapperComponent},
9591
) {
9692
let root
9793
if (hydrate) {
98-
act(() => {
94+
await act(() => {
9995
root = ReactDOMClient.hydrateRoot(
10096
container,
10197
strictModeIfNeeded(wrapUiIfNeeded(ui, WrapperComponent)),
@@ -120,45 +116,53 @@ function createConcurrentRoot(
120116
// Nothing to do since hydration happens when creating the root object.
121117
},
122118
render(element) {
123-
root.render(element)
119+
return act(() => {
120+
root.render(element)
121+
})
124122
},
125123
unmount() {
126-
root.unmount()
124+
return act(() => {
125+
root.unmount()
126+
})
127127
},
128128
}
129129
}
130130

131-
function createLegacyRoot(container) {
131+
async function createLegacyRoot(container) {
132132
return {
133133
hydrate(element) {
134-
ReactDOM.hydrate(element, container)
134+
return act(() => {
135+
ReactDOM.hydrate(element, container)
136+
})
135137
},
136138
render(element) {
137-
ReactDOM.render(element, container)
139+
return act(() => {
140+
ReactDOM.render(element, container)
141+
})
138142
},
139143
unmount() {
140-
ReactDOM.unmountComponentAtNode(container)
144+
return act(() => {
145+
ReactDOM.unmountComponentAtNode(container)
146+
})
141147
},
142148
}
143149
}
144150

145-
function renderRoot(
151+
async function renderRoot(
146152
ui,
147153
{baseElement, container, hydrate, queries, root, wrapper: WrapperComponent},
148154
) {
149-
act(() => {
150-
if (hydrate) {
151-
root.hydrate(
152-
strictModeIfNeeded(wrapUiIfNeeded(ui, WrapperComponent)),
153-
container,
154-
)
155-
} else {
156-
root.render(
157-
strictModeIfNeeded(wrapUiIfNeeded(ui, WrapperComponent)),
158-
container,
159-
)
160-
}
161-
})
155+
if (hydrate) {
156+
await root.hydrate(
157+
strictModeIfNeeded(wrapUiIfNeeded(ui, WrapperComponent)),
158+
container,
159+
)
160+
} else {
161+
await root.render(
162+
strictModeIfNeeded(wrapUiIfNeeded(ui, WrapperComponent)),
163+
container,
164+
)
165+
}
162166

163167
return {
164168
container,
@@ -170,12 +174,10 @@ function renderRoot(
170174
: // eslint-disable-next-line no-console,
171175
console.log(prettyDOM(el, maxLength, options)),
172176
unmount: () => {
173-
act(() => {
174-
root.unmount()
175-
})
177+
return root.unmount()
176178
},
177-
rerender: rerenderUi => {
178-
renderRoot(rerenderUi, {
179+
rerender: async rerenderUi => {
180+
await renderRoot(rerenderUi, {
179181
container,
180182
baseElement,
181183
root,
@@ -200,7 +202,7 @@ function renderRoot(
200202
}
201203
}
202204

203-
function render(
205+
async function render(
204206
ui,
205207
{
206208
container,
@@ -242,7 +244,7 @@ function render(
242244
// eslint-disable-next-line no-negated-condition -- we want to map the evolution of this over time. The root is created first. Only later is it re-used so we don't want to read the case that happens later first.
243245
if (!mountedContainers.has(container)) {
244246
const createRootImpl = legacyRoot ? createLegacyRoot : createConcurrentRoot
245-
root = createRootImpl(container, {
247+
root = await createRootImpl(container, {
246248
hydrate,
247249
onCaughtError,
248250
onRecoverableError,
@@ -276,20 +278,22 @@ function render(
276278
})
277279
}
278280

279-
function cleanup() {
280-
mountedRootEntries.forEach(({root, container}) => {
281-
act(() => {
282-
root.unmount()
283-
})
284-
if (container.parentNode === document.body) {
285-
document.body.removeChild(container)
286-
}
287-
})
281+
async function cleanup() {
282+
await Promise.all(
283+
mountedRootEntries.map(async ({root, container}) => {
284+
await act(() => {
285+
root.unmount()
286+
})
287+
if (container.parentNode === document.body) {
288+
document.body.removeChild(container)
289+
}
290+
}),
291+
)
288292
mountedRootEntries.length = 0
289293
mountedContainers.clear()
290294
}
291295

292-
function renderHook(renderCallback, options = {}) {
296+
async function renderHook(renderCallback, options = {}) {
293297
const {initialProps, ...renderOptions} = options
294298

295299
if (renderOptions.legacyRoot && typeof ReactDOM.render !== 'function') {
@@ -314,7 +318,7 @@ function renderHook(renderCallback, options = {}) {
314318
return null
315319
}
316320

317-
const {rerender: baseRerender, unmount} = render(
321+
const {rerender: baseRerender, unmount} = await render(
318322
<TestComponent renderCallbackProps={initialProps} />,
319323
renderOptions,
320324
)

0 commit comments

Comments
 (0)