Skip to content

Commit 8e80592

Browse files
authored
Remove state queue from useSyncExternalStore (#22265)
The userspace shim of useSyncExternalStore uses a useState hook because it's the only way to trigger a re-render. We don't actually use the queue to store anything, because we read the current value directly from the store. In the native implementation, we can schedule an update on the fiber directly, without the overhead of a queue.
1 parent 06f98c1 commit 8e80592

File tree

2 files changed

+136
-124
lines changed

2 files changed

+136
-124
lines changed

packages/react-reconciler/src/ReactFiberHooks.new.js

+68-62
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import {
4141
} from './ReactTypeOfMode';
4242
import {
4343
NoLane,
44+
SyncLane,
4445
NoLanes,
4546
isSubsetOfLanes,
4647
mergeLanes,
@@ -49,9 +50,9 @@ import {
4950
isTransitionLane,
5051
markRootEntangled,
5152
markRootMutableRead,
53+
NoTimestamp,
5254
} from './ReactFiberLane.new';
5355
import {
54-
DiscreteEventPriority,
5556
ContinuousEventPriority,
5657
getCurrentUpdatePriority,
5758
setCurrentUpdatePriority,
@@ -147,7 +148,7 @@ export type Hook = {|
147148
memoizedState: any,
148149
baseState: any,
149150
baseQueue: Update<any, any> | null,
150-
queue: UpdateQueue<any, any> | null,
151+
queue: any,
151152
next: Hook | null,
152153
|};
153154

@@ -159,6 +160,11 @@ export type Effect = {|
159160
next: Effect,
160161
|};
161162

163+
type StoreInstance<T> = {|
164+
value: T,
165+
getSnapshot: () => T,
166+
|};
167+
162168
export type FunctionComponentUpdateQueue = {|lastEffect: Effect | null|};
163169

164170
type BasicStateAction<S> = (S => S) | S;
@@ -703,14 +709,15 @@ function mountReducer<S, I, A>(
703709
initialState = ((initialArg: any): S);
704710
}
705711
hook.memoizedState = hook.baseState = initialState;
706-
const queue = (hook.queue = {
712+
const queue: UpdateQueue<S, A> = {
707713
pending: null,
708714
interleaved: null,
709715
lanes: NoLanes,
710716
dispatch: null,
711717
lastRenderedReducer: reducer,
712718
lastRenderedState: (initialState: any),
713-
});
719+
};
720+
hook.queue = queue;
714721
const dispatch: Dispatch<A> = (queue.dispatch = (dispatchAction.bind(
715722
null,
716723
currentlyRenderingFiber,
@@ -1196,7 +1203,7 @@ function useMutableSource<Source, Snapshot>(
11961203
// So if there are interleaved updates, they get pushed to the older queue.
11971204
// When this becomes current, the previous queue and dispatch method will be discarded,
11981205
// including any interleaving updates that occur.
1199-
const newQueue = {
1206+
const newQueue: UpdateQueue<Snapshot, BasicStateAction<Snapshot>> = {
12001207
pending: null,
12011208
interleaved: null,
12021209
lanes: NoLanes,
@@ -1249,86 +1256,86 @@ function mountSyncExternalStore<T>(
12491256
getSnapshot: () => T,
12501257
): T {
12511258
const hook = mountWorkInProgressHook();
1252-
return useSyncExternalStore(hook, subscribe, getSnapshot);
1259+
// Read the current snapshot from the store on every render. This breaks the
1260+
// normal rules of React, and only works because store updates are
1261+
// always synchronous.
1262+
const nextSnapshot = getSnapshot();
1263+
if (__DEV__) {
1264+
if (!didWarnUncachedGetSnapshot) {
1265+
if (nextSnapshot !== getSnapshot()) {
1266+
console.error(
1267+
'The result of getSnapshot should be cached to avoid an infinite loop',
1268+
);
1269+
didWarnUncachedGetSnapshot = true;
1270+
}
1271+
}
1272+
}
1273+
hook.memoizedState = nextSnapshot;
1274+
const inst: StoreInstance<T> = {
1275+
value: nextSnapshot,
1276+
getSnapshot,
1277+
};
1278+
hook.queue = inst;
1279+
return useSyncExternalStore(hook, inst, subscribe, getSnapshot, nextSnapshot);
12531280
}
12541281

12551282
function updateSyncExternalStore<T>(
12561283
subscribe: (() => void) => () => void,
12571284
getSnapshot: () => T,
12581285
): T {
12591286
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
1287+
// Read the current snapshot from the store on every render. This breaks the
1288+
// normal rules of React, and only works because store updates are
12781289
// always synchronous.
1279-
const value = getSnapshot();
1290+
const nextSnapshot = getSnapshot();
12801291
if (__DEV__) {
12811292
if (!didWarnUncachedGetSnapshot) {
1282-
if (value !== getSnapshot()) {
1293+
if (nextSnapshot !== getSnapshot()) {
12831294
console.error(
12841295
'The result of getSnapshot should be cached to avoid an infinite loop',
12851296
);
12861297
didWarnUncachedGetSnapshot = true;
12871298
}
12881299
}
12891300
}
1301+
const prevSnapshot = hook.memoizedState;
1302+
if (!is(prevSnapshot, nextSnapshot)) {
1303+
hook.memoizedState = nextSnapshot;
1304+
markWorkInProgressReceivedUpdate();
1305+
}
1306+
const inst = hook.queue;
1307+
return useSyncExternalStore(hook, inst, subscribe, getSnapshot, nextSnapshot);
1308+
}
12901309

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-
});
1310+
function useSyncExternalStore<T>(
1311+
hook: Hook,
1312+
inst: StoreInstance<T>,
1313+
subscribe: (() => void) => () => void,
1314+
getSnapshot: () => T,
1315+
nextSnapshot: T,
1316+
): T {
1317+
const fiber = currentlyRenderingFiber;
1318+
const dispatcher = ReactCurrentDispatcher.current;
13081319

13091320
// Track the latest getSnapshot function with a ref. This needs to be updated
13101321
// in the layout phase so we can access it during the tearing check that
13111322
// happens on subscribe.
13121323
// TODO: Circumvent SSR warning
13131324
dispatcher.useLayoutEffect(() => {
1314-
inst.value = value;
1325+
inst.value = nextSnapshot;
13151326
inst.getSnapshot = getSnapshot;
13161327

13171328
// Whenever getSnapshot or subscribe changes, we need to check in the
13181329
// commit phase if there was an interleaved mutation. In concurrent mode
13191330
// this can happen all the time, but even in synchronous mode, an earlier
13201331
// effect may have mutated the store.
1332+
// TODO: Move the tearing checks to an earlier, pre-commit phase so that the
1333+
// layout effects always observe a consistent tree.
13211334
if (checkIfSnapshotChanged(inst)) {
13221335
// 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;
1336+
forceStoreRerender(fiber);
13301337
}
1331-
}, [subscribe, value, getSnapshot]);
1338+
}, [subscribe, nextSnapshot, getSnapshot]);
13321339

13331340
dispatcher.useEffect(() => {
13341341
const handleStoreChange = () => {
@@ -1341,13 +1348,7 @@ function useSyncExternalStore<T>(
13411348
// read from the store.
13421349
if (checkIfSnapshotChanged(inst)) {
13431350
// 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+
forceStoreRerender(fiber);
13511352
}
13521353
};
13531354
// Check for changes right before subscribing. Subsequent changes will be
@@ -1357,7 +1358,7 @@ function useSyncExternalStore<T>(
13571358
return subscribe(handleStoreChange);
13581359
}, [subscribe]);
13591360

1360-
return value;
1361+
return nextSnapshot;
13611362
}
13621363

13631364
function checkIfSnapshotChanged(inst) {
@@ -1371,6 +1372,10 @@ function checkIfSnapshotChanged(inst) {
13711372
}
13721373
}
13731374

1375+
function forceStoreRerender(fiber) {
1376+
scheduleUpdateOnFiber(fiber, SyncLane, NoTimestamp);
1377+
}
1378+
13741379
function mountState<S>(
13751380
initialState: (() => S) | S,
13761381
): [S, Dispatch<BasicStateAction<S>>] {
@@ -1380,14 +1385,15 @@ function mountState<S>(
13801385
initialState = initialState();
13811386
}
13821387
hook.memoizedState = hook.baseState = initialState;
1383-
const queue = (hook.queue = {
1388+
const queue: UpdateQueue<S, BasicStateAction<S>> = {
13841389
pending: null,
13851390
interleaved: null,
13861391
lanes: NoLanes,
13871392
dispatch: null,
13881393
lastRenderedReducer: basicStateReducer,
13891394
lastRenderedState: (initialState: any),
1390-
});
1395+
};
1396+
hook.queue = queue;
13911397
const dispatch: Dispatch<
13921398
BasicStateAction<S>,
13931399
> = (queue.dispatch = (dispatchAction.bind(

0 commit comments

Comments
 (0)