@@ -41,6 +41,7 @@ import {
4141} from './ReactTypeOfMode' ;
4242import {
4343 NoLane ,
44+ SyncLane ,
4445 NoLanes ,
4546 isSubsetOfLanes ,
4647 mergeLanes ,
@@ -49,9 +50,9 @@ import {
4950 isTransitionLane ,
5051 markRootEntangled ,
5152 markRootMutableRead ,
53+ NoTimestamp ,
5254} from './ReactFiberLane.new' ;
5355import {
54- DiscreteEventPriority ,
5556 ContinuousEventPriority ,
5657 getCurrentUpdatePriority ,
5758 setCurrentUpdatePriority ,
@@ -147,7 +148,7 @@ export type Hook = {|
147148 memoizedState : any ,
148149 baseState : any ,
149150 baseQueue : Update < any , any> | null ,
150- queue : UpdateQueue < any , any > | null ,
151+ queue : any ,
151152 next : Hook | null ,
152153| } ;
153154
@@ -159,6 +160,11 @@ export type Effect = {|
159160 next : Effect ,
160161| } ;
161162
163+ type StoreInstance < T > = { |
164+ value : T ,
165+ getSnapshot : ( ) => T ,
166+ | } ;
167+
162168export type FunctionComponentUpdateQueue = { | lastEffect : Effect | null | } ;
163169
164170type BasicStateAction < S > = ( S => S ) | S ;
@@ -703,14 +709,15 @@ function mountReducer<S, I, A>(
703709 initialState = ( ( initialArg : any ) : S ) ;
704710 }
705711 hook.memoizedState = hook.baseState = initialState;
706- const queue = (hook.queue = {
712+ const queue: UpdateQueue < S , A > = {
707713 pending : null ,
708714 interleaved : null ,
709715 lanes : NoLanes ,
710716 dispatch : null ,
711717 lastRenderedReducer : reducer ,
712718 lastRenderedState : ( initialState : any ) ,
713- } );
719+ } ;
720+ hook.queue = queue;
714721 const dispatch: Dispatch< A > = (queue.dispatch = (dispatchAction.bind(
715722 null,
716723 currentlyRenderingFiber,
@@ -1196,7 +1203,7 @@ function useMutableSource<Source, Snapshot>(
11961203 // So if there are interleaved updates, they get pushed to the older queue.
11971204 // When this becomes current, the previous queue and dispatch method will be discarded,
11981205 // including any interleaving updates that occur.
1199- const newQueue = {
1206+ const newQueue : UpdateQueue < Snapshot , BasicStateAction < Snapshot >> = {
12001207 pending : null ,
12011208 interleaved : null ,
12021209 lanes : NoLanes ,
@@ -1249,86 +1256,86 @@ function mountSyncExternalStore<T>(
12491256 getSnapshot : ( ) => T ,
12501257) : T {
12511258 const hook = mountWorkInProgressHook ( ) ;
1252- return useSyncExternalStore ( hook , subscribe , getSnapshot ) ;
1259+ // Read the current snapshot from the store on every render. This breaks the
1260+ // normal rules of React, and only works because store updates are
1261+ // always synchronous.
1262+ const nextSnapshot = getSnapshot ( ) ;
1263+ if ( __DEV__ ) {
1264+ if ( ! didWarnUncachedGetSnapshot ) {
1265+ if ( nextSnapshot !== getSnapshot ( ) ) {
1266+ console . error (
1267+ 'The result of getSnapshot should be cached to avoid an infinite loop' ,
1268+ ) ;
1269+ didWarnUncachedGetSnapshot = true ;
1270+ }
1271+ }
1272+ }
1273+ hook . memoizedState = nextSnapshot ;
1274+ const inst : StoreInstance < T > = {
1275+ value : nextSnapshot ,
1276+ getSnapshot,
1277+ } ;
1278+ hook . queue = inst ;
1279+ return useSyncExternalStore ( hook , inst , subscribe , getSnapshot , nextSnapshot ) ;
12531280}
12541281
12551282function updateSyncExternalStore< T > (
12561283 subscribe: (() => void ) => ( ) => void ,
12571284 getSnapshot : ( ) => T ,
12581285) : T {
12591286 const hook = updateWorkInProgressHook ( ) ;
1260- return useSyncExternalStore ( hook , subscribe , getSnapshot ) ;
1261- }
1262-
1263- function useSyncExternalStore< T > (
1264- hook: Hook,
1265- subscribe: (() => void ) => ( ) => void ,
1266- getSnapshot : ( ) => T ,
1267- ) : T {
1268- // TODO: This is a copy-paste of the userspace shim. We can improve the
1269- // built-in implementation using lower-level APIs. We also intend to move
1270- // the tearing checks to an earlier, pre-commit phase so that the layout
1271- // effects always observe a consistent tree.
1272-
1273- const dispatcher = ReactCurrentDispatcher . current ;
1274-
1275- // Read the current snapshot from the store on every render. Again, this
1276- // breaks the rules of React, and only works here because of specific
1277- // implementation details, most importantly that updates are
1287+ // Read the current snapshot from the store on every render. This breaks the
1288+ // normal rules of React, and only works because store updates are
12781289 // always synchronous.
1279- const value = getSnapshot ( ) ;
1290+ const nextSnapshot = getSnapshot ( ) ;
12801291 if ( __DEV__ ) {
12811292 if ( ! didWarnUncachedGetSnapshot ) {
1282- if ( value !== getSnapshot ( ) ) {
1293+ if ( nextSnapshot !== getSnapshot ( ) ) {
12831294 console . error (
12841295 'The result of getSnapshot should be cached to avoid an infinite loop' ,
12851296 ) ;
12861297 didWarnUncachedGetSnapshot = true ;
12871298 }
12881299 }
12891300 }
1301+ const prevSnapshot = hook . memoizedState ;
1302+ if ( ! is ( prevSnapshot , nextSnapshot ) ) {
1303+ hook . memoizedState = nextSnapshot ;
1304+ markWorkInProgressReceivedUpdate ( ) ;
1305+ }
1306+ const inst = hook.queue;
1307+ return useSyncExternalStore(hook, inst, subscribe, getSnapshot, nextSnapshot);
1308+ }
12901309
1291- // Because updates are synchronous, we don't queue them. Instead we force a
1292- // re-render whenever the subscribed state changes by updating an some
1293- // arbitrary useState hook. Then, during render, we call getSnapshot to read
1294- // the current value.
1295- //
1296- // Because we don't actually use the state returned by the useState hook, we
1297- // can save a bit of memory by storing other stuff in that slot.
1298- //
1299- // To implement the early bailout, we need to track some things on a mutable
1300- // object. Usually, we would put that in a useRef hook, but we can stash it in
1301- // our useState hook instead.
1302- //
1303- // To force a re-render, we call forceUpdate({inst}). That works because the
1304- // new object always fails an equality check.
1305- const [ { inst} , forceUpdate ] = dispatcher . useState ( {
1306- inst : { value, getSnapshot} ,
1307- } ) ;
1310+ function useSyncExternalStore < T > (
1311+ hook: Hook,
1312+ inst: StoreInstance< T > ,
1313+ subscribe: (() => void ) => ( ) => void ,
1314+ getSnapshot : ( ) => T ,
1315+ nextSnapshot : T ,
1316+ ) : T {
1317+ const fiber = currentlyRenderingFiber ;
1318+ const dispatcher = ReactCurrentDispatcher . current ;
13081319
13091320 // Track the latest getSnapshot function with a ref. This needs to be updated
13101321 // in the layout phase so we can access it during the tearing check that
13111322 // happens on subscribe.
13121323 // TODO: Circumvent SSR warning
13131324 dispatcher . useLayoutEffect ( ( ) => {
1314- inst . value = value ;
1325+ inst . value = nextSnapshot ;
13151326 inst . getSnapshot = getSnapshot ;
13161327
13171328 // Whenever getSnapshot or subscribe changes, we need to check in the
13181329 // commit phase if there was an interleaved mutation. In concurrent mode
13191330 // this can happen all the time, but even in synchronous mode, an earlier
13201331 // effect may have mutated the store.
1332+ // TODO: Move the tearing checks to an earlier, pre-commit phase so that the
1333+ // layout effects always observe a consistent tree.
13211334 if ( checkIfSnapshotChanged ( inst ) ) {
13221335 // Force a re-render.
1323- const prevTransition = ReactCurrentBatchConfig . transition ;
1324- const prevPriority = getCurrentUpdatePriority ( ) ;
1325- ReactCurrentBatchConfig . transition = 0 ;
1326- setCurrentUpdatePriority ( DiscreteEventPriority ) ;
1327- forceUpdate ( { inst} ) ;
1328- setCurrentUpdatePriority ( prevPriority ) ;
1329- ReactCurrentBatchConfig . transition = prevTransition ;
1336+ forceStoreRerender ( fiber ) ;
13301337 }
1331- } , [ subscribe , value , getSnapshot ] ) ;
1338+ } , [ subscribe , nextSnapshot , getSnapshot ] ) ;
13321339
13331340 dispatcher . useEffect ( ( ) => {
13341341 const handleStoreChange = ( ) => {
@@ -1341,13 +1348,7 @@ function useSyncExternalStore<T>(
13411348 // read from the store.
13421349 if ( checkIfSnapshotChanged ( inst ) ) {
13431350 // Force a re-render.
1344- const prevTransition = ReactCurrentBatchConfig . transition ;
1345- const prevPriority = getCurrentUpdatePriority ( ) ;
1346- ReactCurrentBatchConfig . transition = 0 ;
1347- setCurrentUpdatePriority ( DiscreteEventPriority ) ;
1348- forceUpdate ( { inst} ) ;
1349- setCurrentUpdatePriority ( prevPriority ) ;
1350- ReactCurrentBatchConfig . transition = prevTransition ;
1351+ forceStoreRerender ( fiber ) ;
13511352 }
13521353 } ;
13531354 // Check for changes right before subscribing. Subsequent changes will be
@@ -1357,7 +1358,7 @@ function useSyncExternalStore<T>(
13571358 return subscribe ( handleStoreChange ) ;
13581359 } , [ subscribe ] ) ;
13591360
1360- return value ;
1361+ return nextSnapshot ;
13611362}
13621363
13631364function checkIfSnapshotChanged(inst) {
@@ -1371,6 +1372,10 @@ function checkIfSnapshotChanged(inst) {
13711372 }
13721373}
13731374
1375+ function forceStoreRerender(fiber) {
1376+ scheduleUpdateOnFiber ( fiber , SyncLane , NoTimestamp ) ;
1377+ }
1378+
13741379function mountState< S > (
13751380 initialState: (() => S ) | S ,
13761381) : [ S , Dispatch < BasicStateAction < S > > ] {
@@ -1380,14 +1385,15 @@ function mountState<S>(
13801385 initialState = initialState ( ) ;
13811386 }
13821387 hook.memoizedState = hook.baseState = initialState;
1383- const queue = (hook.queue = {
1388+ const queue: UpdateQueue < S , BasicStateAction < S > > = {
13841389 pending : null ,
13851390 interleaved : null ,
13861391 lanes : NoLanes ,
13871392 dispatch : null ,
13881393 lastRenderedReducer : basicStateReducer ,
13891394 lastRenderedState : ( initialState : any ) ,
1390- } );
1395+ } ;
1396+ hook.queue = queue;
13911397 const dispatch: Dispatch<
13921398 BasicStateAction < S > ,
13931399 > = ( queue . dispatch = ( dispatchAction . bind (
0 commit comments