Skip to content

Commit cae6350

Browse files
authored
act: Resolve to return value of scope function (facebook#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); ```
1 parent e2453e2 commit cae6350

File tree

3 files changed

+56
-15
lines changed

3 files changed

+56
-15
lines changed

packages/react-reconciler/src/__tests__/ReactIsomorphicAct-test.js

+31
Original file line numberDiff line numberDiff line change
@@ -47,4 +47,35 @@ describe('isomorphic act()', () => {
4747
});
4848
expect(root).toMatchRenderedOutput('B');
4949
});
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+
});
5081
});

packages/react-test-renderer/src/ReactTestRenderer.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ import {ConcurrentRoot, LegacyRoot} from 'react-reconciler/src/ReactRootTags';
5151
import {allowConcurrentByDefault} from 'shared/ReactFeatureFlags';
5252

5353
const act_notBatchedInLegacyMode = React.unstable_act;
54-
function act(callback: () => Thenable<mixed>): Thenable<void> {
54+
function act<T>(callback: () => T): Thenable<T> {
5555
return act_notBatchedInLegacyMode(() => {
5656
return batchedUpdates(callback);
5757
});

packages/react/src/ReactAct.js

+24-14
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import enqueueTask from 'shared/enqueueTask';
1515
let actScopeDepth = 0;
1616
let didWarnNoAwaitAct = false;
1717

18-
export function act(callback: () => Thenable<mixed>): Thenable<void> {
18+
export function act<T>(callback: () => T | Thenable<T>): Thenable<T> {
1919
if (__DEV__) {
2020
// `act` calls can be nested, so we track the depth. This represents the
2121
// number of `act` scopes on the stack.
@@ -41,21 +41,22 @@ export function act(callback: () => Thenable<mixed>): Thenable<void> {
4141
typeof result === 'object' &&
4242
typeof result.then === 'function'
4343
) {
44+
const thenableResult: Thenable<T> = (result: any);
4445
// The callback is an async function (i.e. returned a promise). Wait
4546
// for it to resolve before exiting the current scope.
4647
let wasAwaited = false;
47-
const thenable = {
48+
const thenable: Thenable<T> = {
4849
then(resolve, reject) {
4950
wasAwaited = true;
50-
result.then(
51-
() => {
51+
thenableResult.then(
52+
returnValue => {
5253
popActScope(prevActScopeDepth);
5354
if (actScopeDepth === 0) {
5455
// We've exited the outermost act scope. Recursively flush the
5556
// queue until there's no remaining work.
56-
recursivelyFlushAsyncActWork(resolve, reject);
57+
recursivelyFlushAsyncActWork(returnValue, resolve, reject);
5758
} else {
58-
resolve();
59+
resolve(returnValue);
5960
}
6061
},
6162
error => {
@@ -88,6 +89,7 @@ export function act(callback: () => Thenable<mixed>): Thenable<void> {
8889
}
8990
return thenable;
9091
} else {
92+
const returnValue: T = (result: any);
9193
// The callback is not an async function. Exit the current scope
9294
// immediately, without awaiting.
9395
popActScope(prevActScopeDepth);
@@ -100,26 +102,30 @@ export function act(callback: () => Thenable<mixed>): Thenable<void> {
100102
}
101103
// Return a thenable. If the user awaits it, we'll flush again in
102104
// case additional work was scheduled by a microtask.
103-
return {
105+
const thenable: Thenable<T> = {
104106
then(resolve, reject) {
105107
// Confirm we haven't re-entered another `act` scope, in case
106108
// the user does something weird like await the thenable
107109
// multiple times.
108110
if (ReactCurrentActQueue.current === null) {
109111
// Recursively flush the queue until there's no remaining work.
110112
ReactCurrentActQueue.current = [];
111-
recursivelyFlushAsyncActWork(resolve, reject);
113+
recursivelyFlushAsyncActWork(returnValue, resolve, reject);
114+
} else {
115+
resolve(returnValue);
112116
}
113117
},
114118
};
119+
return thenable;
115120
} else {
116121
// Since we're inside a nested `act` scope, the returned thenable
117122
// immediately resolves. The outer scope will flush the queue.
118-
return {
123+
const thenable: Thenable<T> = {
119124
then(resolve, reject) {
120-
resolve();
125+
resolve(returnValue);
121126
},
122127
};
128+
return thenable;
123129
}
124130
}
125131
} else {
@@ -142,7 +148,11 @@ function popActScope(prevActScopeDepth) {
142148
}
143149
}
144150

145-
function recursivelyFlushAsyncActWork(resolve, reject) {
151+
function recursivelyFlushAsyncActWork<T>(
152+
returnValue: T,
153+
resolve: T => mixed,
154+
reject: mixed => mixed,
155+
) {
146156
if (__DEV__) {
147157
const queue = ReactCurrentActQueue.current;
148158
if (queue !== null) {
@@ -152,17 +162,17 @@ function recursivelyFlushAsyncActWork(resolve, reject) {
152162
if (queue.length === 0) {
153163
// No additional work was scheduled. Finish.
154164
ReactCurrentActQueue.current = null;
155-
resolve();
165+
resolve(returnValue);
156166
} else {
157167
// Keep flushing work until there's none left.
158-
recursivelyFlushAsyncActWork(resolve, reject);
168+
recursivelyFlushAsyncActWork(returnValue, resolve, reject);
159169
}
160170
});
161171
} catch (error) {
162172
reject(error);
163173
}
164174
} else {
165-
resolve();
175+
resolve(returnValue);
166176
}
167177
}
168178
}

0 commit comments

Comments
 (0)