@@ -41,6 +41,7 @@ import {
41
41
} from './ReactTypeOfMode' ;
42
42
import {
43
43
NoLane ,
44
+ SyncLane ,
44
45
NoLanes ,
45
46
isSubsetOfLanes ,
46
47
mergeLanes ,
@@ -49,9 +50,9 @@ import {
49
50
isTransitionLane ,
50
51
markRootEntangled ,
51
52
markRootMutableRead ,
53
+ NoTimestamp ,
52
54
} from './ReactFiberLane.new' ;
53
55
import {
54
- DiscreteEventPriority ,
55
56
ContinuousEventPriority ,
56
57
getCurrentUpdatePriority ,
57
58
setCurrentUpdatePriority ,
@@ -147,7 +148,7 @@ export type Hook = {|
147
148
memoizedState : any ,
148
149
baseState : any ,
149
150
baseQueue : Update < any , any> | null ,
150
- queue : UpdateQueue < any , any > | null ,
151
+ queue : any ,
151
152
next : Hook | null ,
152
153
| } ;
153
154
@@ -159,6 +160,11 @@ export type Effect = {|
159
160
next : Effect ,
160
161
| } ;
161
162
163
+ type StoreInstance < T > = { |
164
+ value : T ,
165
+ getSnapshot : ( ) => T ,
166
+ | } ;
167
+
162
168
export type FunctionComponentUpdateQueue = { | lastEffect : Effect | null | } ;
163
169
164
170
type BasicStateAction < S > = ( S => S ) | S ;
@@ -703,14 +709,15 @@ function mountReducer<S, I, A>(
703
709
initialState = ( ( initialArg : any ) : S ) ;
704
710
}
705
711
hook.memoizedState = hook.baseState = initialState;
706
- const queue = (hook.queue = {
712
+ const queue: UpdateQueue < S , A > = {
707
713
pending : null ,
708
714
interleaved : null ,
709
715
lanes : NoLanes ,
710
716
dispatch : null ,
711
717
lastRenderedReducer : reducer ,
712
718
lastRenderedState : ( initialState : any ) ,
713
- } );
719
+ } ;
720
+ hook.queue = queue;
714
721
const dispatch: Dispatch< A > = (queue.dispatch = (dispatchAction.bind(
715
722
null,
716
723
currentlyRenderingFiber,
@@ -1196,7 +1203,7 @@ function useMutableSource<Source, Snapshot>(
1196
1203
// So if there are interleaved updates, they get pushed to the older queue.
1197
1204
// When this becomes current, the previous queue and dispatch method will be discarded,
1198
1205
// including any interleaving updates that occur.
1199
- const newQueue = {
1206
+ const newQueue : UpdateQueue < Snapshot , BasicStateAction < Snapshot >> = {
1200
1207
pending : null ,
1201
1208
interleaved : null ,
1202
1209
lanes : NoLanes ,
@@ -1249,86 +1256,86 @@ function mountSyncExternalStore<T>(
1249
1256
getSnapshot : ( ) => T ,
1250
1257
) : T {
1251
1258
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 ) ;
1253
1280
}
1254
1281
1255
1282
function updateSyncExternalStore< T > (
1256
1283
subscribe: (() => void ) => ( ) => void ,
1257
1284
getSnapshot : ( ) => T ,
1258
1285
) : T {
1259
1286
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
1278
1289
// always synchronous.
1279
- const value = getSnapshot ( ) ;
1290
+ const nextSnapshot = getSnapshot ( ) ;
1280
1291
if ( __DEV__ ) {
1281
1292
if ( ! didWarnUncachedGetSnapshot ) {
1282
- if ( value !== getSnapshot ( ) ) {
1293
+ if ( nextSnapshot !== getSnapshot ( ) ) {
1283
1294
console . error (
1284
1295
'The result of getSnapshot should be cached to avoid an infinite loop' ,
1285
1296
) ;
1286
1297
didWarnUncachedGetSnapshot = true ;
1287
1298
}
1288
1299
}
1289
1300
}
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
+ }
1290
1309
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 ;
1308
1319
1309
1320
// Track the latest getSnapshot function with a ref. This needs to be updated
1310
1321
// in the layout phase so we can access it during the tearing check that
1311
1322
// happens on subscribe.
1312
1323
// TODO: Circumvent SSR warning
1313
1324
dispatcher . useLayoutEffect ( ( ) => {
1314
- inst . value = value ;
1325
+ inst . value = nextSnapshot ;
1315
1326
inst . getSnapshot = getSnapshot ;
1316
1327
1317
1328
// Whenever getSnapshot or subscribe changes, we need to check in the
1318
1329
// commit phase if there was an interleaved mutation. In concurrent mode
1319
1330
// this can happen all the time, but even in synchronous mode, an earlier
1320
1331
// 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.
1321
1334
if ( checkIfSnapshotChanged ( inst ) ) {
1322
1335
// 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 ) ;
1330
1337
}
1331
- } , [ subscribe , value , getSnapshot ] ) ;
1338
+ } , [ subscribe , nextSnapshot , getSnapshot ] ) ;
1332
1339
1333
1340
dispatcher . useEffect ( ( ) => {
1334
1341
const handleStoreChange = ( ) => {
@@ -1341,13 +1348,7 @@ function useSyncExternalStore<T>(
1341
1348
// read from the store.
1342
1349
if ( checkIfSnapshotChanged ( inst ) ) {
1343
1350
// 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 ) ;
1351
1352
}
1352
1353
} ;
1353
1354
// Check for changes right before subscribing. Subsequent changes will be
@@ -1357,7 +1358,7 @@ function useSyncExternalStore<T>(
1357
1358
return subscribe ( handleStoreChange ) ;
1358
1359
} , [ subscribe ] ) ;
1359
1360
1360
- return value ;
1361
+ return nextSnapshot ;
1361
1362
}
1362
1363
1363
1364
function checkIfSnapshotChanged(inst) {
@@ -1371,6 +1372,10 @@ function checkIfSnapshotChanged(inst) {
1371
1372
}
1372
1373
}
1373
1374
1375
+ function forceStoreRerender(fiber) {
1376
+ scheduleUpdateOnFiber ( fiber , SyncLane , NoTimestamp ) ;
1377
+ }
1378
+
1374
1379
function mountState< S > (
1375
1380
initialState: (() => S ) | S ,
1376
1381
) : [ S , Dispatch < BasicStateAction < S > > ] {
@@ -1380,14 +1385,15 @@ function mountState<S>(
1380
1385
initialState = initialState ( ) ;
1381
1386
}
1382
1387
hook.memoizedState = hook.baseState = initialState;
1383
- const queue = (hook.queue = {
1388
+ const queue: UpdateQueue < S , BasicStateAction < S > > = {
1384
1389
pending : null ,
1385
1390
interleaved : null ,
1386
1391
lanes : NoLanes ,
1387
1392
dispatch : null ,
1388
1393
lastRenderedReducer : basicStateReducer ,
1389
1394
lastRenderedState : ( initialState : any ) ,
1390
- } );
1395
+ } ;
1396
+ hook.queue = queue;
1391
1397
const dispatch: Dispatch<
1392
1398
BasicStateAction < S > ,
1393
1399
> = ( queue . dispatch = ( dispatchAction . bind (
0 commit comments