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