Skip to content

Commit b36c421

Browse files
committed
Use an update queue for refreshes
Refreshes are easier than initial mounts because we have a mounted fiber that we can attach the cache to. We don't need to rely on clever pooling tricks; they're just normal updates. More importantly, we're not at risk of dropping requests/data if we run out of lanes, which is especially important for refreshes because they can contain data seeded from a server mutation response; we cannot afford to accidentally evict it.
1 parent bba8139 commit b36c421

8 files changed

+137
-103
lines changed

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

+48-33
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ import type {
2323
OffscreenProps,
2424
OffscreenState,
2525
} from './ReactFiberOffscreenComponent';
26-
import type {CacheInstance} from './ReactFiberCacheComponent';
26+
import type {CacheInstance, Cache} from './ReactFiberCacheComponent';
27+
import type {UpdateQueue} from './ReactUpdateQueue.new';
2728

2829
import checkPropTypes from 'shared/checkPropTypes';
2930

@@ -671,9 +672,11 @@ function updateCacheComponent(
671672
? CacheContext._currentValue
672673
: CacheContext._currentValue2;
673674

674-
let ownCacheInstance: CacheInstance | null = null;
675+
let cacheInstance: CacheInstance | null = null;
675676
if (current === null) {
676677
// This is a newly mounted component. Request a fresh cache.
678+
// TODO: Fast path when parent cache component is also a new mount? We can
679+
// check `parentCacheInstance.provider.alternate`.
677680
const root = getWorkInProgressRoot();
678681
invariant(
679682
root !== null,
@@ -684,62 +687,74 @@ function updateCacheComponent(
684687
// This may be the same as the parent cache, like if the current render
685688
// spawned from a previous render that already committed. Otherwise, this
686689
// is the root of a cache consistency boundary.
690+
let initialState;
687691
if (freshCache !== parentCacheInstance.cache) {
688-
ownCacheInstance = {
692+
cacheInstance = {
689693
cache: freshCache,
690694
provider: workInProgress,
691695
};
692-
pushProvider(workInProgress, CacheContext, ownCacheInstance);
696+
initialState = {
697+
cache: freshCache,
698+
};
699+
pushProvider(workInProgress, CacheContext, cacheInstance);
693700
// No need to propagate the refresh, because this is a new tree.
694701
} else {
695702
// Use the parent cache
696-
ownCacheInstance = null;
703+
cacheInstance = null;
704+
initialState = {
705+
cache: null,
706+
};
697707
}
708+
// Initialize an update queue. We use this for refreshes.
709+
workInProgress.memoizedState = initialState;
710+
initializeUpdateQueue(workInProgress);
698711
} else {
699712
// This component already mounted.
700713
if (includesSomeLane(renderLanes, updateLanes)) {
701-
// A refresh was scheduled.
702-
const root = getWorkInProgressRoot();
703-
invariant(
704-
root !== null,
705-
'Expected a work-in-progress root. This is a bug in React. Please ' +
706-
'file an issue.',
707-
);
708-
const freshCache = requestFreshCache(root, renderLanes);
709-
if (
710-
parentCacheInstance === null ||
711-
freshCache !== parentCacheInstance.cache
712-
) {
713-
ownCacheInstance = {
714-
cache: freshCache,
714+
// An refresh was scheduled. If it was an refresh on this fiber, then we
715+
// will have an update in the queue. Otherwise, it must have been an
716+
// update on a parent, propagated via context.
717+
cloneUpdateQueue(current, workInProgress);
718+
processUpdateQueue(workInProgress, null, null, renderLanes);
719+
const prevCache: Cache | null = current.memoizedState.cache;
720+
const nextCache: Cache | null = workInProgress.memoizedState.cache;
721+
722+
if (nextCache !== prevCache && nextCache !== null) {
723+
// Received a refresh.
724+
cacheInstance = {
725+
cache: nextCache,
715726
provider: workInProgress,
716727
};
717-
pushProvider(workInProgress, CacheContext, ownCacheInstance);
728+
pushProvider(workInProgress, CacheContext, cacheInstance);
718729
// Refreshes propagate through the entire subtree. The refreshed cache
719730
// will override nested caches.
720731
propagateCacheRefresh(workInProgress, renderLanes);
721732
} else {
722-
// The fresh cache is the same as the parent cache.
723-
ownCacheInstance = null;
733+
// A parent cache boundary refreshed. So we can use the cache context.
734+
cacheInstance = null;
735+
736+
// If the update queue is empty, disconnect the old cache from the tree
737+
// so it can be garbage collected.
738+
if (workInProgress.lanes === NoLanes) {
739+
const updateQueue: UpdateQueue<any> = (workInProgress.updateQueue: any);
740+
workInProgress.memoizedState = updateQueue.baseState = {cache: null};
741+
}
724742
}
725743
} else {
726744
// Reuse the memoized cache.
727-
const prevCacheInstance: CacheInstance | null = current.memoizedState;
728-
if (prevCacheInstance !== null) {
729-
ownCacheInstance = prevCacheInstance;
745+
cacheInstance = current.stateNode;
746+
if (cacheInstance !== null) {
730747
// There was no refresh, so no need to propagate to nested boundaries.
731-
pushProvider(workInProgress, CacheContext, ownCacheInstance);
732-
} else {
733-
ownCacheInstance = null;
748+
pushProvider(workInProgress, CacheContext, cacheInstance);
734749
}
735750
}
736751
}
737752

738-
// If this CacheComponent is the root of its tree, then `memoizedState` will
739-
// point to a cache object. Otherwise, a null state indicates that this
753+
// If this CacheComponent is the root of its tree, then `stateNode` will
754+
// point to a cache instance. Otherwise, a null instance indicates that this
740755
// CacheComponent inherits from a parent boundary. We can use this to infer
741756
// whether to push/pop the cache context.
742-
workInProgress.memoizedState = ownCacheInstance;
757+
workInProgress.stateNode = cacheInstance;
743758

744759
const nextChildren = workInProgress.pendingProps.children;
745760
reconcileChildren(current, workInProgress, nextChildren, renderLanes);
@@ -3349,11 +3364,11 @@ function beginWork(
33493364
}
33503365
case CacheComponent: {
33513366
if (enableCache) {
3352-
const ownCacheInstance: CacheInstance | null =
3353-
workInProgress.memoizedState;
3367+
const ownCacheInstance: CacheInstance | null = current.stateNode;
33543368
if (ownCacheInstance !== null) {
33553369
pushProvider(workInProgress, CacheContext, ownCacheInstance);
33563370
}
3371+
workInProgress.stateNode = ownCacheInstance;
33573372
}
33583373
break;
33593374
}

packages/react-reconciler/src/ReactFiberBeginWork.old.js

+48-33
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ import type {
2323
OffscreenProps,
2424
OffscreenState,
2525
} from './ReactFiberOffscreenComponent';
26-
import type {CacheInstance} from './ReactFiberCacheComponent';
26+
import type {CacheInstance, Cache} from './ReactFiberCacheComponent';
27+
import type {UpdateQueue} from './ReactUpdateQueue.new';
2728

2829
import checkPropTypes from 'shared/checkPropTypes';
2930

@@ -671,9 +672,11 @@ function updateCacheComponent(
671672
? CacheContext._currentValue
672673
: CacheContext._currentValue2;
673674

674-
let ownCacheInstance: CacheInstance | null = null;
675+
let cacheInstance: CacheInstance | null = null;
675676
if (current === null) {
676677
// This is a newly mounted component. Request a fresh cache.
678+
// TODO: Fast path when parent cache component is also a new mount? We can
679+
// check `parentCacheInstance.provider.alternate`.
677680
const root = getWorkInProgressRoot();
678681
invariant(
679682
root !== null,
@@ -684,62 +687,74 @@ function updateCacheComponent(
684687
// This may be the same as the parent cache, like if the current render
685688
// spawned from a previous render that already committed. Otherwise, this
686689
// is the root of a cache consistency boundary.
690+
let initialState;
687691
if (freshCache !== parentCacheInstance.cache) {
688-
ownCacheInstance = {
692+
cacheInstance = {
689693
cache: freshCache,
690694
provider: workInProgress,
691695
};
692-
pushProvider(workInProgress, CacheContext, ownCacheInstance);
696+
initialState = {
697+
cache: freshCache,
698+
};
699+
pushProvider(workInProgress, CacheContext, cacheInstance);
693700
// No need to propagate the refresh, because this is a new tree.
694701
} else {
695702
// Use the parent cache
696-
ownCacheInstance = null;
703+
cacheInstance = null;
704+
initialState = {
705+
cache: null,
706+
};
697707
}
708+
// Initialize an update queue. We use this for refreshes.
709+
workInProgress.memoizedState = initialState;
710+
initializeUpdateQueue(workInProgress);
698711
} else {
699712
// This component already mounted.
700713
if (includesSomeLane(renderLanes, updateLanes)) {
701-
// A refresh was scheduled.
702-
const root = getWorkInProgressRoot();
703-
invariant(
704-
root !== null,
705-
'Expected a work-in-progress root. This is a bug in React. Please ' +
706-
'file an issue.',
707-
);
708-
const freshCache = requestFreshCache(root, renderLanes);
709-
if (
710-
parentCacheInstance === null ||
711-
freshCache !== parentCacheInstance.cache
712-
) {
713-
ownCacheInstance = {
714-
cache: freshCache,
714+
// An refresh was scheduled. If it was an refresh on this fiber, then we
715+
// will have an update in the queue. Otherwise, it must have been an
716+
// update on a parent, propagated via context.
717+
cloneUpdateQueue(current, workInProgress);
718+
processUpdateQueue(workInProgress, null, null, renderLanes);
719+
const prevCache: Cache | null = current.memoizedState.cache;
720+
const nextCache: Cache | null = workInProgress.memoizedState.cache;
721+
722+
if (nextCache !== prevCache && nextCache !== null) {
723+
// Received a refresh.
724+
cacheInstance = {
725+
cache: nextCache,
715726
provider: workInProgress,
716727
};
717-
pushProvider(workInProgress, CacheContext, ownCacheInstance);
728+
pushProvider(workInProgress, CacheContext, cacheInstance);
718729
// Refreshes propagate through the entire subtree. The refreshed cache
719730
// will override nested caches.
720731
propagateCacheRefresh(workInProgress, renderLanes);
721732
} else {
722-
// The fresh cache is the same as the parent cache.
723-
ownCacheInstance = null;
733+
// A parent cache boundary refreshed. So we can use the cache context.
734+
cacheInstance = null;
735+
736+
// If the update queue is empty, disconnect the old cache from the tree
737+
// so it can be garbage collected.
738+
if (workInProgress.lanes === NoLanes) {
739+
const updateQueue: UpdateQueue<any> = (workInProgress.updateQueue: any);
740+
workInProgress.memoizedState = updateQueue.baseState = {cache: null};
741+
}
724742
}
725743
} else {
726744
// Reuse the memoized cache.
727-
const prevCacheInstance: CacheInstance | null = current.memoizedState;
728-
if (prevCacheInstance !== null) {
729-
ownCacheInstance = prevCacheInstance;
745+
cacheInstance = current.stateNode;
746+
if (cacheInstance !== null) {
730747
// There was no refresh, so no need to propagate to nested boundaries.
731-
pushProvider(workInProgress, CacheContext, ownCacheInstance);
732-
} else {
733-
ownCacheInstance = null;
748+
pushProvider(workInProgress, CacheContext, cacheInstance);
734749
}
735750
}
736751
}
737752

738-
// If this CacheComponent is the root of its tree, then `memoizedState` will
739-
// point to a cache object. Otherwise, a null state indicates that this
753+
// If this CacheComponent is the root of its tree, then `stateNode` will
754+
// point to a cache instance. Otherwise, a null instance indicates that this
740755
// CacheComponent inherits from a parent boundary. We can use this to infer
741756
// whether to push/pop the cache context.
742-
workInProgress.memoizedState = ownCacheInstance;
757+
workInProgress.stateNode = cacheInstance;
743758

744759
const nextChildren = workInProgress.pendingProps.children;
745760
reconcileChildren(current, workInProgress, nextChildren, renderLanes);
@@ -3349,11 +3364,11 @@ function beginWork(
33493364
}
33503365
case CacheComponent: {
33513366
if (enableCache) {
3352-
const ownCacheInstance: CacheInstance | null =
3353-
workInProgress.memoizedState;
3367+
const ownCacheInstance: CacheInstance | null = current.stateNode;
33543368
if (ownCacheInstance !== null) {
33553369
pushProvider(workInProgress, CacheContext, ownCacheInstance);
33563370
}
3371+
workInProgress.stateNode = ownCacheInstance;
33573372
}
33583373
break;
33593374
}

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

+1-2
Original file line numberDiff line numberDiff line change
@@ -1492,8 +1492,7 @@ function completeWork(
14921492
}
14931493
case CacheComponent: {
14941494
if (enableCache) {
1495-
const ownCacheInstance: CacheInstance | null =
1496-
workInProgress.memoizedState;
1495+
const ownCacheInstance: CacheInstance | null = workInProgress.stateNode;
14971496
if (ownCacheInstance !== null) {
14981497
// This is a cache provider.
14991498
popProvider(CacheContext, workInProgress);

packages/react-reconciler/src/ReactFiberCompleteWork.old.js

+1-2
Original file line numberDiff line numberDiff line change
@@ -1492,8 +1492,7 @@ function completeWork(
14921492
}
14931493
case CacheComponent: {
14941494
if (enableCache) {
1495-
const ownCacheInstance: CacheInstance | null =
1496-
workInProgress.memoizedState;
1495+
const ownCacheInstance: CacheInstance | null = workInProgress.stateNode;
14971496
if (ownCacheInstance !== null) {
14981497
// This is a cache provider.
14991498
popProvider(CacheContext, workInProgress);

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

+15-14
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,6 @@ import {
4646
setCurrentUpdateLanePriority,
4747
higherLanePriority,
4848
DefaultLanePriority,
49-
transferCacheToSpawnedLane,
5049
} from './ReactFiberLane.new';
5150
import {readContext} from './ReactFiberNewContext.new';
5251
import {
@@ -1744,28 +1743,30 @@ function refreshCache<T>(
17441743
// TODO: Does Cache work in legacy mode? Should decide and write a test.
17451744
const root = scheduleUpdateOnFiber(provider, lane, eventTime);
17461745

1747-
let seededCache = null;
1746+
const seededCache = new Map();
17481747
if (seedKey !== null && seedKey !== undefined && root !== null) {
1749-
// TODO: Warn if wrong type
1750-
seededCache = new Map([[seedKey, seedValue]]);
1751-
transferCacheToSpawnedLane(root, seededCache, lane);
1748+
// Seed the cache with the value passed by the caller. This could be from
1749+
// a server mutation, or it could be a streaming response.
1750+
seededCache.set(seedKey, seedValue);
17521751
}
17531752

1753+
// Schedule an update on the cache boundary to trigger a refresh.
1754+
const refreshUpdate = createUpdate(eventTime, lane);
1755+
let payload;
17541756
if (provider.tag === HostRoot) {
1755-
const refreshUpdate = createUpdate(eventTime, lane);
1756-
refreshUpdate.payload = {
1757+
payload = {
17571758
cacheInstance: {
17581759
provider: provider,
1759-
cache:
1760-
// For the root cache, we won't bother to lazily initialize the
1761-
// map. Seed an empty one. This saves use the trouble of having
1762-
// to use an updater function. Maybe we should use this approach
1763-
// for non-root refreshes, too.
1764-
seededCache !== null ? seededCache : new Map(),
1760+
cache: seededCache,
17651761
},
17661762
};
1767-
enqueueUpdate(provider, refreshUpdate);
1763+
} else {
1764+
payload = {
1765+
cache: seededCache,
1766+
};
17681767
}
1768+
refreshUpdate.payload = payload;
1769+
enqueueUpdate(provider, refreshUpdate);
17691770
} finally {
17701771
ReactCurrentBatchConfig.transition = prevTransition;
17711772
}

0 commit comments

Comments
 (0)