Skip to content

Commit 86da5ce

Browse files
author
Brian Vaughn
committed
Recreate queue and dispatch when source/subscribe/getSnapshot change
1 parent db511f4 commit 86da5ce

File tree

2 files changed

+48
-87
lines changed

2 files changed

+48
-87
lines changed

packages/react-reconciler/src/ReactFiberHooks.js

Lines changed: 47 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -999,56 +999,44 @@ function useMutableSource<Source, Snapshot>(
999999
subscribe,
10001000
}: MutableSourceMemoizedState<Source, Snapshot>);
10011001

1002-
// Sync the values needed by our subscribe function after each commit.
1002+
// Sync the values needed by our subscription handler after each commit.
10031003
dispatcher.useEffect(() => {
1004-
const didGetSnapshotChange = !is(refs.getSnapshot, getSnapshot);
10051004
refs.getSnapshot = getSnapshot;
10061005

10071006
// Normally the dispatch function for a state hook never changes,
1008-
// but in the case of this hook, it will change if getSnapshot changes.
1009-
// In that case, the subscription below will have cloesd over the previous function,
1010-
// so we use a ref to ensure that handleChange() always has the latest version.
1007+
// but this hook recreates the queue in certain cases to avoid updates from stale sources.
1008+
// handleChange() below needs to reference the dispatch function without re-subscribing,
1009+
// so we use a ref to ensure that it always has the latest version.
10111010
refs.setSnapshot = setSnapshot;
10121011

1013-
// This effect runs on mount, even though getSnapshot hasn't changed.
1014-
// In that case we can avoid the additional checks for a changed snapshot,
1015-
// because the subscription effect below will cover this.
1016-
if (didGetSnapshotChange) {
1017-
// Because getSnapshot is shared with subscriptions via a ref,
1018-
// we don't resubscribe when getSnapshot changes.
1019-
// This means that we also don't check for any missed mutations
1020-
// between the render and the passive commit though.
1021-
// So we need to check here, just like when we newly subscribe.
1022-
const maybeNewVersion = getVersion(source._source);
1023-
if (!is(version, maybeNewVersion)) {
1024-
const maybeNewSnapshot = getSnapshot(source._source);
1025-
if (!is(snapshot, maybeNewSnapshot)) {
1026-
setSnapshot(maybeNewSnapshot);
1027-
1028-
const currentTime = requestCurrentTimeForUpdate();
1029-
const suspenseConfig = requestCurrentSuspenseConfig();
1030-
const expirationTime = computeExpirationForFiber(
1031-
currentTime,
1032-
fiber,
1033-
suspenseConfig,
1034-
);
1035-
setPendingExpirationTime(root, expirationTime);
1036-
1037-
// If the source mutated between render and now,
1038-
// there may be state updates already scheduled from the old getSnapshot.
1039-
// Those updates should not commit without this value.
1040-
// There is no mechanism currently to associate these updates though,
1041-
// so for now we fall back to synchronously flushing all pending updates.
1042-
// TODO: Improve this later.
1043-
markRootExpiredAtTime(root, getLastPendingExpirationTime(root));
1044-
}
1012+
// Check for a possible change between when we last rendered now.
1013+
const maybeNewVersion = getVersion(source._source);
1014+
if (!is(version, maybeNewVersion)) {
1015+
const maybeNewSnapshot = getSnapshot(source._source);
1016+
if (!is(snapshot, maybeNewSnapshot)) {
1017+
setSnapshot(maybeNewSnapshot);
1018+
1019+
const currentTime = requestCurrentTimeForUpdate();
1020+
const suspenseConfig = requestCurrentSuspenseConfig();
1021+
const expirationTime = computeExpirationForFiber(
1022+
currentTime,
1023+
fiber,
1024+
suspenseConfig,
1025+
);
1026+
setPendingExpirationTime(root, expirationTime);
1027+
1028+
// If the source mutated between render and now,
1029+
// there may be state updates already scheduled from the old getSnapshot.
1030+
// Those updates should not commit without this value.
1031+
// There is no mechanism currently to associate these updates though,
1032+
// so for now we fall back to synchronously flushing all pending updates.
1033+
// TODO: Improve this later.
1034+
markRootExpiredAtTime(root, getLastPendingExpirationTime(root));
10451035
}
10461036
}
1047-
}, [getSnapshot]);
1037+
}, [getSnapshot, source, subscribe]);
10481038

