Skip to content

Commit ea5478c

Browse files
committed
Cache refreshing
Implements useRefresh, a method to invalidate the cache and request new data. It will find the nearest <Cache /> boundary, clear its cache, and schedule an update to re-render with fresh data. We had discussed calling this method `useCacheInvalidation`. The problem I have with that name is that it is bad. I went with `useRefresh` because it, by contrast, is good. One might object is that it clashes with the name for "Fast Refresh" but I disagree. It's experimental anyway so we can bikeshed the name before release.
1 parent d8127ae commit ea5478c

25 files changed

+879
-45
lines changed

packages/react-dom/src/server/ReactPartialRendererHooks.js

+5
Original file line numberDiff line numberDiff line change
@@ -489,6 +489,10 @@ function useOpaqueIdentifier(): OpaqueIDType {
489489
);
490490
}
491491

492+
function useRefresh(): () => void {
493+
invariant(false, 'Not implemented.');
494+
}
495+
492496
function noop(): void {}
493497

494498
export let currentPartialRenderer: PartialRenderer = (null: any);
@@ -520,4 +524,5 @@ export const Dispatcher: DispatcherType = {
520524

521525
if (enableCache) {
522526
Dispatcher.getCacheForType = getCacheForType;
527+
Dispatcher.useRefresh = useRefresh;
523528
}

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

+80-16
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ import {
133133
isSuspenseInstanceFallback,
134134
registerSuspenseInstanceRetry,
135135
supportsHydration,
136+
isPrimaryRenderer,
136137
} from './ReactFiberHostConfig';
137138
import type {SuspenseInstance} from './ReactFiberHostConfig';
138139
import {shouldSuspend} from './ReactFiberReconciler';
@@ -151,6 +152,7 @@ import {findFirstSuspended} from './ReactFiberSuspenseComponent.new';
151152
import {
152153
pushProvider,
153154
propagateContextChange,
155+
propagateCacheRefresh,
154156
readContext,
155157
prepareToReadContext,
156158
calculateChangedBits,
@@ -662,22 +664,82 @@ function updateCacheComponent(
662664
return null;
663665
}
664666

665-
const root = getWorkInProgressRoot();
666-
invariant(
667-
root !== null,
668-
'Expected a work-in-progress root. This is a bug in React. Please ' +
669-
'file an issue.',
670-
);
667+
// Read directly from the context. We don't set up a context dependency
668+
// because the propagation function automatically includes CacheComponents in
669+
// its search.
670+
const parentCache: Cache | null = isPrimaryRenderer
671+
? CacheContext._currentValue
672+
: CacheContext._currentValue2;
671673

672-
const cache: Cache =
673-
current === null
674-
? requestFreshCache(root, renderLanes)
675-
: current.memoizedState;
676-
677-
// TODO: Propagate changes, once refreshing exists.
678-
pushProvider(workInProgress, CacheContext, cache);
674+
let ownCache: Cache | null = null;
675+
if (parentCache !== null && parentCache.providers === null) {
676+
// The parent boundary also has a new cache. We're either inside a new tree,
677+
// or there was a refresh. In both cases, we should use the parent cache.
678+
ownCache = null;
679+
} else {
680+
if (current === null) {
681+
// This is a newly mounted component. Request a fresh cache.
682+
const root = getWorkInProgressRoot();
683+
invariant(
684+
root !== null,
685+
'Expected a work-in-progress root. This is a bug in React. Please ' +
686+
'file an issue.',
687+
);
688+
const freshCache = requestFreshCache(root, renderLanes);
689+
// This may be the same as the parent cache, like if the current render
690+
// spawned from a previous render that already committed. Otherwise, this
691+
// is the root of a cache consistency boundary.
692+
if (freshCache !== parentCache) {
693+
ownCache = freshCache;
694+
pushProvider(workInProgress, CacheContext, freshCache);
695+
// No need to propagate the refresh, because this is a new tree.
696+
} else {
697+
// Use the parent cache
698+
ownCache = null;
699+
}
700+
} else {
701+
// This component already mounted.
702+
if (includesSomeLane(renderLanes, updateLanes)) {
703+
// A refresh was scheduled.
704+
const root = getWorkInProgressRoot();
705+
invariant(
706+
root !== null,
707+
'Expected a work-in-progress root. This is a bug in React. Please ' +
708+
'file an issue.',
709+
);
710+
const freshCache = requestFreshCache(root, renderLanes);
711+
if (freshCache !== parentCache) {
712+
ownCache = freshCache;
713+
pushProvider(workInProgress, CacheContext, freshCache);
714+
// Refreshes propagate through the entire subtree. The refreshed cache
715+
// will override nested caches.
716+
propagateCacheRefresh(workInProgress, renderLanes);
717+
} else {
718+
// The fresh cache is the same as the parent cache. I think this
719+
// unreachable in practice, because this means the parent cache was
720+
// refreshed in the same render. So we would have already handled this
721+
// in the earlier branch, where we check if the parent is new.
722+
ownCache = null;
723+
}
724+
} else {
725+
// Reuse the memoized cache.
726+
const prevCache: Cache | null = current.memoizedState;
727+
if (prevCache !== null) {
728+
ownCache = prevCache;
729+
// There was no refresh, so no need to propagate to nested boundaries.
730+
pushProvider(workInProgress, CacheContext, prevCache);
731+
} else {
732+
ownCache = null;
733+
}
734+
}
735+
}
736+
}
679737

680-
workInProgress.memoizedState = cache;
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
740+
// CacheComponent inherits from a parent boundary. We can use this to infer
741+
// whether to push/pop the cache context.
742+
workInProgress.memoizedState = ownCache;
681743

682744
const nextChildren = workInProgress.pendingProps.children;
683745
reconcileChildren(current, workInProgress, nextChildren, renderLanes);
@@ -3273,8 +3335,10 @@ function beginWork(
32733335
}
32743336
case CacheComponent: {
32753337
if (enableCache) {
3276-
const cache: Cache = current.memoizedState;
3277-
pushProvider(workInProgress, CacheContext, cache);
3338+
const ownCache: Cache | null = workInProgress.memoizedState;
3339+
if (ownCache !== null) {
3340+
pushProvider(workInProgress, CacheContext, ownCache);
3341+
}
32783342
}
32793343
break;
32803344
}

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

+80-16
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ import {
133133
isSuspenseInstanceFallback,
134134
registerSuspenseInstanceRetry,
135135
supportsHydration,
136+
isPrimaryRenderer,
136137
} from './ReactFiberHostConfig';
137138
import type {SuspenseInstance} from './ReactFiberHostConfig';
138139
import {shouldSuspend} from './ReactFiberReconciler';
@@ -151,6 +152,7 @@ import {findFirstSuspended} from './ReactFiberSuspenseComponent.old';
151152
import {
152153
pushProvider,
153154
propagateContextChange,
155+
propagateCacheRefresh,
154156
readContext,
155157
prepareToReadContext,
156158
calculateChangedBits,
@@ -662,22 +664,82 @@ function updateCacheComponent(
662664
return null;
663665
}
664666

665-
const root = getWorkInProgressRoot();
666-
invariant(
667-
root !== null,
668-
'Expected a work-in-progress root. This is a bug in React. Please ' +
669-
'file an issue.',
670-
);
667+
// Read directly from the context. We don't set up a context dependency
668+
// because the propagation function automatically includes CacheComponents in
669+
// its search.
670+
const parentCache: Cache | null = isPrimaryRenderer
671+
? CacheContext._currentValue
672+
: CacheContext._currentValue2;
671673

672-
const cache: Cache =
673-
current === null
674-
? requestFreshCache(root, renderLanes)
675-
: current.memoizedState;
676-
677-
// TODO: Propagate changes, once refreshing exists.
678-
pushProvider(workInProgress, CacheContext, cache);
674+
let ownCache: Cache | null = null;
675+
if (parentCache !== null && parentCache.providers === null) {
676+
// The parent boundary also has a new cache. We're either inside a new tree,
677+
// or there was a refresh. In both cases, we should use the parent cache.
678+
ownCache = null;
679+
} else {
680+
if (current === null) {
681+
// This is a newly mounted component. Request a fresh cache.
682+
const root = getWorkInProgressRoot();
683+
invariant(
684+
root !== null,
685+
'Expected a work-in-progress root. This is a bug in React. Please ' +
686+
'file an issue.',
687+
);
688+
const freshCache = requestFreshCache(root, renderLanes);
689+
// This may be the same as the parent cache, like if the current render
690+
// spawned from a previous render that already committed. Otherwise, this
691+
// is the root of a cache consistency boundary.
692+
if (freshCache !== parentCache) {
693+
ownCache = freshCache;
694+
pushProvider(workInProgress, CacheContext, freshCache);
695+
// No need to propagate the refresh, because this is a new tree.
696+
} else {
697+
// Use the parent cache
698+
ownCache = null;
699+
}
700+
} else {
701+
// This component already mounted.
702+
if (includesSomeLane(renderLanes, updateLanes)) {
703+
// A refresh was scheduled.
704+
const root = getWorkInProgressRoot();
705+
invariant(
706+
root !== null,
707+
'Expected a work-in-progress root. This is a bug in React. Please ' +
708+
'file an issue.',
709+
);
710+
const freshCache = requestFreshCache(root, renderLanes);
711+
if (freshCache !== parentCache) {
712+
ownCache = freshCache;
713+
pushProvider(workInProgress, CacheContext, freshCache);
714+
// Refreshes propagate through the entire subtree. The refreshed cache
715+
// will override nested caches.
716+
propagateCacheRefresh(workInProgress, renderLanes);
717+
} else {
718+
// The fresh cache is the same as the parent cache. I think this
719+
// unreachable in practice, because this means the parent cache was
720+
// refreshed in the same render. So we would have already handled this
721+
// in the earlier branch, where we check if the parent is new.
722+
ownCache = null;
723+
}
724+
} else {
725+
// Reuse the memoized cache.
726+
const prevCache: Cache | null = current.memoizedState;
727+
if (prevCache !== null) {
728+
ownCache = prevCache;
729+
// There was no refresh, so no need to propagate to nested boundaries.
730+
pushProvider(workInProgress, CacheContext, prevCache);
731+
} else {
732+
ownCache = null;
733+
}
734+
}
735+
}
736+
}
679737

680-
workInProgress.memoizedState = cache;
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
740+
// CacheComponent inherits from a parent boundary. We can use this to infer
741+
// whether to push/pop the cache context.
742+
workInProgress.memoizedState = ownCache;
681743

682744
const nextChildren = workInProgress.pendingProps.children;
683745
reconcileChildren(current, workInProgress, nextChildren, renderLanes);
@@ -3273,8 +3335,10 @@ function beginWork(
32733335
}
32743336
case CacheComponent: {
32753337
if (enableCache) {
3276-
const cache: Cache = current.memoizedState;
3277-
pushProvider(workInProgress, CacheContext, cache);
3338+
const ownCache: Cache | null = workInProgress.memoizedState;
3339+
if (ownCache !== null) {
3340+
pushProvider(workInProgress, CacheContext, ownCache);
3341+
}
32783342
}
32793343
break;
32803344
}

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

+25
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import {
3636
enableFundamentalAPI,
3737
enableSuspenseCallback,
3838
enableScopeAPI,
39+
enableCache,
3940
} from 'shared/ReactFeatureFlags';
4041
import {
4142
FunctionComponent,
@@ -793,6 +794,30 @@ function commitLifeCycles(
793794
case OffscreenComponent:
794795
case LegacyHiddenComponent:
795796
return;
797+
case CacheComponent: {
798+
if (enableCache) {
799+
if (current !== null) {
800+
const oldCache: Cache | null = current.memoizedState;
801+
if (oldCache !== null) {
802+
const oldCacheProviders = oldCache.providers;
803+
if (oldCacheProviders) {
804+
oldCacheProviders.delete(current);
805+
oldCacheProviders.delete(finishedWork);
806+
}
807+
}
808+
}
809+
const newCache: Cache | null = finishedWork.memoizedState;
810+
if (newCache !== null) {
811+
const newCacheProviders = newCache.providers;
812+
if (newCacheProviders === null) {
813+
newCache.providers = new Set([finishedWork]);
814+
} else {
815+
newCacheProviders.add(finishedWork);
816+
}
817+
}
818+
}
819+
return;
820+
}
796821
}
797822
invariant(
798823
false,

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

+25
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import {
3737
enableSuspenseCallback,
3838
enableScopeAPI,
3939
enableDoubleInvokingEffects,
40+
enableCache,
4041
} from 'shared/ReactFeatureFlags';
4142
import {
4243
FunctionComponent,
@@ -794,6 +795,30 @@ function commitLifeCycles(
794795
case OffscreenComponent:
795796
case LegacyHiddenComponent:
796797
return;
798+
case CacheComponent: {
799+
if (enableCache) {
800+
if (current !== null) {
801+
const oldCache: Cache | null = current.memoizedState;
802+
if (oldCache !== null) {
803+
const oldCacheProviders = oldCache.providers;
804+
if (oldCacheProviders) {
805+
oldCacheProviders.delete(current);
806+
oldCacheProviders.delete(finishedWork);
807+
}
808+
}
809+
}
810+
const newCache: Cache | null = finishedWork.memoizedState;
811+
if (newCache !== null) {
812+
const newCacheProviders = newCache.providers;
813+
if (newCacheProviders === null) {
814+
newCache.providers = new Set([finishedWork]);
815+
} else {
816+
newCacheProviders.add(finishedWork);
817+
}
818+
}
819+
}
820+
return;
821+
}
797822
}
798823
invariant(
799824
false,

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

+22-1
Original file line numberDiff line numberDiff line change
@@ -1488,7 +1488,28 @@ function completeWork(
14881488
}
14891489
case CacheComponent: {
14901490
if (enableCache) {
1491-
popProvider(CacheContext, workInProgress);
1491+
// If the cache provided by this boundary has changed, schedule an
1492+
// effect to add this component to the cache's providers, and to remove
1493+
// it from the old cache.
1494+
// TODO: Schedule for Passive phase
1495+
const ownCache: Cache | null = workInProgress.memoizedState;
1496+
if (current === null) {
1497+
if (ownCache !== null) {
1498+
// This is a cache provider.
1499+
popProvider(CacheContext, workInProgress);
1500+
// Set up a refresh subscription.
1501+
workInProgress.flags |= Update;
1502+
}
1503+
} else {
1504+
if (ownCache !== null) {
1505+
// This is a cache provider.
1506+
popProvider(CacheContext, workInProgress);
1507+
}
1508+
if (ownCache !== current.memoizedState) {
1509+
// Cache changed. Create or update a refresh subscription.
1510+
workInProgress.flags |= Update;
1511+
}
1512+
}
14921513
bubbleProperties(workInProgress);
14931514
return null;
14941515
}

0 commit comments

Comments
 (0)