@@ -51,6 +51,7 @@ import {
5151 markRootMutableRead ,
5252} from './ReactFiberLane.new' ;
5353import {
54+ DiscreteEventPriority ,
5455 ContinuousEventPriority ,
5556 getCurrentUpdatePriority ,
5657 setCurrentUpdatePriority ,
@@ -136,6 +137,7 @@ export type UpdateQueue<S, A> = {|
136137
137138let didWarnAboutMismatchedHooksForComponent ;
138139let didWarnAboutUseOpaqueIdentifier ;
140+ let didWarnUncachedGetSnapshot ;
139141if ( __DEV__ ) {
140142 didWarnAboutUseOpaqueIdentifier = { } ;
141143 didWarnAboutMismatchedHooksForComponent = new Set ( ) ;
@@ -1246,14 +1248,127 @@ function mountSyncExternalStore<T>(
12461248 subscribe: (() => void ) => ( ) => void ,
12471249 getSnapshot : ( ) => T ,
12481250) : T {
1249- throw new Error ( 'Not yet implemented' ) ;
1251+ const hook = mountWorkInProgressHook ( ) ;
1252+ return useSyncExternalStore ( hook , subscribe , getSnapshot ) ;
12501253}
12511254
12521255function updateSyncExternalStore< T > (
12531256 subscribe: (() => void ) => ( ) => void ,
12541257 getSnapshot : ( ) => T ,
12551258) : T {
1256- throw new Error ( 'Not yet implemented' ) ;
1259+ 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
1278+ // always synchronous.
1279+ const value = getSnapshot ( ) ;
1280+ if ( __DEV__ ) {
1281+ if ( ! didWarnUncachedGetSnapshot ) {
1282+ if ( value !== getSnapshot ( ) ) {
1283+ console . error (
1284+ 'The result of getSnapshot should be cached to avoid an infinite loop' ,
1285+ ) ;
1286+ didWarnUncachedGetSnapshot = true ;
1287+ }
1288+ }
1289+ }
1290+
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+ } ) ;
1308+
1309+ // Track the latest getSnapshot function with a ref. This needs to be updated
1310+ // in the layout phase so we can access it during the tearing check that
1311+ // happens on subscribe.
1312+ // TODO: Circumvent SSR warning
1313+ dispatcher . useLayoutEffect ( ( ) => {
1314+ inst . value = value ;
1315+ inst . getSnapshot = getSnapshot ;
1316+
1317+ // Whenever getSnapshot or subscribe changes, we need to check in the
1318+ // commit phase if there was an interleaved mutation. In concurrent mode
1319+ // this can happen all the time, but even in synchronous mode, an earlier
1320+ // effect may have mutated the store.
1321+ if ( checkIfSnapshotChanged ( inst ) ) {
1322+ // 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 ;
1330+ }
1331+ } , [ subscribe , value , getSnapshot ] ) ;
1332+
1333+ dispatcher . useEffect ( ( ) => {
1334+ const handleStoreChange = ( ) => {
1335+ // TODO: Because there is no cross-renderer API for batching updates, it's
1336+ // up to the consumer of this library to wrap their subscription event
1337+ // with unstable_batchedUpdates. Should we try to detect when this isn't
1338+ // the case and print a warning in development?
1339+
1340+ // The store changed. Check if the snapshot changed since the last time we
1341+ // read from the store.
1342+ if ( checkIfSnapshotChanged ( inst ) ) {
1343+ // 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+ }
1352+ } ;
1353+ // Check for changes right before subscribing. Subsequent changes will be
1354+ // detected in the subscription handler.
1355+ handleStoreChange ( ) ;
1356+ // Subscribe to the store and return a clean-up function.
1357+ return subscribe ( handleStoreChange ) ;
1358+ } , [ subscribe ] ) ;
1359+
1360+ return value ;
1361+ }
1362+
1363+ function checkIfSnapshotChanged(inst) {
1364+ const latestGetSnapshot = inst . getSnapshot ;
1365+ const prevValue = inst . value ;
1366+ try {
1367+ const nextValue = latestGetSnapshot ( ) ;
1368+ return ! is ( prevValue , nextValue ) ;
1369+ } catch ( error ) {
1370+ return true ;
1371+ }
12571372}
12581373
12591374function mountState< S > (
0 commit comments