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>); - // Attach a listener to read the return state of the action. As soon as this - // resolves, we can run the next action in the sequence. + // Attach a listener to read the return state of the action. As soon as + // this resolves, we can run the next action in the sequence. thenable.then( - (nextState: S) => { + (nextState: Awaited) => { 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>); setState(rejectedThenable); - finishRunningFormStateAction(actionQueue, setState); + finishRunningFormStateAction(actionQueue, (setState: any)); } finally { ReactCurrentBatchConfig.transition = prevTransition; @@ -1999,7 +2000,7 @@ function runFormStateAction( 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>)) : (actionResult: any); const actionQueueHook = updateWorkInProgressHook(); @@ -2152,16 +2155,16 @@ function updateFormStateImpl( 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 = T extends null | void + ? T // special case for `null | undefined` when not in `--strictNullChecks` mode + : T extends Object // `await` only unwraps object types with a callable then. Non-object types are not unwrapped. + ? T extends {then(onfulfilled: infer F): any} // thenable, extracts the first argument to `then()` + ? F extends (value: infer V) => any // if the argument to `then` is callable, extracts the argument + ? Awaited // recursively unwrap the value + : empty // the argument to `then` was not callable. + : T // argument was not an object + : T; // non-thenable