@@ -149,11 +149,13 @@ import type {ThenableState} from './ReactFiberThenable';
149149import type { BatchConfigTransition } from './ReactFiberTracingMarkerComponent' ;
150150import { requestAsyncActionContext } from './ReactFiberAsyncAction' ;
151151import { HostTransitionContext } from './ReactFiberHostContext' ;
152+ import { requestTransitionLane } from './ReactFiberRootScheduler' ;
152153
153154const { ReactCurrentDispatcher, ReactCurrentBatchConfig} = ReactSharedInternals ;
154155
155156export type Update < S , A > = {
156157 lane : Lane ,
158+ revertLane : Lane ,
157159 action : A ,
158160 hasEagerState : boolean ,
159161 eagerState : S | null ,
@@ -1136,6 +1138,14 @@ function updateReducer<S, I, A>(
11361138 init ?: I => S ,
11371139) : [ S , Dispatch < A > ] {
11381140 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 > ] {
11391149 const queue = hook . queue ;
11401150
11411151 if ( queue === null ) {
@@ -1146,10 +1156,8 @@ function updateReducer<S, I, A>(
11461156
11471157 queue . lastRenderedReducer = reducer ;
11481158
1149- const current : Hook = ( currentHook : any ) ;
1150-
11511159 // The last rebase update that is NOT part of the base state.
1152- let baseQueue = current . baseQueue ;
1160+ let baseQueue = hook . baseQueue ;
11531161
11541162 // The last pending update that hasn't been processed yet.
11551163 const pendingQueue = queue . pending ;
@@ -1180,7 +1188,7 @@ function updateReducer<S, I, A>(
11801188 if ( baseQueue !== null ) {
11811189 // We have a queue to process.
11821190 const first = baseQueue . next ;
1183- let newState = current . baseState ;
1191+ let newState = hook . baseState ;
11841192
11851193 let newBaseState = null ;
11861194 let newBaseQueueFirst = null ;
@@ -1206,6 +1214,7 @@ function updateReducer<S, I, A>(
12061214 // update/state.
12071215 const clone : Update < S , A> = {
12081216 lane : updateLane ,
1217+ revertLane : update . revertLane ,
12091218 action : update . action ,
12101219 hasEagerState : update . hasEagerState ,
12111220 eagerState : update . eagerState ,
@@ -1228,18 +1237,68 @@ function updateReducer<S, I, A>(
12281237 } else {
12291238 // This update does have sufficient priority.
12301239
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+ }
12431302 }
12441303
12451304 // Process this update.
@@ -1899,56 +1958,106 @@ function mountStateImpl<S>(initialState: (() => S) | S): Hook {
18991958 lastRenderedState : ( initialState : any ) ,
19001959 } ;
19011960 hook.queue = queue;
1902- const dispatch: Dispatch< BasicStateAction < S > > = ( dispatchSetState . bind (
1903- null ,
1904- currentlyRenderingFiber ,
1905- queue ,
1906- ) : any ) ;
1907- queue . dispatch = dispatch ;
19081961 return hook;
19091962}
19101963
19111964function mountState < S > (
19121965 initialState: (() => S ) | S ,
19131966) : [ S , Dispatch < BasicStateAction < S > > ] {
19141967 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 ] ;
19161976}
19171977
19181978function updateState< S > (
19191979 initialState: (() => S ) | S ,
19201980) : [ S , Dispatch < BasicStateAction < S > > ] {
1921- return updateReducer ( basicStateReducer , ( initialState : any ) ) ;
1981+ return updateReducer ( basicStateReducer , initialState ) ;
19221982}
19231983
19241984function rerenderState< S > (
19251985 initialState: (() => S ) | S ,
19261986) : [ S , Dispatch < BasicStateAction < S > > ] {
1927- return rerenderReducer ( basicStateReducer , ( initialState : any ) ) ;
1987+ return rerenderReducer ( basicStateReducer , initialState ) ;
19281988}
19291989
19301990function mountOptimisticState< S , A > (
19311991 passthrough: S,
19321992 reducer: ?(S, A) => S ,
19331993) : [ 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 ] ;
19362014}
19372015
19382016function updateOptimisticState< S , A > (
19392017 passthrough: S,
19402018 reducer: ?(S, A) => S ,
19412019) : [ 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 ) ;
19442035}
19452036
19462037function rerenderOptimisticState< S , A > (
19472038 passthrough: S,
19482039 reducer: ?(S, A) => S ,
19492040) : [ 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];
19522061}
19532062
19542063function pushEffect (
@@ -2490,9 +2599,15 @@ function startTransition<S>(
24902599 higherEventPriority ( previousPriority , ContinuousEventPriority ) ,
24912600 ) ;
24922601
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+
24932610 const prevTransition = ReactCurrentBatchConfig . transition ;
2494- ReactCurrentBatchConfig . transition = null ;
2495- dispatchSetState ( fiber , queue , pendingState ) ;
24962611 const currentTransition = ( ReactCurrentBatchConfig . transition =
24972612 ( { } : BatchConfigTransition ) ) ;
24982613
@@ -2827,6 +2942,7 @@ function dispatchReducerAction<S, A>(
28272942
28282943 const update : Update < S , A > = {
28292944 lane ,
2945+ revertLane : NoLane ,
28302946 action ,
28312947 hasEagerState : false ,
28322948 eagerState : null ,
@@ -2865,6 +2981,7 @@ function dispatchSetState<S, A>(
28652981
28662982 const update : Update < S , A > = {
28672983 lane ,
2984+ revertLane : NoLane ,
28682985 action ,
28692986 hasEagerState : false ,
28702987 eagerState : null ,
@@ -2928,6 +3045,54 @@ function dispatchSetState<S, A>(
29283045 markUpdateInDevTools ( fiber , lane , action ) ;
29293046}
29303047
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+
29313096function isRenderPhaseUpdate(fiber: Fiber): boolean {
29323097 const alternate = fiber . alternate ;
29333098 return (
0 commit comments