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