Skip to content

Commit fde2923

Browse files
author
Brian Vaughn
committed
useMutableSource: bugfix for new getSnapshot with mutation
1 parent dbd85a0 commit fde2923

File tree

2 files changed

+316
-18
lines changed

2 files changed

+316
-18
lines changed

packages/react-reconciler/src/ReactFiberHooks.js

Lines changed: 65 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import type {
3030
import ReactSharedInternals from 'shared/ReactSharedInternals';
3131
import {enableUseEventAPI} from 'shared/ReactFeatureFlags';
3232

33+
import {markRootExpiredAtTime} from './ReactFiberRoot';
3334
import {NoWork, Sync} from './ReactFiberExpirationTime';
3435
import {readContext} from './ReactFiberNewContext';
3536
import {createDeprecatedResponderListener} from './ReactFiberDeprecatedEvents';
@@ -885,6 +886,7 @@ function rerenderReducer<S, I, A>(
885886
type 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

Comments
 (0)