@@ -51,6 +51,7 @@ import {
51
51
markRootMutableRead ,
52
52
} from './ReactFiberLane.new' ;
53
53
import {
54
+ DiscreteEventPriority ,
54
55
ContinuousEventPriority ,
55
56
getCurrentUpdatePriority ,
56
57
setCurrentUpdatePriority ,
@@ -1246,14 +1247,126 @@ function mountSyncExternalStore<T>(
1246
1247
subscribe: (() => void ) => ( ) => void ,
1247
1248
getSnapshot : ( ) => T ,
1248
1249
) : T {
1249
- throw new Error ( 'Not yet implemented' ) ;
1250
+ const hook = mountWorkInProgressHook ( ) ;
1251
+ return useSyncExternalStore ( hook , subscribe , getSnapshot ) ;
1250
1252
}
1251
1253
1252
1254
function updateSyncExternalStore< T > (
1253
1255
subscribe: (() => void ) => ( ) => void ,
1254
1256
getSnapshot : ( ) => T ,
1255
1257
) : T {
1256
- throw new Error ( 'Not yet implemented' ) ;
1258
+ const hook = updateWorkInProgressHook ( ) ;
1259
+ return useSyncExternalStore ( hook , subscribe , getSnapshot ) ;
1260
+ }
1261
+
1262
+ function useSyncExternalStore< T > (
1263
+ hook: Hook,
1264
+ subscribe: (() => void ) => ( ) => void ,
1265
+ getSnapshot : ( ) => T ,
1266
+ ) : T {
1267
+ // TODO: This is a copy-paste of the userspace shim. We can improve the
1268
+ // built-in implementation using lower-level APIs. We also intend to move
1269
+ // the tearing checks to an earlier, pre-commit phase so that the layout
1270
+ // effects always observe a consistent tree.
1271
+
1272
+ const dispatcher = ReactCurrentDispatcher . current ;
1273
+
1274
+ // Read the current snapshot from the store on every render. Again, this
1275
+ // breaks the rules of React, and only works here because of specific
1276
+ // implementation details, most importantly that updates are
1277
+ // always synchronous.
1278
+ const value = getSnapshot ( ) ;
1279
+
1280
+ // Because updates are synchronous, we don't queue them. Instead we force a
1281
+ // re-render whenever the subscribed state changes by updating an some
1282
+ // arbitrary useState hook. Then, during render, we call getSnapshot to read
1283
+ // the current value.
1284
+ //
1285
+ // Because we don't actually use the state returned by the useState hook, we
1286
+ // can save a bit of memory by storing other stuff in that slot.
1287
+ //
1288
+ // To implement the early bailout, we need to track some things on a mutable
1289
+ // object. Usually, we would put that in a useRef hook, but we can stash it in
1290
+ // our useState hook instead.
1291
+ //
1292
+ // To force a re-render, we call forceUpdate({inst}). That works because the
1293
+ // new object always fails an equality check.
1294
+ const [ { inst} , forceUpdate ] = dispatcher . useState ( {
1295
+ inst : { value, getSnapshot} ,
1296
+ } ) ;
1297
+
1298
+ // Track the latest getSnapshot function with a ref. This needs to be updated
1299
+ // in the layout phase so we can access it during the tearing check that
1300
+ // happens on subscribe.
1301
+ // TODO: Circumvent SSR warning
1302
+ dispatcher . useLayoutEffect ( ( ) => {
1303
+ inst . value = value ;
1304
+ inst . getSnapshot = getSnapshot ;
1305
+
1306
+ // Whenever getSnapshot or subscribe changes, we need to check in the
1307
+ // commit phase if there was an interleaved mutation. In concurrent mode
1308
+ // this can happen all the time, but even in synchronous mode, an earlier
1309
+ // effect may have mutated the store.
1310
+ if ( checkIfSnapshotChanged ( inst ) ) {
1311
+ // Force a re-render.
1312
+ const prevTransition = ReactCurrentBatchConfig . transition ;
1313
+ const prevPriority = getCurrentUpdatePriority ( ) ;
1314
+ ReactCurrentBatchConfig . transition = 0 ;
1315
+ setCurrentUpdatePriority ( DiscreteEventPriority ) ;
1316
+ forceUpdate ( { inst} ) ;
1317
+ setCurrentUpdatePriority ( prevPriority ) ;
1318
+ ReactCurrentBatchConfig . transition = prevTransition ;
1319
+ }
1320
+ } , [ subscribe , value , getSnapshot ] ) ;
1321
+
1322
+ dispatcher . useEffect ( ( ) => {
1323
+ // Check for changes right before subscribing. Subsequent changes will be
1324
+ // detected in the subscription handler.
1325
+ if ( checkIfSnapshotChanged ( inst ) ) {
1326
+ // Force a re-render.
1327
+ const prevTransition = ReactCurrentBatchConfig . transition ;
1328
+ const prevPriority = getCurrentUpdatePriority ( ) ;
1329
+ ReactCurrentBatchConfig . transition = 0 ;
1330
+ setCurrentUpdatePriority ( DiscreteEventPriority ) ;
1331
+ forceUpdate ( { inst} ) ;
1332
+ setCurrentUpdatePriority ( prevPriority ) ;
1333
+ ReactCurrentBatchConfig . transition = prevTransition ;
1334
+ }
1335
+ const handleStoreChange = ( ) => {
1336
+ // TODO: Because there is no cross-renderer API for batching updates, it's
1337
+ // up to the consumer of this library to wrap their subscription event
1338
+ // with unstable_batchedUpdates. Should we try to detect when this isn't
1339
+ // the case and print a warning in development?
1340
+
1341
+ // The store changed. Check if the snapshot changed since the last time we
1342
+ // read from the store.
1343
+ if ( checkIfSnapshotChanged ( inst ) ) {
1344
+ // Force a re-render.
1345
+ const prevTransition = ReactCurrentBatchConfig . transition ;
1346
+ const prevPriority = getCurrentUpdatePriority ( ) ;
1347
+ ReactCurrentBatchConfig . transition = 0 ;
1348
+ setCurrentUpdatePriority ( DiscreteEventPriority ) ;
1349
+ forceUpdate ( { inst} ) ;
1350
+ setCurrentUpdatePriority ( prevPriority ) ;
1351
+ ReactCurrentBatchConfig . transition = prevTransition ;
1352
+ }
1353
+ } ;
1354
+ // Subscribe to the store and return a clean-up function.
1355
+ return subscribe ( handleStoreChange ) ;
1356
+ } , [ subscribe ] ) ;
1357
+
1358
+ return value ;
1359
+ }
1360
+
1361
+ function checkIfSnapshotChanged(inst) {
1362
+ const latestGetSnapshot = inst . getSnapshot ;
1363
+ const prevValue = inst . value ;
1364
+ try {
1365
+ const nextValue = latestGetSnapshot ( ) ;
1366
+ return ! is ( prevValue , nextValue ) ;
1367
+ } catch ( error ) {
1368
+ return true ;
1369
+ }
1257
1370
}
1258
1371
1259
1372
function mountState< S > (
0 commit comments