Skip to content

Commit c489011

Browse files
committed
Use async act
1 parent 8c50ec7 commit c489011

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, 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)),
@@ -116,45 +112,53 @@ function createConcurrentRoot(
116112
// Nothing to do since hydration happens when creating the root object.
117113
},
118114
render(element) {
119-
root.render(element)
115+
return act(() => {
116+
root.render(element)
117+
})
120118
},
121119
unmount() {
122-
root.unmount()
120+
return act(() => {
121+
root.unmount()
122+
})
123123
},
124124
}
125125
}
126126

127-
function createLegacyRoot(container) {
127+
async function createLegacyRoot(container) {
128128
return {
129129
hydrate(element) {
130-
ReactDOM.hydrate(element, container)
130+
return act(() => {
131+
ReactDOM.hydrate(element, container)
132+
})
131133
},
132134
render(element) {
133-
ReactDOM.render(element, container)
135+
return act(() => {
136+
ReactDOM.render(element, container)
137+
})
134138
},
135139
unmount() {
136-
ReactDOM.unmountComponentAtNode(container)
140+
return act(() => {
141+
ReactDOM.unmountComponentAtNode(container)
142+
})
137143
},
138144
}
139145
}
140146

141-
function renderRoot(
147+
async function renderRoot(
142148
ui,
143149
{baseElement, container, hydrate, queries, root, wrapper: WrapperComponent},
144150
) {
145-
act(() => {
146-
if (hydrate) {
147-
root.hydrate(
148-
strictModeIfNeeded(wrapUiIfNeeded(ui, WrapperComponent)),
149-
container,
150-
)
151-
} else {
152-
root.render(
153-
strictModeIfNeeded(wrapUiIfNeeded(ui, WrapperComponent)),
154-
container,
155-
)
156-
}
157-
})
151+
if (hydrate) {
152+
await root.hydrate(
153+
strictModeIfNeeded(wrapUiIfNeeded(ui, WrapperComponent)),
154+
container,
155+
)
156+
} else {
157+
await root.render(
158+
strictModeIfNeeded(wrapUiIfNeeded(ui, WrapperComponent)),
159+
container,
160+
)
161+
}
158162

159163
return {
160164
container,
@@ -166,12 +170,10 @@ function renderRoot(
166170
: // eslint-disable-next-line no-console,
167171
console.log(prettyDOM(el, maxLength, options)),
168172
unmount: () => {
169-
act(() => {
170-
root.unmount()
171-
})
173+
return root.unmount()
172174
},
173-
rerender: rerenderUi => {
174-
renderRoot(rerenderUi, {
175+
rerender: async rerenderUi => {
176+
await renderRoot(rerenderUi, {
175177
container,
176178
baseElement,
177179
root,
@@ -196,7 +198,7 @@ function renderRoot(
196198
}
197199
}
198200

199-
function render(
201+
async function render(
200202
ui,
201203
{
202204
container,
@@ -230,7 +232,7 @@ function render(
230232
// 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.
231233
if (!mountedContainers.has(container)) {
232234
const createRootImpl = legacyRoot ? createLegacyRoot : createConcurrentRoot
233-
root = createRootImpl(container, {hydrate, ui, wrapper})
235+
root = await createRootImpl(container, {hydrate, ui, wrapper})
234236

235237
mountedRootEntries.push({container, root})
236238
// we'll add it to the mounted containers regardless of whether it's actually
@@ -258,20 +260,22 @@ function render(
258260
})
259261
}
260262

261-
function cleanup() {
262-
mountedRootEntries.forEach(({root, container}) => {
263-
act(() => {
264-
root.unmount()
265-
})
266-
if (container.parentNode === document.body) {
267-
document.body.removeChild(container)
268-
}
269-
})
263+
async function cleanup() {
264+
await Promise.all(
265+
mountedRootEntries.map(async ({root, container}) => {
266+
await act(() => {
267+
root.unmount()
268+
})
269+
if (container.parentNode === document.body) {
270+
document.body.removeChild(container)
271+
}
272+
}),
273+
)
270274
mountedRootEntries.length = 0
271275
mountedContainers.clear()
272276
}
273277

274-
function renderHook(renderCallback, options = {}) {
278+
async function renderHook(renderCallback, options = {}) {
275279
const {initialProps, ...renderOptions} = options
276280

277281
if (renderOptions.legacyRoot && typeof ReactDOM.render !== 'function') {
@@ -296,7 +300,7 @@ function renderHook(renderCallback, options = {}) {
296300
return null
297301
}
298302

299-
const {rerender: baseRerender, unmount} = render(
303+
const {rerender: baseRerender, unmount} = await render(
300304
<TestComponent renderCallbackProps={initialProps} />,
301305
renderOptions,
302306
)

0 commit comments

Comments
 (0)