`act`: Resolve to return value of scope function (#21759)
When migrating some internal tests I found it annoying that I couldn't return anything from the `act` scope. You would have to declare the variable on the outside then assign to it. But this doesn't play well with type systems — when you use the variable, you have to check the type. Before: ```js let renderer; act(() => { renderer = ReactTestRenderer.create(<App />); }) // Type system can't tell that renderer is never undefined renderer?.root.findByType(Component); ``` After: ```js const renderer = await act(() => { return ReactTestRenderer.create(<App />); }) renderer.root.findByType(Component); ```
Andrew Clark committed
Jun 26, 2021 at 14:51 UTC
cae635054e17a6f107a39d328649137b83f25972
3 files changed
+56
-15
packages/react-reconciler/src/__tests__/ReactIsomorphicAct-test.js
+31
@@ -47,4 +47,35 @@ describe('isomorphic act()', () => {
47
});
48
expect(root).toMatchRenderedOutput('B');
49
});
50
+
51
+ // @gate __DEV__
52
+ test('return value – sync callback', async () => {
53
+ expect(await act(() => 'hi')).toEqual('hi');
54
+ });
55
+
56
+ // @gate __DEV__
57
+ test('return value – sync callback, nested', async () => {
58
+ const returnValue = await act(() => {
59
+ return act(() => 'hi');
60
+ });
61
+ expect(returnValue).toEqual('hi');
62
+ });
63
+
64
+ // @gate __DEV__
65
+ test('return value – async callback', async () => {
66
+ const returnValue = await act(async () => {
67
+ return await Promise.resolve('hi');
68
+ });
69
+ expect(returnValue).toEqual('hi');
70
+ });
71
+
72
+ // @gate __DEV__
73
+ test('return value – async callback, nested', async () => {
74
+ const returnValue = await act(async () => {
75
+ return await act(async () => {
76
+ return await Promise.resolve('hi');
77
+ });
78
+ });
79
+ expect(returnValue).toEqual('hi');
80
+ });
81
});
packages/react-test-renderer/src/ReactTestRenderer.js
+1
-1
@@ -51,7 +51,7 @@ import {ConcurrentRoot, LegacyRoot} from 'react-reconciler/src/ReactRootTags';
51
import {allowConcurrentByDefault} from 'shared/ReactFeatureFlags';
52
53
const act_notBatchedInLegacyMode = React.unstable_act;
54
-function act(callback: () => Thenable<mixed>): Thenable<void> {
54
+function act<T>(callback: () => T): Thenable<T> {
55
return act_notBatchedInLegacyMode(() => {
56
return batchedUpdates(callback);
57
});
packages/react/src/ReactAct.js
+24
-14
@@ -15,7 +15,7 @@ import enqueueTask from 'shared/enqueueTask';
15
let actScopeDepth = 0;
16
let didWarnNoAwaitAct = false;
17
18
-export function act(callback: () => Thenable<mixed>): Thenable<void> {
18
+export function act<T>(callback: () => T | Thenable<T>): Thenable<T> {
19
if (__DEV__) {
20
// `act` calls can be nested, so we track the depth. This represents the
21
// number of `act` scopes on the stack.
@@ -41,21 +41,22 @@ export function act(callback: () => Thenable<mixed>): Thenable<void> {
41
typeof result === 'object' &&
42
typeof result.then === 'function'
43
) {
44
+ const thenableResult: Thenable<T> = (result: any);
45
// The callback is an async function (i.e. returned a promise). Wait
46
// for it to resolve before exiting the current scope.
47
let wasAwaited = false;
47
- const thenable = {
48
+ const thenable: Thenable<T> = {
49
then(resolve, reject) {
50
wasAwaited = true;
50
- result.then(
51
- () => {
51
+ thenableResult.then(
52
+ returnValue => {
53
popActScope(prevActScopeDepth);
54
if (actScopeDepth === 0) {
55
// We've exited the outermost act scope. Recursively flush the
56
// queue until there's no remaining work.
56
- recursivelyFlushAsyncActWork(resolve, reject);
57
+ recursivelyFlushAsyncActWork(returnValue, resolve, reject);
58
} else {
58
- resolve();
59
+ resolve(returnValue);
60
}
61
},
62
error => {
@@ -88,6 +89,7 @@ export function act(callback: () => Thenable<mixed>): Thenable<void> {
89
}
90
return thenable;
91
} else {
92
+ const returnValue: T = (result: any);
93
// The callback is not an async function. Exit the current scope
94
// immediately, without awaiting.
95
popActScope(prevActScopeDepth);
@@ -100,7 +102,7 @@ export function act(callback: () => Thenable<mixed>): Thenable<void> {
102
}
103
// Return a thenable. If the user awaits it, we'll flush again in
104
// case additional work was scheduled by a microtask.
103
- return {
105
+ const thenable: Thenable<T> = {
106
then(resolve, reject) {
107
// Confirm we haven't re-entered another `act` scope, in case
108
// the user does something weird like await the thenable
@@ -108,18 +110,22 @@ export function act(callback: () => Thenable<mixed>): Thenable<void> {
110
if (ReactCurrentActQueue.current === null) {
111
// Recursively flush the queue until there's no remaining work.
112
ReactCurrentActQueue.current = [];
111
- recursivelyFlushAsyncActWork(resolve, reject);
113
+ recursivelyFlushAsyncActWork(returnValue, resolve, reject);
114
+ } else {
115
+ resolve(returnValue);
116
}
117
},
118
};
119
+ return thenable;
120
} else {
121
// Since we're inside a nested `act` scope, the returned thenable
122
// immediately resolves. The outer scope will flush the queue.
118
- return {
123
+ const thenable: Thenable<T> = {
124
then(resolve, reject) {
120
- resolve();
125
+ resolve(returnValue);
126
},
127
};
128
+ return thenable;
129
}
130
}
131
} else {
@@ -142,7 +148,11 @@ function popActScope(prevActScopeDepth) {
148
}
149
}
150
145
-function recursivelyFlushAsyncActWork(resolve, reject) {
151
+function recursivelyFlushAsyncActWork<T>(
152
+ returnValue: T,
153
+ resolve: T => mixed,
154
+ reject: mixed => mixed,
155
+) {
156
if (__DEV__) {
157
const queue = ReactCurrentActQueue.current;
158
if (queue !== null) {
@@ -152,17 +162,17 @@ function recursivelyFlushAsyncActWork(resolve, reject) {
162
if (queue.length === 0) {
163
// No additional work was scheduled. Finish.
164
ReactCurrentActQueue.current = null;
155
- resolve();
165
+ resolve(returnValue);
166
} else {
167
// Keep flushing work until there's none left.
158
- recursivelyFlushAsyncActWork(resolve, reject);
168
+ recursivelyFlushAsyncActWork(returnValue, resolve, reject);
169
}
170
});
171
} catch (error) {
172
reject(error);
173
}
174
} else {
165
- resolve();
175
+ resolve(returnValue);
176
}
177
}
178
}