@@ -30,6 +30,7 @@ import type {
3030import ReactSharedInternals from 'shared/ReactSharedInternals' ;
3131import { enableUseEventAPI } from 'shared/ReactFeatureFlags' ;
3232
33+ import { markRootExpiredAtTime } from './ReactFiberRoot' ;
3334import { NoWork , Sync } from './ReactFiberExpirationTime' ;
3435import { readContext } from './ReactFiberNewContext' ;
3536import { createDeprecatedResponderListener } from './ReactFiberDeprecatedEvents' ;
@@ -885,6 +886,7 @@ function rerenderReducer<S, I, A>(
885886type MutableSourceMemoizedState < Source , Snapshot > = { |
886887 refs : {
887888 getSnapshot : MutableSourceGetSnapshotFn < Source , Snapshot> ,
889+ setSnapshot : Snapshot => void ,
888890 } ,
889891 source : MutableSource < any > ,
890892 subscribe : MutableSourceSubscribeFn < Source , Snapshot > ,
@@ -998,7 +1000,40 @@ function useMutableSource<Source, Snapshot>(
9981000
9991001 // Sync the values needed by our subscribe function after each commit.
10001002 dispatcher . useEffect ( ( ) => {
1003+ const didGetSnapshotChange = ! is ( refs . getSnapshot , getSnapshot ) ;
10011004 refs . getSnapshot = getSnapshot ;
1005+
1006+ // Normally the dispatch function for a state hook never changes,
1007+ // but in the case of this hook, it will change if getSnapshot changes.
1008+ // In that case, the subscription below will have cloesd over the previous function,
1009+ // so we use a ref to ensure that handleChange() always has the latest version.
1010+ refs . setSnapshot = setSnapshot ;
1011+
1012+ // This effect runs on mount, even though getSnapshot hasn't changed.
1013+ // In that case we can avoid the additional checks for a changed snapshot,
1014+ // because the subscription effect below will cover this.
1015+ if ( didGetSnapshotChange ) {
1016+ // Because getSnapshot is shared with subscriptions via a ref,
1017+ // we don't resubscribe when getSnapshot changes.
1018+ // This means that we also don't check for any missed mutations
1019+ // between the render and the passive commit though.
1020+ // So we need to check here, just like when we newly subscribe.
1021+ const maybeNewVersion = getVersion ( source . _source ) ;
1022+ if ( ! is ( version , maybeNewVersion ) ) {
1023+ const maybeNewSnapshot = getSnapshot ( source . _source ) ;
1024+ if ( ! is ( snapshot , maybeNewSnapshot ) ) {
1025+ setSnapshot ( maybeNewSnapshot ) ;
1026+
1027+ // If the source mutated between render and now,
1028+ // there may be state updates already scheduled from the old getSnapshot.
1029+ // Those updates should not commit without this value.
1030+ // There is no mechanism currently to associate these updates though,
1031+ // so for now we fall back to synchronously flushing all pending updates.
1032+ // TODO: Improve this later.
1033+ markRootExpiredAtTime ( root , root . mutableSourcePendingUpdateTime ) ;
1034+ }
1035+ }
1036+ }
10021037 } , [ getSnapshot ] ) ;
10031038
10041039 // If we got a new source or subscribe function,
@@ -1007,8 +1042,10 @@ function useMutableSource<Source, Snapshot>(
10071042 dispatcher . useEffect ( ( ) => {
10081043 const handleChange = ( ) => {
10091044 const latestGetSnapshot = refs . getSnapshot ;
1045+ const latestSetSnapshot = refs . setSnapshot ;
1046+
10101047 try {
1011- setSnapshot ( latestGetSnapshot ( source . _source ) ) ;
1048+ latestSetSnapshot ( latestGetSnapshot ( source . _source ) ) ;
10121049
10131050 // Record a pending mutable source update with the same expiration time.
10141051 const currentTime = requestCurrentTimeForUpdate ( ) ;
@@ -1025,9 +1062,11 @@ function useMutableSource<Source, Snapshot>(
10251062 // e.g. it might try to read from a part of the store that no longer exists.
10261063 // In this case we should still schedule an update with React.
10271064 // Worst case the selector will throw again and then an error boundary will handle it.
1028- setSnapshot ( ( ) => {
1029- throw error ;
1030- } ) ;
1065+ latestSetSnapshot (
1066+ ( ( ) => {
1067+ throw error ;
1068+ } : any ) ,
1069+ ) ;
10311070 }
10321071 } ;
10331072
@@ -1063,11 +1102,31 @@ function useMutableSource<Source, Snapshot>(
10631102 //
10641103 // In both cases, we need to throw away pending udpates (since they are no longer relevant)
10651104 // and treat reading from the source as we do in the mount case.
1105+ const didGetSnapshotChange = ! is ( prevGetSnapshot , getSnapshot ) ;
10661106 if (
10671107 ! is ( prevSource , source ) ||
10681108 ! is ( prevSubscribe , subscribe ) ||
1069- ! is ( prevGetSnapshot , getSnapshot )
1109+ didGetSnapshotChange
10701110 ) {
1111+ if ( didGetSnapshotChange ) {
1112+ // Create a new queue and setState method,
1113+ // So if there are interleaved updates, they get pushed to the older queue.
1114+ // When this becomes current, the previous queue and dispatch method will be discarded,
1115+ // including any interleaving updates that occur.
1116+ const newQueue = {
1117+ pending : null ,
1118+ dispatch : null ,
1119+ lastRenderedReducer : basicStateReducer ,
1120+ lastRenderedState : snapshot ,
1121+ } ;
1122+ newQueue . dispatch = setSnapshot = ( dispatchAction . bind (
1123+ null ,
1124+ currentlyRenderingFiber ,
1125+ newQueue ,
1126+ ) : any ) ;
1127+ stateHook . queue = newQueue ;
1128+ }
1129+
10711130 stateHook.baseQueue = null;
10721131 snapshot = readFromUnsubcribedMutableSource(root, source, getSnapshot);
10731132 stateHook.memoizedState = stateHook.baseState = snapshot;
@@ -1085,6 +1144,7 @@ function mountMutableSource<Source, Snapshot>(
10851144 hook . memoizedState = ( {
10861145 refs : {
10871146 getSnapshot,
1147+ setSnapshot : ( null : any ) ,
10881148 } ,
10891149 source,
10901150 subscribe,
0 commit comments