@@ -149,11 +149,13 @@ import type {ThenableState} from './ReactFiberThenable';
149
149
import type { BatchConfigTransition } from './ReactFiberTracingMarkerComponent' ;
150
150
import { requestAsyncActionContext } from './ReactFiberAsyncAction' ;
151
151
import { HostTransitionContext } from './ReactFiberHostContext' ;
152
+ import { requestTransitionLane } from './ReactFiberRootScheduler' ;
152
153
153
154
const { ReactCurrentDispatcher, ReactCurrentBatchConfig} = ReactSharedInternals ;
154
155
155
156
export type Update < S , A > = {
156
157
lane : Lane ,
158
+ revertLane : Lane ,
157
159
action : A ,
158
160
hasEagerState : boolean ,
159
161
eagerState : S | null ,
@@ -1132,6 +1134,14 @@ function updateReducer<S, I, A>(
1132
1134
init ?: I => S ,
1133
1135
) : [ S , Dispatch < A > ] {
1134
1136
const hook = updateWorkInProgressHook ( ) ;
1137
+ return updateReducerImpl ( hook , ( ( currentHook : any ) : Hook ) , reducer ) ;
1138
+ }
1139
+
1140
+ function updateReducerImpl < S , A > (
1141
+ hook : Hook ,
1142
+ current : Hook ,
1143
+ reducer : ( S , A ) => S ,
1144
+ ) : [ S , Dispatch < A > ] {
1135
1145
const queue = hook . queue ;
1136
1146
1137
1147
if ( queue === null ) {
@@ -1142,10 +1152,8 @@ function updateReducer<S, I, A>(
1142
1152
1143
1153
queue . lastRenderedReducer = reducer ;
1144
1154
1145
- const current : Hook = ( currentHook : any ) ;
1146
-
1147
1155
// The last rebase update that is NOT part of the base state.
1148
- let baseQueue = current . baseQueue ;
1156
+ let baseQueue = hook . baseQueue ;
1149
1157
1150
1158
// The last pending update that hasn't been processed yet.
1151
1159
const pendingQueue = queue . pending ;
@@ -1176,7 +1184,7 @@ function updateReducer<S, I, A>(
1176
1184
if ( baseQueue !== null ) {
1177
1185
// We have a queue to process.
1178
1186
const first = baseQueue . next ;
1179
- let newState = current . baseState ;
1187
+ let newState = hook . baseState ;
1180
1188
1181
1189
let newBaseState = null ;
1182
1190
let newBaseQueueFirst = null ;
@@ -1202,6 +1210,7 @@ function updateReducer<S, I, A>(
1202
1210
// update/state.
1203
1211
const clone : Update < S , A> = {
1204
1212
lane : updateLane ,
1213
+ revertLane : update . revertLane ,
1205
1214
action : update . action ,
1206
1215
hasEagerState : update . hasEagerState ,
1207
1216
eagerState : update . eagerState ,
@@ -1224,18 +1233,68 @@ function updateReducer<S, I, A>(
1224
1233
} else {
1225
1234
// This update does have sufficient priority.
1226
1235
1227
- if ( newBaseQueueLast !== null ) {
1228
- const clone : Update < S , A > = {
1229
- // This update is going to be committed so we never want uncommit
1230
- // it. Using NoLane works because 0 is a subset of all bitmasks, so
1231
- // this will never be skipped by the check above.
1232
- lane : NoLane ,
1233
- action : update . action ,
1234
- hasEagerState : update . hasEagerState ,
1235
- eagerState : update . eagerState ,
1236
- next : ( null : any ) ,
1237
- } ;
1238
- newBaseQueueLast = newBaseQueueLast . next = clone ;
1236
+ // Check if this is an optimistic update.
1237
+ const revertLane = update . revertLane ;
1238
+ if ( revertLane === NoLane ) {
1239
+ // This is not an optimistic update, and we're going to apply it now.
1240
+ // But, if there were earlier updates that were skipped, we need to
1241
+ // leave this update in the queue so it can be rebased later.
1242
+ if ( newBaseQueueLast !== null ) {
1243
+ const clone : Update < S , A> = {
1244
+ // This update is going to be committed so we never want uncommit
1245
+ // it. Using NoLane works because 0 is a subset of all bitmasks, so
1246
+ // this will never be skipped by the check above.
1247
+ lane : NoLane ,
1248
+ revertLane : NoLane ,
1249
+ action : update . action ,
1250
+ hasEagerState : update . hasEagerState ,
1251
+ eagerState : update . eagerState ,
1252
+ next : ( null : any ) ,
1253
+ } ;
1254
+ newBaseQueueLast = newBaseQueueLast . next = clone ;
1255
+ }
1256
+ } else {
1257
+ // This is an optimistic update. If the "revert" priority is
1258
+ // sufficient, don't apply the update. Otherwise, apply the update,
1259
+ // but leave it in the queue so it can be either reverted or
1260
+ // rebased in a subsequent render.
1261
+ if ( isSubsetOfLanes ( renderLanes , revertLane ) ) {
1262
+ // The transition that this optimistic update is associated with
1263
+ // has finished. Pretend the update doesn't exist by skipping
1264
+ // over it.
1265
+ update = update . next ;
1266
+ continue ;
1267
+ } else {
1268
+ const clone : Update < S , A> = {
1269
+ // Once we commit an optimistic update, we shouldn't uncommit it
1270
+ // until the transition it is associated with has finished
1271
+ // (represented by revertLane). Using NoLane here works because 0
1272
+ // is a subset of all bitmasks, so this will never be skipped by
1273
+ // the check above.
1274
+ lane : NoLane ,
1275
+ // Reuse the same revertLane so we know when the transition
1276
+ // has finished.
1277
+ revertLane : update . revertLane ,
1278
+ action : update . action ,
1279
+ hasEagerState : update . hasEagerState ,
1280
+ eagerState : update . eagerState ,
1281
+ next : ( null : any ) ,
1282
+ } ;
1283
+ if ( newBaseQueueLast === null ) {
1284
+ newBaseQueueFirst = newBaseQueueLast = clone ;
1285
+ newBaseState = newState ;
1286
+ } else {
1287
+ newBaseQueueLast = newBaseQueueLast . next = clone ;
1288
+ }
1289
+ // Update the remaining priority in the queue.
1290
+ // TODO: Don't need to accumulate this. Instead, we can remove
1291
+ // renderLanes from the original lanes.
1292
+ currentlyRenderingFiber . lanes = mergeLanes (
1293
+ currentlyRenderingFiber . lanes ,
1294
+ revertLane ,
1295
+ ) ;
1296
+ markSkippedUpdateLanes ( revertLane ) ;
1297
+ }
1239
1298
}
1240
1299
1241
1300
// Process this update.
@@ -1895,56 +1954,106 @@ function mountStateImpl<S>(initialState: (() => S) | S): Hook {
1895
1954
lastRenderedState : ( initialState : any ) ,
1896
1955
} ;
1897
1956
hook.queue = queue;
1898
- const dispatch: Dispatch< BasicStateAction < S > > = ( dispatchSetState . bind (
1899
- null ,
1900
- currentlyRenderingFiber ,
1901
- queue ,
1902
- ) : any ) ;
1903
- queue . dispatch = dispatch ;
1904
1957
return hook;
1905
1958
}
1906
1959
1907
1960
function mountState < S > (
1908
1961
initialState: (() => S ) | S ,
1909
1962
) : [ S , Dispatch < BasicStateAction < S > > ] {
1910
1963
const hook = mountStateImpl ( initialState ) ;
1911
- return [ hook . memoizedState , hook . queue . dispatch ] ;
1964
+ const queue = hook . queue ;
1965
+ const dispatch : Dispatch < BasicStateAction < S >> = ( dispatchSetState . bind (
1966
+ null ,
1967
+ currentlyRenderingFiber ,
1968
+ queue ,
1969
+ ) : any ) ;
1970
+ queue . dispatch = dispatch ;
1971
+ return [ hook . memoizedState , dispatch ] ;
1912
1972
}
1913
1973
1914
1974
function updateState< S > (
1915
1975
initialState: (() => S ) | S ,
1916
1976
) : [ S , Dispatch < BasicStateAction < S > > ] {
1917
- return updateReducer ( basicStateReducer , ( initialState : any ) ) ;
1977
+ return updateReducer ( basicStateReducer , initialState ) ;
1918
1978
}
1919
1979
1920
1980
function rerenderState< S > (
1921
1981
initialState: (() => S ) | S ,
1922
1982
) : [ S , Dispatch < BasicStateAction < S > > ] {
1923
- return rerenderReducer ( basicStateReducer , ( initialState : any ) ) ;
1983
+ return rerenderReducer ( basicStateReducer , initialState ) ;
1924
1984
}
1925
1985
1926
1986
function mountOptimisticState< S , A > (
1927
1987
passthrough: S,
1928
1988
reducer: ?(S, A) => S ,
1929
1989
) : [ S , ( A ) => void ] {
1930
- // $FlowFixMe - TODO: Actual implementation
1931
- return mountState ( passthrough ) ;
1990
+ const hook = mountWorkInProgressHook ( ) ;
1991
+ hook . memoizedState = hook . baseState = passthrough ;
1992
+ const queue : UpdateQueue < S , A > = {
1993
+ pending : null ,
1994
+ lanes : NoLanes ,
1995
+ dispatch : null ,
1996
+ // Optimistic state does not use the eager update optimization.
1997
+ lastRenderedReducer : null ,
1998
+ lastRenderedState : null ,
1999
+ } ;
2000
+ hook . queue = queue ;
2001
+ // This is different than the normal setState function.
2002
+ const dispatch : A => void = ( dispatchOptimisticSetState . bind (
2003
+ null ,
2004
+ currentlyRenderingFiber ,
2005
+ true ,
2006
+ queue ,
2007
+ ) : any ) ;
2008
+ queue . dispatch = dispatch ;
2009
+ return [ passthrough , dispatch ] ;
1932
2010
}
1933
2011
1934
2012
function updateOptimisticState< S , A > (
1935
2013
passthrough: S,
1936
2014
reducer: ?(S, A) => S ,
1937
2015
) : [ S , ( A ) => void ] {
1938
- // $FlowFixMe - TODO: Actual implementation
1939
- return updateState ( passthrough ) ;
2016
+ const hook = updateWorkInProgressHook ( ) ;
2017
+
2018
+ // Optimistic updates are always rebased on top of the latest value passed in
2019
+ // as an argument. It's called a passthrough because if there are no pending
2020
+ // updates, it will be returned as-is.
2021
+ //
2022
+ // Reset the base state and memoized state to the passthrough. Future
2023
+ // updates will be applied on top of this.
2024
+ hook . baseState = hook . memoizedState = passthrough ;
2025
+
2026
+ // If a reducer is not provided, default to the same one used by useState.
2027
+ const resolvedReducer : ( S , A ) = > S =
2028
+ typeof reducer === 'function' ? reducer : ( basicStateReducer : any ) ;
2029
+
2030
+ return updateReducerImpl ( hook , ( ( currentHook : any ) : Hook ) , resolvedReducer ) ;
1940
2031
}
1941
2032
1942
2033
function rerenderOptimisticState< S , A > (
1943
2034
passthrough: S,
1944
2035
reducer: ?(S, A) => S ,
1945
2036
) : [ S , ( A ) => void ] {
1946
- // $FlowFixMe - TODO: Actual implementation
1947
- return rerenderState ( passthrough ) ;
2037
+ // Unlike useState, useOptimisticState doesn't support render phase updates.
2038
+ // Also unlike useState, we need to replay all pending updates again in case
2039
+ // the passthrough value changed.
2040
+ //
2041
+ // So instead of a forked re-render implementation that knows how to handle
2042
+ // render phase udpates, we can use the same implementation as during a
2043
+ // regular mount or update.
2044
+
2045
+ if ( currentHook !== null ) {
2046
+ // This is an update. Process the update queue.
2047
+ return updateOptimisticState ( passthrough , reducer ) ;
2048
+ }
2049
+
2050
+ // This is a mount. No updates to process.
2051
+ const hook = updateWorkInProgressHook();
2052
+ // Reset the base state and memoized state to the passthrough. Future
2053
+ // updates will be applied on top of this.
2054
+ hook.baseState = hook.memoizedState = passthrough;
2055
+ const dispatch = hook.queue.dispatch;
2056
+ return [passthrough, dispatch];
1948
2057
}
1949
2058
1950
2059
function pushEffect (
@@ -2486,9 +2595,15 @@ function startTransition<S>(
2486
2595
higherEventPriority ( previousPriority , ContinuousEventPriority ) ,
2487
2596
) ;
2488
2597
2598
+ // We don't really need to use an optimistic update here, because we schedule
2599
+ // a second "revert" update below (which we use to suspend the transition
2600
+ // until the async action scope has finished). But we'll use an optimistic
2601
+ // update anyway to make it less likely the behavior accidentally diverges;
2602
+ // for example, both an optimistic update and this one should share the
2603
+ // same lane.
2604
+ dispatchOptimisticSetState ( fiber , false , queue , pendingState ) ;
2605
+
2489
2606
const prevTransition = ReactCurrentBatchConfig . transition ;
2490
- ReactCurrentBatchConfig . transition = null ;
2491
- dispatchSetState ( fiber , queue , pendingState ) ;
2492
2607
const currentTransition = ( ReactCurrentBatchConfig . transition =
2493
2608
( { } : BatchConfigTransition ) ) ;
2494
2609
@@ -2823,6 +2938,7 @@ function dispatchReducerAction<S, A>(
2823
2938
2824
2939
const update : Update < S , A > = {
2825
2940
lane ,
2941
+ revertLane : NoLane ,
2826
2942
action ,
2827
2943
hasEagerState : false ,
2828
2944
eagerState : null ,
@@ -2861,6 +2977,7 @@ function dispatchSetState<S, A>(
2861
2977
2862
2978
const update : Update < S , A > = {
2863
2979
lane ,
2980
+ revertLane : NoLane ,
2864
2981
action ,
2865
2982
hasEagerState : false ,
2866
2983
eagerState : null ,
@@ -2924,6 +3041,54 @@ function dispatchSetState<S, A>(
2924
3041
markUpdateInDevTools ( fiber , lane , action ) ;
2925
3042
}
2926
3043
3044
+ function dispatchOptimisticSetState < S , A > (
3045
+ fiber: Fiber,
3046
+ throwIfDuringRender: boolean,
3047
+ queue: UpdateQueue< S , A > ,
3048
+ action: A,
3049
+ ): void {
3050
+ const update : Update < S , A > = {
3051
+ // An optimistic update commits synchronously.
3052
+ lane : SyncLane ,
3053
+ // After committing, the optimistic update is "reverted" using the same
3054
+ // lane as the transition it's associated with.
3055
+ //
3056
+ // TODO: Warn if there's no transition/action associated with this
3057
+ // optimistic update.
3058
+ revertLane : requestTransitionLane ( ) ,
3059
+ action,
3060
+ hasEagerState : false ,
3061
+ eagerState : null ,
3062
+ next : ( null : any ) ,
3063
+ } ;
3064
+
3065
+ if ( isRenderPhaseUpdate ( fiber ) ) {
3066
+ // When calling startTransition during render, this warns instead of
3067
+ // throwing because throwing would be a breaking change. setOptimisticState
3068
+ // is a new API so it's OK to throw.
3069
+ if ( throwIfDuringRender ) {
3070
+ throw new Error ( 'Cannot update optimistic state while rendering.' ) ;
3071
+ } else {
3072
+ // startTransition was called during render. We don't need to do anything
3073
+ // besides warn here because the render phase update would be overidden by
3074
+ // the second update, anyway. We can remove this branch and make it throw
3075
+ // in a future release.
3076
+ if ( __DEV__ ) {
3077
+ console . error ( 'Cannot call startTransition state while rendering.' ) ;
3078
+ }
3079
+ }
3080
+ } else {
3081
+ const root = enqueueConcurrentHookUpdate ( fiber , queue , update , SyncLane ) ;
3082
+ if ( root !== null ) {
3083
+ scheduleUpdateOnFiber ( root , fiber , SyncLane ) ;
3084
+ // Optimistic updates are always synchronous, so we don't need to call
3085
+ // entangleTransitionUpdate here.
3086
+ }
3087
+ }
3088
+
3089
+ markUpdateInDevTools ( fiber , SyncLane , action ) ;
3090
+ }
3091
+
2927
3092
function isRenderPhaseUpdate(fiber: Fiber): boolean {
2928
3093
const alternate = fiber . alternate ;
2929
3094
return (
0 commit comments