@samitouri / QOS-React-2 / commits / cae635054e

`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 }