137
} from './ReactFiberThenable';
138
import type {ThenableState} from './ReactFiberThenable';
139
import type {BatchConfigTransition} from './ReactFiberTracingMarkerComponent';
140
-import {requestAsyncActionContext} from './ReactFiberAsyncAction';
140
+import {
141
+ requestAsyncActionContext,
142
+ requestSyncActionContext,
143
+} from './ReactFiberAsyncAction';
144
import {HostTransitionContext} from './ReactFiberHostContext';
145
import {requestTransitionLane} from './ReactFiberRootScheduler';
146
1857
return [passthrough, dispatch];
1858
}
1859
1857
-function TODO_formStateDispatch() {
1858
- throw new Error('Not implemented.');
1860
+// useFormState actions run sequentially, because each action receives the
1861
+// previous state as an argument. We store pending actions on a queue.
1862
+type FormStateActionQueue<S, P> = {
1863
+ // This is the most recent state returned from an action. It's updated as
1864
+ // soon as the action finishes running.
1865
+ state: S,
1866
+ // A stable dispatch method, passed to the user.
1867
+ dispatch: Dispatch<P>,
1868
+ // This is the most recent action function that was rendered. It's updated
1869
+ // during the commit phase.
1870
+ action: (S, P) => Promise<S>,
1871
+ // This is a circular linked list of pending action payloads. It incudes the
1872
+ // action that is currently running.
1873
+ pending: FormStateActionQueueNode<P> | null,
1874
+};
1875
+
1876
+type FormStateActionQueueNode<P> = {
1877
+ payload: P,
1878
+ // This is never null because it's part of a circular linked list.
1879
+ next: FormStateActionQueueNode<P>,
1880
+};
1881
+
1882
+function dispatchFormState<S, P>(
1883
+ fiber: Fiber,
1884
+ actionQueue: FormStateActionQueue<S, P>,
1885
+ setState: Dispatch<Thenable<S>>,
1886
+ payload: P,
1887
+): void {
1888
+ if (isRenderPhaseUpdate(fiber)) {
1889
+ throw new Error('Cannot update form state while rendering.');
1890
+ }
1891
+ const last = actionQueue.pending;
1892
+ if (last === null) {
1893
+ // There are no pending actions; this is the first one. We can run
1894
+ // it immediately.
1895
+ const newLast: FormStateActionQueueNode<P> = {
1896
+ payload,
1897
+ next: (null: any), // circular
1898
+ };
1899
+ newLast.next = actionQueue.pending = newLast;
1900
+
1901
+ runFormStateAction(actionQueue, setState, payload);
1902
+ } else {
1903
+ // There's already an action running. Add to the queue.
1904
+ const first = last.next;
1905
+ const newLast: FormStateActionQueueNode<P> = {
1906
+ payload,
1907
+ next: first,
1908
+ };
1909
+ last.next = newLast;
1910
+ }
1911
+}
1912
+
1913
+function runFormStateAction<S, P>(
1914
+ actionQueue: FormStateActionQueue<S, P>,
1915
+ setState: Dispatch<Thenable<S>>,
1916
+ payload: P,
1917
+) {
1918
+ const action = actionQueue.action;
1919
+ const prevState = actionQueue.state;
1920
+
1921
+ // This is a fork of startTransition
1922
+ const prevTransition = ReactCurrentBatchConfig.transition;
1923
+ ReactCurrentBatchConfig.transition = ({}: BatchConfigTransition);
1924
+ const currentTransition = ReactCurrentBatchConfig.transition;
1925
+ if (__DEV__) {
1926
+ ReactCurrentBatchConfig.transition._updatedFibers = new Set();
1927
+ }
1928
+ try {
1929
+ const promise = action(prevState, payload);
1930
+
1931
+ if (__DEV__) {
1932
+ if (
1933
+ promise === null ||
1934
+ typeof promise !== 'object' ||
1935
+ typeof (promise: any).then !== 'function'
1936
+ ) {
1937
+ console.error(
1938
+ 'The action passed to useFormState must be an async function.',
1939
+ );
1940
+ }
1941
+ }
1942
+
1943
+ // Attach a listener to read the return state of the action. As soon as this
1944
+ // resolves, we can run the next action in the sequence.
1945
+ promise.then(
1946
+ (nextState: S) => {
1947
+ actionQueue.state = nextState;
1948
+ finishRunningFormStateAction(actionQueue, setState);
1949
+ },
1950
+ () => finishRunningFormStateAction(actionQueue, setState),
1951
+ );
1952
+
1953
+ // Create a thenable that resolves once the current async action scope has
1954
+ // finished. Then stash that thenable in state. We'll unwrap it with the
1955
+ // `use` algorithm during render. This is the same logic used
1956
+ // by startTransition.
1957
+ const entangledThenable: Thenable<S> = requestAsyncActionContext(
1958
+ promise,
1959
+ null,
1960
+ );
1961
+ setState(entangledThenable);
1962
+ } finally {
1963
+ ReactCurrentBatchConfig.transition = prevTransition;
1964
+
1965
+ if (__DEV__) {
1966
+ if (prevTransition === null && currentTransition._updatedFibers) {
1967
+ const updatedFibersCount = currentTransition._updatedFibers.size;
1968
+ currentTransition._updatedFibers.clear();
1969
+ if (updatedFibersCount > 10) {
1970
+ console.warn(
1971
+ 'Detected a large number of updates inside startTransition. ' +
1972
+ 'If this is due to a subscription please re-write it to use React provided hooks. ' +
1973
+ 'Otherwise concurrent mode guarantees are off the table.',
1974
+ );
1975
+ }
1976
+ }
1977
+ }
1978
+ }
1979
+}
1980
+
1981
+function finishRunningFormStateAction<S, P>(
1982
+ actionQueue: FormStateActionQueue<S, P>,
1983
+ setState: Dispatch<Thenable<S>>,
1984
+) {
1985
+ // The action finished running. Pop it from the queue and run the next pending
1986
+ // action, if there are any.
1987
+ const last = actionQueue.pending;
1988
+ if (last !== null) {
1989
+ const first = last.next;
1990
+ if (first === last) {
1991
+ // This was the last action in the queue.
1992
+ actionQueue.pending = null;
1993
+ } else {
1994
+ // Remove the first node from the circular queue.
1995
+ const next = first.next;
1996
+ last.next = next;
1997
+
1998
+ // Run the next action.
1999
+ runFormStateAction(actionQueue, setState, next.payload);
2000
+ }
2001
+ }
2002
+}
2003
+
2004
+function formStateReducer<S>(oldState: S, newState: S): S {
2005
+ return newState;
2006
}
2007
2008
function mountFormState<S, P>(
1862
- action: (S, P) => S,
2009
+ action: (S, P) => Promise<S>,
2010
initialState: S,
2011
url?: string,
2012
): [S, (P) => void] {
1866
- // TODO: Not yet implemented
1867
- return [initialState, TODO_formStateDispatch];
2013
+ // State hook. The state is stored in a thenable which is then unwrapped by
2014
+ // the `use` algorithm during render.
2015
+ const stateHook = mountWorkInProgressHook();
2016
+ stateHook.memoizedState = stateHook.baseState = {
2017
+ status: 'fulfilled',
2018
+ value: initialState,
2019
+ };
2020
+ const stateQueue: UpdateQueue<Thenable<S>, Thenable<S>> = {
2021
+ pending: null,
2022
+ lanes: NoLanes,
2023
+ dispatch: null,
2024
+ lastRenderedReducer: formStateReducer,
2025
+ lastRenderedState: (initialState: any),
2026
+ };
2027
+ stateHook.queue = stateQueue;
2028
+ const setState: Dispatch<Thenable<S>> = (dispatchSetState.bind(
2029
+ null,
2030
+ currentlyRenderingFiber,
2031
+ stateQueue,
2032
+ ): any);
2033
+ stateQueue.dispatch = setState;
2034
+
2035
+ // Action queue hook. This is used to queue pending actions. The queue is
2036
+ // shared between all instances of the hook. Similar to a regular state queue,
2037
+ // but different because the actions are run sequentially, and they run in
2038
+ // an event instead of during render.
2039
+ const actionQueueHook = mountWorkInProgressHook();
2040
+ const actionQueue: FormStateActionQueue<S, P> = {
2041
+ state: initialState,
2042
+ dispatch: (null: any), // circular
2043
+ action,
2044
+ pending: null,
2045
+ };
2046
+ actionQueueHook.queue = actionQueue;
2047
+ const dispatch = dispatchFormState.bind(
2048
+ null,
2049
+ currentlyRenderingFiber,
2050
+ actionQueue,
2051
+ setState,
2052
+ );
2053
+ actionQueue.dispatch = dispatch;
2054
+
2055
+ // Stash the action function on the memoized state of the hook. We'll use this
2056
+ // to detect when the action function changes so we can update it in
2057
+ // an effect.
2058
+ actionQueueHook.memoizedState = action;
2059
+
2060
+ return [initialState, dispatch];
2061
}
2062
2063
function updateFormState<S, P>(
1871
- action: (S, P) => S,
2064
+ action: (S, P) => Promise<S>,
2065
initialState: S,
2066
url?: string,
2067
): [S, (P) => void] {
1875
- // TODO: Not yet implemented
1876
- return [initialState, TODO_formStateDispatch];
2068
+ const stateHook = updateWorkInProgressHook();
2069
+ const currentStateHook = ((currentHook: any): Hook);
2070
+ return updateFormStateImpl(
2071
+ stateHook,
2072
+ currentStateHook,
2073
+ action,
2074
+ initialState,
2075
+ url,
2076
+ );
2077
+}
2078
+
2079
+function updateFormStateImpl<S, P>(
2080
+ stateHook: Hook,
2081
+ currentStateHook: Hook,
2082
+ action: (S, P) => Promise<S>,
2083
+ initialState: S,
2084
+ url?: string,
2085
+): [S, (P) => void] {
2086
+ const [thenable] = updateReducerImpl<Thenable<S>, Thenable<S>>(
2087
+ stateHook,
2088
+ currentStateHook,
2089
+ formStateReducer,
2090
+ );
2091
+
2092
+ // This will suspend until the action finishes.
2093
+ const state = useThenable(thenable);
2094
+
2095
+ const actionQueueHook = updateWorkInProgressHook();
2096
+ const actionQueue = actionQueueHook.queue;
2097
+ const dispatch = actionQueue.dispatch;
2098
+
2099
+ // Check if a new action was passed. If so, update it in an effect.
2100
+ const prevAction = actionQueueHook.memoizedState;
2101
+ if (action !== prevAction) {
2102
+ currentlyRenderingFiber.flags |= PassiveEffect;
2103
+ pushEffect(
2104
+ HookHasEffect | HookPassive,
2105
+ formStateActionEffect.bind(null, actionQueue, action),
2106
+ createEffectInstance(),
2107
+ null,
2108
+ );
2109
+ }
2110
+
2111
+ return [state, dispatch];
2112
+}
2113
+
2114
+function formStateActionEffect<S, P>(
2115
+ actionQueue: FormStateActionQueue<S, P>,
2116
+ action: (S, P) => Promise<S>,
2117
+): void {
2118
+ actionQueue.action = action;
2119
}
2120
2121
function rerenderFormState<S, P>(
1880
- action: (S, P) => S,
2122
+ action: (S, P) => Promise<S>,
2123
initialState: S,
2124
url?: string,
2125
): [S, (P) => void] {
1884
- // TODO: Not yet implemented
1885
- return [initialState, TODO_formStateDispatch];
2126
+ // Unlike useState, useFormState doesn't support render phase updates.
2127
+ // Also unlike useState, we need to replay all pending updates again in case
2128
+ // the passthrough value changed.
2129
+ //
2130
+ // So instead of a forked re-render implementation that knows how to handle
2131
+ // render phase udpates, we can use the same implementation as during a
2132
+ // regular mount or update.
2133
+ const stateHook = updateWorkInProgressHook();
2134
+ const currentStateHook = currentHook;
2135
+
2136
+ if (currentStateHook !== null) {
2137
+ // This is an update. Process the update queue.
2138
+ return updateFormStateImpl(
2139
+ stateHook,
2140
+ currentStateHook,
2141
+ action,
2142
+ initialState,
2143
+ url,
2144
+ );
2145
+ }
2146
+
2147
+ // This is a mount. No updates to process.
2148
+ const state = stateHook.memoizedState;
2149
+
2150
+ const actionQueueHook = updateWorkInProgressHook();
2151
+ const actionQueue = actionQueueHook.queue;
2152
+ const dispatch = actionQueue.dispatch;
2153
+
2154
+ // This may have changed during the rerender.
2155
+ actionQueueHook.memoizedState = action;
2156
+
2157
+ return [state, dispatch];
2158
}
2159
2160
function pushEffect(
2731
if (enableAsyncActions) {
2732
const returnValue = callback();
2733
2462
- // This is either `finishedState` or a thenable that resolves to
2463
- // `finishedState`, depending on whether the action scope is an async
2464
- // function. In the async case, the resulting render will suspend until
2465
- // the async action scope has finished.
2466
- const maybeThenable = requestAsyncActionContext(
2467
- returnValue,
2468
- finishedState,
2469
- );
2470
- dispatchSetState(fiber, queue, maybeThenable);
2734
+ // Check if we're inside an async action scope. If so, we'll entangle
2735
+ // this new action with the existing scope.
2736
+ //
2737
+ // If we're not already inside an async action scope, and this action is
2738
+ // async, then we'll create a new async scope.
2739
+ //
2740
+ // In the async case, the resulting render will suspend until the async
2741
+ // action scope has finished.
2742
+ if (
2743
+ returnValue !== null &&
2744
+ typeof returnValue === 'object' &&
2745
+ typeof returnValue.then === 'function'
2746
+ ) {
2747
+ const thenable = ((returnValue: any): Thenable<mixed>);
2748
+ // This is a thenable that resolves to `finishedState` once the async
2749
+ // action scope has finished.
2750
+ const entangledResult = requestAsyncActionContext(
2751
+ thenable,
2752
+ finishedState,
2753
+ );
2754
+ dispatchSetState(fiber, queue, entangledResult);
2755
+ } else {
2756
+ // This is either `finishedState` or a thenable that resolves to
2757
+ // `finishedState`, depending on whether we're inside an async
2758
+ // action scope.
2759
+ const entangledResult = requestSyncActionContext(
2760
+ returnValue,
2761
+ finishedState,
2762
+ );
2763
+ dispatchSetState(fiber, queue, entangledResult);
2764
+ }
2765
} else {
2766
// Async actions are not enabled.
2767
dispatchSetState(fiber, queue, finishedState);
3626
useHostTransitionStatus;
3627
(HooksDispatcherOnMountInDEV: Dispatcher).useFormState =
3628
function useFormState<S, P>(
3335
- action: (S, P) => S,
3629
+ action: (S, P) => Promise<S>,
3630
initialState: S,
3631
url?: string,
3632
): [S, (P) => void] {
3796
useHostTransitionStatus;
3797
(HooksDispatcherOnMountWithHookTypesInDEV: Dispatcher).useFormState =
3798
function useFormState<S, P>(
3505
- action: (S, P) => S,
3799
+ action: (S, P) => Promise<S>,
3800
initialState: S,
3801
url?: string,
3802
): [S, (P) => void] {
3968
useHostTransitionStatus;
3969
(HooksDispatcherOnUpdateInDEV: Dispatcher).useFormState =
3970
function useFormState<S, P>(
3677
- action: (S, P) => S,
3971
+ action: (S, P) => Promise<S>,
3972
initialState: S,
3973
url?: string,
3974
): [S, (P) => void] {
4140
useHostTransitionStatus;
4141
(HooksDispatcherOnRerenderInDEV: Dispatcher).useFormState =
4142
function useFormState<S, P>(
3849
- action: (S, P) => S,
4143
+ action: (S, P) => Promise<S>,
4144
initialState: S,
4145
url?: string,
4146
): [S, (P) => void] {
4333
useHostTransitionStatus;
4334
(InvalidNestedHooksDispatcherOnMountInDEV: Dispatcher).useFormState =
4335
function useFormState<S, P>(
4042
- action: (S, P) => S,
4336
+ action: (S, P) => Promise<S>,
4337
initialState: S,
4338
url?: string,
4339
): [S, (P) => void] {
4531
useHostTransitionStatus;
4532
(InvalidNestedHooksDispatcherOnUpdateInDEV: Dispatcher).useFormState =
4533
function useFormState<S, P>(
4240
- action: (S, P) => S,
4534
+ action: (S, P) => Promise<S>,
4535
initialState: S,
4536
url?: string,
4537
): [S, (P) => void] {
4729
useHostTransitionStatus;
4730
(InvalidNestedHooksDispatcherOnRerenderInDEV: Dispatcher).useFormState =
4731
function useFormState<S, P>(
4438
- action: (S, P) => S,
4732
+ action: (S, P) => Promise<S>,
4733
initialState: S,
4734
url?: string,
4735
): [S, (P) => void] {