1049-
// If we got a new source or subscribe function,
1050-
// we'll need to subscribe in a passive effect,
1051-
// and also check for any changes that fire between render and subscribe.
1039+
// If we got a new source or subscribe function, re-subscribe in a passive effect.
10521040
dispatcher.useEffect(() => {
10531041
const handleChange = () => {
10541042
const latestGetSnapshot = refs.getSnapshot;
@@ -1089,29 +1077,6 @@ function useMutableSource<Source, Snapshot>(
10891077
}
10901078
}
10911079

1092-
// Check for a possible change between when we last rendered and when we just subscribed.
1093-
const maybeNewVersion = getVersion(source._source);
1094-
if (!is(version, maybeNewVersion)) {
1095-
const maybeNewSnapshot = getSnapshot(source._source);
1096-
if (!is(snapshot, maybeNewSnapshot)) {
1097-
setSnapshot(maybeNewSnapshot);
1098-
1099-
const currentTime = requestCurrentTimeForUpdate();
1100-
const suspenseConfig = requestCurrentSuspenseConfig();
1101-
const expirationTime = computeExpirationForFiber(
1102-
currentTime,
1103-
fiber,
1104-
suspenseConfig,
1105-
);
1106-
setPendingExpirationTime(root, expirationTime);
1107-
1108-
// We missed a mutation before committing.
1109-
// It's possible that other components using this source also have pending updates scheduled.
1110-
// In that case, we should ensure they all commit together.
1111-
markRootExpiredAtTime(root, getLastPendingExpirationTime(root));
1112-
}
1113-
}
1114-
11151080
return unsubscribe;
11161081
}, [source, subscribe]);
11171082

@@ -1126,31 +1091,27 @@ function useMutableSource<Source, Snapshot>(
11261091
//
11271092
// In both cases, we need to throw away pending udpates (since they are no longer relevant)
11281093
// and treat reading from the source as we do in the mount case.
1129-
const didGetSnapshotChange = !is(prevGetSnapshot, getSnapshot);
11301094
if (
1095+
!is(prevGetSnapshot, getSnapshot) ||
11311096
!is(prevSource, source) ||
1132-
!is(prevSubscribe, subscribe) ||
1133-
didGetSnapshotChange
1097+
!is(prevSubscribe, subscribe)
11341098
) {
1135-
if (didGetSnapshotChange) {
1136-
// Create a new queue and setState method,
1137-
// So if there are interleaved updates, they get pushed to the older queue.
1138-
// When this becomes current, the previous queue and dispatch method will be discarded,
1139-
// including any interleaving updates that occur.
1140-
const newQueue = {
1141-
pending: null,
1142-
dispatch: null,
1143-
lastRenderedReducer: basicStateReducer,
1144-
lastRenderedState: snapshot,
1145-
};
1146-
newQueue.dispatch = setSnapshot = (dispatchAction.bind(
1147-
null,
1148-
currentlyRenderingFiber,
1149-
newQueue,
1150-
): any);
1151-
stateHook.queue = newQueue;
1152-
}
1153-
1099+
// Create a new queue and setState method,
1100+
// So if there are interleaved updates, they get pushed to the older queue.
1101+
// When this becomes current, the previous queue and dispatch method will be discarded,
1102+
// including any interleaving updates that occur.
1103+
const newQueue = {
1104+
pending: null,
1105+
dispatch: null,
1106+
lastRenderedReducer: basicStateReducer,
1107+
lastRenderedState: snapshot,
1108+
};
1109+
newQueue.dispatch = setSnapshot = (dispatchAction.bind(
1110+
null,
1111+
currentlyRenderingFiber,
1112+
newQueue,
1113+
): any);
1114+
stateHook.queue = newQueue;
11541115
stateHook.baseQueue = null;
11551116
snapshot = readFromUnsubcribedMutableSource(root, source, getSnapshot);
11561117
stateHook.memoizedState = stateHook.baseState = snapshot;

packages/react-reconciler/src/__tests__/useMutableSource-test.internal.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -302,7 +302,7 @@ describe('useMutableSource', () => {
302302
/>,
303303
() => Scheduler.unstable_yieldValue('Sync effect'),
304304
);
305-
expect(Scheduler).toFlushAndYieldThrough(['only:a-one', 'Sync effect']);
305+
expect(Scheduler).toFlushAndYield(['only:a-one', 'Sync effect']);
306306
ReactNoop.flushPassiveEffects();
307307
expect(sourceA.listenerCount).toBe(1);
308308

0 commit comments

Comments
 (0)