Skip to content

Commit

Permalink
Add implicit root-level cache
Browse files Browse the repository at this point in the history
If `getCacheForType` or `useRefresh` cannot find a parent <Cache />,
they will access a top-level cache associated with the root. The
behavior is effectively the same as if you wrapped the entire tree in a
<Cache /> boundary.
  • Loading branch information
acdlite committed Dec 15, 2020
1 parent b13ea9e commit c5e89a0
Show file tree
Hide file tree
Showing 13 changed files with 180 additions and 10 deletions.
14 changes: 14 additions & 0 deletions packages/react-reconciler/src/ReactFiberBeginWork.new.js
Original file line number Diff line number Diff line change
Expand Up @@ -1104,6 +1104,15 @@ function updateHostRoot(current, workInProgress, renderLanes) {
const nextState = workInProgress.memoizedState;
// Caution: React DevTools currently depends on this property
// being called "element".

if (enableCache) {
const nextCacheInstance: CacheInstance = nextState.cacheInstance;
pushProvider(workInProgress, CacheContext, nextCacheInstance);
if (nextCacheInstance !== prevState.cacheInstance) {
propagateCacheRefresh(workInProgress, renderLanes);
}
}

const nextChildren = nextState.element;
if (nextChildren === prevChildren) {
resetHydrationState();
Expand Down Expand Up @@ -3174,6 +3183,11 @@ function beginWork(
switch (workInProgress.tag) {
case HostRoot:
pushHostRootContext(workInProgress);
if (enableCache) {
const nextCacheInstance: CacheInstance =
current.memoizedState.cacheInstance;
pushProvider(workInProgress, CacheContext, nextCacheInstance);
}
resetHydrationState();
break;
case HostComponent:
Expand Down
14 changes: 14 additions & 0 deletions packages/react-reconciler/src/ReactFiberBeginWork.old.js
Original file line number Diff line number Diff line change
Expand Up @@ -1104,6 +1104,15 @@ function updateHostRoot(current, workInProgress, renderLanes) {
const nextState = workInProgress.memoizedState;
// Caution: React DevTools currently depends on this property
// being called "element".

if (enableCache) {
const nextCacheInstance: CacheInstance = nextState.cacheInstance;
pushProvider(workInProgress, CacheContext, nextCacheInstance);
if (nextCacheInstance !== prevState.cacheInstance) {
propagateCacheRefresh(workInProgress, renderLanes);
}
}

const nextChildren = nextState.element;
if (nextChildren === prevChildren) {
resetHydrationState();
Expand Down Expand Up @@ -3174,6 +3183,11 @@ function beginWork(
switch (workInProgress.tag) {
case HostRoot:
pushHostRootContext(workInProgress);
if (enableCache) {
const nextCacheInstance: CacheInstance =
current.memoizedState.cacheInstance;
pushProvider(workInProgress, CacheContext, nextCacheInstance);
}
resetHydrationState();
break;
case HostComponent:
Expand Down
3 changes: 3 additions & 0 deletions packages/react-reconciler/src/ReactFiberCompleteWork.new.js
Original file line number Diff line number Diff line change
Expand Up @@ -813,6 +813,9 @@ function completeWork(
return null;
}
case HostRoot: {
if (enableCache) {
popProvider(CacheContext, workInProgress);
}
popHostContainer(workInProgress);
popTopLevelLegacyContextObject(workInProgress);
resetMutableSourceWorkInProgressVersions();
Expand Down
3 changes: 3 additions & 0 deletions packages/react-reconciler/src/ReactFiberCompleteWork.old.js
Original file line number Diff line number Diff line change
Expand Up @@ -813,6 +813,9 @@ function completeWork(
return null;
}
case HostRoot: {
if (enableCache) {
popProvider(CacheContext, workInProgress);
}
popHostContainer(workInProgress);
popTopLevelLegacyContextObject(workInProgress);
resetMutableSourceWorkInProgressVersions();
Expand Down
27 changes: 23 additions & 4 deletions packages/react-reconciler/src/ReactFiberHooks.new.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {
enableUseRefAccessWarning,
} from 'shared/ReactFeatureFlags';

import {HostRoot} from './ReactWorkTags';
import {NoMode, BlockingMode, DebugTracingMode} from './ReactTypeOfMode';
import {
NoLane,
Expand Down Expand Up @@ -94,6 +95,7 @@ import {getIsRendering} from './ReactCurrentFiber';
import {logStateUpdateScheduled} from './DebugTracing';
import {markStateUpdateScheduled} from './SchedulingProfiler';
import {CacheContext} from './ReactFiberCacheComponent';
import {createUpdate, enqueueUpdate} from './ReactUpdateQueue.new';

const {ReactCurrentDispatcher, ReactCurrentBatchConfig} = ReactSharedInternals;

Expand Down Expand Up @@ -1741,12 +1743,31 @@ function refreshCache<T>(
try {
const eventTime = requestEventTime();
const lane = requestUpdateLane(provider);
// TODO: Does Cache work in legacy mode? Should decide and write a test.
const root = scheduleUpdateOnFiber(provider, lane, eventTime);

let seededCache = null;
if (seedKey !== null && seedKey !== undefined && root !== null) {
// TODO: Warn if wrong type
const seededCache = new Map([[seedKey, seedValue]]);
seededCache = new Map([[seedKey, seedValue]]);
transferCacheToSpawnedLane(root, seededCache, lane);
}

if (provider.tag === HostRoot) {
const refreshUpdate = createUpdate(eventTime, lane);
refreshUpdate.payload = {
cacheInstance: {
provider: provider,
cache:
// For the root cache, we won't bother to lazily initialize the
// map. Seed an empty one. This saves use the trouble of having
// to use an updater function. Maybe we should use this approach
// for non-root refreshes, too.
seededCache !== null ? seededCache : new Map(),
},
};
enqueueUpdate(provider, refreshUpdate);
}
} finally {
ReactCurrentBatchConfig.transition = prevTransition;
}
Expand Down Expand Up @@ -1869,9 +1890,7 @@ function getCacheForType<T>(resourceType: () => T): T {
const cacheInstance: CacheInstance | null = readContext(CacheContext);
invariant(
cacheInstance !== null,
'Tried to fetch data, but no cache was found. To fix, wrap your ' +
"component in a <Cache /> boundary. It doesn't need to be a direct " +
'parent; it can be anywhere in the ancestor path',
'Internal React error: Should always have a cache.',
);
let cache = cacheInstance.cache;
if (cache === null) {
Expand Down
27 changes: 23 additions & 4 deletions packages/react-reconciler/src/ReactFiberHooks.old.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import {
enableDoubleInvokingEffects,
} from 'shared/ReactFeatureFlags';

import {HostRoot} from './ReactWorkTags';
import {
NoMode,
BlockingMode,
Expand Down Expand Up @@ -102,6 +103,7 @@ import {getIsRendering} from './ReactCurrentFiber';
import {logStateUpdateScheduled} from './DebugTracing';
import {markStateUpdateScheduled} from './SchedulingProfiler';
import {CacheContext} from './ReactFiberCacheComponent';
import {createUpdate, enqueueUpdate} from './ReactUpdateQueue.old';

const {ReactCurrentDispatcher, ReactCurrentBatchConfig} = ReactSharedInternals;

Expand Down Expand Up @@ -1812,12 +1814,31 @@ function refreshCache<T>(
try {
const eventTime = requestEventTime();
const lane = requestUpdateLane(provider);
// TODO: Does Cache work in legacy mode? Should decide and write a test.
const root = scheduleUpdateOnFiber(provider, lane, eventTime);

let seededCache = null;
if (seedKey !== null && seedKey !== undefined && root !== null) {
// TODO: Warn if wrong type
const seededCache = new Map([[seedKey, seedValue]]);
seededCache = new Map([[seedKey, seedValue]]);
transferCacheToSpawnedLane(root, seededCache, lane);
}

if (provider.tag === HostRoot) {
const refreshUpdate = createUpdate(eventTime, lane);
refreshUpdate.payload = {
cacheInstance: {
provider: provider,
cache:
// For the root cache, we won't bother to lazily initialize the
// map. Seed an empty one. This saves use the trouble of having
// to use an updater function. Maybe we should use this approach
// for non-root refreshes, too.
seededCache !== null ? seededCache : new Map(),
},
};
enqueueUpdate(provider, refreshUpdate);
}
} finally {
ReactCurrentBatchConfig.transition = prevTransition;
}
Expand Down Expand Up @@ -1940,9 +1961,7 @@ function getCacheForType<T>(resourceType: () => T): T {
const cacheInstance: CacheInstance | null = readContext(CacheContext);
invariant(
cacheInstance !== null,
'Tried to fetch data, but no cache was found. To fix, wrap your ' +
"component in a <Cache /> boundary. It doesn't need to be a direct " +
'parent; it can be anywhere in the ancestor path',
'Internal React error: Should always have a cache.',
);
let cache = cacheInstance.cache;
if (cache === null) {
Expand Down
12 changes: 12 additions & 0 deletions packages/react-reconciler/src/ReactFiberRoot.new.js
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,18 @@ export function createFiberRoot(
root.current = uninitializedFiber;
uninitializedFiber.stateNode = root;

const initialState = {
element: null,
// For the root cache, we won't bother to lazily initialize the map. Seed an
// empty one. This saves use the trouble of having to initialize in an
// updater function.
cacheInstance: {
cache: new Map(),
provider: uninitializedFiber,
},
};
uninitializedFiber.memoizedState = initialState;

initializeUpdateQueue(uninitializedFiber);

return root;
Expand Down
12 changes: 12 additions & 0 deletions packages/react-reconciler/src/ReactFiberRoot.old.js
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,18 @@ export function createFiberRoot(
root.current = uninitializedFiber;
uninitializedFiber.stateNode = root;

const initialState = {
element: null,
// For the root cache, we won't bother to lazily initialize the map. Seed an
// empty one. This saves use the trouble of having to initialize in an
// updater function.
cacheInstance: {
cache: new Map(),
provider: uninitializedFiber,
},
};
uninitializedFiber.memoizedState = initialState;

initializeUpdateQueue(uninitializedFiber);

return root;
Expand Down
6 changes: 6 additions & 0 deletions packages/react-reconciler/src/ReactFiberUnwindWork.new.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,9 @@ function unwindWork(workInProgress: Fiber, renderLanes: Lanes) {
return null;
}
case HostRoot: {
if (enableCache) {
popProvider(CacheContext, workInProgress);
}
popHostContainer(workInProgress);
popTopLevelLegacyContextObject(workInProgress);
resetMutableSourceWorkInProgressVersions();
Expand Down Expand Up @@ -156,6 +159,9 @@ function unwindInterruptedWork(interruptedWork: Fiber) {
break;
}
case HostRoot: {
if (enableCache) {
popProvider(CacheContext, interruptedWork);
}
popHostContainer(interruptedWork);
popTopLevelLegacyContextObject(interruptedWork);
resetMutableSourceWorkInProgressVersions();
Expand Down
6 changes: 6 additions & 0 deletions packages/react-reconciler/src/ReactFiberUnwindWork.old.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,9 @@ function unwindWork(workInProgress: Fiber, renderLanes: Lanes) {
return null;
}
case HostRoot: {
if (enableCache) {
popProvider(CacheContext, workInProgress);
}
popHostContainer(workInProgress);
popTopLevelLegacyContextObject(workInProgress);
resetMutableSourceWorkInProgressVersions();
Expand Down Expand Up @@ -156,6 +159,9 @@ function unwindInterruptedWork(interruptedWork: Fiber) {
break;
}
case HostRoot: {
if (enableCache) {
popProvider(CacheContext, interruptedWork);
}
popHostContainer(interruptedWork);
popTopLevelLegacyContextObject(interruptedWork);
resetMutableSourceWorkInProgressVersions();
Expand Down
2 changes: 1 addition & 1 deletion packages/react-reconciler/src/ReactInternalTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,7 @@ type BaseFiberRootProperties = {|
entangledLanes: Lanes,
entanglements: LaneMap<Lanes>,

caches: Array<Cache | null> | null,
caches: LaneMap<Cache | null> | null,
pooledCache: Cache | null,
|};

Expand Down
62 changes: 62 additions & 0 deletions packages/react-reconciler/src/__tests__/ReactCache-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,26 @@ describe('ReactCache', () => {
expect(root).toMatchRenderedOutput('A');
});

// @gate experimental
test('root acts as implicit cache boundary', async () => {
const root = ReactNoop.createRoot();
await ReactNoop.act(async () => {
root.render(
<Suspense fallback={<Text text="Loading..." />}>
<AsyncText text="A" />
</Suspense>,
);
});
expect(Scheduler).toHaveYielded(['Cache miss! [A]', 'Loading...']);
expect(root).toMatchRenderedOutput('Loading...');

await ReactNoop.act(async () => {
await resolveText('A');
});
expect(Scheduler).toHaveYielded(['A']);
expect(root).toMatchRenderedOutput('A');
});

// @gate experimental
test('multiple new Cache boundaries in the same update share the same, fresh cache', async () => {
function App({text}) {
Expand Down Expand Up @@ -404,6 +424,48 @@ describe('ReactCache', () => {
expect(root).toMatchRenderedOutput('A [v2]');
});

// @gate experimental
test('refresh the root cache', async () => {
let refresh;
function App() {
refresh = useRefresh();
return <AsyncText showVersion={true} text="A" />;
}

// Mount initial data
const root = ReactNoop.createRoot();
await ReactNoop.act(async () => {
root.render(
<Suspense fallback={<Text text="Loading..." />}>
<App />
</Suspense>,
);
});
expect(Scheduler).toHaveYielded(['Cache miss! [A]', 'Loading...']);
expect(root).toMatchRenderedOutput('Loading...');

await ReactNoop.act(async () => {
await resolveText('A');
});
expect(Scheduler).toHaveYielded(['A [v1]']);
expect(root).toMatchRenderedOutput('A [v1]');

// Mutate the text service, then refresh for new data.
mutateRemoteTextService();
await ReactNoop.act(async () => {
refresh();
});
expect(Scheduler).toHaveYielded(['Cache miss! [A]', 'Loading...']);
expect(root).toMatchRenderedOutput('A [v1]');

await ReactNoop.act(async () => {
await resolveText('A');
});
// Note that the version has updated
expect(Scheduler).toHaveYielded(['A [v2]']);
expect(root).toMatchRenderedOutput('A [v2]');
});

// @gate experimental
test('refresh a cache with seed data', async () => {
let refresh;
Expand Down
2 changes: 1 addition & 1 deletion scripts/error-codes/codes.json
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,6 @@
"381": "This feature is not supported by ReactSuspenseTestUtils.",
"382": "This query has received more parameters than the last time the same query was used. Always pass the exact number of parameters that the query needs.",
"383": "This query has received fewer parameters than the last time the same query was used. Always pass the exact number of parameters that the query needs.",
"384": "Tried to fetch data, but no cache was found. To fix, wrap your component in a <Cache /> boundary. It doesn't need to be a direct parent; it can be anywhere in the ancestor path",
"384": "Internal React error: Should always have a cache.",
"385": "Refreshing the cache is not supported in Server Components."
}

0 comments on commit c5e89a0

Please sign in to comment.