Skip to content

Commit

Permalink
Cache refreshing
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
acdlite committed Dec 14, 2020
1 parent 9e18d18 commit c6f9de1
Show file tree
Hide file tree
Showing 25 changed files with 878 additions and 44 deletions.
5 changes: 5 additions & 0 deletions packages/react-dom/src/server/ReactPartialRendererHooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -489,6 +489,10 @@ function useOpaqueIdentifier(): OpaqueIDType {
);
}
function useRefresh(): () => void {
invariant(false, 'Not implemented.');
}

function noop(): void {}

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

if (enableCache) {
Dispatcher.getCacheForType = getCacheForType;
Dispatcher.useRefresh = useRefresh;
}
96 changes: 80 additions & 16 deletions packages/react-reconciler/src/ReactFiberBeginWork.new.js
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ import {
isSuspenseInstanceFallback,
registerSuspenseInstanceRetry,
supportsHydration,
isPrimaryRenderer,
} from './ReactFiberHostConfig';
import type {SuspenseInstance} from './ReactFiberHostConfig';
import {shouldSuspend} from './ReactFiberReconciler';
Expand All @@ -151,6 +152,7 @@ import {findFirstSuspended} from './ReactFiberSuspenseComponent.new';
import {
pushProvider,
propagateContextChange,
propagateCacheRefresh,
readContext,
prepareToReadContext,
calculateChangedBits,
Expand Down Expand Up @@ -662,22 +664,82 @@ function updateCacheComponent(
return null;
}

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

const cache: Cache =
current === null
? requestFreshCache(root, renderLanes)
: current.memoizedState;

// TODO: Propagate changes, once refreshing exists.
pushProvider(workInProgress, CacheContext, cache);
let ownCache: Cache | null = null;
if (parentCache !== null && parentCache.providers === null) {
// The parent boundary also has a new cache. We're either inside a new tree,
// or there was a refresh. In both cases, we should use the parent cache.
ownCache = null;
} else {
if (current === null) {
// This is a newly mounted component. Request a fresh cache.
const root = getWorkInProgressRoot();
invariant(
root !== null,
'Expected a work-in-progress root. This is a bug in React. Please ' +
'file an issue.',
);
const freshCache = requestFreshCache(root, renderLanes);
// This may be the same as the parent cache, like if the current render
// spawned from a previous render that already committed. Otherwise, this
// is the root of a cache consistency boundary.
if (freshCache !== parentCache) {
ownCache = freshCache;
pushProvider(workInProgress, CacheContext, freshCache);
// No need to propagate the refresh, because this is a new tree.
} else {
// Use the parent cache
ownCache = null;
}
} else {
// This component already mounted.
if (includesSomeLane(renderLanes, updateLanes)) {
// A refresh was scheduled.
const root = getWorkInProgressRoot();
invariant(
root !== null,
'Expected a work-in-progress root. This is a bug in React. Please ' +
'file an issue.',
);
const freshCache = requestFreshCache(root, renderLanes);
if (freshCache !== parentCache) {
ownCache = freshCache;
pushProvider(workInProgress, CacheContext, freshCache);
// Refreshes propagate through the entire subtree. The refreshed cache
// will override nested caches.
propagateCacheRefresh(workInProgress, renderLanes);
} else {
// The fresh cache is the same as the parent cache. I think this
// unreachable in practice, because this means the parent cache was
// refreshed in the same render. So we would have already handled this
// in the earlier branch, where we check if the parent is new.
ownCache = null;
}
} else {
// Reuse the memoized cache.
const prevCache: Cache | null = current.memoizedState;
if (prevCache !== null) {
ownCache = prevCache;
// There was no refresh, so no need to propagate to nested boundaries.
pushProvider(workInProgress, CacheContext, prevCache);
} else {
ownCache = null;
}
}
}
}

workInProgress.memoizedState = cache;
// If this CacheComponent is the root of its tree, then `memoizedState` will
// point to a cache object. Otherwise, a null state indicates that this
// CacheComponent inherits from a parent boundary. We can use this to infer
// whether to push/pop the cache context.
workInProgress.memoizedState = ownCache;

const nextChildren = workInProgress.pendingProps.children;
reconcileChildren(current, workInProgress, nextChildren, renderLanes);
Expand Down Expand Up @@ -3273,8 +3335,10 @@ function beginWork(
}
case CacheComponent: {
if (enableCache) {
const cache: Cache = current.memoizedState;
pushProvider(workInProgress, CacheContext, cache);
const ownCache: Cache | null = workInProgress.memoizedState;
if (ownCache !== null) {
pushProvider(workInProgress, CacheContext, ownCache);
}
}
break;
}
Expand Down
96 changes: 80 additions & 16 deletions packages/react-reconciler/src/ReactFiberBeginWork.old.js
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ import {
isSuspenseInstanceFallback,
registerSuspenseInstanceRetry,
supportsHydration,
isPrimaryRenderer,
} from './ReactFiberHostConfig';
import type {SuspenseInstance} from './ReactFiberHostConfig';
import {shouldSuspend} from './ReactFiberReconciler';
Expand All @@ -151,6 +152,7 @@ import {findFirstSuspended} from './ReactFiberSuspenseComponent.old';
import {
pushProvider,
propagateContextChange,
propagateCacheRefresh,
readContext,
prepareToReadContext,
calculateChangedBits,
Expand Down Expand Up @@ -662,22 +664,82 @@ function updateCacheComponent(
return null;
}

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

const cache: Cache =
current === null
? requestFreshCache(root, renderLanes)
: current.memoizedState;

// TODO: Propagate changes, once refreshing exists.
pushProvider(workInProgress, CacheContext, cache);
let ownCache: Cache | null = null;
if (parentCache !== null && parentCache.providers === null) {
// The parent boundary also has a new cache. We're either inside a new tree,
// or there was a refresh. In both cases, we should use the parent cache.
ownCache = null;
} else {
if (current === null) {
// This is a newly mounted component. Request a fresh cache.
const root = getWorkInProgressRoot();
invariant(
root !== null,
'Expected a work-in-progress root. This is a bug in React. Please ' +
'file an issue.',
);
const freshCache = requestFreshCache(root, renderLanes);
// This may be the same as the parent cache, like if the current render
// spawned from a previous render that already committed. Otherwise, this
// is the root of a cache consistency boundary.
if (freshCache !== parentCache) {
ownCache = freshCache;
pushProvider(workInProgress, CacheContext, freshCache);
// No need to propagate the refresh, because this is a new tree.
} else {
// Use the parent cache
ownCache = null;
}
} else {
// This component already mounted.
if (includesSomeLane(renderLanes, updateLanes)) {
// A refresh was scheduled.
const root = getWorkInProgressRoot();
invariant(
root !== null,
'Expected a work-in-progress root. This is a bug in React. Please ' +
'file an issue.',
);
const freshCache = requestFreshCache(root, renderLanes);
if (freshCache !== parentCache) {
ownCache = freshCache;
pushProvider(workInProgress, CacheContext, freshCache);
// Refreshes propagate through the entire subtree. The refreshed cache
// will override nested caches.
propagateCacheRefresh(workInProgress, renderLanes);
} else {
// The fresh cache is the same as the parent cache. I think this
// unreachable in practice, because this means the parent cache was
// refreshed in the same render. So we would have already handled this
// in the earlier branch, where we check if the parent is new.
ownCache = null;
}
} else {
// Reuse the memoized cache.
const prevCache: Cache | null = current.memoizedState;
if (prevCache !== null) {
ownCache = prevCache;
// There was no refresh, so no need to propagate to nested boundaries.
pushProvider(workInProgress, CacheContext, prevCache);
} else {
ownCache = null;
}
}
}
}

workInProgress.memoizedState = cache;
// If this CacheComponent is the root of its tree, then `memoizedState` will
// point to a cache object. Otherwise, a null state indicates that this
// CacheComponent inherits from a parent boundary. We can use this to infer
// whether to push/pop the cache context.
workInProgress.memoizedState = ownCache;

const nextChildren = workInProgress.pendingProps.children;
reconcileChildren(current, workInProgress, nextChildren, renderLanes);
Expand Down Expand Up @@ -3273,8 +3335,10 @@ function beginWork(
}
case CacheComponent: {
if (enableCache) {
const cache: Cache = current.memoizedState;
pushProvider(workInProgress, CacheContext, cache);
const ownCache: Cache | null = workInProgress.memoizedState;
if (ownCache !== null) {
pushProvider(workInProgress, CacheContext, ownCache);
}
}
break;
}
Expand Down
25 changes: 25 additions & 0 deletions packages/react-reconciler/src/ReactFiberCommitWork.new.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import {
enableFundamentalAPI,
enableSuspenseCallback,
enableScopeAPI,
enableCache,
} from 'shared/ReactFeatureFlags';
import {
FunctionComponent,
Expand Down Expand Up @@ -793,6 +794,30 @@ function commitLifeCycles(
case OffscreenComponent:
case LegacyHiddenComponent:
return;
case CacheComponent: {
if (enableCache) {
if (current !== null) {
const oldCache: Cache | null = current.memoizedState;
if (oldCache !== null) {
const oldCacheProviders = oldCache.providers;
if (oldCacheProviders) {
oldCacheProviders.delete(current);
oldCacheProviders.delete(finishedWork);
}
}
}
const newCache: Cache | null = finishedWork.memoizedState;
if (newCache !== null) {
const newCacheProviders = newCache.providers;
if (newCacheProviders === null) {
newCache.providers = new Set([finishedWork]);
} else {
newCacheProviders.add(finishedWork);
}
}
}
return;
}
}
invariant(
false,
Expand Down
25 changes: 25 additions & 0 deletions packages/react-reconciler/src/ReactFiberCommitWork.old.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import {
enableSuspenseCallback,
enableScopeAPI,
enableDoubleInvokingEffects,
enableCache,
} from 'shared/ReactFeatureFlags';
import {
FunctionComponent,
Expand Down Expand Up @@ -794,6 +795,30 @@ function commitLifeCycles(
case OffscreenComponent:
case LegacyHiddenComponent:
return;
case CacheComponent: {
if (enableCache) {
if (current !== null) {
const oldCache: Cache | null = current.memoizedState;
if (oldCache !== null) {
const oldCacheProviders = oldCache.providers;
if (oldCacheProviders) {
oldCacheProviders.delete(current);
oldCacheProviders.delete(finishedWork);
}
}
}
const newCache: Cache | null = finishedWork.memoizedState;
if (newCache !== null) {
const newCacheProviders = newCache.providers;
if (newCacheProviders === null) {
newCache.providers = new Set([finishedWork]);
} else {
newCacheProviders.add(finishedWork);
}
}
}
return;
}
}
invariant(
false,
Expand Down
23 changes: 22 additions & 1 deletion packages/react-reconciler/src/ReactFiberCompleteWork.new.js
Original file line number Diff line number Diff line change
Expand Up @@ -1488,7 +1488,28 @@ function completeWork(
}
case CacheComponent: {
if (enableCache) {
popProvider(CacheContext, workInProgress);
// If the cache provided by this boundary has changed, schedule an
// effect to add this component to the cache's providers, and to remove
// it from the old cache.
// TODO: Schedule for Passive phase
const ownCache: Cache | null = workInProgress.memoizedState;
if (current === null) {
if (ownCache !== null) {
// This is a cache provider.
popProvider(CacheContext, workInProgress);
// Set up a refresh subscription.
workInProgress.flags |= Update;
}
} else {
if (ownCache !== null) {
// This is a cache provider.
popProvider(CacheContext, workInProgress);
}
if (ownCache !== current.memoizedState) {
// Cache changed. Create or update a refresh subscription.
workInProgress.flags |= Update;
}
}
bubbleProperties(workInProgress);
return null;
}
Expand Down
Loading

0 comments on commit c6f9de1

Please sign in to comment.