44
SyncLane,
45
NoLanes,
46
isSubsetOfLanes,
47
+ includesBlockingLane,
48
mergeLanes,
49
removeLanes,
50
intersectLanes,
69
PassiveStatic as PassiveStaticEffect,
70
StaticMask as StaticMaskEffect,
71
Update as UpdateEffect,
72
+ StoreConsistency,
73
} from './ReactFiberFlags';
74
import {
75
HasEffect as HookHasEffect,
168
getSnapshot: () => T,
169
|};
170
169
-export type FunctionComponentUpdateQueue = {|lastEffect: Effect | null|};
171
+type StoreConsistencyCheck<T> = {|
172
+ value: T,
173
+ getSnapshot: () => T,
174
+|};
175
+
176
+export type FunctionComponentUpdateQueue = {|
177
+ lastEffect: Effect | null,
178
+ stores: Array<StoreConsistencyCheck<any>> | null,
179
+|};
180
181
type BasicStateAction<S> = (S => S) | S;
182
699
function createFunctionComponentUpdateQueue(): FunctionComponentUpdateQueue {
700
return {
701
lastEffect: null,
702
+ stores: null,
703
};
704
}
705
1267
subscribe: (() => void) => () => void,
1268
getSnapshot: () => T,
1269
): T {
1270
+ const fiber = currentlyRenderingFiber;
1271
const hook = mountWorkInProgressHook();
1272
// Read the current snapshot from the store on every render. This breaks the
1273
// normal rules of React, and only works because store updates are
1289
getSnapshot,
1290
};
1291
hook.queue = inst;
1280
- return useSyncExternalStore(hook, inst, subscribe, getSnapshot, nextSnapshot);
1292
+
1293
+ // Schedule an effect to subscribe to the store.
1294
+ mountEffect(subscribeToStore.bind(null, fiber, inst, subscribe), [subscribe]);
1295
+
1296
+ // Schedule an effect to update the mutable instance fields. We will update
1297
+ // this whenever subscribe, getSnapshot, or value changes. Because there's no
1298
+ // clean-up function, and we track the deps correctly, we can call pushEffect
1299
+ // directly, without storing any additional state. For the same reason, we
1300
+ // don't need to set a static flag, either.
1301
+ // TODO: We can move this to the passive phase once we add a pre-commit
1302
+ // consistency check. See the next comment.
1303
+ fiber.flags |= PassiveEffect;
1304
+ pushEffect(
1305
+ HookHasEffect | HookPassive,
1306
+ updateStoreInstance.bind(null, fiber, inst, nextSnapshot, getSnapshot),
1307
+ undefined,
1308
+ null,
1309
+ );
1310
+
1311
+ // Unless we're rendering a blocking lane, schedule a consistency check. Right
1312
+ // before committing, we will walk the tree and check if any of the stores
1313
+ // were mutated.
1314
+ const root: FiberRoot | null = getWorkInProgressRoot();
1315
+ invariant(
1316
+ root !== null,
1317
+ 'Expected a work-in-progress root. This is a bug in React. Please file an issue.',
1318
+ );
1319
+ if (!includesBlockingLane(root, renderLanes)) {
1320
+ pushStoreConsistencyCheck(fiber, getSnapshot, nextSnapshot);
1321
+ }
1322
+
1323
+ return nextSnapshot;
1324
}
1325
1326
function updateSyncExternalStore<T>(
1327
subscribe: (() => void) => () => void,
1328
getSnapshot: () => T,
1329
): T {
1330
+ const fiber = currentlyRenderingFiber;
1331
const hook = updateWorkInProgressHook();
1332
// Read the current snapshot from the store on every render. This breaks the
1333
// normal rules of React, and only works because store updates are
1344
}
1345
}
1346
const prevSnapshot = hook.memoizedState;
1303
- if (!is(prevSnapshot, nextSnapshot)) {
1347
+ const snapshotChanged = !is(prevSnapshot, nextSnapshot);
1348
+ if (snapshotChanged) {
1349
hook.memoizedState = nextSnapshot;
1350
markWorkInProgressReceivedUpdate();
1351
}
1352
const inst = hook.queue;
1308
- return useSyncExternalStore(hook, inst, subscribe, getSnapshot, nextSnapshot);
1353
+
1354
+ updateEffect(subscribeToStore.bind(null, fiber, inst, subscribe), [
1355
+ subscribe,
1356
+ ]);
1357
+
1358
+ // Whenever getSnapshot or subscribe changes, we need to check in the
1359
+ // commit phase if there was an interleaved mutation. In concurrent mode
1360
+ // this can happen all the time, but even in synchronous mode, an earlier
1361
+ // effect may have mutated the store.
1362
+ if (
1363
+ inst.getSnapshot !== getSnapshot ||
1364
+ snapshotChanged ||
1365
+ // Check if the susbcribe function changed. We can save some memory by
1366
+ // checking whether we scheduled a subscription effect above.
1367
+ (workInProgressHook !== null &&
1368
+ workInProgressHook.memoizedState.tag & HookHasEffect)
1369
+ ) {
1370
+ fiber.flags |= PassiveEffect;
1371
+ pushEffect(
1372
+ HookHasEffect | HookPassive,
1373
+ updateStoreInstance.bind(null, fiber, inst, nextSnapshot, getSnapshot),
1374
+ undefined,
1375
+ null,
1376
+ );
1377
+
1378
+ // Unless we're rendering a blocking lane, schedule a consistency check.
1379
+ // Right before committing, we will walk the tree and check if any of the
1380
+ // stores were mutated.
1381
+ const root: FiberRoot | null = getWorkInProgressRoot();
1382
+ invariant(
1383
+ root !== null,
1384
+ 'Expected a work-in-progress root. This is a bug in React. Please file an issue.',
1385
+ );
1386
+ if (!includesBlockingLane(root, renderLanes)) {
1387
+ pushStoreConsistencyCheck(fiber, getSnapshot, nextSnapshot);
1388
+ }
1389
+ }
1390
+
1391
+ return nextSnapshot;
1392
}
1393
1311
-function useSyncExternalStore<T>(
1312
- hook: Hook,
1313
- inst: StoreInstance<T>,
1314
- subscribe: (() => void) => () => void,
1394
+function pushStoreConsistencyCheck<T>(
1395
+ fiber: Fiber,
1396
getSnapshot: () => T,
1397
+ renderedSnapshot: T,
1398
+) {
1399
+ fiber.flags |= StoreConsistency;
1400
+ const check: StoreConsistencyCheck<T> = {
1401
+ getSnapshot,
1402
+ value: renderedSnapshot,
1403
+ };
1404
+ let componentUpdateQueue: null | FunctionComponentUpdateQueue = (currentlyRenderingFiber.updateQueue: any);
1405
+ if (componentUpdateQueue === null) {
1406
+ componentUpdateQueue = createFunctionComponentUpdateQueue();
1407
+ currentlyRenderingFiber.updateQueue = (componentUpdateQueue: any);
1408
+ componentUpdateQueue.stores = [check];
1409
+ } else {
1410
+ const stores = componentUpdateQueue.stores;
1411
+ if (stores === null) {
1412
+ componentUpdateQueue.stores = [check];
1413
+ } else {
1414
+ stores.push(check);
1415
+ }
1416
+ }
1417
+}
1418
+
1419
+function updateStoreInstance<T>(
1420
+ fiber: Fiber,
1421
+ inst: StoreInstance<T>,
1422
nextSnapshot: T,
1317
-): T {
1318
- const fiber = currentlyRenderingFiber;
1319
- const dispatcher = ReactCurrentDispatcher.current;
1423
+ getSnapshot: () => T,
1424
+) {
1425
+ // These are updated in the passive phase
1426
+ inst.value = nextSnapshot;
1427
+ inst.getSnapshot = getSnapshot;
1428
+
1429
+ // Something may have been mutated in between render and commit. This could
1430
+ // have been in an event that fired before the passive effects, or it could
1431
+ // have been in a layout effect. In that case, we would have used the old
1432
+ // snapsho and getSnapshot values to bail out. We need to check one more time.
1433
+ if (checkIfSnapshotChanged(inst)) {
1434
+ // Force a re-render.
1435
+ forceStoreRerender(fiber);
1436
+ }
1437
+}
1438
1321
- // Track the latest getSnapshot function with a ref. This needs to be updated
1322
- // in the layout phase so we can access it during the tearing check that
1323
- // happens on subscribe.
1324
- // TODO: Circumvent SSR warning
1325
- dispatcher.useLayoutEffect(() => {
1326
- inst.value = nextSnapshot;
1327
- inst.getSnapshot = getSnapshot;
1328
-
1329
- // Whenever getSnapshot or subscribe changes, we need to check in the
1330
- // commit phase if there was an interleaved mutation. In concurrent mode
1331
- // this can happen all the time, but even in synchronous mode, an earlier
1332
- // effect may have mutated the store.
1333
- // TODO: Move the tearing checks to an earlier, pre-commit phase so that the
1334
- // layout effects always observe a consistent tree.
1439
+function subscribeToStore(fiber, inst, subscribe) {
1440
+ const handleStoreChange = () => {
1441
+ // The store changed. Check if the snapshot changed since the last time we
1442
+ // read from the store.
1443
if (checkIfSnapshotChanged(inst)) {
1444
// Force a re-render.
1445
forceStoreRerender(fiber);
1446
}
1339
- }, [subscribe, nextSnapshot, getSnapshot]);
1340
-
1341
- dispatcher.useEffect(() => {
1342
- const handleStoreChange = () => {
1343
- // TODO: Because there is no cross-renderer API for batching updates, it's
1344
- // up to the consumer of this library to wrap their subscription event
1345
- // with unstable_batchedUpdates. Should we try to detect when this isn't
1346
- // the case and print a warning in development?
1347
-
1348
- // The store changed. Check if the snapshot changed since the last time we
1349
- // read from the store.
1350
- if (checkIfSnapshotChanged(inst)) {
1351
- // Force a re-render.
1352
- forceStoreRerender(fiber);
1353
- }
1354
- };
1355
- // Check for changes right before subscribing. Subsequent changes will be
1356
- // detected in the subscription handler.
1357
- handleStoreChange();
1358
- // Subscribe to the store and return a clean-up function.
1359
- return subscribe(handleStoreChange);
1360
- }, [subscribe]);
1361
-
1362
- return nextSnapshot;
1447
+ };
1448
+ // Subscribe to the store and return a clean-up function.
1449
+ return subscribe(handleStoreChange);
1450
}
1451
1452
function checkIfSnapshotChanged(inst) {