diff --git a/packages/react-dom-bindings/src/shared/ReactDOMFormActions.js b/packages/react-dom-bindings/src/shared/ReactDOMFormActions.js
index 8c0f764d6cab5..b2ac098bcee70 100644
--- a/packages/react-dom-bindings/src/shared/ReactDOMFormActions.js
+++ b/packages/react-dom-bindings/src/shared/ReactDOMFormActions.js
@@ -8,6 +8,7 @@
*/
import type {Dispatcher} from 'react-reconciler/src/ReactInternalTypes';
+import type {Awaited} from 'shared/ReactTypes';
import {enableAsyncActions, enableFormActions} from 'shared/ReactFeatureFlags';
import ReactSharedInternals from 'shared/ReactSharedInternals';
@@ -76,10 +77,10 @@ export function useFormStatus(): FormStatus {
}
export function useFormState(
- action: (S, P) => Promise,
- initialState: S,
+ action: (Awaited, P) => S,
+ initialState: Awaited,
permalink?: string,
-): [S, (P) => void] {
+): [Awaited, (P) => void] {
if (!(enableFormActions && enableAsyncActions)) {
throw new Error('Not implemented.');
} else {
diff --git a/packages/react-dom/index.experimental.js b/packages/react-dom/index.experimental.js
index e946fee656329..012eb6866e8a4 100644
--- a/packages/react-dom/index.experimental.js
+++ b/packages/react-dom/index.experimental.js
@@ -31,6 +31,7 @@ export {
version,
} from './src/client/ReactDOM';
+import type {Awaited} from 'shared/ReactTypes';
import type {FormStatus} from 'react-dom-bindings/src/shared/ReactDOMFormActions';
import {useFormStatus, useFormState} from './src/client/ReactDOM';
@@ -45,10 +46,10 @@ export function experimental_useFormStatus(): FormStatus {
}
export function experimental_useFormState(
- action: (S, P) => Promise,
- initialState: S,
+ action: (Awaited, P) => S,
+ initialState: Awaited,
permalink?: string,
-): [S, (P) => void] {
+): [Awaited, (P) => void] {
if (__DEV__) {
console.error(
'useFormState is now in canary. Remove the experimental_ prefix. ' +
diff --git a/packages/react-reconciler/src/ReactFiberHooks.js b/packages/react-reconciler/src/ReactFiberHooks.js
index 5beaefc08cc96..e58c5484ef920 100644
--- a/packages/react-reconciler/src/ReactFiberHooks.js
+++ b/packages/react-reconciler/src/ReactFiberHooks.js
@@ -13,6 +13,7 @@ import type {
Usable,
Thenable,
RejectedThenable,
+ Awaited,
} from 'shared/ReactTypes';
import type {
Fiber,
@@ -1871,12 +1872,12 @@ function rerenderOptimistic(
type FormStateActionQueue = {
// This is the most recent state returned from an action. It's updated as
// soon as the action finishes running.
- state: S,
+ state: Awaited,
// A stable dispatch method, passed to the user.
dispatch: Dispatch
,
// This is the most recent action function that was rendered. It's updated
// during the commit phase.
- action: (S, P) => Promise,
+ action: (Awaited, P) => S,
// This is a circular linked list of pending action payloads. It incudes the
// action that is currently running.
pending: FormStateActionQueueNode
| null, @@ -1891,7 +1892,7 @@ type FormStateActionQueueNode
= {
function dispatchFormState(
fiber: Fiber,
actionQueue: FormStateActionQueue,
- setState: Dispatch>,
+ setState: Dispatch>,
payload: P,
): void {
if (isRenderPhaseUpdate(fiber)) {
@@ -1907,7 +1908,7 @@ function dispatchFormState(
};
newLast.next = actionQueue.pending = newLast;
- runFormStateAction(actionQueue, setState, payload);
+ runFormStateAction(actionQueue, (setState: any), payload);
} else {
// There's already an action running. Add to the queue.
const first = last.next;
@@ -1921,7 +1922,7 @@ function dispatchFormState(
function runFormStateAction(
actionQueue: FormStateActionQueue,
- setState: Dispatch>,
+ setState: Dispatch>,
payload: P,
) {
const action = actionQueue.action;
@@ -1942,42 +1943,42 @@ function runFormStateAction(
// $FlowFixMe[method-unbinding]
typeof returnValue.then === 'function'
) {
- const thenable = ((returnValue: any): Thenable);
+ const thenable = ((returnValue: any): Thenable) => {
actionQueue.state = nextState;
- finishRunningFormStateAction(actionQueue, setState);
+ finishRunningFormStateAction(actionQueue, (setState: any));
},
- () => finishRunningFormStateAction(actionQueue, setState),
+ () => finishRunningFormStateAction(actionQueue, (setState: any)),
);
const entangledResult = requestAsyncActionContext(thenable, null);
- setState(entangledResult);
+ setState((entangledResult: any));
} else {
- // This is either `finishedState` or a thenable that resolves to
- // `finishedState`, depending on whether we're inside an async
- // action scope.
+ // This is either `returnValue` or a thenable that resolves to
+ // `returnValue`, depending on whether we're inside an async action scope.
const entangledResult = requestSyncActionContext(returnValue, null);
- setState(entangledResult);
+ setState((entangledResult: any));
- const nextState = ((returnValue: any): S);
+ const nextState = ((returnValue: any): Awaited);
actionQueue.state = nextState;
- finishRunningFormStateAction(actionQueue, setState);
+ finishRunningFormStateAction(actionQueue, (setState: any));
}
} catch (error) {
// This is a trick to get the `useFormState` hook to rethrow the error.
// When it unwraps the thenable with the `use` algorithm, the error
// will be thrown.
- const rejectedThenable: RejectedThenable = {
+ const rejectedThenable: S = ({
then() {},
status: 'rejected',
reason: error,
- };
+ // $FlowFixMe: Not sure why this doesn't work
+ }: RejectedThenable(
function finishRunningFormStateAction(
actionQueue: FormStateActionQueue,
- setState: Dispatch>,
+ setState: Dispatch>,
) {
// The action finished running. Pop it from the queue and run the next pending
// action, if there are any.
@@ -2015,7 +2016,7 @@ function finishRunningFormStateAction(
last.next = next;
// Run the next action.
- runFormStateAction(actionQueue, setState, next.payload);
+ runFormStateAction(actionQueue, (setState: any), next.payload);
}
}
}
@@ -2025,11 +2026,11 @@ function formStateReducer(oldState: S, newState: S): S {
}
function mountFormState(
- action: (S, P) => Promise,
- initialStateProp: S,
+ action: (Awaited, P) => S,
+ initialStateProp: Awaited,
permalink?: string,
-): [S, (P) => void] {
- let initialState = initialStateProp;
+): [Awaited, (P) => void] {
+ let initialState: Awaited = initialStateProp;
if (getIsHydrating()) {
const root: FiberRoot = (getWorkInProgressRoot(): any);
const ssrFormState = root.formState;
@@ -2050,18 +2051,20 @@ function mountFormState(
// the `use` algorithm during render.
const stateHook = mountWorkInProgressHook();
stateHook.memoizedState = stateHook.baseState = initialState;
- const stateQueue: UpdateQueue, S | Thenable> = {
+ // TODO: Typing this "correctly" results in recursion limit errors
+ // const stateQueue: UpdateQueue, S | Awaited> = {
+ const stateQueue = {
pending: null,
lanes: NoLanes,
- dispatch: null,
+ dispatch: (null: any),
lastRenderedReducer: formStateReducer,
lastRenderedState: initialState,
};
stateHook.queue = stateQueue;
- const setState: Dispatch> = (dispatchSetState.bind(
+ const setState: Dispatch> = (dispatchSetState.bind(
null,
currentlyRenderingFiber,
- stateQueue,
+ ((stateQueue: any): UpdateQueue, S | Awaited>),
): any);
stateQueue.dispatch = setState;
@@ -2077,7 +2080,7 @@ function mountFormState(
pending: null,
};
actionQueueHook.queue = actionQueue;
- const dispatch = dispatchFormState.bind(
+ const dispatch = (dispatchFormState: any).bind(
null,
currentlyRenderingFiber,
actionQueue,
@@ -2094,10 +2097,10 @@ function mountFormState(
}
function updateFormState(
- action: (S, P) => Promise,
- initialState: S,
+ action: (Awaited, P) => S,
+ initialState: Awaited,
permalink?: string,
-): [S, (P) => void] {
+): [Awaited, (P) => void] {
const stateHook = updateWorkInProgressHook();
const currentStateHook = ((currentHook: any): Hook);
return updateFormStateImpl(
@@ -2112,10 +2115,10 @@ function updateFormState(
function updateFormStateImpl(
stateHook: Hook,
currentStateHook: Hook,
- action: (S, P) => Promise,
- initialState: S,
+ action: (Awaited, P) => S,
+ initialState: Awaited,
permalink?: string,
-): [S, (P) => void] {
+): [Awaited, (P) => void] {
const [actionResult] = updateReducerImpl, S | Thenable>(
stateHook,
currentStateHook,
@@ -2123,12 +2126,12 @@ function updateFormStateImpl(
);
// This will suspend until the action finishes.
- const state: S =
+ const state: Awaited =
typeof actionResult === 'object' &&
actionResult !== null &&
// $FlowFixMe[method-unbinding]
typeof actionResult.then === 'function'
- ? useThenable(((actionResult: any): Thenable))
+ ? useThenable(((actionResult: any): Thenable(
function formStateActionEffect(
actionQueue: FormStateActionQueue,
- action: (S, P) => Promise,
+ action: (Awaited, P) => S,
): void {
actionQueue.action = action;
}
function rerenderFormState(
- action: (S, P) => Promise,
- initialState: S,
+ action: (Awaited, P) => S,
+ initialState: Awaited,
permalink?: string,
-): [S, (P) => void] {
+): [Awaited, (P) => void] {
// Unlike useState, useFormState doesn't support render phase updates.
// Also unlike useState, we need to replay all pending updates again in case
// the passthrough value changed.
@@ -2184,7 +2187,7 @@ function rerenderFormState(
}
// This is a mount. No updates to process.
- const state: S = stateHook.memoizedState;
+ const state: Awaited = stateHook.memoizedState;
const actionQueueHook = updateWorkInProgressHook();
const actionQueue = actionQueueHook.queue;
@@ -3735,10 +3738,10 @@ if (__DEV__) {
useHostTransitionStatus;
(HooksDispatcherOnMountInDEV: Dispatcher).useFormState =
function useFormState(
- action: (S, P) => Promise,
- initialState: S,
+ action: (Awaited, P) => S,
+ initialState: Awaited,
permalink?: string,
- ): [S, (P) => void] {
+ ): [Awaited, (P) => void] {
currentHookNameInDev = 'useFormState';
mountHookTypesDev();
return mountFormState(action, initialState, permalink);
@@ -3905,10 +3908,10 @@ if (__DEV__) {
useHostTransitionStatus;
(HooksDispatcherOnMountWithHookTypesInDEV: Dispatcher).useFormState =
function useFormState(
- action: (S, P) => Promise,
- initialState: S,
+ action: (Awaited, P) => S,
+ initialState: Awaited,
permalink?: string,
- ): [S, (P) => void] {
+ ): [Awaited, (P) => void] {
currentHookNameInDev = 'useFormState';
updateHookTypesDev();
return mountFormState(action, initialState, permalink);
@@ -4077,10 +4080,10 @@ if (__DEV__) {
useHostTransitionStatus;
(HooksDispatcherOnUpdateInDEV: Dispatcher).useFormState =
function useFormState(
- action: (S, P) => Promise,
- initialState: S,
+ action: (Awaited, P) => S,
+ initialState: Awaited,
permalink?: string,
- ): [S, (P) => void] {
+ ): [Awaited, (P) => void] {
currentHookNameInDev = 'useFormState';
updateHookTypesDev();
return updateFormState(action, initialState, permalink);
@@ -4249,10 +4252,10 @@ if (__DEV__) {
useHostTransitionStatus;
(HooksDispatcherOnRerenderInDEV: Dispatcher).useFormState =
function useFormState(
- action: (S, P) => Promise,
- initialState: S,
+ action: (Awaited, P) => S,
+ initialState: Awaited,
permalink?: string,
- ): [S, (P) => void] {
+ ): [Awaited, (P) => void] {
currentHookNameInDev = 'useFormState';
updateHookTypesDev();
return rerenderFormState(action, initialState, permalink);
@@ -4442,10 +4445,10 @@ if (__DEV__) {
useHostTransitionStatus;
(InvalidNestedHooksDispatcherOnMountInDEV: Dispatcher).useFormState =
function useFormState(
- action: (S, P) => Promise,
- initialState: S,
+ action: (Awaited, P) => S,
+ initialState: Awaited,
permalink?: string,
- ): [S, (P) => void] {
+ ): [Awaited, (P) => void] {
currentHookNameInDev = 'useFormState';
warnInvalidHookAccess();
mountHookTypesDev();
@@ -4640,10 +4643,10 @@ if (__DEV__) {
useHostTransitionStatus;
(InvalidNestedHooksDispatcherOnUpdateInDEV: Dispatcher).useFormState =
function useFormState(
- action: (S, P) => Promise,
- initialState: S,
+ action: (Awaited, P) => S,
+ initialState: Awaited,
permalink?: string,
- ): [S, (P) => void] {
+ ): [Awaited, (P) => void] {
currentHookNameInDev = 'useFormState';
warnInvalidHookAccess();
updateHookTypesDev();
@@ -4838,10 +4841,10 @@ if (__DEV__) {
useHostTransitionStatus;
(InvalidNestedHooksDispatcherOnRerenderInDEV: Dispatcher).useFormState =
function useFormState(
- action: (S, P) => Promise,
- initialState: S,
+ action: (Awaited, P) => S,
+ initialState: Awaited,
permalink?: string,
- ): [S, (P) => void] {
+ ): [Awaited, (P) => void] {
currentHookNameInDev = 'useFormState';
warnInvalidHookAccess();
updateHookTypesDev();
diff --git a/packages/react-reconciler/src/ReactInternalTypes.js b/packages/react-reconciler/src/ReactInternalTypes.js
index 9378a41477e0f..e03ac26a0103c 100644
--- a/packages/react-reconciler/src/ReactInternalTypes.js
+++ b/packages/react-reconciler/src/ReactInternalTypes.js
@@ -15,6 +15,7 @@ import type {
Wakeable,
Usable,
ReactFormState,
+ Awaited,
} from 'shared/ReactTypes';
import type {WorkTag} from './ReactWorkTags';
import type {TypeOfMode} from './ReactTypeOfMode';
@@ -418,10 +419,10 @@ export type Dispatcher = {
reducer: ?(S, A) => S,
) => [S, (A) => void],
useFormState?: (
- action: (S, P) => Promise,
- initialState: S,
+ action: (Awaited, P) => S,
+ initialState: Awaited,
permalink?: string,
- ) => [S, (P) => void],
+ ) => [Awaited, (P) => void],
};
export type CacheDispatcher = {
diff --git a/packages/react-server/src/ReactFizzHooks.js b/packages/react-server/src/ReactFizzHooks.js
index 63d1327b85963..52ea64f4a5c75 100644
--- a/packages/react-server/src/ReactFizzHooks.js
+++ b/packages/react-server/src/ReactFizzHooks.js
@@ -15,6 +15,7 @@ import type {
Thenable,
Usable,
ReactCustomFormAction,
+ Awaited,
} from 'shared/ReactTypes';
import type {ResumableState} from './ReactFizzConfig';
@@ -612,10 +613,10 @@ function createPostbackFormStateKey(
}
function useFormState(
- action: (S, P) => Promise,
- initialState: S,
+ action: (Awaited, P) => S,
+ initialState: Awaited,
permalink?: string,
-): [S, (P) => void] {
+): [Awaited, (P) => void] {
resolveCurrentlyRenderingComponent();
// Count the number of useFormState hooks per component. We also use this to
diff --git a/packages/shared/ReactTypes.js b/packages/shared/ReactTypes.js
index 7bdbb09bf656b..db44433e69333 100644
--- a/packages/shared/ReactTypes.js
+++ b/packages/shared/ReactTypes.js
@@ -184,3 +184,13 @@ export type ReactFormState = [
ReferenceId /* Server Reference ID */,
number /* number of bound arguments */,
];
+
+export type Awaited