Skip to content

Commit c5e89a0

Browse files
committed
Add implicit root-level cache
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.
1 parent b13ea9e commit c5e89a0

13 files changed

+180
-10
lines changed

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

+14
Original file line numberDiff line numberDiff line change
@@ -1104,6 +1104,15 @@ function updateHostRoot(current, workInProgress, renderLanes) {
11041104
const nextState = workInProgress.memoizedState;
11051105
// Caution: React DevTools currently depends on this property
11061106
// being called "element".
1107+
1108+
if (enableCache) {
1109+
const nextCacheInstance: CacheInstance = nextState.cacheInstance;
1110+
pushProvider(workInProgress, CacheContext, nextCacheInstance);
1111+
if (nextCacheInstance !== prevState.cacheInstance) {
1112+
propagateCacheRefresh(workInProgress, renderLanes);
1113+
}
1114+
}
1115+
11071116
const nextChildren = nextState.element;
11081117
if (nextChildren === prevChildren) {
11091118
resetHydrationState();
@@ -3174,6 +3183,11 @@ function beginWork(
31743183
switch (workInProgress.tag) {
31753184
case HostRoot:
31763185
pushHostRootContext(workInProgress);
3186+
if (enableCache) {
3187+
const nextCacheInstance: CacheInstance =
3188+
current.memoizedState.cacheInstance;
3189+
pushProvider(workInProgress, CacheContext, nextCacheInstance);
3190+
}
31773191
resetHydrationState();
31783192
break;
31793193
case HostComponent:

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

+14
Original file line numberDiff line numberDiff line change
@@ -1104,6 +1104,15 @@ function updateHostRoot(current, workInProgress, renderLanes) {
11041104
const nextState = workInProgress.memoizedState;
11051105
// Caution: React DevTools currently depends on this property
11061106
// being called "element".
1107+
1108+
if (enableCache) {
1109+
const nextCacheInstance: CacheInstance = nextState.cacheInstance;
1110+
pushProvider(workInProgress, CacheContext, nextCacheInstance);
1111+
if (nextCacheInstance !== prevState.cacheInstance) {
1112+
propagateCacheRefresh(workInProgress, renderLanes);
1113+
}
1114+
}
1115+
11071116
const nextChildren = nextState.element;
11081117
if (nextChildren === prevChildren) {
11091118
resetHydrationState();
@@ -3174,6 +3183,11 @@ function beginWork(
31743183
switch (workInProgress.tag) {
31753184
case HostRoot:
31763185
pushHostRootContext(workInProgress);
3186+
if (enableCache) {
3187+
const nextCacheInstance: CacheInstance =
3188+
current.memoizedState.cacheInstance;
3189+
pushProvider(workInProgress, CacheContext, nextCacheInstance);
3190+
}
31773191
resetHydrationState();
31783192
break;
31793193
case HostComponent:

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

+3
Original file line numberDiff line numberDiff line change
@@ -813,6 +813,9 @@ function completeWork(
813813
return null;
814814
}
815815
case HostRoot: {
816+
if (enableCache) {
817+
popProvider(CacheContext, workInProgress);
818+
}
816819
popHostContainer(workInProgress);
817820
popTopLevelLegacyContextObject(workInProgress);
818821
resetMutableSourceWorkInProgressVersions();

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

+3
Original file line numberDiff line numberDiff line change
@@ -813,6 +813,9 @@ function completeWork(
813813
return null;
814814
}
815815
case HostRoot: {
816+
if (enableCache) {
817+
popProvider(CacheContext, workInProgress);
818+
}
816819
popHostContainer(workInProgress);
817820
popTopLevelLegacyContextObject(workInProgress);
818821
resetMutableSourceWorkInProgressVersions();

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

+23-4
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import {
3131
enableUseRefAccessWarning,
3232
} from 'shared/ReactFeatureFlags';
3333

34+
import {HostRoot} from './ReactWorkTags';
3435
import {NoMode, BlockingMode, DebugTracingMode} from './ReactTypeOfMode';
3536
import {
3637
NoLane,
@@ -94,6 +95,7 @@ import {getIsRendering} from './ReactCurrentFiber';
9495
import {logStateUpdateScheduled} from './DebugTracing';
9596
import {markStateUpdateScheduled} from './SchedulingProfiler';
9697
import {CacheContext} from './ReactFiberCacheComponent';
98+
import {createUpdate, enqueueUpdate} from './ReactUpdateQueue.new';
9799

98100
const {ReactCurrentDispatcher, ReactCurrentBatchConfig} = ReactSharedInternals;
99101

@@ -1741,12 +1743,31 @@ function refreshCache<T>(
17411743
try {
17421744
const eventTime = requestEventTime();
17431745
const lane = requestUpdateLane(provider);
1746+
// TODO: Does Cache work in legacy mode? Should decide and write a test.
17441747
const root = scheduleUpdateOnFiber(provider, lane, eventTime);
1748+
1749+
let seededCache = null;
17451750
if (seedKey !== null && seedKey !== undefined && root !== null) {
17461751
// TODO: Warn if wrong type
1747-
const seededCache = new Map([[seedKey, seedValue]]);
1752+
seededCache = new Map([[seedKey, seedValue]]);
17481753
transferCacheToSpawnedLane(root, seededCache, lane);
17491754
}
1755+
1756+
if (provider.tag === HostRoot) {
1757+
const refreshUpdate = createUpdate(eventTime, lane);
1758+
refreshUpdate.payload = {
1759+
cacheInstance: {
1760+
provider: provider,
1761+
cache:
1762+
// For the root cache, we won't bother to lazily initialize the
1763+
// map. Seed an empty one. This saves use the trouble of having
1764+
// to use an updater function. Maybe we should use this approach
1765+
// for non-root refreshes, too.
1766+
seededCache !== null ? seededCache : new Map(),
1767+
},
1768+
};
1769+
enqueueUpdate(provider, refreshUpdate);
1770+
}
17501771
} finally {
17511772
ReactCurrentBatchConfig.transition = prevTransition;
17521773
}
@@ -1869,9 +1890,7 @@ function getCacheForType<T>(resourceType: () => T): T {
18691890
const cacheInstance: CacheInstance | null = readContext(CacheContext);
18701891
invariant(
18711892
cacheInstance !== null,
1872-
'Tried to fetch data, but no cache was found. To fix, wrap your ' +
1873-
"component in a <Cache /> boundary. It doesn't need to be a direct " +
1874-
'parent; it can be anywhere in the ancestor path',
1893+
'Internal React error: Should always have a cache.',
18751894
);
18761895
let cache = cacheInstance.cache;
18771896
if (cache === null) {

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

+23-4
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import {
3232
enableDoubleInvokingEffects,
3333
} from 'shared/ReactFeatureFlags';
3434

35+
import {HostRoot} from './ReactWorkTags';
3536
import {
3637
NoMode,
3738
BlockingMode,
@@ -102,6 +103,7 @@ import {getIsRendering} from './ReactCurrentFiber';
102103
import {logStateUpdateScheduled} from './DebugTracing';
103104
import {markStateUpdateScheduled} from './SchedulingProfiler';
104105
import {CacheContext} from './ReactFiberCacheComponent';
106+
import {createUpdate, enqueueUpdate} from './ReactUpdateQueue.old';
105107

106108
const {ReactCurrentDispatcher, ReactCurrentBatchConfig} = ReactSharedInternals;
107109

@@ -1812,12 +1814,31 @@ function refreshCache<T>(
18121814
try {
18131815
const eventTime = requestEventTime();
18141816
const lane = requestUpdateLane(provider);
1817+
// TODO: Does Cache work in legacy mode? Should decide and write a test.
18151818
const root = scheduleUpdateOnFiber(provider, lane, eventTime);
1819+
1820+
let seededCache = null;
18161821
if (seedKey !== null && seedKey !== undefined && root !== null) {
18171822
// TODO: Warn if wrong type
1818-
const seededCache = new Map([[seedKey, seedValue]]);
1823+
seededCache = new Map([[seedKey, seedValue]]);
18191824
transferCacheToSpawnedLane(root, seededCache, lane);
18201825
}
1826+
1827+
if (provider.tag === HostRoot) {
1828+
const refreshUpdate = createUpdate(eventTime, lane);
1829+
refreshUpdate.payload = {
1830+
cacheInstance: {
1831+
provider: provider,
1832+
cache:
1833+
// For the root cache, we won't bother to lazily initialize the
1834+
// map. Seed an empty one. This saves use the trouble of having
1835+
// to use an updater function. Maybe we should use this approach
1836+
// for non-root refreshes, too.
1837+
seededCache !== null ? seededCache : new Map(),
1838+
},
1839+
};
1840+
enqueueUpdate(provider, refreshUpdate);
1841+
}
18211842
} finally {
18221843
ReactCurrentBatchConfig.transition = prevTransition;
18231844
}
@@ -1940,9 +1961,7 @@ function getCacheForType<T>(resourceType: () => T): T {
19401961
const cacheInstance: CacheInstance | null = readContext(CacheContext);
19411962
invariant(
19421963
cacheInstance !== null,
1943-
'Tried to fetch data, but no cache was found. To fix, wrap your ' +
1944-
"component in a <Cache /> boundary. It doesn't need to be a direct " +
1945-
'parent; it can be anywhere in the ancestor path',
1964+
'Internal React error: Should always have a cache.',
19461965
);
19471966
let cache = cacheInstance.cache;
19481967
if (cache === null) {

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

+12
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,18 @@ export function createFiberRoot(
103103
root.current = uninitializedFiber;
104104
uninitializedFiber.stateNode = root;
105105

106+
const initialState = {
107+
element: null,
108+
// For the root cache, we won't bother to lazily initialize the map. Seed an
109+
// empty one. This saves use the trouble of having to initialize in an
110+
// updater function.
111+
cacheInstance: {
112+
cache: new Map(),
113+
provider: uninitializedFiber,
114+
},
115+
};
116+
uninitializedFiber.memoizedState = initialState;
117+
106118
initializeUpdateQueue(uninitializedFiber);
107119

108120
return root;

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

+12
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,18 @@ export function createFiberRoot(
103103
root.current = uninitializedFiber;
104104
uninitializedFiber.stateNode = root;
105105

106+
const initialState = {
107+
element: null,
108+
// For the root cache, we won't bother to lazily initialize the map. Seed an
109+
// empty one. This saves use the trouble of having to initialize in an
110+
// updater function.
111+
cacheInstance: {
112+
cache: new Map(),
113+
provider: uninitializedFiber,
114+
},
115+
};
116+
uninitializedFiber.memoizedState = initialState;
117+
106118
initializeUpdateQueue(uninitializedFiber);
107119

108120
return root;

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

+6
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,9 @@ function unwindWork(workInProgress: Fiber, renderLanes: Lanes) {
7070
return null;
7171
}
7272
case HostRoot: {
73+
if (enableCache) {
74+
popProvider(CacheContext, workInProgress);
75+
}
7376
popHostContainer(workInProgress);
7477
popTopLevelLegacyContextObject(workInProgress);
7578
resetMutableSourceWorkInProgressVersions();
@@ -156,6 +159,9 @@ function unwindInterruptedWork(interruptedWork: Fiber) {
156159
break;
157160
}
158161
case HostRoot: {
162+
if (enableCache) {
163+
popProvider(CacheContext, interruptedWork);
164+
}
159165
popHostContainer(interruptedWork);
160166
popTopLevelLegacyContextObject(interruptedWork);
161167
resetMutableSourceWorkInProgressVersions();

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

+6
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,9 @@ function unwindWork(workInProgress: Fiber, renderLanes: Lanes) {
7070
return null;
7171
}
7272
case HostRoot: {
73+
if (enableCache) {
74+
popProvider(CacheContext, workInProgress);
75+
}
7376
popHostContainer(workInProgress);
7477
popTopLevelLegacyContextObject(workInProgress);
7578
resetMutableSourceWorkInProgressVersions();
@@ -156,6 +159,9 @@ function unwindInterruptedWork(interruptedWork: Fiber) {
156159
break;
157160
}
158161
case HostRoot: {
162+
if (enableCache) {
163+
popProvider(CacheContext, interruptedWork);
164+
}
159165
popHostContainer(interruptedWork);
160166
popTopLevelLegacyContextObject(interruptedWork);
161167
resetMutableSourceWorkInProgressVersions();

packages/react-reconciler/src/ReactInternalTypes.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -238,7 +238,7 @@ type BaseFiberRootProperties = {|
238238
entangledLanes: Lanes,
239239
entanglements: LaneMap<Lanes>,
240240

241-
caches: Array<Cache | null> | null,
241+
caches: LaneMap<Cache | null> | null,
242242
pooledCache: Cache | null,
243243
|};
244244

packages/react-reconciler/src/__tests__/ReactCache-test.js

+62
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,26 @@ describe('ReactCache', () => {
150150
expect(root).toMatchRenderedOutput('A');
151151
});
152152

153+
// @gate experimental
154+
test('root acts as implicit cache boundary', async () => {
155+
const root = ReactNoop.createRoot();
156+
await ReactNoop.act(async () => {
157+
root.render(
158+
<Suspense fallback={<Text text="Loading..." />}>
159+
<AsyncText text="A" />
160+
</Suspense>,
161+
);
162+
});
163+
expect(Scheduler).toHaveYielded(['Cache miss! [A]', 'Loading...']);
164+
expect(root).toMatchRenderedOutput('Loading...');
165+
166+
await ReactNoop.act(async () => {
167+
await resolveText('A');
168+
});
169+
expect(Scheduler).toHaveYielded(['A']);
170+
expect(root).toMatchRenderedOutput('A');
171+
});
172+
153173
// @gate experimental
154174
test('multiple new Cache boundaries in the same update share the same, fresh cache', async () => {
155175
function App({text}) {
@@ -404,6 +424,48 @@ describe('ReactCache', () => {
404424
expect(root).toMatchRenderedOutput('A [v2]');
405425
});
406426

427+
// @gate experimental
428+
test('refresh the root cache', async () => {
429+
let refresh;
430+
function App() {
431+
refresh = useRefresh();
432+
return <AsyncText showVersion={true} text="A" />;
433+
}
434+
435+
// Mount initial data
436+
const root = ReactNoop.createRoot();
437+
await ReactNoop.act(async () => {
438+
root.render(
439+
<Suspense fallback={<Text text="Loading..." />}>
440+
<App />
441+
</Suspense>,
442+
);
443+
});
444+
expect(Scheduler).toHaveYielded(['Cache miss! [A]', 'Loading...']);
445+
expect(root).toMatchRenderedOutput('Loading...');
446+
447+
await ReactNoop.act(async () => {
448+
await resolveText('A');
449+
});
450+
expect(Scheduler).toHaveYielded(['A [v1]']);
451+
expect(root).toMatchRenderedOutput('A [v1]');
452+
453+
// Mutate the text service, then refresh for new data.
454+
mutateRemoteTextService();
455+
await ReactNoop.act(async () => {
456+
refresh();
457+
});
458+
expect(Scheduler).toHaveYielded(['Cache miss! [A]', 'Loading...']);
459+
expect(root).toMatchRenderedOutput('A [v1]');
460+
461+
await ReactNoop.act(async () => {
462+
await resolveText('A');
463+
});
464+
// Note that the version has updated
465+
expect(Scheduler).toHaveYielded(['A [v2]']);
466+
expect(root).toMatchRenderedOutput('A [v2]');
467+
});
468+
407469
// @gate experimental
408470
test('refresh a cache with seed data', async () => {
409471
let refresh;

scripts/error-codes/codes.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -372,6 +372,6 @@
372372
"381": "This feature is not supported by ReactSuspenseTestUtils.",
373373
"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.",
374374
"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.",
375-
"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",
375+
"384": "Internal React error: Should always have a cache.",
376376
"385": "Refreshing the cache is not supported in Server Components."
377377
}

0 commit comments

Comments
 (0)