Skip to content

Commit 165424e

Browse files
committed
Update internal Flow type
Updates our internal Flow type for useFormState to allow for synchronous actions. I tried to make it match what the TypeScript equivalent would be, so although Flow does not include an Awaited type helper, I copied the one from TypeScript and added it to our internal types. Unfortunately this led to a few "Recursion limit exceeded" Flow errors in the Fiber implementation. I wasn't able to figure out whether these were legit type errors, or if the types were correct but the recursiveness of the types tripped some internal limit. But since this only affects the type coverage of our internal implementation and not the public API, I sprinkled in some `any`s and called it a day.
1 parent 670f958 commit 165424e

File tree

6 files changed

+95
-78
lines changed

6 files changed

+95
-78
lines changed

packages/react-dom-bindings/src/shared/ReactDOMFormActions.js

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
*/
99

1010
import type {Dispatcher} from 'react-reconciler/src/ReactInternalTypes';
11+
import type {Awaited} from 'shared/ReactTypes';
1112

1213
import {enableAsyncActions, enableFormActions} from 'shared/ReactFeatureFlags';
1314
import ReactSharedInternals from 'shared/ReactSharedInternals';
@@ -76,10 +77,10 @@ export function useFormStatus(): FormStatus {
7677
}
7778

7879
export function useFormState<S, P>(
79-
action: (S, P) => Promise<S>,
80-
initialState: S,
80+
action: (Awaited<S>, P) => S,
81+
initialState: Awaited<S>,
8182
permalink?: string,
82-
): [S, (P) => void] {
83+
): [Awaited<S>, (P) => void] {
8384
if (!(enableFormActions && enableAsyncActions)) {
8485
throw new Error('Not implemented.');
8586
} else {

packages/react-dom/index.experimental.js

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ export {
3131
version,
3232
} from './src/client/ReactDOM';
3333

34+
import type {Awaited} from 'shared/ReactTypes';
3435
import type {FormStatus} from 'react-dom-bindings/src/shared/ReactDOMFormActions';
3536
import {useFormStatus, useFormState} from './src/client/ReactDOM';
3637

@@ -45,10 +46,10 @@ export function experimental_useFormStatus(): FormStatus {
4546
}
4647

4748
export function experimental_useFormState<S, P>(
48-
action: (S, P) => Promise<S>,
49-
initialState: S,
49+
action: (Awaited<S>, P) => S,
50+
initialState: Awaited<S>,
5051
permalink?: string,
51-
): [S, (P) => void] {
52+
): [Awaited<S>, (P) => void] {
5253
if (__DEV__) {
5354
console.error(
5455
'useFormState is now in canary. Remove the experimental_ prefix. ' +

packages/react-reconciler/src/ReactFiberHooks.js

Lines changed: 69 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import type {
1313
Usable,
1414
Thenable,
1515
RejectedThenable,
16+
Awaited,
1617
} from 'shared/ReactTypes';
1718
import type {
1819
Fiber,
@@ -1871,12 +1872,12 @@ function rerenderOptimistic<S, A>(
18711872
type FormStateActionQueue<S, P> = {
18721873
// This is the most recent state returned from an action. It's updated as
18731874
// soon as the action finishes running.
1874-
state: S,
1875+
state: Awaited<S>,
18751876
// A stable dispatch method, passed to the user.
18761877
dispatch: Dispatch<P>,
18771878
// This is the most recent action function that was rendered. It's updated
18781879
// during the commit phase.
1879-
action: (S, P) => Promise<S>,
1880+
action: (Awaited<S>, P) => S,
18801881
// This is a circular linked list of pending action payloads. It incudes the
18811882
// action that is currently running.
18821883
pending: FormStateActionQueueNode<P> | null,
@@ -1891,7 +1892,7 @@ type FormStateActionQueueNode<P> = {
18911892
function dispatchFormState<S, P>(
18921893
fiber: Fiber,
18931894
actionQueue: FormStateActionQueue<S, P>,
1894-
setState: Dispatch<S | Thenable<S>>,
1895+
setState: Dispatch<S | Awaited<S>>,
18951896
payload: P,
18961897
): void {
18971898
if (isRenderPhaseUpdate(fiber)) {
@@ -1907,7 +1908,7 @@ function dispatchFormState<S, P>(
19071908
};
19081909
newLast.next = actionQueue.pending = newLast;
19091910

1910-
runFormStateAction(actionQueue, setState, payload);
1911+
runFormStateAction(actionQueue, (setState: any), payload);
19111912
} else {
19121913
// There's already an action running. Add to the queue.
19131914
const first = last.next;
@@ -1921,7 +1922,7 @@ function dispatchFormState<S, P>(
19211922

19221923
function runFormStateAction<S, P>(
19231924
actionQueue: FormStateActionQueue<S, P>,
1924-
setState: Dispatch<S | Thenable<S>>,
1925+
setState: Dispatch<S | Awaited<S>>,
19251926
payload: P,
19261927
) {
19271928
const action = actionQueue.action;
@@ -1942,42 +1943,42 @@ function runFormStateAction<S, P>(
19421943
// $FlowFixMe[method-unbinding]
19431944
typeof returnValue.then === 'function'
19441945
) {
1945-
const thenable = ((returnValue: any): Thenable<S>);
1946+
const thenable = ((returnValue: any): Thenable<Awaited<S>>);
19461947

1947-
// Attach a listener to read the return state of the action. As soon as this
1948-
// resolves, we can run the next action in the sequence.
1948+
// Attach a listener to read the return state of the action. As soon as
1949+
// this resolves, we can run the next action in the sequence.
19491950
thenable.then(
1950-
(nextState: S) => {
1951+
(nextState: Awaited<S>) => {
19511952
actionQueue.state = nextState;
1952-
finishRunningFormStateAction(actionQueue, setState);
1953+
finishRunningFormStateAction(actionQueue, (setState: any));
19531954
},
1954-
() => finishRunningFormStateAction(actionQueue, setState),
1955+
() => finishRunningFormStateAction(actionQueue, (setState: any)),
19551956
);
19561957

19571958
const entangledResult = requestAsyncActionContext<S>(thenable, null);
1958-
setState(entangledResult);
1959+
setState((entangledResult: any));
19591960
} else {
1960-
// This is either `finishedState` or a thenable that resolves to
1961-
// `finishedState`, depending on whether we're inside an async
1962-
// action scope.
1961+
// This is either `returnValue` or a thenable that resolves to
1962+
// `returnValue`, depending on whether we're inside an async action scope.
19631963
const entangledResult = requestSyncActionContext<S>(returnValue, null);
1964-
setState(entangledResult);
1964+
setState((entangledResult: any));
19651965

1966-
const nextState = ((returnValue: any): S);
1966+
const nextState = ((returnValue: any): Awaited<S>);
19671967
actionQueue.state = nextState;
1968-
finishRunningFormStateAction(actionQueue, setState);
1968+
finishRunningFormStateAction(actionQueue, (setState: any));
19691969
}
19701970
} catch (error) {
19711971
// This is a trick to get the `useFormState` hook to rethrow the error.
19721972
// When it unwraps the thenable with the `use` algorithm, the error
19731973
// will be thrown.
1974-
const rejectedThenable: RejectedThenable<S> = {
1974+
const rejectedThenable: S = ({
19751975
then() {},
19761976
status: 'rejected',
19771977
reason: error,
1978-
};
1978+
// $FlowFixMe: Not sure why this doesn't work
1979+
}: RejectedThenable<Awaited<S>>);
19791980
setState(rejectedThenable);
1980-
finishRunningFormStateAction(actionQueue, setState);
1981+
finishRunningFormStateAction(actionQueue, (setState: any));
19811982
} finally {
19821983
ReactCurrentBatchConfig.transition = prevTransition;
19831984

@@ -1999,7 +2000,7 @@ function runFormStateAction<S, P>(
19992000

20002001
function finishRunningFormStateAction<S, P>(
20012002
actionQueue: FormStateActionQueue<S, P>,
2002-
setState: Dispatch<S | Thenable<S>>,
2003+
setState: Dispatch<S | Awaited<S>>,
20032004
) {
20042005
// The action finished running. Pop it from the queue and run the next pending
20052006
// action, if there are any.
@@ -2015,7 +2016,7 @@ function finishRunningFormStateAction<S, P>(
20152016
last.next = next;
20162017

20172018
// Run the next action.
2018-
runFormStateAction(actionQueue, setState, next.payload);
2019+
runFormStateAction(actionQueue, (setState: any), next.payload);
20192020
}
20202021
}
20212022
}
@@ -2025,11 +2026,11 @@ function formStateReducer<S>(oldState: S, newState: S): S {
20252026
}
20262027

20272028
function mountFormState<S, P>(
2028-
action: (S, P) => Promise<S>,
2029-
initialStateProp: S,
2029+
action: (Awaited<S>, P) => S,
2030+
initialStateProp: Awaited<S>,
20302031
permalink?: string,
2031-
): [S, (P) => void] {
2032-
let initialState = initialStateProp;
2032+
): [Awaited<S>, (P) => void] {
2033+
let initialState: Awaited<S> = initialStateProp;
20332034
if (getIsHydrating()) {
20342035
const root: FiberRoot = (getWorkInProgressRoot(): any);
20352036
const ssrFormState = root.formState;
@@ -2050,18 +2051,20 @@ function mountFormState<S, P>(
20502051
// the `use` algorithm during render.
20512052
const stateHook = mountWorkInProgressHook();
20522053
stateHook.memoizedState = stateHook.baseState = initialState;
2053-
const stateQueue: UpdateQueue<S | Thenable<S>, S | Thenable<S>> = {
2054+
// TODO: Typing this "correctly" results in recursion limit errors
2055+
// const stateQueue: UpdateQueue<S | Awaited<S>, S | Awaited<S>> = {
2056+
const stateQueue = {
20542057
pending: null,
20552058
lanes: NoLanes,
2056-
dispatch: null,
2059+
dispatch: (null: any),
20572060
lastRenderedReducer: formStateReducer,
20582061
lastRenderedState: initialState,
20592062
};
20602063
stateHook.queue = stateQueue;
2061-
const setState: Dispatch<S | Thenable<S>> = (dispatchSetState.bind(
2064+
const setState: Dispatch<S | Awaited<S>> = (dispatchSetState.bind(
20622065
null,
20632066
currentlyRenderingFiber,
2064-
stateQueue,
2067+
((stateQueue: any): UpdateQueue<S | Awaited<S>, S | Awaited<S>>),
20652068
): any);
20662069
stateQueue.dispatch = setState;
20672070

@@ -2077,7 +2080,7 @@ function mountFormState<S, P>(
20772080
pending: null,
20782081
};
20792082
actionQueueHook.queue = actionQueue;
2080-
const dispatch = dispatchFormState.bind(
2083+
const dispatch = (dispatchFormState: any).bind(
20812084
null,
20822085
currentlyRenderingFiber,
20832086
actionQueue,
@@ -2094,10 +2097,10 @@ function mountFormState<S, P>(
20942097
}
20952098

20962099
function updateFormState<S, P>(
2097-
action: (S, P) => Promise<S>,
2098-
initialState: S,
2100+
action: (Awaited<S>, P) => S,
2101+
initialState: Awaited<S>,
20992102
permalink?: string,
2100-
): [S, (P) => void] {
2103+
): [Awaited<S>, (P) => void] {
21012104
const stateHook = updateWorkInProgressHook();
21022105
const currentStateHook = ((currentHook: any): Hook);
21032106
return updateFormStateImpl(
@@ -2112,23 +2115,23 @@ function updateFormState<S, P>(
21122115
function updateFormStateImpl<S, P>(
21132116
stateHook: Hook,
21142117
currentStateHook: Hook,
2115-
action: (S, P) => Promise<S>,
2116-
initialState: S,
2118+
action: (Awaited<S>, P) => S,
2119+
initialState: Awaited<S>,
21172120
permalink?: string,
2118-
): [S, (P) => void] {
2121+
): [Awaited<S>, (P) => void] {
21192122
const [actionResult] = updateReducerImpl<S | Thenable<S>, S | Thenable<S>>(
21202123
stateHook,
21212124
currentStateHook,
21222125
formStateReducer,
21232126
);
21242127

21252128
// This will suspend until the action finishes.
2126-
const state: S =
2129+
const state: Awaited<S> =
21272130
typeof actionResult === 'object' &&
21282131
actionResult !== null &&
21292132
// $FlowFixMe[method-unbinding]
21302133
typeof actionResult.then === 'function'
2131-
? useThenable(((actionResult: any): Thenable<S>))
2134+
? useThenable(((actionResult: any): Thenable<Awaited<S>>))
21322135
: (actionResult: any);
21332136

21342137
const actionQueueHook = updateWorkInProgressHook();
@@ -2152,16 +2155,16 @@ function updateFormStateImpl<S, P>(
21522155

21532156
function formStateActionEffect<S, P>(
21542157
actionQueue: FormStateActionQueue<S, P>,
2155-
action: (S, P) => Promise<S>,
2158+
action: (Awaited<S>, P) => S,
21562159
): void {
21572160
actionQueue.action = action;
21582161
}
21592162

21602163
function rerenderFormState<S, P>(
2161-
action: (S, P) => Promise<S>,
2162-
initialState: S,
2164+
action: (Awaited<S>, P) => S,
2165+
initialState: Awaited<S>,
21632166
permalink?: string,
2164-
): [S, (P) => void] {
2167+
): [Awaited<S>, (P) => void] {
21652168
// Unlike useState, useFormState doesn't support render phase updates.
21662169
// Also unlike useState, we need to replay all pending updates again in case
21672170
// the passthrough value changed.
@@ -2184,7 +2187,7 @@ function rerenderFormState<S, P>(
21842187
}
21852188

21862189
// This is a mount. No updates to process.
2187-
const state: S = stateHook.memoizedState;
2190+
const state: Awaited<S> = stateHook.memoizedState;
21882191

21892192
const actionQueueHook = updateWorkInProgressHook();
21902193
const actionQueue = actionQueueHook.queue;
@@ -3735,10 +3738,10 @@ if (__DEV__) {
37353738
useHostTransitionStatus;
37363739
(HooksDispatcherOnMountInDEV: Dispatcher).useFormState =
37373740
function useFormState<S, P>(
3738-
action: (S, P) => Promise<S>,
3739-
initialState: S,
3741+
action: (Awaited<S>, P) => S,
3742+
initialState: Awaited<S>,
37403743
permalink?: string,
3741-
): [S, (P) => void] {
3744+
): [Awaited<S>, (P) => void] {
37423745
currentHookNameInDev = 'useFormState';
37433746
mountHookTypesDev();
37443747
return mountFormState(action, initialState, permalink);
@@ -3905,10 +3908,10 @@ if (__DEV__) {
39053908
useHostTransitionStatus;
39063909
(HooksDispatcherOnMountWithHookTypesInDEV: Dispatcher).useFormState =
39073910
function useFormState<S, P>(
3908-
action: (S, P) => Promise<S>,
3909-
initialState: S,
3911+
action: (Awaited<S>, P) => S,
3912+
initialState: Awaited<S>,
39103913
permalink?: string,
3911-
): [S, (P) => void] {
3914+
): [Awaited<S>, (P) => void] {
39123915
currentHookNameInDev = 'useFormState';
39133916
updateHookTypesDev();
39143917
return mountFormState(action, initialState, permalink);
@@ -4077,10 +4080,10 @@ if (__DEV__) {
40774080
useHostTransitionStatus;
40784081
(HooksDispatcherOnUpdateInDEV: Dispatcher).useFormState =
40794082
function useFormState<S, P>(
4080-
action: (S, P) => Promise<S>,
4081-
initialState: S,
4083+
action: (Awaited<S>, P) => S,
4084+
initialState: Awaited<S>,
40824085
permalink?: string,
4083-
): [S, (P) => void] {
4086+
): [Awaited<S>, (P) => void] {
40844087
currentHookNameInDev = 'useFormState';
40854088
updateHookTypesDev();
40864089
return updateFormState(action, initialState, permalink);
@@ -4249,10 +4252,10 @@ if (__DEV__) {
42494252
useHostTransitionStatus;
42504253
(HooksDispatcherOnRerenderInDEV: Dispatcher).useFormState =
42514254
function useFormState<S, P>(
4252-
action: (S, P) => Promise<S>,
4253-
initialState: S,
4255+
action: (Awaited<S>, P) => S,
4256+
initialState: Awaited<S>,
42544257
permalink?: string,
4255-
): [S, (P) => void] {
4258+
): [Awaited<S>, (P) => void] {
42564259
currentHookNameInDev = 'useFormState';
42574260
updateHookTypesDev();
42584261
return rerenderFormState(action, initialState, permalink);
@@ -4442,10 +4445,10 @@ if (__DEV__) {
44424445
useHostTransitionStatus;
44434446
(InvalidNestedHooksDispatcherOnMountInDEV: Dispatcher).useFormState =
44444447
function useFormState<S, P>(
4445-
action: (S, P) => Promise<S>,
4446-
initialState: S,
4448+
action: (Awaited<S>, P) => S,
4449+
initialState: Awaited<S>,
44474450
permalink?: string,
4448-
): [S, (P) => void] {
4451+
): [Awaited<S>, (P) => void] {
44494452
currentHookNameInDev = 'useFormState';
44504453
warnInvalidHookAccess();
44514454
mountHookTypesDev();
@@ -4640,10 +4643,10 @@ if (__DEV__) {
46404643
useHostTransitionStatus;
46414644
(InvalidNestedHooksDispatcherOnUpdateInDEV: Dispatcher).useFormState =
46424645
function useFormState<S, P>(
4643-
action: (S, P) => Promise<S>,
4644-
initialState: S,
4646+
action: (Awaited<S>, P) => S,
4647+
initialState: Awaited<S>,
46454648
permalink?: string,
4646-
): [S, (P) => void] {
4649+
): [Awaited<S>, (P) => void] {
46474650
currentHookNameInDev = 'useFormState';
46484651
warnInvalidHookAccess();
46494652
updateHookTypesDev();
@@ -4838,10 +4841,10 @@ if (__DEV__) {
48384841
useHostTransitionStatus;
48394842
(InvalidNestedHooksDispatcherOnRerenderInDEV: Dispatcher).useFormState =
48404843
function useFormState<S, P>(
4841-
action: (S, P) => Promise<S>,
4842-
initialState: S,
4844+
action: (Awaited<S>, P) => S,
4845+
initialState: Awaited<S>,
48434846
permalink?: string,
4844-
): [S, (P) => void] {
4847+
): [Awaited<S>, (P) => void] {
48454848
currentHookNameInDev = 'useFormState';
48464849
warnInvalidHookAccess();
48474850
updateHookTypesDev();

0 commit comments

Comments
 (0)