`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); ```
This commit is contained in:
parent
e2453e2007
commit
cae635054e
|
@ -47,4 +47,35 @@ describe('isomorphic act()', () => {
|
||||||
});
|
});
|
||||||
expect(root).toMatchRenderedOutput('B');
|
expect(root).toMatchRenderedOutput('B');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// @gate __DEV__
|
||||||
|
test('return value – sync callback', async () => {
|
||||||
|
expect(await act(() => 'hi')).toEqual('hi');
|
||||||
|
});
|
||||||
|
|
||||||
|
// @gate __DEV__
|
||||||
|
test('return value – sync callback, nested', async () => {
|
||||||
|
const returnValue = await act(() => {
|
||||||
|
return act(() => 'hi');
|
||||||
|
});
|
||||||
|
expect(returnValue).toEqual('hi');
|
||||||
|
});
|
||||||
|
|
||||||
|
// @gate __DEV__
|
||||||
|
test('return value – async callback', async () => {
|
||||||
|
const returnValue = await act(async () => {
|
||||||
|
return await Promise.resolve('hi');
|
||||||
|
});
|
||||||
|
expect(returnValue).toEqual('hi');
|
||||||
|
});
|
||||||
|
|
||||||
|
// @gate __DEV__
|
||||||
|
test('return value – async callback, nested', async () => {
|
||||||
|
const returnValue = await act(async () => {
|
||||||
|
return await act(async () => {
|
||||||
|
return await Promise.resolve('hi');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
expect(returnValue).toEqual('hi');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -51,7 +51,7 @@ import {ConcurrentRoot, LegacyRoot} from 'react-reconciler/src/ReactRootTags';
|
||||||
import {allowConcurrentByDefault} from 'shared/ReactFeatureFlags';
|
import {allowConcurrentByDefault} from 'shared/ReactFeatureFlags';
|
||||||
|
|
||||||
const act_notBatchedInLegacyMode = React.unstable_act;
|
const act_notBatchedInLegacyMode = React.unstable_act;
|
||||||
function act(callback: () => Thenable<mixed>): Thenable<void> {
|
function act<T>(callback: () => T): Thenable<T> {
|
||||||
return act_notBatchedInLegacyMode(() => {
|
return act_notBatchedInLegacyMode(() => {
|
||||||
return batchedUpdates(callback);
|
return batchedUpdates(callback);
|
||||||
});
|
});
|
||||||
|
|
|
@ -15,7 +15,7 @@ import enqueueTask from 'shared/enqueueTask';
|
||||||
let actScopeDepth = 0;
|
let actScopeDepth = 0;
|
||||||
let didWarnNoAwaitAct = false;
|
let didWarnNoAwaitAct = false;
|
||||||
|
|
||||||
export function act(callback: () => Thenable<mixed>): Thenable<void> {
|
export function act<T>(callback: () => T | Thenable<T>): Thenable<T> {
|
||||||
if (__DEV__) {
|
if (__DEV__) {
|
||||||
// `act` calls can be nested, so we track the depth. This represents the
|
// `act` calls can be nested, so we track the depth. This represents the
|
||||||
// number of `act` scopes on the stack.
|
// number of `act` scopes on the stack.
|
||||||
|
@ -41,21 +41,22 @@ export function act(callback: () => Thenable<mixed>): Thenable<void> {
|
||||||
typeof result === 'object' &&
|
typeof result === 'object' &&
|
||||||
typeof result.then === 'function'
|
typeof result.then === 'function'
|
||||||
) {
|
) {
|
||||||
|
const thenableResult: Thenable<T> = (result: any);
|
||||||
// The callback is an async function (i.e. returned a promise). Wait
|
// The callback is an async function (i.e. returned a promise). Wait
|
||||||
// for it to resolve before exiting the current scope.
|
// for it to resolve before exiting the current scope.
|
||||||
let wasAwaited = false;
|
let wasAwaited = false;
|
||||||
const thenable = {
|
const thenable: Thenable<T> = {
|
||||||
then(resolve, reject) {
|
then(resolve, reject) {
|
||||||
wasAwaited = true;
|
wasAwaited = true;
|
||||||
result.then(
|
thenableResult.then(
|
||||||
() => {
|
returnValue => {
|
||||||
popActScope(prevActScopeDepth);
|
popActScope(prevActScopeDepth);
|
||||||
if (actScopeDepth === 0) {
|
if (actScopeDepth === 0) {
|
||||||
// We've exited the outermost act scope. Recursively flush the
|
// We've exited the outermost act scope. Recursively flush the
|
||||||
// queue until there's no remaining work.
|
// queue until there's no remaining work.
|
||||||
recursivelyFlushAsyncActWork(resolve, reject);
|
recursivelyFlushAsyncActWork(returnValue, resolve, reject);
|
||||||
} else {
|
} else {
|
||||||
resolve();
|
resolve(returnValue);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
error => {
|
error => {
|
||||||
|
@ -88,6 +89,7 @@ export function act(callback: () => Thenable<mixed>): Thenable<void> {
|
||||||
}
|
}
|
||||||
return thenable;
|
return thenable;
|
||||||
} else {
|
} else {
|
||||||
|
const returnValue: T = (result: any);
|
||||||
// The callback is not an async function. Exit the current scope
|
// The callback is not an async function. Exit the current scope
|
||||||
// immediately, without awaiting.
|
// immediately, without awaiting.
|
||||||
popActScope(prevActScopeDepth);
|
popActScope(prevActScopeDepth);
|
||||||
|
@ -100,7 +102,7 @@ export function act(callback: () => Thenable<mixed>): Thenable<void> {
|
||||||
}
|
}
|
||||||
// Return a thenable. If the user awaits it, we'll flush again in
|
// Return a thenable. If the user awaits it, we'll flush again in
|
||||||
// case additional work was scheduled by a microtask.
|
// case additional work was scheduled by a microtask.
|
||||||
return {
|
const thenable: Thenable<T> = {
|
||||||
then(resolve, reject) {
|
then(resolve, reject) {
|
||||||
// Confirm we haven't re-entered another `act` scope, in case
|
// Confirm we haven't re-entered another `act` scope, in case
|
||||||
// the user does something weird like await the thenable
|
// the user does something weird like await the thenable
|
||||||
|
@ -108,18 +110,22 @@ export function act(callback: () => Thenable<mixed>): Thenable<void> {
|
||||||
if (ReactCurrentActQueue.current === null) {
|
if (ReactCurrentActQueue.current === null) {
|
||||||
// Recursively flush the queue until there's no remaining work.
|
// Recursively flush the queue until there's no remaining work.
|
||||||
ReactCurrentActQueue.current = [];
|
ReactCurrentActQueue.current = [];
|
||||||
recursivelyFlushAsyncActWork(resolve, reject);
|
recursivelyFlushAsyncActWork(returnValue, resolve, reject);
|
||||||
|
} else {
|
||||||
|
resolve(returnValue);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
return thenable;
|
||||||
} else {
|
} else {
|
||||||
// Since we're inside a nested `act` scope, the returned thenable
|
// Since we're inside a nested `act` scope, the returned thenable
|
||||||
// immediately resolves. The outer scope will flush the queue.
|
// immediately resolves. The outer scope will flush the queue.
|
||||||
return {
|
const thenable: Thenable<T> = {
|
||||||
then(resolve, reject) {
|
then(resolve, reject) {
|
||||||
resolve();
|
resolve(returnValue);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
return thenable;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -142,7 +148,11 @@ function popActScope(prevActScopeDepth) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function recursivelyFlushAsyncActWork(resolve, reject) {
|
function recursivelyFlushAsyncActWork<T>(
|
||||||
|
returnValue: T,
|
||||||
|
resolve: T => mixed,
|
||||||
|
reject: mixed => mixed,
|
||||||
|
) {
|
||||||
if (__DEV__) {
|
if (__DEV__) {
|
||||||
const queue = ReactCurrentActQueue.current;
|
const queue = ReactCurrentActQueue.current;
|
||||||
if (queue !== null) {
|
if (queue !== null) {
|
||||||
|
@ -152,17 +162,17 @@ function recursivelyFlushAsyncActWork(resolve, reject) {
|
||||||
if (queue.length === 0) {
|
if (queue.length === 0) {
|
||||||
// No additional work was scheduled. Finish.
|
// No additional work was scheduled. Finish.
|
||||||
ReactCurrentActQueue.current = null;
|
ReactCurrentActQueue.current = null;
|
||||||
resolve();
|
resolve(returnValue);
|
||||||
} else {
|
} else {
|
||||||
// Keep flushing work until there's none left.
|
// Keep flushing work until there's none left.
|
||||||
recursivelyFlushAsyncActWork(resolve, reject);
|
recursivelyFlushAsyncActWork(returnValue, resolve, reject);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
reject(error);
|
reject(error);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
resolve();
|
resolve(returnValue);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue