@@ -1787,7 +1787,7 @@ function mountSyncExternalStore<T>(
17871787 return nextSnapshot;
17881788}
17891789
1790- function updateSyncExternalStore < T > (
1790+ function rerenderSyncExternalStore < T > (
17911791 subscribe: (() => void ) => ( ) => void ,
17921792 getSnapshot : ( ) => T ,
17931793 getServerSnapshot ?: ( ) => T ,
@@ -1822,7 +1822,83 @@ function updateSyncExternalStore<T>(
18221822 }
18231823 }
18241824 }
1825- const prevSnapshot = ( currentHook || hook ) . memoizedState ;
1825+ let snapshotChanged = true ;
1826+ if ( currentHook !== null ) {
1827+ const prevSnapshot = currentHook . memoizedState ;
1828+ snapshotChanged = ! is ( prevSnapshot , nextSnapshot ) ;
1829+ if ( snapshotChanged ) {
1830+ hook . memoizedState = nextSnapshot ;
1831+ markWorkInProgressReceivedUpdate ( ) ;
1832+ }
1833+ }
1834+ const inst = hook . queue ;
1835+
1836+ updateEffect ( subscribeToStore . bind ( null , fiber , inst , subscribe ) , [
1837+ subscribe ,
1838+ ] ) ;
1839+
1840+ // Whenever getSnapshot or subscribe changes, we need to check in the
1841+ // commit phase if there was an interleaved mutation. In concurrent mode
1842+ // this can happen all the time, but even in synchronous mode, an earlier
1843+ // effect may have mutated the store.
1844+ if (
1845+ inst . getSnapshot !== getSnapshot ||
1846+ snapshotChanged ||
1847+ // Check if the subscribe function changed. We can save some memory by
1848+ // checking whether we scheduled a subscription effect above.
1849+ ( workInProgressHook !== null &&
1850+ workInProgressHook . memoizedState . tag & HookHasEffect )
1851+ ) {
1852+ fiber . flags |= PassiveEffect ;
1853+ pushEffect (
1854+ HookHasEffect | HookPassive ,
1855+ updateStoreInstance . bind ( null , fiber , inst , nextSnapshot , getSnapshot ) ,
1856+ createEffectInstance ( ) ,
1857+ null ,
1858+ ) ;
1859+
1860+ // Unless we're rendering a blocking lane, schedule a consistency check.
1861+ // Right before committing, we will walk the tree and check if any of the
1862+ // stores were mutated.
1863+ const root : FiberRoot | null = getWorkInProgressRoot ( ) ;
1864+
1865+ if ( root === null ) {
1866+ throw new Error (
1867+ 'Expected a work-in-progress root. This is a bug in React. Please file an issue.' ,
1868+ ) ;
1869+ }
1870+
1871+ if ( ! isHydrating && ! includesBlockingLane ( root , renderLanes ) ) {
1872+ pushStoreConsistencyCheck ( fiber , getSnapshot , nextSnapshot ) ;
1873+ }
1874+ }
1875+
1876+ return nextSnapshot;
1877+ }
1878+
1879+ function updateSyncExternalStore < T > (
1880+ subscribe: (() => void ) => ( ) => void ,
1881+ getSnapshot : ( ) => T ,
1882+ getServerSnapshot ?: ( ) => T ,
1883+ ) : T {
1884+ const fiber = currentlyRenderingFiber ;
1885+ const hook = updateWorkInProgressHook ( ) ;
1886+ // Read the current snapshot from the store on every render. This breaks the
1887+ // normal rules of React, and only works because store updates are
1888+ // always synchronous.
1889+ const nextSnapshot = getSnapshot ( ) ;
1890+ if ( __DEV__ ) {
1891+ if ( ! didWarnUncachedGetSnapshot ) {
1892+ const cachedSnapshot = getSnapshot ( ) ;
1893+ if ( ! is ( nextSnapshot , cachedSnapshot ) ) {
1894+ console . error (
1895+ 'The result of getSnapshot should be cached to avoid an infinite loop' ,
1896+ ) ;
1897+ didWarnUncachedGetSnapshot = true ;
1898+ }
1899+ }
1900+ }
1901+ const prevSnapshot = ( ( currentHook : any ) : Hook ) . memoizedState ;
18261902 const snapshotChanged = ! is ( prevSnapshot , nextSnapshot ) ;
18271903 if ( snapshotChanged ) {
18281904 hook . memoizedState = nextSnapshot ;
@@ -1865,7 +1941,7 @@ function updateSyncExternalStore<T>(
18651941 ) ;
18661942 }
18671943
1868- if ( ! isHydrating && ! includesBlockingLane ( root , renderLanes ) ) {
1944+ if ( ! includesBlockingLane ( root , renderLanes ) ) {
18691945 pushStoreConsistencyCheck ( fiber , getSnapshot , nextSnapshot ) ;
18701946 }
18711947 }
@@ -2228,7 +2304,8 @@ function updateEffectImpl(
22282304 const effect : Effect = hook . memoizedState ;
22292305 const inst = effect . inst ;
22302306
2231- // currentHook is null when rerendering after a render phase state update.
2307+ // currentHook is null on initial mount when rerendering after a render phase
2308+ // state update or for strict mode.
22322309 if ( currentHook !== null ) {
22332310 if ( nextDeps !== null ) {
22342311 const prevEffect : Effect = currentHook . memoizedState ;
@@ -3315,7 +3392,7 @@ const HooksDispatcherOnRerender: Dispatcher = {
33153392 useDeferredValue : rerenderDeferredValue ,
33163393 useTransition : rerenderTransition ,
33173394 useMutableSource : updateMutableSource ,
3318- useSyncExternalStore : updateSyncExternalStore ,
3395+ useSyncExternalStore : rerenderSyncExternalStore ,
33193396 useId : updateId ,
33203397} ;
33213398if (enableCache) {
@@ -4002,7 +4079,11 @@ if (__DEV__) {
40024079 ) : T {
40034080 currentHookNameInDev = 'useSyncExternalStore' ;
40044081 updateHookTypesDev ( ) ;
4005- return updateSyncExternalStore ( subscribe , getSnapshot , getServerSnapshot ) ;
4082+ return rerenderSyncExternalStore (
4083+ subscribe ,
4084+ getSnapshot ,
4085+ getServerSnapshot ,
4086+ ) ;
40064087 } ,
40074088 useId(): string {
40084089 currentHookNameInDev = 'useId' ;
@@ -4583,7 +4664,11 @@ if (__DEV__) {
45834664 currentHookNameInDev = 'useSyncExternalStore' ;
45844665 warnInvalidHookAccess ( ) ;
45854666 updateHookTypesDev ( ) ;
4586- return updateSyncExternalStore ( subscribe , getSnapshot , getServerSnapshot ) ;
4667+ return rerenderSyncExternalStore (
4668+ subscribe ,
4669+ getSnapshot ,
4670+ getServerSnapshot ,
4671+ ) ;
45874672 } ,
45884673 useId(): string {
45894674 currentHookNameInDev = 'useId' ;
0 commit comments