Add built-in Suspense cache with support for invalidation (refreshing) (#20456)
Andrew Clark committed
Dec 18, 2020 at 12:57 UTC
efc57e5cbbd618f3c98d941c48eec859026c2dcb
39 files changed
+2387
-59
packages/react-dom/src/server/ReactPartialRendererHooks.js
+5
@@ -489,6 +489,10 @@ function useOpaqueIdentifier(): OpaqueIDType {
489
);
490
}
491
492
+function useCacheRefresh(): <T>(?() => T, ?T) => void {
493
+ invariant(false, 'Not implemented.');
494
+}
495
+
496
function noop(): void {}
497
498
export let currentPartialRenderer: PartialRenderer = (null: any);
@@ -520,4 +524,5 @@ export const Dispatcher: DispatcherType = {
524
525
if (enableCache) {
526
Dispatcher.getCacheForType = getCacheForType;
527
+ Dispatcher.useCacheRefresh = useCacheRefresh;
528
}
packages/react-reconciler/src/ReactFiber.new.js
+26
@@ -27,6 +27,7 @@ import {
27
enableProfilerTimer,
28
enableFundamentalAPI,
29
enableScopeAPI,
30
+ enableCache,
31
} from 'shared/ReactFeatureFlags';
32
import {NoFlags, Placement, StaticMask} from './ReactFiberFlags';
33
import {ConcurrentRoot, BlockingRoot} from './ReactRootTags';
@@ -54,6 +55,7 @@ import {
55
ScopeComponent,
56
OffscreenComponent,
57
LegacyHiddenComponent,
58
+ CacheComponent,
59
} from './ReactWorkTags';
60
import getComponentName from 'shared/getComponentName';
61
@@ -88,6 +90,7 @@ import {
90
REACT_SCOPE_TYPE,
91
REACT_OFFSCREEN_TYPE,
92
REACT_LEGACY_HIDDEN_TYPE,
93
+ REACT_CACHE_TYPE,
94
} from 'shared/ReactSymbols';
95
96
export type {Fiber};
@@ -501,6 +504,11 @@ export function createFiberFromTypeAndProps(
504
return createFiberFromScope(type, pendingProps, mode, lanes, key);
505
}
506
// eslint-disable-next-line no-fallthrough
507
+ case REACT_CACHE_TYPE:
508
+ if (enableCache) {
509
+ return createFiberFromCache(pendingProps, mode, lanes, key);
510
+ }
511
+ // eslint-disable-next-line no-fallthrough
512
default: {
513
if (typeof type === 'object' && type !== null) {
514
switch (type.$$typeof) {
@@ -745,6 +753,24 @@ export function createFiberFromLegacyHidden(
753
return fiber;
754
}
755
756
+export function createFiberFromCache(
757
+ pendingProps: any,
758
+ mode: TypeOfMode,
759
+ lanes: Lanes,
760
+ key: null | string,
761
+) {
762
+ const fiber = createFiber(CacheComponent, pendingProps, key, mode);
763
+ // TODO: The Cache fiber shouldn't have a type. It has a tag.
764
+ // This needs to be fixed in getComponentName so that it relies on the tag
765
+ // instead.
766
+ if (__DEV__) {
767
+ fiber.type = REACT_CACHE_TYPE;
768
+ }
769
+ fiber.elementType = REACT_CACHE_TYPE;
770
+ fiber.lanes = lanes;
771
+ return fiber;
772
+}
773
+
774
export function createFiberFromText(
775
content: string,
776
mode: TypeOfMode,
packages/react-reconciler/src/ReactFiber.old.js
+26
@@ -27,6 +27,7 @@ import {
27
enableProfilerTimer,
28
enableFundamentalAPI,
29
enableScopeAPI,
30
+ enableCache,
31
} from 'shared/ReactFeatureFlags';
32
import {NoFlags, Placement, StaticMask} from './ReactFiberFlags';
33
import {ConcurrentRoot, BlockingRoot} from './ReactRootTags';
@@ -54,6 +55,7 @@ import {
55
ScopeComponent,
56
OffscreenComponent,
57
LegacyHiddenComponent,
58
+ CacheComponent,
59
} from './ReactWorkTags';
60
import getComponentName from 'shared/getComponentName';
61
@@ -88,6 +90,7 @@ import {
90
REACT_SCOPE_TYPE,
91
REACT_OFFSCREEN_TYPE,
92
REACT_LEGACY_HIDDEN_TYPE,
93
+ REACT_CACHE_TYPE,
94
} from 'shared/ReactSymbols';
95
96
export type {Fiber};
@@ -501,6 +504,11 @@ export function createFiberFromTypeAndProps(
504
return createFiberFromScope(type, pendingProps, mode, lanes, key);
505
}
506
// eslint-disable-next-line no-fallthrough
507
+ case REACT_CACHE_TYPE:
508
+ if (enableCache) {
509
+ return createFiberFromCache(pendingProps, mode, lanes, key);
510
+ }
511
+ // eslint-disable-next-line no-fallthrough
512
default: {
513
if (typeof type === 'object' && type !== null) {
514
switch (type.$$typeof) {
@@ -745,6 +753,24 @@ export function createFiberFromLegacyHidden(
753
return fiber;
754
}
755
756
+export function createFiberFromCache(
757
+ pendingProps: any,
758
+ mode: TypeOfMode,
759
+ lanes: Lanes,
760
+ key: null | string,
761
+) {
762
+ const fiber = createFiber(CacheComponent, pendingProps, key, mode);
763
+ // TODO: The Cache fiber shouldn't have a type. It has a tag.
764
+ // This needs to be fixed in getComponentName so that it relies on the tag
765
+ // instead.
766
+ if (__DEV__) {
767
+ fiber.type = REACT_CACHE_TYPE;
768
+ }
769
+ fiber.elementType = REACT_CACHE_TYPE;
770
+ fiber.lanes = lanes;
771
+ return fiber;
772
+}
773
+
774
export function createFiberFromText(
775
content: string,
776
mode: TypeOfMode,
packages/react-reconciler/src/ReactFiberBeginWork.new.js
+222
-4
@@ -23,6 +23,12 @@ import type {
23
OffscreenProps,
24
OffscreenState,
25
} from './ReactFiberOffscreenComponent';
26
+import type {
27
+ Cache,
28
+ CacheComponentState,
29
+ SpawnedCachePool,
30
+} from './ReactFiberCacheComponent.new';
31
+import type {UpdateQueue} from './ReactUpdateQueue.new';
32
33
import checkPropTypes from 'shared/checkPropTypes';
34
@@ -50,6 +56,7 @@ import {
56
ScopeComponent,
57
OffscreenComponent,
58
LegacyHiddenComponent,
59
+ CacheComponent,
60
} from './ReactWorkTags';
61
import {
62
NoFlags,
@@ -76,6 +83,7 @@ import {
83
enableFundamentalAPI,
84
warnAboutDefaultPropsOnFunctionComponents,
85
enableScopeAPI,
86
+ enableCache,
87
} from 'shared/ReactFeatureFlags';
88
import invariant from 'shared/invariant';
89
import shallowEqual from 'shared/shallowEqual';
@@ -129,6 +137,7 @@ import {
137
isSuspenseInstanceFallback,
138
registerSuspenseInstanceRetry,
139
supportsHydration,
140
+ isPrimaryRenderer,
141
} from './ReactFiberHostConfig';
142
import type {SuspenseInstance} from './ReactFiberHostConfig';
143
import {shouldSuspend} from './ReactFiberReconciler';
@@ -201,6 +210,16 @@ import {
210
} from './ReactFiberWorkLoop.new';
211
import {unstable_wrap as Schedule_tracing_wrap} from 'scheduler/tracing';
212
import {setWorkInProgressVersion} from './ReactMutableSource.new';
213
+import {
214
+ requestCacheFromPool,
215
+ pushCacheProvider,
216
+ pushRootCachePool,
217
+ CacheContext,
218
+ getSuspendedCachePool,
219
+ restoreSpawnedCachePool,
220
+ getOffscreenDeferredCachePool,
221
+} from './ReactFiberCacheComponent.new';
222
+import {MAX_SIGNED_31_BIT_INT} from './MaxInts';
223
224
import {disableLogs, reenableLogs} from 'shared/ConsolePatchingDev';
225
@@ -576,23 +595,39 @@ function updateOffscreenComponent(
595
const prevState: OffscreenState | null =
596
current !== null ? current.memoizedState : null;
597
598
+ // If this is not null, this is a cache pool that was carried over from the
599
+ // previous render. We will push this to the cache pool context so that we can
600
+ // resume in-flight requests.
601
+ let spawnedCachePool: SpawnedCachePool | null = null;
602
+
603
if (
604
nextProps.mode === 'hidden' ||
605
nextProps.mode === 'unstable-defer-without-hiding'
606
) {
607
+ // Rendering a hidden tree.
608
if ((workInProgress.mode & ConcurrentMode) === NoMode) {
609
// In legacy sync mode, don't defer the subtree. Render it now.
610
// TODO: Figure out what we should do in Blocking mode.
611
const nextState: OffscreenState = {
612
baseLanes: NoLanes,
613
+ cachePool: null,
614
};
615
workInProgress.memoizedState = nextState;
616
pushRenderLanes(workInProgress, renderLanes);
617
} else if (!includesSomeLane(renderLanes, (OffscreenLane: Lane))) {
618
+ // We're hidden, and we're not rendering at Offscreen. We will bail out
619
+ // and resume this tree later.
620
let nextBaseLanes;
621
if (prevState !== null) {
622
const prevBaseLanes = prevState.baseLanes;
623
nextBaseLanes = mergeLanes(prevBaseLanes, renderLanes);
624
+ if (enableCache) {
625
+ // Save the cache pool so we can resume later.
626
+ spawnedCachePool = getOffscreenDeferredCachePool();
627
+ // We don't need to push to the cache pool because we're about to
628
+ // bail out. There won't be a context mismatch because we only pop
629
+ // the cache pool if `updateQueue` is non-null.
630
+ }
631
} else {
632
nextBaseLanes = renderLanes;
633
}
@@ -606,16 +641,35 @@ function updateOffscreenComponent(
641
);
642
const nextState: OffscreenState = {
643
baseLanes: nextBaseLanes,
644
+ cachePool: spawnedCachePool,
645
};
646
workInProgress.memoizedState = nextState;
647
+ workInProgress.updateQueue = null;
648
// We're about to bail out, but we need to push this to the stack anyway
649
// to avoid a push/pop misalignment.
650
pushRenderLanes(workInProgress, nextBaseLanes);
651
return null;
652
} else {
653
+ // This is the second render. The surrounding visible content has already
654
+ // committed. Now we resume rendering the hidden tree.
655
+
656
+ if (enableCache && prevState !== null) {
657
+ // If the render that spawned this one accessed the cache pool, resume
658
+ // using the same cache. Unless the parent changed, since that means
659
+ // there was a refresh.
660
+ const prevCachePool = prevState.cachePool;
661
+ if (prevCachePool !== null) {
662
+ spawnedCachePool = restoreSpawnedCachePool(
663
+ workInProgress,
664
+ prevCachePool,
665
+ );
666
+ }
667
+ }
668
+
669
// Rendering at offscreen, so we can clear the base lanes.
670
const nextState: OffscreenState = {
671
baseLanes: NoLanes,
672
+ cachePool: null,
673
};
674
workInProgress.memoizedState = nextState;
675
// Push the lanes that were skipped when we bailed out.
@@ -624,9 +678,26 @@ function updateOffscreenComponent(
678
pushRenderLanes(workInProgress, subtreeRenderLanes);
679
}
680
} else {
681
+ // Rendering a visible tree.
682
let subtreeRenderLanes;
683
if (prevState !== null) {
684
+ // We're going from hidden -> visible.
685
+
686
subtreeRenderLanes = mergeLanes(prevState.baseLanes, renderLanes);
687
+
688
+ if (enableCache) {
689
+ // If the render that spawned this one accessed the cache pool, resume
690
+ // using the same cache. Unless the parent changed, since that means
691
+ // there was a refresh.
692
+ const prevCachePool = prevState.cachePool;
693
+ if (prevCachePool !== null) {
694
+ spawnedCachePool = restoreSpawnedCachePool(
695
+ workInProgress,
696
+ prevCachePool,
697
+ );
698
+ }
699
+ }
700
+
701
// Since we're not hidden anymore, reset the state
702
workInProgress.memoizedState = null;
703
} else {
@@ -638,6 +709,12 @@ function updateOffscreenComponent(
709
pushRenderLanes(workInProgress, subtreeRenderLanes);
710
}
711
712
+ if (enableCache) {
713
+ // If we have a cache pool from a previous render attempt, then this will be
714
+ // non-null. We use this to infer whether to push/pop the cache context.
715
+ workInProgress.updateQueue = spawnedCachePool;
716
+ }
717
+
718
reconcileChildren(current, workInProgress, nextChildren, renderLanes);
719
return workInProgress.child;
720
}
@@ -647,6 +724,79 @@ function updateOffscreenComponent(
724
// fork the function.
725
const updateLegacyHiddenComponent = updateOffscreenComponent;
726
727
+function updateCacheComponent(
728
+ current: Fiber | null,
729
+ workInProgress: Fiber,
730
+ updateLanes: Lanes,
731
+ renderLanes: Lanes,
732
+) {
733
+ if (!enableCache) {
734
+ return null;
735
+ }
736
+
737
+ prepareToReadContext(workInProgress, renderLanes);
738
+ const parentCache = readContext(CacheContext);
739
+
740
+ if (current === null) {
741
+ // Initial mount. Request a fresh cache from the pool.
742
+ const freshCache = requestCacheFromPool(renderLanes);
743
+ const initialState: CacheComponentState = {
744
+ parent: parentCache,
745
+ cache: freshCache,
746
+ };
747
+ workInProgress.memoizedState = initialState;
748
+ initializeUpdateQueue(workInProgress);
749
+ pushCacheProvider(workInProgress, freshCache);
750
+ } else {
751
+ // Check for updates
752
+ if (includesSomeLane(renderLanes, updateLanes)) {
753
+ cloneUpdateQueue(current, workInProgress);
754
+ processUpdateQueue(workInProgress, null, null, renderLanes);
755
+ }
756
+ const prevState: CacheComponentState = current.memoizedState;
757
+ const nextState: CacheComponentState = workInProgress.memoizedState;
758
+
759
+ // Compare the new parent cache to the previous to see detect there was
760
+ // a refresh.
761
+ if (prevState.parent !== parentCache) {
762
+ // Refresh in parent. Update the parent.
763
+ const derivedState: CacheComponentState = {
764
+ parent: parentCache,
765
+ cache: parentCache,
766
+ };
767
+
768
+ // Copied from getDerivedStateFromProps implementation. Once the update
769
+ // queue is empty, persist the derived state onto the base state.
770
+ workInProgress.memoizedState = derivedState;
771
+ if (workInProgress.lanes === NoLanes) {
772
+ const updateQueue: UpdateQueue<any> = (workInProgress.updateQueue: any);
773
+ workInProgress.memoizedState = updateQueue.baseState = derivedState;
774
+ }
775
+
776
+ pushCacheProvider(workInProgress, parentCache);
777
+ // No need to propagate a context change because the refreshed parent
778
+ // already did.
779
+ } else {
780
+ // The parent didn't refresh. Now check if this cache did.
781
+ const nextCache = nextState.cache;
782
+ pushCacheProvider(workInProgress, nextCache);
783
+ if (nextCache !== prevState.cache) {
784
+ // This cache refreshed. Propagate a context change.
785
+ propagateContextChange(
786
+ workInProgress,
787
+ CacheContext,
788
+ MAX_SIGNED_31_BIT_INT,
789
+ renderLanes,
790
+ );
791
+ }
792
+ }
793
+ }
794
+
795
+ const nextChildren = workInProgress.pendingProps.children;
796
+ reconcileChildren(current, workInProgress, nextChildren, renderLanes);
797
+ return workInProgress.child;
798
+}
799
+
800
function updateFragment(
801
current: Fiber | null,
802
workInProgress: Fiber,
@@ -991,10 +1141,28 @@ function updateHostRoot(current, workInProgress, renderLanes) {
1141
);
1142
const nextProps = workInProgress.pendingProps;
1143
const prevState = workInProgress.memoizedState;
994
- const prevChildren = prevState !== null ? prevState.element : null;
1144
+ const prevChildren = prevState.element;
1145
cloneUpdateQueue(current, workInProgress);
1146
processUpdateQueue(workInProgress, nextProps, null, renderLanes);
1147
const nextState = workInProgress.memoizedState;
1148
+
1149
+ const root: FiberRoot = workInProgress.stateNode;
1150
+
1151
+ if (enableCache) {
1152
+ const nextCache: Cache = nextState.cache;
1153
+ pushRootCachePool(root);
1154
+ pushCacheProvider(workInProgress, nextCache);
1155
+ if (nextCache !== prevState.cache) {
1156
+ // The root cache refreshed.
1157
+ propagateContextChange(
1158
+ workInProgress,
1159
+ CacheContext,
1160
+ MAX_SIGNED_31_BIT_INT,
1161
+ renderLanes,
1162
+ );
1163
+ }
1164
+ }
1165
+
1166
// Caution: React DevTools currently depends on this property
1167
// being called "element".
1168
const nextChildren = nextState.element;
@@ -1002,7 +1170,6 @@ function updateHostRoot(current, workInProgress, renderLanes) {
1170
resetHydrationState();
1171
return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
1172
}
1005
- const root: FiberRoot = workInProgress.stateNode;
1173
if (root.hydrate && enterHydrationState(workInProgress)) {
1174
// If we don't have any current children this might be the first pass.
1175
// We always try to hydrate. If this isn't a hydration pass there won't
@@ -1562,6 +1729,7 @@ const SUSPENDED_MARKER: SuspenseState = {
1729
function mountSuspenseOffscreenState(renderLanes: Lanes): OffscreenState {
1730
return {
1731
baseLanes: renderLanes,
1732
+ cachePool: getSuspendedCachePool(),
1733
};
1734
}
1735
@@ -1569,8 +1737,33 @@ function updateSuspenseOffscreenState(
1737
prevOffscreenState: OffscreenState,
1738
renderLanes: Lanes,
1739
): OffscreenState {
1740
+ let cachePool: SpawnedCachePool | null = null;
1741
+ if (enableCache) {
1742
+ const prevCachePool: SpawnedCachePool | null = prevOffscreenState.cachePool;
1743
+ if (prevCachePool !== null) {
1744
+ const parentCache = isPrimaryRenderer
1745
+ ? CacheContext._currentValue
1746
+ : CacheContext._currentValue2;
1747
+ if (prevCachePool.parent !== parentCache) {
1748
+ // Detected a refresh in the parent. This overrides any previously
1749
+ // suspended cache.
1750
+ cachePool = {
1751
+ parent: parentCache,
1752
+ pool: parentCache,
1753
+ };
1754
+ } else {
1755
+ // We can reuse the cache from last time. The only thing that would have
1756
+ // overridden it is a parent refresh, which we checked for above.
1757
+ cachePool = prevCachePool;
1758
+ }
1759
+ } else {
1760
+ // If there's no previous cache pool, grab the current one.
1761
+ cachePool = getSuspendedCachePool();
1762
+ }
1763
+ }
1764
return {
1765
baseLanes: mergeLanes(prevOffscreenState.baseLanes, renderLanes),
1766
+ cachePool,
1767
};
1768
}
1769
@@ -2801,7 +2994,7 @@ function updateContextProvider(
2994
}
2995
}
2996
2804
- pushProvider(workInProgress, newValue);
2997
+ pushProvider(workInProgress, context, newValue);
2998
2999
if (oldProps !== null) {
3000
const oldValue = oldProps.value;
@@ -3068,6 +3261,12 @@ function beginWork(
3261
switch (workInProgress.tag) {
3262
case HostRoot:
3263
pushHostRootContext(workInProgress);
3264
+ if (enableCache) {
3265
+ const root: FiberRoot = workInProgress.stateNode;
3266
+ const cache: Cache = current.memoizedState.cache;
3267
+ pushCacheProvider(workInProgress, cache);
3268
+ pushRootCachePool(root);
3269
+ }
3270
resetHydrationState();
3271
break;
3272
case HostComponent:
@@ -3088,7 +3287,8 @@ function beginWork(
3287
break;
3288
case ContextProvider: {
3289
const newValue = workInProgress.memoizedProps.value;
3091
- pushProvider(workInProgress, newValue);
3290
+ const context: ReactContext<any> = workInProgress.type._context;
3291
+ pushProvider(workInProgress, context, newValue);
3292
break;
3293
}
3294
case Profiler:
@@ -3234,6 +3434,13 @@ function beginWork(
3434
workInProgress.lanes = NoLanes;
3435
return updateOffscreenComponent(current, workInProgress, renderLanes);
3436
}
3437
+ case CacheComponent: {
3438
+ if (enableCache) {
3439
+ const cache: Cache = current.memoizedState.cache;
3440
+ pushCacheProvider(workInProgress, cache);
3441
+ }
3442
+ break;
3443
+ }
3444
}
3445
return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
3446
} else {
@@ -3418,6 +3625,17 @@ function beginWork(
3625
case LegacyHiddenComponent: {
3626
return updateLegacyHiddenComponent(current, workInProgress, renderLanes);
3627
}
3628
+ case CacheComponent: {
3629
+ if (enableCache) {
3630
+ return updateCacheComponent(
3631
+ current,
3632
+ workInProgress,
3633
+ updateLanes,
3634
+ renderLanes,
3635
+ );
3636
+ }
3637
+ break;
3638
+ }
3639
}
3640
invariant(
3641
false,
packages/react-reconciler/src/ReactFiberBeginWork.old.js
+222
-4
@@ -23,6 +23,12 @@ import type {
23
OffscreenProps,
24
OffscreenState,
25
} from './ReactFiberOffscreenComponent';
26
+import type {
27
+ Cache,
28
+ CacheComponentState,
29
+ SpawnedCachePool,
30
+} from './ReactFiberCacheComponent.old';
31
+import type {UpdateQueue} from './ReactUpdateQueue.old';
32
33
import checkPropTypes from 'shared/checkPropTypes';
34
@@ -50,6 +56,7 @@ import {
56
ScopeComponent,
57
OffscreenComponent,
58
LegacyHiddenComponent,
59
+ CacheComponent,
60
} from './ReactWorkTags';
61
import {
62
NoFlags,
@@ -76,6 +83,7 @@ import {
83
enableFundamentalAPI,
84
warnAboutDefaultPropsOnFunctionComponents,
85
enableScopeAPI,
86
+ enableCache,
87
} from 'shared/ReactFeatureFlags';
88
import invariant from 'shared/invariant';
89
import shallowEqual from 'shared/shallowEqual';
@@ -129,6 +137,7 @@ import {
137
isSuspenseInstanceFallback,
138
registerSuspenseInstanceRetry,
139
supportsHydration,
140
+ isPrimaryRenderer,
141
} from './ReactFiberHostConfig';
142
import type {SuspenseInstance} from './ReactFiberHostConfig';
143
import {shouldSuspend} from './ReactFiberReconciler';
@@ -201,6 +210,16 @@ import {
210
} from './ReactFiberWorkLoop.old';
211
import {unstable_wrap as Schedule_tracing_wrap} from 'scheduler/tracing';
212
import {setWorkInProgressVersion} from './ReactMutableSource.old';
213
+import {
214
+ requestCacheFromPool,
215
+ pushCacheProvider,
216
+ pushRootCachePool,
217
+ CacheContext,
218
+ getSuspendedCachePool,
219
+ restoreSpawnedCachePool,
220
+ getOffscreenDeferredCachePool,
221
+} from './ReactFiberCacheComponent.old';
222
+import {MAX_SIGNED_31_BIT_INT} from './MaxInts';
223
224
import {disableLogs, reenableLogs} from 'shared/ConsolePatchingDev';
225
@@ -576,23 +595,39 @@ function updateOffscreenComponent(
595
const prevState: OffscreenState | null =
596
current !== null ? current.memoizedState : null;
597
598
+ // If this is not null, this is a cache pool that was carried over from the
599
+ // previous render. We will push this to the cache pool context so that we can
600
+ // resume in-flight requests.
601
+ let spawnedCachePool: SpawnedCachePool | null = null;
602
+
603
if (
604
nextProps.mode === 'hidden' ||
605
nextProps.mode === 'unstable-defer-without-hiding'
606
) {
607
+ // Rendering a hidden tree.
608
if ((workInProgress.mode & ConcurrentMode) === NoMode) {
609
// In legacy sync mode, don't defer the subtree. Render it now.
610
// TODO: Figure out what we should do in Blocking mode.
611
const nextState: OffscreenState = {
612
baseLanes: NoLanes,
613
+ cachePool: null,
614
};
615
workInProgress.memoizedState = nextState;
616
pushRenderLanes(workInProgress, renderLanes);
617
} else if (!includesSomeLane(renderLanes, (OffscreenLane: Lane))) {
618
+ // We're hidden, and we're not rendering at Offscreen. We will bail out
619
+ // and resume this tree later.
620
let nextBaseLanes;
621
if (prevState !== null) {
622
const prevBaseLanes = prevState.baseLanes;
623
nextBaseLanes = mergeLanes(prevBaseLanes, renderLanes);
624
+ if (enableCache) {
625
+ // Save the cache pool so we can resume later.
626
+ spawnedCachePool = getOffscreenDeferredCachePool();
627
+ // We don't need to push to the cache pool because we're about to
628
+ // bail out. There won't be a context mismatch because we only pop
629
+ // the cache pool if `updateQueue` is non-null.
630
+ }
631
} else {
632
nextBaseLanes = renderLanes;
633
}
@@ -606,16 +641,35 @@ function updateOffscreenComponent(
641
);
642
const nextState: OffscreenState = {
643
baseLanes: nextBaseLanes,
644
+ cachePool: spawnedCachePool,
645
};
646
workInProgress.memoizedState = nextState;
647
+ workInProgress.updateQueue = null;
648
// We're about to bail out, but we need to push this to the stack anyway
649
// to avoid a push/pop misalignment.
650
pushRenderLanes(workInProgress, nextBaseLanes);
651
return null;
652
} else {
653
+ // This is the second render. The surrounding visible content has already
654
+ // committed. Now we resume rendering the hidden tree.
655
+
656
+ if (enableCache && prevState !== null) {
657
+ // If the render that spawned this one accessed the cache pool, resume
658
+ // using the same cache. Unless the parent changed, since that means
659
+ // there was a refresh.
660
+ const prevCachePool = prevState.cachePool;
661
+ if (prevCachePool !== null) {
662
+ spawnedCachePool = restoreSpawnedCachePool(
663
+ workInProgress,
664
+ prevCachePool,
665
+ );
666
+ }
667
+ }
668
+
669
// Rendering at offscreen, so we can clear the base lanes.
670
const nextState: OffscreenState = {
671
baseLanes: NoLanes,
672
+ cachePool: null,
673
};
674
workInProgress.memoizedState = nextState;
675
// Push the lanes that were skipped when we bailed out.
@@ -624,9 +678,26 @@ function updateOffscreenComponent(
678
pushRenderLanes(workInProgress, subtreeRenderLanes);
679
}
680
} else {
681
+ // Rendering a visible tree.
682
let subtreeRenderLanes;
683
if (prevState !== null) {
684
+ // We're going from hidden -> visible.
685
+
686
subtreeRenderLanes = mergeLanes(prevState.baseLanes, renderLanes);
687
+
688
+ if (enableCache) {
689
+ // If the render that spawned this one accessed the cache pool, resume
690
+ // using the same cache. Unless the parent changed, since that means
691
+ // there was a refresh.
692
+ const prevCachePool = prevState.cachePool;
693
+ if (prevCachePool !== null) {
694
+ spawnedCachePool = restoreSpawnedCachePool(
695
+ workInProgress,
696
+ prevCachePool,
697
+ );
698
+ }
699
+ }
700
+
701
// Since we're not hidden anymore, reset the state
702
workInProgress.memoizedState = null;
703
} else {
@@ -638,6 +709,12 @@ function updateOffscreenComponent(
709
pushRenderLanes(workInProgress, subtreeRenderLanes);
710
}
711
712
+ if (enableCache) {
713
+ // If we have a cache pool from a previous render attempt, then this will be
714
+ // non-null. We use this to infer whether to push/pop the cache context.
715
+ workInProgress.updateQueue = spawnedCachePool;
716
+ }
717
+
718
reconcileChildren(current, workInProgress, nextChildren, renderLanes);
719
return workInProgress.child;
720
}
@@ -647,6 +724,79 @@ function updateOffscreenComponent(
724
// fork the function.
725
const updateLegacyHiddenComponent = updateOffscreenComponent;
726
727
+function updateCacheComponent(
728
+ current: Fiber | null,
729
+ workInProgress: Fiber,
730
+ updateLanes: Lanes,
731
+ renderLanes: Lanes,
732
+) {
733
+ if (!enableCache) {
734
+ return null;
735
+ }
736
+
737
+ prepareToReadContext(workInProgress, renderLanes);
738
+ const parentCache = readContext(CacheContext);
739
+
740
+ if (current === null) {
741
+ // Initial mount. Request a fresh cache from the pool.
742
+ const freshCache = requestCacheFromPool(renderLanes);
743
+ const initialState: CacheComponentState = {
744
+ parent: parentCache,
745
+ cache: freshCache,
746
+ };
747
+ workInProgress.memoizedState = initialState;
748
+ initializeUpdateQueue(workInProgress);
749
+ pushCacheProvider(workInProgress, freshCache);
750
+ } else {
751
+ // Check for updates
752
+ if (includesSomeLane(renderLanes, updateLanes)) {
753
+ cloneUpdateQueue(current, workInProgress);
754
+ processUpdateQueue(workInProgress, null, null, renderLanes);
755
+ }
756
+ const prevState: CacheComponentState = current.memoizedState;
757
+ const nextState: CacheComponentState = workInProgress.memoizedState;
758
+
759
+ // Compare the new parent cache to the previous to see detect there was
760
+ // a refresh.
761
+ if (prevState.parent !== parentCache) {
762
+ // Refresh in parent. Update the parent.
763
+ const derivedState: CacheComponentState = {
764
+ parent: parentCache,
765
+ cache: parentCache,
766
+ };
767
+
768
+ // Copied from getDerivedStateFromProps implementation. Once the update
769
+ // queue is empty, persist the derived state onto the base state.
770
+ workInProgress.memoizedState = derivedState;
771
+ if (workInProgress.lanes === NoLanes) {
772
+ const updateQueue: UpdateQueue<any> = (workInProgress.updateQueue: any);
773
+ workInProgress.memoizedState = updateQueue.baseState = derivedState;
774
+ }
775
+
776
+ pushCacheProvider(workInProgress, parentCache);
777
+ // No need to propagate a context change because the refreshed parent
778
+ // already did.
779
+ } else {
780
+ // The parent didn't refresh. Now check if this cache did.
781
+ const nextCache = nextState.cache;
782
+ pushCacheProvider(workInProgress, nextCache);
783
+ if (nextCache !== prevState.cache) {
784
+ // This cache refreshed. Propagate a context change.
785
+ propagateContextChange(
786
+ workInProgress,
787
+ CacheContext,
788
+ MAX_SIGNED_31_BIT_INT,
789
+ renderLanes,
790
+ );
791
+ }
792
+ }
793
+ }
794
+
795
+ const nextChildren = workInProgress.pendingProps.children;
796
+ reconcileChildren(current, workInProgress, nextChildren, renderLanes);
797
+ return workInProgress.child;
798
+}
799
+
800
function updateFragment(
801
current: Fiber | null,
802
workInProgress: Fiber,
@@ -991,10 +1141,28 @@ function updateHostRoot(current, workInProgress, renderLanes) {
1141
);
1142
const nextProps = workInProgress.pendingProps;
1143
const prevState = workInProgress.memoizedState;
994
- const prevChildren = prevState !== null ? prevState.element : null;
1144
+ const prevChildren = prevState.element;
1145
cloneUpdateQueue(current, workInProgress);
1146
processUpdateQueue(workInProgress, nextProps, null, renderLanes);
1147
const nextState = workInProgress.memoizedState;
1148
+
1149
+ const root: FiberRoot = workInProgress.stateNode;
1150
+
1151
+ if (enableCache) {
1152
+ const nextCache: Cache = nextState.cache;
1153
+ pushRootCachePool(root);
1154
+ pushCacheProvider(workInProgress, nextCache);
1155
+ if (nextCache !== prevState.cache) {
1156
+ // The root cache refreshed.
1157
+ propagateContextChange(
1158
+ workInProgress,
1159
+ CacheContext,
1160
+ MAX_SIGNED_31_BIT_INT,
1161
+ renderLanes,
1162
+ );
1163
+ }
1164
+ }
1165
+
1166
// Caution: React DevTools currently depends on this property
1167
// being called "element".
1168
const nextChildren = nextState.element;
@@ -1002,7 +1170,6 @@ function updateHostRoot(current, workInProgress, renderLanes) {
1170
resetHydrationState();
1171
return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
1172
}
1005
- const root: FiberRoot = workInProgress.stateNode;
1173
if (root.hydrate && enterHydrationState(workInProgress)) {
1174
// If we don't have any current children this might be the first pass.
1175
// We always try to hydrate. If this isn't a hydration pass there won't
@@ -1562,6 +1729,7 @@ const SUSPENDED_MARKER: SuspenseState = {
1729
function mountSuspenseOffscreenState(renderLanes: Lanes): OffscreenState {
1730
return {
1731
baseLanes: renderLanes,
1732
+ cachePool: getSuspendedCachePool(),
1733
};
1734
}
1735
@@ -1569,8 +1737,33 @@ function updateSuspenseOffscreenState(
1737
prevOffscreenState: OffscreenState,
1738
renderLanes: Lanes,
1739
): OffscreenState {
1740
+ let cachePool: SpawnedCachePool | null = null;
1741
+ if (enableCache) {
1742
+ const prevCachePool: SpawnedCachePool | null = prevOffscreenState.cachePool;
1743
+ if (prevCachePool !== null) {
1744
+ const parentCache = isPrimaryRenderer
1745
+ ? CacheContext._currentValue
1746
+ : CacheContext._currentValue2;
1747
+ if (prevCachePool.parent !== parentCache) {
1748
+ // Detected a refresh in the parent. This overrides any previously
1749
+ // suspended cache.
1750
+ cachePool = {
1751
+ parent: parentCache,
1752
+ pool: parentCache,
1753
+ };
1754
+ } else {
1755
+ // We can reuse the cache from last time. The only thing that would have
1756
+ // overridden it is a parent refresh, which we checked for above.
1757
+ cachePool = prevCachePool;
1758
+ }
1759
+ } else {
1760
+ // If there's no previous cache pool, grab the current one.
1761
+ cachePool = getSuspendedCachePool();
1762
+ }
1763
+ }
1764
return {
1765
baseLanes: mergeLanes(prevOffscreenState.baseLanes, renderLanes),
1766
+ cachePool,
1767
};
1768
}
1769
@@ -2801,7 +2994,7 @@ function updateContextProvider(
2994
}
2995
}
2996
2804
- pushProvider(workInProgress, newValue);
2997
+ pushProvider(workInProgress, context, newValue);
2998
2999
if (oldProps !== null) {
3000
const oldValue = oldProps.value;
@@ -3068,6 +3261,12 @@ function beginWork(
3261
switch (workInProgress.tag) {
3262
case HostRoot:
3263
pushHostRootContext(workInProgress);
3264
+ if (enableCache) {
3265
+ const root: FiberRoot = workInProgress.stateNode;
3266
+ const cache: Cache = current.memoizedState.cache;
3267
+ pushCacheProvider(workInProgress, cache);
3268
+ pushRootCachePool(root);
3269
+ }
3270
resetHydrationState();
3271
break;
3272
case HostComponent:
@@ -3088,7 +3287,8 @@ function beginWork(
3287
break;
3288
case ContextProvider: {
3289
const newValue = workInProgress.memoizedProps.value;
3091
- pushProvider(workInProgress, newValue);
3290
+ const context: ReactContext<any> = workInProgress.type._context;
3291
+ pushProvider(workInProgress, context, newValue);
3292
break;
3293
}
3294
case Profiler:
@@ -3234,6 +3434,13 @@ function beginWork(
3434
workInProgress.lanes = NoLanes;
3435
return updateOffscreenComponent(current, workInProgress, renderLanes);
3436
}
3437
+ case CacheComponent: {
3438
+ if (enableCache) {
3439
+ const cache: Cache = current.memoizedState.cache;
3440
+ pushCacheProvider(workInProgress, cache);
3441
+ }
3442
+ break;
3443
+ }
3444
}
3445
return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
3446
} else {
@@ -3418,6 +3625,17 @@ function beginWork(
3625
case LegacyHiddenComponent: {
3626
return updateLegacyHiddenComponent(current, workInProgress, renderLanes);
3627
}
3628
+ case CacheComponent: {
3629
+ if (enableCache) {
3630
+ return updateCacheComponent(
3631
+ current,
3632
+ workInProgress,
3633
+ updateLanes,
3634
+ renderLanes,
3635
+ );
3636
+ }
3637
+ break;
3638
+ }
3639
}
3640
invariant(
3641
false,
packages/react-reconciler/src/ReactFiberCacheComponent.new.js
new
+204
@@ -0,0 +1,204 @@
1
+/**
2
+ * Copyright (c) Facebook, Inc. and its affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @flow
8
+ */
9
+
10
+import type {ReactContext} from 'shared/ReactTypes';
11
+import type {FiberRoot} from './ReactInternalTypes';
12
+import type {Lanes} from './ReactFiberLane.new';
13
+import type {StackCursor} from './ReactFiberStack.new';
14
+
15
+import {enableCache} from 'shared/ReactFeatureFlags';
16
+import {REACT_CONTEXT_TYPE} from 'shared/ReactSymbols';
17
+
18
+import {isPrimaryRenderer} from './ReactFiberHostConfig';
19
+import {createCursor, push, pop} from './ReactFiberStack.new';
20
+import {pushProvider, popProvider} from './ReactFiberNewContext.new';
21
+
22
+export type Cache = Map<() => mixed, mixed>;
23
+
24
+export type CacheComponentState = {|
25
+ +parent: Cache,
26
+ +cache: Cache,
27
+|};
28
+
29
+export type SpawnedCachePool = {|
30
+ +parent: Cache,
31
+ +pool: Cache,
32
+|};
33
+
34
+export const CacheContext: ReactContext<Cache> = enableCache
35
+ ? {
36
+ $$typeof: REACT_CONTEXT_TYPE,
37
+ // We don't use Consumer/Provider for Cache components. So we'll cheat.
38
+ Consumer: (null: any),
39
+ Provider: (null: any),
40
+ _calculateChangedBits: null,
41
+ // We'll initialize these at the root.
42
+ _currentValue: (null: any),
43
+ _currentValue2: (null: any),
44
+ _threadCount: 0,
45
+ }
46
+ : (null: any);
47
+
48
+if (__DEV__ && enableCache) {
49
+ CacheContext._currentRenderer = null;
50
+ CacheContext._currentRenderer2 = null;
51
+}
52
+
53
+// The cache that newly mounted Cache boundaries should use. It's either
54
+// retrieved from the cache pool, or the result of a refresh.
55
+let pooledCache: Cache | null = null;
56
+
57
+// When retrying a Suspense/Offscreen boundary, we override pooledCache with the
58
+// cache from the render that suspended.
59
+const prevFreshCacheOnStack: StackCursor<Cache | null> = createCursor(null);
60
+
61
+export function pushCacheProvider(workInProgress: Fiber, cache: Cache) {
62
+ if (!enableCache) {
63
+ return;
64
+ }
65
+ pushProvider(workInProgress, CacheContext, cache);
66
+}
67
+
68
+export function popCacheProvider(workInProgress: Fiber, cache: Cache) {
69
+ if (!enableCache) {
70
+ return;
71
+ }
72
+ popProvider(CacheContext, workInProgress);
73
+}
74
+
75
+export function requestCacheFromPool(renderLanes: Lanes): Cache {
76
+ if (!enableCache) {
77
+ return (null: any);
78
+ }
79
+ if (pooledCache !== null) {
80
+ return pooledCache;
81
+ }
82
+ // Create a fresh cache.
83
+ pooledCache = new Map();
84
+ return pooledCache;
85
+}
86
+
87
+export function pushRootCachePool(root: FiberRoot) {
88
+ if (!enableCache) {
89
+ return;
90
+ }
91
+ // When we start rendering a tree, read the pooled cache for this render
92
+ // from `root.pooledCache`. If it's currently `null`, we will lazily
93
+ // initialize it the first type it's requested. However, we only mutate
94
+ // the root itself during the complete/unwind phase of the HostRoot.
95
+ pooledCache = root.pooledCache;
96
+}
97
+
98
+export function popRootCachePool(root: FiberRoot, renderLanes: Lanes) {
99
+ if (!enableCache) {
100
+ return;
101
+ }
102
+ // The `pooledCache` variable points to the cache that was used for new
103
+ // cache boundaries during this render, if any. Stash it on the root so that
104
+ // parallel transitions may share the same cache. We will clear this field
105
+ // once all the transitions that depend on it (which we track with
106
+ // `pooledCacheLanes`) have committed.
107
+ root.pooledCache = pooledCache;
108
+ if (pooledCache !== null) {
109
+ root.pooledCacheLanes |= renderLanes;
110
+ }
111
+}
112
+
113
+export function restoreSpawnedCachePool(
114
+ offscreenWorkInProgress: Fiber,
115
+ prevCachePool: SpawnedCachePool,
116
+): SpawnedCachePool | null {
117
+ if (!enableCache) {
118
+ return (null: any);
119
+ }
120
+ const nextParentCache = isPrimaryRenderer
121
+ ? CacheContext._currentValue
122
+ : CacheContext._currentValue2;
123
+ if (nextParentCache !== prevCachePool.parent) {
124
+ // There was a refresh. Don't bother restoring anything since the refresh
125
+ // will override it.
126
+ return null;
127
+ } else {
128
+ // No refresh. Resume with the previous cache. This will override the cache
129
+ // pool so that any new Cache boundaries in the subtree use this one instead
130
+ // of requesting a fresh one.
131
+ push(prevFreshCacheOnStack, pooledCache, offscreenWorkInProgress);
132
+ pooledCache = prevCachePool.pool;
133
+
134
+ // Return the cache pool to signal that we did in fact push it. We will
135
+ // assign this to the field on the fiber so we know to pop the context.
136
+ return prevCachePool;
137
+ }
138
+}
139
+
140
+// Note: Ideally, `popCachePool` would return this value, and then we would pass
141
+// it to `getSuspendedCachePool`. But factoring reasons, those two functions are
142
+// in different phases/files. They are always called in sequence, though, so we
143
+// can stash the value here temporarily.
144
+let _suspendedPooledCache: Cache | null = null;
145
+
146
+export function popCachePool(workInProgress: Fiber) {
147
+ if (!enableCache) {
148
+ return;
149
+ }
150
+ _suspendedPooledCache = pooledCache;
151
+ pooledCache = prevFreshCacheOnStack.current;
152
+ pop(prevFreshCacheOnStack, workInProgress);
153
+}
154
+
155
+export function getSuspendedCachePool(): SpawnedCachePool | null {
156
+ if (!enableCache) {
157
+ return null;
158
+ }
159
+
160
+ // We check the cache on the stack first, since that's the one any new Caches
161
+ // would have accessed.
162
+ let pool = pooledCache;
163
+ if (pool === null) {
164
+ // There's no pooled cache above us in the stack. However, a child in the
165
+ // suspended tree may have requested a fresh cache pool. If so, we would
166
+ // have unwound it with `popCachePool`.
167
+ if (_suspendedPooledCache !== null) {
168
+ pool = _suspendedPooledCache;
169
+ _suspendedPooledCache = null;
170
+ } else {
171
+ // There's no suspended cache pool.
172
+ return null;
173
+ }
174
+ }
175
+
176
+ return {
177
+ // We must also save the parent, so that when we resume we can detect
178
+ // a refresh.
179
+ parent: isPrimaryRenderer
180
+ ? CacheContext._currentValue
181
+ : CacheContext._currentValue2,
182
+ pool,
183
+ };
184
+}
185
+
186
+export function getOffscreenDeferredCachePool(): SpawnedCachePool | null {
187
+ if (!enableCache) {
188
+ return null;
189
+ }
190
+
191
+ if (pooledCache === null) {
192
+ // There's no deferred cache pool.
193
+ return null;
194
+ }
195
+
196
+ return {
197
+ // We must also store the parent, so that when we resume we can detect
198
+ // a refresh.
199
+ parent: isPrimaryRenderer
200
+ ? CacheContext._currentValue
201
+ : CacheContext._currentValue2,
202
+ pool: pooledCache,
203
+ };
204
+}
packages/react-reconciler/src/ReactFiberCacheComponent.old.js
new
+204
@@ -0,0 +1,204 @@
1
+/**
2
+ * Copyright (c) Facebook, Inc. and its affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @flow
8
+ */
9
+
10
+import type {ReactContext} from 'shared/ReactTypes';
11
+import type {FiberRoot} from './ReactInternalTypes';
12
+import type {Lanes} from './ReactFiberLane.old';
13
+import type {StackCursor} from './ReactFiberStack.old';
14
+
15
+import {enableCache} from 'shared/ReactFeatureFlags';
16
+import {REACT_CONTEXT_TYPE} from 'shared/ReactSymbols';
17
+
18
+import {isPrimaryRenderer} from './ReactFiberHostConfig';
19
+import {createCursor, push, pop} from './ReactFiberStack.old';
20
+import {pushProvider, popProvider} from './ReactFiberNewContext.old';
21
+
22
+export type Cache = Map<() => mixed, mixed>;
23
+
24
+export type CacheComponentState = {|
25
+ +parent: Cache,
26
+ +cache: Cache,
27
+|};
28
+
29
+export type SpawnedCachePool = {|
30
+ +parent: Cache,
31
+ +pool: Cache,
32
+|};
33
+
34
+export const CacheContext: ReactContext<Cache> = enableCache
35
+ ? {
36
+ $$typeof: REACT_CONTEXT_TYPE,
37
+ // We don't use Consumer/Provider for Cache components. So we'll cheat.
38
+ Consumer: (null: any),
39
+ Provider: (null: any),
40
+ _calculateChangedBits: null,
41
+ // We'll initialize these at the root.
42
+ _currentValue: (null: any),
43
+ _currentValue2: (null: any),
44
+ _threadCount: 0,
45
+ }
46
+ : (null: any);
47
+
48
+if (__DEV__ && enableCache) {
49
+ CacheContext._currentRenderer = null;
50
+ CacheContext._currentRenderer2 = null;
51
+}
52
+
53
+// The cache that newly mounted Cache boundaries should use. It's either
54
+// retrieved from the cache pool, or the result of a refresh.
55
+let pooledCache: Cache | null = null;
56
+
57
+// When retrying a Suspense/Offscreen boundary, we override pooledCache with the
58
+// cache from the render that suspended.
59
+const prevFreshCacheOnStack: StackCursor<Cache | null> = createCursor(null);
60
+
61
+export function pushCacheProvider(workInProgress: Fiber, cache: Cache) {
62
+ if (!enableCache) {
63
+ return;
64
+ }
65
+ pushProvider(workInProgress, CacheContext, cache);
66
+}
67
+
68
+export function popCacheProvider(workInProgress: Fiber, cache: Cache) {
69
+ if (!enableCache) {
70
+ return;
71
+ }
72
+ popProvider(CacheContext, workInProgress);
73
+}
74
+
75
+export function requestCacheFromPool(renderLanes: Lanes): Cache {
76
+ if (!enableCache) {
77
+ return (null: any);
78
+ }
79
+ if (pooledCache !== null) {
80
+ return pooledCache;
81
+ }
82
+ // Create a fresh cache.
83
+ pooledCache = new Map();
84
+ return pooledCache;
85
+}
86
+
87
+export function pushRootCachePool(root: FiberRoot) {
88
+ if (!enableCache) {
89
+ return;
90
+ }
91
+ // When we start rendering a tree, read the pooled cache for this render
92
+ // from `root.pooledCache`. If it's currently `null`, we will lazily
93
+ // initialize it the first type it's requested. However, we only mutate
94
+ // the root itself during the complete/unwind phase of the HostRoot.
95
+ pooledCache = root.pooledCache;
96
+}
97
+
98
+export function popRootCachePool(root: FiberRoot, renderLanes: Lanes) {
99
+ if (!enableCache) {
100
+ return;
101
+ }
102
+ // The `pooledCache` variable points to the cache that was used for new
103
+ // cache boundaries during this render, if any. Stash it on the root so that
104
+ // parallel transitions may share the same cache. We will clear this field
105
+ // once all the transitions that depend on it (which we track with
106
+ // `pooledCacheLanes`) have committed.
107
+ root.pooledCache = pooledCache;
108
+ if (pooledCache !== null) {
109
+ root.pooledCacheLanes |= renderLanes;
110
+ }
111
+}
112
+
113
+export function restoreSpawnedCachePool(
114
+ offscreenWorkInProgress: Fiber,
115
+ prevCachePool: SpawnedCachePool,
116
+): SpawnedCachePool | null {
117
+ if (!enableCache) {
118
+ return (null: any);
119
+ }
120
+ const nextParentCache = isPrimaryRenderer
121
+ ? CacheContext._currentValue
122
+ : CacheContext._currentValue2;
123
+ if (nextParentCache !== prevCachePool.parent) {
124
+ // There was a refresh. Don't bother restoring anything since the refresh
125
+ // will override it.
126
+ return null;
127
+ } else {
128
+ // No refresh. Resume with the previous cache. This will override the cache
129
+ // pool so that any new Cache boundaries in the subtree use this one instead
130
+ // of requesting a fresh one.
131
+ push(prevFreshCacheOnStack, pooledCache, offscreenWorkInProgress);
132
+ pooledCache = prevCachePool.pool;
133
+
134
+ // Return the cache pool to signal that we did in fact push it. We will
135
+ // assign this to the field on the fiber so we know to pop the context.
136
+ return prevCachePool;
137
+ }
138
+}
139
+
140
+// Note: Ideally, `popCachePool` would return this value, and then we would pass
141
+// it to `getSuspendedCachePool`. But factoring reasons, those two functions are
142
+// in different phases/files. They are always called in sequence, though, so we
143
+// can stash the value here temporarily.
144
+let _suspendedPooledCache: Cache | null = null;
145
+
146
+export function popCachePool(workInProgress: Fiber) {
147
+ if (!enableCache) {
148
+ return;
149
+ }
150
+ _suspendedPooledCache = pooledCache;
151
+ pooledCache = prevFreshCacheOnStack.current;
152
+ pop(prevFreshCacheOnStack, workInProgress);
153
+}
154
+
155
+export function getSuspendedCachePool(): SpawnedCachePool | null {
156
+ if (!enableCache) {
157
+ return null;
158
+ }
159
+
160
+ // We check the cache on the stack first, since that's the one any new Caches
161
+ // would have accessed.
162
+ let pool = pooledCache;
163
+ if (pool === null) {
164
+ // There's no pooled cache above us in the stack. However, a child in the
165
+ // suspended tree may have requested a fresh cache pool. If so, we would
166
+ // have unwound it with `popCachePool`.
167
+ if (_suspendedPooledCache !== null) {
168
+ pool = _suspendedPooledCache;
169
+ _suspendedPooledCache = null;
170
+ } else {
171
+ // There's no suspended cache pool.
172
+ return null;
173
+ }
174
+ }
175
+
176
+ return {
177
+ // We must also save the parent, so that when we resume we can detect
178
+ // a refresh.
179
+ parent: isPrimaryRenderer
180
+ ? CacheContext._currentValue
181
+ : CacheContext._currentValue2,
182
+ pool,
183
+ };
184
+}
185
+
186
+export function getOffscreenDeferredCachePool(): SpawnedCachePool | null {
187
+ if (!enableCache) {
188
+ return null;
189
+ }
190
+
191
+ if (pooledCache === null) {
192
+ // There's no deferred cache pool.
193
+ return null;
194
+ }
195
+
196
+ return {
197
+ // We must also store the parent, so that when we resume we can detect
198
+ // a refresh.
199
+ parent: isPrimaryRenderer
200
+ ? CacheContext._currentValue
201
+ : CacheContext._currentValue2,
202
+ pool: pooledCache,
203
+ };
204
+}
packages/react-reconciler/src/ReactFiberCompleteWork.new.js
+33
-2
@@ -12,6 +12,7 @@ import type {Lanes, Lane} from './ReactFiberLane.new';
12
import type {
13
ReactFundamentalComponentInstance,
14
ReactScopeInstance,
15
+ ReactContext,
16
} from 'shared/ReactTypes';
17
import type {FiberRoot} from './ReactInternalTypes';
18
import type {
@@ -27,6 +28,7 @@ import type {
28
} from './ReactFiberSuspenseComponent.new';
29
import type {SuspenseContext} from './ReactFiberSuspenseContext.new';
30
import type {OffscreenState} from './ReactFiberOffscreenComponent';
31
+import type {Cache, SpawnedCachePool} from './ReactFiberCacheComponent.new';
32
33
import {resetWorkInProgressVersions as resetMutableSourceWorkInProgressVersions} from './ReactMutableSource.new';
34
@@ -56,6 +58,7 @@ import {
58
ScopeComponent,
59
OffscreenComponent,
60
LegacyHiddenComponent,
61
+ CacheComponent,
62
} from './ReactWorkTags';
63
import {
64
NoMode,
@@ -132,6 +135,7 @@ import {
135
enableFundamentalAPI,
136
enableScopeAPI,
137
enableProfilerTimer,
138
+ enableCache,
139
} from 'shared/ReactFeatureFlags';
140
import {
141
markSpawnedWork,
@@ -153,6 +157,11 @@ import {
157
import {resetChildFibers} from './ReactChildFiber.new';
158
import {createScopeInstance} from './ReactFiberScope.new';
159
import {transferActualDuration} from './ReactProfilerTimer.new';
160
+import {
161
+ popCacheProvider,
162
+ popRootCachePool,
163
+ popCachePool,
164
+} from './ReactFiberCacheComponent.new';
165
166
function markUpdate(workInProgress: Fiber) {
167
// Tag the fiber with an update effect. This turns a Placement into
@@ -808,10 +817,16 @@ function completeWork(
817
return null;
818
}
819
case HostRoot: {
820
+ const fiberRoot = (workInProgress.stateNode: FiberRoot);
821
+ if (enableCache) {
822
+ popRootCachePool(fiberRoot, renderLanes);
823
+
824
+ const cache: Cache = workInProgress.memoizedState.cache;
825
+ popCacheProvider(workInProgress, cache);
826
+ }
827
popHostContainer(workInProgress);
828
popTopLevelLegacyContextObject(workInProgress);
829
resetMutableSourceWorkInProgressVersions();
814
- const fiberRoot = (workInProgress.stateNode: FiberRoot);
830
if (fiberRoot.pendingContext) {
831
fiberRoot.context = fiberRoot.pendingContext;
832
fiberRoot.pendingContext = null;
@@ -1135,7 +1150,8 @@ function completeWork(
1150
return null;
1151
case ContextProvider:
1152
// Pop provider fiber
1138
- popProvider(workInProgress);
1153
+ const context: ReactContext<any> = workInProgress.type._context;
1154
+ popProvider(context, workInProgress);
1155
bubbleProperties(workInProgress);
1156
return null;
1157
case IncompleteClassComponent: {
@@ -1479,8 +1495,23 @@ function completeWork(
1495
bubbleProperties(workInProgress);
1496
}
1497
1498
+ if (enableCache) {
1499
+ const spawnedCachePool: SpawnedCachePool | null = (workInProgress.updateQueue: any);
1500
+ if (spawnedCachePool !== null) {
1501
+ popCachePool(workInProgress);
1502
+ }
1503
+ }
1504
+
1505
return null;
1506
}
1507
+ case CacheComponent: {
1508
+ if (enableCache) {
1509
+ const cache: Cache = workInProgress.memoizedState.cache;
1510
+ popCacheProvider(workInProgress, cache);
1511
+ bubbleProperties(workInProgress);
1512
+ return null;
1513
+ }
1514
+ }
1515
}
1516
invariant(
1517
false,
packages/react-reconciler/src/ReactFiberCompleteWork.old.js
+33
-2
@@ -12,6 +12,7 @@ import type {Lanes, Lane} from './ReactFiberLane.old';
12
import type {
13
ReactFundamentalComponentInstance,
14
ReactScopeInstance,
15
+ ReactContext,
16
} from 'shared/ReactTypes';
17
import type {FiberRoot} from './ReactInternalTypes';
18
import type {
@@ -27,6 +28,7 @@ import type {
28
} from './ReactFiberSuspenseComponent.old';
29
import type {SuspenseContext} from './ReactFiberSuspenseContext.old';
30
import type {OffscreenState} from './ReactFiberOffscreenComponent';
31
+import type {Cache, SpawnedCachePool} from './ReactFiberCacheComponent.old';
32
33
import {resetWorkInProgressVersions as resetMutableSourceWorkInProgressVersions} from './ReactMutableSource.old';
34
@@ -56,6 +58,7 @@ import {
58
ScopeComponent,
59
OffscreenComponent,
60
LegacyHiddenComponent,
61
+ CacheComponent,
62
} from './ReactWorkTags';
63
import {
64
NoMode,
@@ -132,6 +135,7 @@ import {
135
enableFundamentalAPI,
136
enableScopeAPI,
137
enableProfilerTimer,
138
+ enableCache,
139
} from 'shared/ReactFeatureFlags';
140
import {
141
markSpawnedWork,
@@ -153,6 +157,11 @@ import {
157
import {resetChildFibers} from './ReactChildFiber.old';
158
import {createScopeInstance} from './ReactFiberScope.old';
159
import {transferActualDuration} from './ReactProfilerTimer.old';
160
+import {
161
+ popCacheProvider,
162
+ popRootCachePool,
163
+ popCachePool,
164
+} from './ReactFiberCacheComponent.old';
165
166
function markUpdate(workInProgress: Fiber) {
167
// Tag the fiber with an update effect. This turns a Placement into
@@ -808,10 +817,16 @@ function completeWork(
817
return null;
818
}
819
case HostRoot: {
820
+ const fiberRoot = (workInProgress.stateNode: FiberRoot);
821
+ if (enableCache) {
822
+ popRootCachePool(fiberRoot, renderLanes);
823
+
824
+ const cache: Cache = workInProgress.memoizedState.cache;
825
+ popCacheProvider(workInProgress, cache);
826
+ }
827
popHostContainer(workInProgress);
828
popTopLevelLegacyContextObject(workInProgress);
829
resetMutableSourceWorkInProgressVersions();
814
- const fiberRoot = (workInProgress.stateNode: FiberRoot);
830
if (fiberRoot.pendingContext) {
831
fiberRoot.context = fiberRoot.pendingContext;
832
fiberRoot.pendingContext = null;
@@ -1135,7 +1150,8 @@ function completeWork(
1150
return null;
1151
case ContextProvider:
1152
// Pop provider fiber
1138
- popProvider(workInProgress);
1153
+ const context: ReactContext<any> = workInProgress.type._context;
1154
+ popProvider(context, workInProgress);
1155
bubbleProperties(workInProgress);
1156
return null;
1157
case IncompleteClassComponent: {
@@ -1479,8 +1495,23 @@ function completeWork(
1495
bubbleProperties(workInProgress);
1496
}
1497
1498
+ if (enableCache) {
1499
+ const spawnedCachePool: SpawnedCachePool | null = (workInProgress.updateQueue: any);
1500
+ if (spawnedCachePool !== null) {
1501
+ popCachePool(workInProgress);
1502
+ }
1503
+ }
1504
+
1505
return null;
1506
}
1507
+ case CacheComponent: {
1508
+ if (enableCache) {
1509
+ const cache: Cache = workInProgress.memoizedState.cache;
1510
+ popCacheProvider(workInProgress, cache);
1511
+ bubbleProperties(workInProgress);
1512
+ return null;
1513
+ }
1514
+ }
1515
}
1516
invariant(
1517
false,
packages/react-reconciler/src/ReactFiberHooks.new.js
+105
-1
@@ -19,6 +19,7 @@ import type {HookFlags} from './ReactHookEffectTags';
19
import type {ReactPriorityLevel} from './ReactInternalTypes';
20
import type {FiberRoot} from './ReactInternalTypes';
21
import type {OpaqueIDType} from './ReactFiberHostConfig';
22
+import type {Cache} from './ReactFiberCacheComponent.new';
23
24
import ReactSharedInternals from 'shared/ReactSharedInternals';
25
import {
@@ -46,6 +47,7 @@ import {
47
DefaultLanePriority,
48
} from './ReactFiberLane.new';
49
import {readContext} from './ReactFiberNewContext.new';
50
+import {HostRoot, CacheComponent} from './ReactWorkTags';
51
import {
52
Update as UpdateEffect,
53
Passive as PassiveEffect,
@@ -92,6 +94,8 @@ import {
94
import {getIsRendering} from './ReactCurrentFiber';
95
import {logStateUpdateScheduled} from './DebugTracing';
96
import {markStateUpdateScheduled} from './SchedulingProfiler';
97
+import {CacheContext} from './ReactFiberCacheComponent.new';
98
+import {createUpdate, enqueueUpdate} from './ReactUpdateQueue.new';
99
100
const {ReactCurrentDispatcher, ReactCurrentBatchConfig} = ReactSharedInternals;
101
@@ -1707,6 +1711,55 @@ function rerenderOpaqueIdentifier(): OpaqueIDType | void {
1711
return id;
1712
}
1713
1714
+function mountRefresh() {
1715
+ const hook = mountWorkInProgressHook();
1716
+ const refresh = (hook.memoizedState = refreshCache.bind(
1717
+ null,
1718
+ currentlyRenderingFiber,
1719
+ ));
1720
+ return refresh;
1721
+}
1722
+
1723
+function updateRefresh() {
1724
+ const hook = updateWorkInProgressHook();
1725
+ return hook.memoizedState;
1726
+}
1727
+
1728
+function refreshCache<T>(fiber: Fiber, seedKey: ?() => T, seedValue: T) {
1729
+ // TODO: Does Cache work in legacy mode? Should decide and write a test.
1730
+ // TODO: Consider warning if the refresh is at discrete priority, or if we
1731
+ // otherwise suspect that it wasn't batched properly.
1732
+ let provider = fiber.return;
1733
+ while (provider !== null) {
1734
+ switch (provider.tag) {
1735
+ case CacheComponent:
1736
+ case HostRoot: {
1737
+ const lane = requestUpdateLane(provider);
1738
+ const eventTime = requestEventTime();
1739
+ const root = scheduleUpdateOnFiber(provider, lane, eventTime);
1740
+
1741
+ const seededCache = new Map();
1742
+ if (seedKey !== null && seedKey !== undefined && root !== null) {
1743
+ // Seed the cache with the value passed by the caller. This could be
1744
+ // from a server mutation, or it could be a streaming response.
1745
+ seededCache.set(seedKey, seedValue);
1746
+ }
1747
+
1748
+ // Schedule an update on the cache boundary to trigger a refresh.
1749
+ const refreshUpdate = createUpdate(eventTime, lane);
1750
+ const payload = {
1751
+ cache: seededCache,
1752
+ };
1753
+ refreshUpdate.payload = payload;
1754
+ enqueueUpdate(provider, refreshUpdate);
1755
+ return;
1756
+ }
1757
+ }
1758
+ provider = provider.return;
1759
+ }
1760
+ // TODO: Warn if unmounted?
1761
+}
1762
+
1763
function dispatchAction<S, A>(
1764
fiber: Fiber,
1765
queue: UpdateQueue<S, A>,
@@ -1818,7 +1871,16 @@ function dispatchAction<S, A>(
1871
}
1872
1873
function getCacheForType<T>(resourceType: () => T): T {
1821
- invariant(false, 'Not implemented.');
1874
+ if (!enableCache) {
1875
+ invariant(false, 'Not implemented.');
1876
+ }
1877
+ const cache: Cache = readContext(CacheContext);
1878
+ let cacheForType: T | void = (cache.get(resourceType): any);
1879
+ if (cacheForType === undefined) {
1880
+ cacheForType = resourceType();
1881
+ cache.set(resourceType, cacheForType);
1882
+ }
1883
+ return cacheForType;
1884
}
1885
1886
export const ContextOnlyDispatcher: Dispatcher = {
@@ -1843,6 +1905,7 @@ export const ContextOnlyDispatcher: Dispatcher = {
1905
};
1906
if (enableCache) {
1907
(ContextOnlyDispatcher: Dispatcher).getCacheForType = getCacheForType;
1908
+ (ContextOnlyDispatcher: Dispatcher).useCacheRefresh = throwInvalidHookError;
1909
}
1910
1911
const HooksDispatcherOnMount: Dispatcher = {
@@ -1865,6 +1928,10 @@ const HooksDispatcherOnMount: Dispatcher = {
1928
1929
unstable_isNewReconciler: enableNewReconciler,
1930
};
1931
+if (enableCache) {
1932
+ (HooksDispatcherOnMount: Dispatcher).getCacheForType = getCacheForType;
1933
+ (HooksDispatcherOnMount: Dispatcher).useCacheRefresh = mountRefresh;
1934
+}
1935
1936
const HooksDispatcherOnUpdate: Dispatcher = {
1937
readContext,
@@ -1888,6 +1955,7 @@ const HooksDispatcherOnUpdate: Dispatcher = {
1955
};
1956
if (enableCache) {
1957
(HooksDispatcherOnUpdate: Dispatcher).getCacheForType = getCacheForType;
1958
+ (HooksDispatcherOnUpdate: Dispatcher).useCacheRefresh = updateRefresh;
1959
}
1960
1961
const HooksDispatcherOnRerender: Dispatcher = {
@@ -1912,6 +1980,7 @@ const HooksDispatcherOnRerender: Dispatcher = {
1980
};
1981
if (enableCache) {
1982
(HooksDispatcherOnRerender: Dispatcher).getCacheForType = getCacheForType;
1983
+ (HooksDispatcherOnRerender: Dispatcher).useCacheRefresh = updateRefresh;
1984
}
1985
1986
let HooksDispatcherOnMountInDEV: Dispatcher | null = null;
@@ -2069,6 +2138,11 @@ if (__DEV__) {
2138
};
2139
if (enableCache) {
2140
(HooksDispatcherOnMountInDEV: Dispatcher).getCacheForType = getCacheForType;
2141
+ (HooksDispatcherOnMountInDEV: Dispatcher).useCacheRefresh = function useCacheRefresh() {
2142
+ currentHookNameInDev = 'useCacheRefresh';
2143
+ mountHookTypesDev();
2144
+ return mountRefresh();
2145
+ };
2146
}
2147
2148
HooksDispatcherOnMountWithHookTypesInDEV = {
@@ -2194,6 +2268,11 @@ if (__DEV__) {
2268
};
2269
if (enableCache) {
2270
(HooksDispatcherOnMountWithHookTypesInDEV: Dispatcher).getCacheForType = getCacheForType;
2271
+ (HooksDispatcherOnMountWithHookTypesInDEV: Dispatcher).useCacheRefresh = function useCacheRefresh() {
2272
+ currentHookNameInDev = 'useCacheRefresh';
2273
+ updateHookTypesDev();
2274
+ return mountRefresh();
2275
+ };
2276
}
2277
2278
HooksDispatcherOnUpdateInDEV = {
@@ -2319,6 +2398,11 @@ if (__DEV__) {
2398
};
2399
if (enableCache) {
2400
(HooksDispatcherOnUpdateInDEV: Dispatcher).getCacheForType = getCacheForType;
2401
+ (HooksDispatcherOnUpdateInDEV: Dispatcher).useCacheRefresh = function useCacheRefresh() {
2402
+ currentHookNameInDev = 'useCacheRefresh';
2403
+ updateHookTypesDev();
2404
+ return updateRefresh();
2405
+ };
2406
}
2407
2408
HooksDispatcherOnRerenderInDEV = {
@@ -2445,6 +2529,11 @@ if (__DEV__) {
2529
};
2530
if (enableCache) {
2531
(HooksDispatcherOnRerenderInDEV: Dispatcher).getCacheForType = getCacheForType;
2532
+ (HooksDispatcherOnRerenderInDEV: Dispatcher).useCacheRefresh = function useCacheRefresh() {
2533
+ currentHookNameInDev = 'useCacheRefresh';
2534
+ updateHookTypesDev();
2535
+ return updateRefresh();
2536
+ };
2537
}
2538
2539
InvalidNestedHooksDispatcherOnMountInDEV = {
@@ -2585,6 +2674,11 @@ if (__DEV__) {
2674
};
2675
if (enableCache) {
2676
(InvalidNestedHooksDispatcherOnMountInDEV: Dispatcher).getCacheForType = getCacheForType;
2677
+ (InvalidNestedHooksDispatcherOnMountInDEV: Dispatcher).useCacheRefresh = function useCacheRefresh() {
2678
+ currentHookNameInDev = 'useCacheRefresh';
2679
+ updateHookTypesDev();
2680
+ return mountRefresh();
2681
+ };
2682
}
2683
2684
InvalidNestedHooksDispatcherOnUpdateInDEV = {
@@ -2725,6 +2819,11 @@ if (__DEV__) {
2819
};
2820
if (enableCache) {
2821
(InvalidNestedHooksDispatcherOnUpdateInDEV: Dispatcher).getCacheForType = getCacheForType;
2822
+ (InvalidNestedHooksDispatcherOnUpdateInDEV: Dispatcher).useCacheRefresh = function useCacheRefresh() {
2823
+ currentHookNameInDev = 'useCacheRefresh';
2824
+ updateHookTypesDev();
2825
+ return updateRefresh();
2826
+ };
2827
}
2828
2829
InvalidNestedHooksDispatcherOnRerenderInDEV = {
@@ -2866,5 +2965,10 @@ if (__DEV__) {
2965
};
2966
if (enableCache) {
2967
(InvalidNestedHooksDispatcherOnRerenderInDEV: Dispatcher).getCacheForType = getCacheForType;
2968
+ (InvalidNestedHooksDispatcherOnRerenderInDEV: Dispatcher).useCacheRefresh = function useCacheRefresh() {
2969
+ currentHookNameInDev = 'useCacheRefresh';
2970
+ updateHookTypesDev();
2971
+ return updateRefresh();
2972
+ };
2973
}
2974
}
packages/react-reconciler/src/ReactFiberHooks.old.js
+105
-1
@@ -19,6 +19,7 @@ import type {HookFlags} from './ReactHookEffectTags';
19
import type {ReactPriorityLevel} from './ReactInternalTypes';
20
import type {FiberRoot} from './ReactInternalTypes';
21
import type {OpaqueIDType} from './ReactFiberHostConfig';
22
+import type {Cache} from './ReactFiberCacheComponent.old';
23
24
import ReactSharedInternals from 'shared/ReactSharedInternals';
25
import {
@@ -46,6 +47,7 @@ import {
47
DefaultLanePriority,
48
} from './ReactFiberLane.old';
49
import {readContext} from './ReactFiberNewContext.old';
50
+import {HostRoot, CacheComponent} from './ReactWorkTags';
51
import {
52
Update as UpdateEffect,
53
Passive as PassiveEffect,
@@ -92,6 +94,8 @@ import {
94
import {getIsRendering} from './ReactCurrentFiber';
95
import {logStateUpdateScheduled} from './DebugTracing';
96
import {markStateUpdateScheduled} from './SchedulingProfiler';
97
+import {CacheContext} from './ReactFiberCacheComponent.old';
98
+import {createUpdate, enqueueUpdate} from './ReactUpdateQueue.old';
99
100
const {ReactCurrentDispatcher, ReactCurrentBatchConfig} = ReactSharedInternals;
101
@@ -1707,6 +1711,55 @@ function rerenderOpaqueIdentifier(): OpaqueIDType | void {
1711
return id;
1712
}
1713
1714
+function mountRefresh() {
1715
+ const hook = mountWorkInProgressHook();
1716
+ const refresh = (hook.memoizedState = refreshCache.bind(
1717
+ null,
1718
+ currentlyRenderingFiber,
1719
+ ));
1720
+ return refresh;
1721
+}
1722
+
1723
+function updateRefresh() {
1724
+ const hook = updateWorkInProgressHook();
1725
+ return hook.memoizedState;
1726
+}
1727
+
1728
+function refreshCache<T>(fiber: Fiber, seedKey: ?() => T, seedValue: T) {
1729
+ // TODO: Does Cache work in legacy mode? Should decide and write a test.
1730
+ // TODO: Consider warning if the refresh is at discrete priority, or if we
1731
+ // otherwise suspect that it wasn't batched properly.
1732
+ let provider = fiber.return;
1733
+ while (provider !== null) {
1734
+ switch (provider.tag) {
1735
+ case CacheComponent:
1736
+ case HostRoot: {
1737
+ const lane = requestUpdateLane(provider);
1738
+ const eventTime = requestEventTime();
1739
+ const root = scheduleUpdateOnFiber(provider, lane, eventTime);
1740
+
1741
+ const seededCache = new Map();
1742
+ if (seedKey !== null && seedKey !== undefined && root !== null) {
1743
+ // Seed the cache with the value passed by the caller. This could be
1744
+ // from a server mutation, or it could be a streaming response.
1745
+ seededCache.set(seedKey, seedValue);
1746
+ }
1747
+
1748
+ // Schedule an update on the cache boundary to trigger a refresh.
1749
+ const refreshUpdate = createUpdate(eventTime, lane);
1750
+ const payload = {
1751
+ cache: seededCache,
1752
+ };
1753
+ refreshUpdate.payload = payload;
1754
+ enqueueUpdate(provider, refreshUpdate);
1755
+ return;
1756
+ }
1757
+ }
1758
+ provider = provider.return;
1759
+ }
1760
+ // TODO: Warn if unmounted?
1761
+}
1762
+
1763
function dispatchAction<S, A>(
1764
fiber: Fiber,
1765
queue: UpdateQueue<S, A>,
@@ -1818,7 +1871,16 @@ function dispatchAction<S, A>(
1871
}
1872
1873
function getCacheForType<T>(resourceType: () => T): T {
1821
- invariant(false, 'Not implemented.');
1874
+ if (!enableCache) {
1875
+ invariant(false, 'Not implemented.');
1876
+ }
1877
+ const cache: Cache = readContext(CacheContext);
1878
+ let cacheForType: T | void = (cache.get(resourceType): any);
1879
+ if (cacheForType === undefined) {
1880
+ cacheForType = resourceType();
1881
+ cache.set(resourceType, cacheForType);
1882
+ }
1883
+ return cacheForType;
1884
}
1885
1886
export const ContextOnlyDispatcher: Dispatcher = {
@@ -1843,6 +1905,7 @@ export const ContextOnlyDispatcher: Dispatcher = {
1905
};
1906
if (enableCache) {
1907
(ContextOnlyDispatcher: Dispatcher).getCacheForType = getCacheForType;
1908
+ (ContextOnlyDispatcher: Dispatcher).useCacheRefresh = throwInvalidHookError;
1909
}
1910
1911
const HooksDispatcherOnMount: Dispatcher = {
@@ -1865,6 +1928,10 @@ const HooksDispatcherOnMount: Dispatcher = {
1928
1929
unstable_isNewReconciler: enableNewReconciler,
1930
};
1931
+if (enableCache) {
1932
+ (HooksDispatcherOnMount: Dispatcher).getCacheForType = getCacheForType;
1933
+ (HooksDispatcherOnMount: Dispatcher).useCacheRefresh = mountRefresh;
1934
+}
1935
1936
const HooksDispatcherOnUpdate: Dispatcher = {
1937
readContext,
@@ -1888,6 +1955,7 @@ const HooksDispatcherOnUpdate: Dispatcher = {
1955
};
1956
if (enableCache) {
1957
(HooksDispatcherOnUpdate: Dispatcher).getCacheForType = getCacheForType;
1958
+ (HooksDispatcherOnUpdate: Dispatcher).useCacheRefresh = updateRefresh;
1959
}
1960
1961
const HooksDispatcherOnRerender: Dispatcher = {
@@ -1912,6 +1980,7 @@ const HooksDispatcherOnRerender: Dispatcher = {
1980
};
1981
if (enableCache) {
1982
(HooksDispatcherOnRerender: Dispatcher).getCacheForType = getCacheForType;
1983
+ (HooksDispatcherOnRerender: Dispatcher).useCacheRefresh = updateRefresh;
1984
}
1985
1986
let HooksDispatcherOnMountInDEV: Dispatcher | null = null;
@@ -2069,6 +2138,11 @@ if (__DEV__) {
2138
};
2139
if (enableCache) {
2140
(HooksDispatcherOnMountInDEV: Dispatcher).getCacheForType = getCacheForType;
2141
+ (HooksDispatcherOnMountInDEV: Dispatcher).useCacheRefresh = function useCacheRefresh() {
2142
+ currentHookNameInDev = 'useCacheRefresh';
2143
+ mountHookTypesDev();
2144
+ return mountRefresh();
2145
+ };
2146
}
2147
2148
HooksDispatcherOnMountWithHookTypesInDEV = {
@@ -2194,6 +2268,11 @@ if (__DEV__) {
2268
};
2269
if (enableCache) {
2270
(HooksDispatcherOnMountWithHookTypesInDEV: Dispatcher).getCacheForType = getCacheForType;
2271
+ (HooksDispatcherOnMountWithHookTypesInDEV: Dispatcher).useCacheRefresh = function useCacheRefresh() {
2272
+ currentHookNameInDev = 'useCacheRefresh';
2273
+ updateHookTypesDev();
2274
+ return mountRefresh();
2275
+ };
2276
}
2277
2278
HooksDispatcherOnUpdateInDEV = {
@@ -2319,6 +2398,11 @@ if (__DEV__) {
2398
};
2399
if (enableCache) {
2400
(HooksDispatcherOnUpdateInDEV: Dispatcher).getCacheForType = getCacheForType;
2401
+ (HooksDispatcherOnUpdateInDEV: Dispatcher).useCacheRefresh = function useCacheRefresh() {
2402
+ currentHookNameInDev = 'useCacheRefresh';
2403
+ updateHookTypesDev();
2404
+ return updateRefresh();
2405
+ };
2406
}
2407
2408
HooksDispatcherOnRerenderInDEV = {
@@ -2445,6 +2529,11 @@ if (__DEV__) {
2529
};
2530
if (enableCache) {
2531
(HooksDispatcherOnRerenderInDEV: Dispatcher).getCacheForType = getCacheForType;
2532
+ (HooksDispatcherOnRerenderInDEV: Dispatcher).useCacheRefresh = function useCacheRefresh() {
2533
+ currentHookNameInDev = 'useCacheRefresh';
2534
+ updateHookTypesDev();
2535
+ return updateRefresh();
2536
+ };
2537
}
2538
2539
InvalidNestedHooksDispatcherOnMountInDEV = {
@@ -2585,6 +2674,11 @@ if (__DEV__) {
2674
};
2675
if (enableCache) {
2676
(InvalidNestedHooksDispatcherOnMountInDEV: Dispatcher).getCacheForType = getCacheForType;
2677
+ (InvalidNestedHooksDispatcherOnMountInDEV: Dispatcher).useCacheRefresh = function useCacheRefresh() {
2678
+ currentHookNameInDev = 'useCacheRefresh';
2679
+ updateHookTypesDev();
2680
+ return mountRefresh();
2681
+ };
2682
}
2683
2684
InvalidNestedHooksDispatcherOnUpdateInDEV = {
@@ -2725,6 +2819,11 @@ if (__DEV__) {
2819
};
2820
if (enableCache) {
2821
(InvalidNestedHooksDispatcherOnUpdateInDEV: Dispatcher).getCacheForType = getCacheForType;
2822
+ (InvalidNestedHooksDispatcherOnUpdateInDEV: Dispatcher).useCacheRefresh = function useCacheRefresh() {
2823
+ currentHookNameInDev = 'useCacheRefresh';
2824
+ updateHookTypesDev();
2825
+ return updateRefresh();
2826
+ };
2827
}
2828
2829
InvalidNestedHooksDispatcherOnRerenderInDEV = {
@@ -2866,5 +2965,10 @@ if (__DEV__) {
2965
};
2966
if (enableCache) {
2967
(InvalidNestedHooksDispatcherOnRerenderInDEV: Dispatcher).getCacheForType = getCacheForType;
2968
+ (InvalidNestedHooksDispatcherOnRerenderInDEV: Dispatcher).useCacheRefresh = function useCacheRefresh() {
2969
+ currentHookNameInDev = 'useCacheRefresh';
2970
+ updateHookTypesDev();
2971
+ return updateRefresh();
2972
+ };
2973
}
2974
}
packages/react-reconciler/src/ReactFiberLane.new.js
+10
@@ -36,6 +36,7 @@ export type Lane = number;
36
export type LaneMap<T> = Array<T>;
37
38
import invariant from 'shared/invariant';
39
+import {enableCache} from 'shared/ReactFeatureFlags';
40
41
import {
42
ImmediatePriority as ImmediateSchedulerPriority,
@@ -737,6 +738,15 @@ export function markRootFinished(root: FiberRoot, remainingLanes: Lanes) {
738
739
root.entangledLanes &= remainingLanes;
740
741
+ if (enableCache) {
742
+ const pooledCacheLanes = (root.pooledCacheLanes &= remainingLanes);
743
+ if (pooledCacheLanes === NoLanes) {
744
+ // None of the remaining work relies on the cache pool. Clear it so
745
+ // subsequent requests get a new cache.
746
+ root.pooledCache = null;
747
+ }
748
+ }
749
+
750
const entanglements = root.entanglements;
751
const eventTimes = root.eventTimes;
752
const expirationTimes = root.expirationTimes;
packages/react-reconciler/src/ReactFiberLane.old.js
+10
@@ -36,6 +36,7 @@ export type Lane = number;
36
export type LaneMap<T> = Array<T>;
37
38
import invariant from 'shared/invariant';
39
+import {enableCache} from 'shared/ReactFeatureFlags';
40
41
import {
42
ImmediatePriority as ImmediateSchedulerPriority,
@@ -737,6 +738,15 @@ export function markRootFinished(root: FiberRoot, remainingLanes: Lanes) {
738
739
root.entangledLanes &= remainingLanes;
740
741
+ if (enableCache) {
742
+ const pooledCacheLanes = (root.pooledCacheLanes &= remainingLanes);
743
+ if (pooledCacheLanes === NoLanes) {
744
+ // None of the remaining work relies on the cache pool. Clear it so
745
+ // subsequent requests get a new cache.
746
+ root.pooledCache = null;
747
+ }
748
+ }
749
+
750
const entanglements = root.entanglements;
751
const eventTimes = root.eventTimes;
752
const expirationTimes = root.expirationTimes;
packages/react-reconciler/src/ReactFiberNewContext.new.js
+11
-9
@@ -72,9 +72,11 @@ export function exitDisallowedContextReadInDEV(): void {
72
}
73
}
74
75
-export function pushProvider<T>(providerFiber: Fiber, nextValue: T): void {
76
- const context: ReactContext<T> = providerFiber.type._context;
77
-
75
+export function pushProvider<T>(
76
+ providerFiber: Fiber,
77
+ context: ReactContext<T>,
78
+ nextValue: T,
79
+): void {
80
if (isPrimaryRenderer) {
81
push(valueCursor, context._currentValue, providerFiber);
82
@@ -112,12 +114,12 @@ export function pushProvider<T>(providerFiber: Fiber, nextValue: T): void {
114
}
115
}
116
115
-export function popProvider(providerFiber: Fiber): void {
117
+export function popProvider(
118
+ context: ReactContext<any>,
119
+ providerFiber: Fiber,
120
+): void {
121
const currentValue = valueCursor.current;
117
-
122
pop(valueCursor, providerFiber);
119
-
120
- const context: ReactContext<any> = providerFiber.type._context;
123
if (isPrimaryRenderer) {
124
context._currentValue = currentValue;
125
} else {
@@ -179,9 +181,9 @@ export function scheduleWorkOnParentPath(
181
}
182
}
183
182
-export function propagateContextChange(
184
+export function propagateContextChange<T>(
185
workInProgress: Fiber,
184
- context: ReactContext<mixed>,
186
+ context: ReactContext<T>,
187
changedBits: number,
188
renderLanes: Lanes,
189
): void {
packages/react-reconciler/src/ReactFiberNewContext.old.js
+11
-9
@@ -72,9 +72,11 @@ export function exitDisallowedContextReadInDEV(): void {
72
}
73
}
74
75
-export function pushProvider<T>(providerFiber: Fiber, nextValue: T): void {
76
- const context: ReactContext<T> = providerFiber.type._context;
77
-
75
+export function pushProvider<T>(
76
+ providerFiber: Fiber,
77
+ context: ReactContext<T>,
78
+ nextValue: T,
79
+): void {
80
if (isPrimaryRenderer) {
81
push(valueCursor, context._currentValue, providerFiber);
82
@@ -112,12 +114,12 @@ export function pushProvider<T>(providerFiber: Fiber, nextValue: T): void {
114
}
115
}
116
115
-export function popProvider(providerFiber: Fiber): void {
117
+export function popProvider(
118
+ context: ReactContext<any>,
119
+ providerFiber: Fiber,
120
+): void {
121
const currentValue = valueCursor.current;
117
-
122
pop(valueCursor, providerFiber);
119
-
120
- const context: ReactContext<any> = providerFiber.type._context;
123
if (isPrimaryRenderer) {
124
context._currentValue = currentValue;
125
} else {
@@ -179,9 +181,9 @@ export function scheduleWorkOnParentPath(
181
}
182
}
183
182
-export function propagateContextChange(
184
+export function propagateContextChange<T>(
185
workInProgress: Fiber,
184
- context: ReactContext<mixed>,
186
+ context: ReactContext<T>,
187
changedBits: number,
188
renderLanes: Lanes,
189
): void {
packages/react-reconciler/src/ReactFiberOffscreenComponent.js
+2
@@ -9,6 +9,7 @@
9
10
import type {ReactNodeList} from 'shared/ReactTypes';
11
import type {Lanes} from './ReactFiberLane.old';
12
+import type {SpawnedCachePool} from './ReactFiberCacheComponent.new';
13
14
export type OffscreenProps = {|
15
// TODO: Pick an API before exposing the Offscreen type. I've chosen an enum
@@ -28,4 +29,5 @@ export type OffscreenState = {|
29
// will represent the pending work that must be included in the render in
30
// order to unhide the component.
31
baseLanes: Lanes,
32
+ cachePool: SpawnedCachePool | null,
33
|};
packages/react-reconciler/src/ReactFiberRoot.new.js
+21
@@ -21,6 +21,7 @@ import {
21
import {
22
enableSchedulerTracing,
23
enableSuspenseCallback,
24
+ enableCache,
25
} from 'shared/ReactFeatureFlags';
26
import {unstable_getThreadID} from 'scheduler/tracing';
27
import {initializeUpdateQueue} from './ReactUpdateQueue.new';
@@ -52,6 +53,11 @@ function FiberRootNode(containerInfo, tag, hydrate) {
53
this.entangledLanes = NoLanes;
54
this.entanglements = createLaneMap(NoLanes);
55
56
+ if (enableCache) {
57
+ this.pooledCache = null;
58
+ this.pooledCacheLanes = NoLanes;
59
+ }
60
+
61
if (supportsHydration) {
62
this.mutableSourceEagerHydrationData = null;
63
}
@@ -97,6 +103,21 @@ export function createFiberRoot(
103
root.current = uninitializedFiber;
104
uninitializedFiber.stateNode = root;
105
106
+ if (enableCache) {
107
+ const initialCache = new Map();
108
+ root.pooledCache = initialCache;
109
+ const initialState = {
110
+ element: null,
111
+ cache: initialCache,
112
+ };
113
+ uninitializedFiber.memoizedState = initialState;
114
+ } else {
115
+ const initialState = {
116
+ element: null,
117
+ };
118
+ uninitializedFiber.memoizedState = initialState;
119
+ }
120
+
121
initializeUpdateQueue(uninitializedFiber);
122
123
return root;
packages/react-reconciler/src/ReactFiberRoot.old.js
+21
@@ -21,6 +21,7 @@ import {
21
import {
22
enableSchedulerTracing,
23
enableSuspenseCallback,
24
+ enableCache,
25
} from 'shared/ReactFeatureFlags';
26
import {unstable_getThreadID} from 'scheduler/tracing';
27
import {initializeUpdateQueue} from './ReactUpdateQueue.old';
@@ -52,6 +53,11 @@ function FiberRootNode(containerInfo, tag, hydrate) {
53
this.entangledLanes = NoLanes;
54
this.entanglements = createLaneMap(NoLanes);
55
56
+ if (enableCache) {
57
+ this.pooledCache = null;
58
+ this.pooledCacheLanes = NoLanes;
59
+ }
60
+
61
if (supportsHydration) {
62
this.mutableSourceEagerHydrationData = null;
63
}
@@ -97,6 +103,21 @@ export function createFiberRoot(
103
root.current = uninitializedFiber;
104
uninitializedFiber.stateNode = root;
105
106
+ if (enableCache) {
107
+ const initialCache = new Map();
108
+ root.pooledCache = initialCache;
109
+ const initialState = {
110
+ element: null,
111
+ cache: initialCache,
112
+ };
113
+ uninitializedFiber.memoizedState = initialState;
114
+ } else {
115
+ const initialState = {
116
+ element: null,
117
+ };
118
+ uninitializedFiber.memoizedState = initialState;
119
+ }
120
+
121
initializeUpdateQueue(uninitializedFiber);
122
123
return root;
packages/react-reconciler/src/ReactFiberThrow.new.js
+12
-3
@@ -21,6 +21,9 @@ import {
21
HostRoot,
22
SuspenseComponent,
23
IncompleteClassComponent,
24
+ FunctionComponent,
25
+ ForwardRef,
26
+ SimpleMemoComponent,
27
} from './ReactWorkTags';
28
import {
29
DidCapture,
@@ -209,9 +212,15 @@ function throwException(
212
markComponentSuspended(sourceFiber, wakeable);
213
}
214
212
- if ((sourceFiber.mode & BlockingMode) === NoMode) {
213
- // Reset the memoizedState to what it was before we attempted
214
- // to render it.
215
+ // Reset the memoizedState to what it was before we attempted to render it.
216
+ // A legacy mode Suspense quirk, only relevant to hook components.
217
+ const tag = sourceFiber.tag;
218
+ if (
219
+ (sourceFiber.mode & BlockingMode) === NoMode &&
220
+ (tag === FunctionComponent ||
221
+ tag === ForwardRef ||
222
+ tag === SimpleMemoComponent)
223
+ ) {
224
const currentSource = sourceFiber.alternate;
225
if (currentSource) {
226
sourceFiber.updateQueue = currentSource.updateQueue;
packages/react-reconciler/src/ReactFiberThrow.old.js
+12
-3
@@ -21,6 +21,9 @@ import {
21
HostRoot,
22
SuspenseComponent,
23
IncompleteClassComponent,
24
+ FunctionComponent,
25
+ ForwardRef,
26
+ SimpleMemoComponent,
27
} from './ReactWorkTags';
28
import {
29
DidCapture,
@@ -209,9 +212,15 @@ function throwException(
212
markComponentSuspended(sourceFiber, wakeable);
213
}
214
212
- if ((sourceFiber.mode & BlockingMode) === NoMode) {
213
- // Reset the memoizedState to what it was before we attempted
214
- // to render it.
215
+ // Reset the memoizedState to what it was before we attempted to render it.
216
+ // A legacy mode Suspense quirk, only relevant to hook components.
217
+ const tag = sourceFiber.tag;
218
+ if (
219
+ (sourceFiber.mode & BlockingMode) === NoMode &&
220
+ (tag === FunctionComponent ||
221
+ tag === ForwardRef ||
222
+ tag === SimpleMemoComponent)
223
+ ) {
224
const currentSource = sourceFiber.alternate;
225
if (currentSource) {
226
sourceFiber.updateQueue = currentSource.updateQueue;
packages/react-reconciler/src/ReactFiberUnwindWork.new.js
+54
-4
@@ -7,9 +7,11 @@
7
* @flow
8
*/
9
10
-import type {Fiber} from './ReactInternalTypes';
10
+import type {ReactContext} from 'shared/ReactTypes';
11
+import type {Fiber, FiberRoot} from './ReactInternalTypes';
12
import type {Lanes} from './ReactFiberLane.new';
13
import type {SuspenseState} from './ReactFiberSuspenseComponent.new';
14
+import type {Cache, SpawnedCachePool} from './ReactFiberCacheComponent.new';
15
16
import {resetWorkInProgressVersions as resetMutableSourceWorkInProgressVersions} from './ReactMutableSource.new';
17
import {
@@ -22,12 +24,14 @@ import {
24
SuspenseListComponent,
25
OffscreenComponent,
26
LegacyHiddenComponent,
27
+ CacheComponent,
28
} from './ReactWorkTags';
29
import {DidCapture, NoFlags, ShouldCapture} from './ReactFiberFlags';
30
import {NoMode, ProfileMode} from './ReactTypeOfMode';
31
import {
32
enableSuspenseServerRenderer,
33
enableProfilerTimer,
34
+ enableCache,
35
} from 'shared/ReactFeatureFlags';
36
37
import {popHostContainer, popHostContext} from './ReactFiberHostContext.new';
@@ -40,6 +44,11 @@ import {
44
} from './ReactFiberContext.new';
45
import {popProvider} from './ReactFiberNewContext.new';
46
import {popRenderLanes} from './ReactFiberWorkLoop.new';
47
+import {
48
+ popCacheProvider,
49
+ popRootCachePool,
50
+ popCachePool,
51
+} from './ReactFiberCacheComponent.new';
52
import {transferActualDuration} from './ReactProfilerTimer.new';
53
54
import invariant from 'shared/invariant';
@@ -65,6 +74,13 @@ function unwindWork(workInProgress: Fiber, renderLanes: Lanes) {
74
return null;
75
}
76
case HostRoot: {
77
+ if (enableCache) {
78
+ const root: FiberRoot = workInProgress.stateNode;
79
+ popRootCachePool(root, renderLanes);
80
+
81
+ const cache: Cache = workInProgress.memoizedState.cache;
82
+ popCacheProvider(workInProgress, cache);
83
+ }
84
popHostContainer(workInProgress);
85
popTopLevelLegacyContextObject(workInProgress);
86
resetMutableSourceWorkInProgressVersions();
@@ -120,18 +136,31 @@ function unwindWork(workInProgress: Fiber, renderLanes: Lanes) {
136
popHostContainer(workInProgress);
137
return null;
138
case ContextProvider:
123
- popProvider(workInProgress);
139
+ const context: ReactContext<any> = workInProgress.type._context;
140
+ popProvider(context, workInProgress);
141
return null;
142
case OffscreenComponent:
143
case LegacyHiddenComponent:
144
popRenderLanes(workInProgress);
145
+ if (enableCache) {
146
+ const spawnedCachePool: SpawnedCachePool | null = (workInProgress.updateQueue: any);
147
+ if (spawnedCachePool !== null) {
148
+ popCachePool(workInProgress);
149
+ }
150
+ }
151
+ return null;
152
+ case CacheComponent:
153
+ if (enableCache) {
154
+ const cache: Cache = workInProgress.memoizedState.cache;
155
+ popCacheProvider(workInProgress, cache);
156
+ }
157
return null;
158
default:
159
return null;
160
}
161
}
162
134
-function unwindInterruptedWork(interruptedWork: Fiber) {
163
+function unwindInterruptedWork(interruptedWork: Fiber, renderLanes: Lanes) {
164
switch (interruptedWork.tag) {
165
case ClassComponent: {
166
const childContextTypes = interruptedWork.type.childContextTypes;
@@ -141,6 +170,13 @@ function unwindInterruptedWork(interruptedWork: Fiber) {
170
break;
171
}
172
case HostRoot: {
173
+ if (enableCache) {
174
+ const root: FiberRoot = interruptedWork.stateNode;
175
+ popRootCachePool(root, renderLanes);
176
+
177
+ const cache: Cache = interruptedWork.memoizedState.cache;
178
+ popCacheProvider(interruptedWork, cache);
179
+ }
180
popHostContainer(interruptedWork);
181
popTopLevelLegacyContextObject(interruptedWork);
182
resetMutableSourceWorkInProgressVersions();
@@ -160,11 +196,25 @@ function unwindInterruptedWork(interruptedWork: Fiber) {
196
popSuspenseContext(interruptedWork);
197
break;
198
case ContextProvider:
163
- popProvider(interruptedWork);
199
+ const context: ReactContext<any> = interruptedWork.type._context;
200
+ popProvider(context, interruptedWork);
201
break;
202
case OffscreenComponent:
203
case LegacyHiddenComponent:
204
popRenderLanes(interruptedWork);
205
+ if (enableCache) {
206
+ const spawnedCachePool: SpawnedCachePool | null = (interruptedWork.updateQueue: any);
207
+ if (spawnedCachePool !== null) {
208
+ popCachePool(interruptedWork);
209
+ }
210
+ }
211
+
212
+ break;
213
+ case CacheComponent:
214
+ if (enableCache) {
215
+ const cache: Cache = interruptedWork.memoizedState.cache;
216
+ popCacheProvider(interruptedWork, cache);
217
+ }
218
break;
219
default:
220
break;
packages/react-reconciler/src/ReactFiberUnwindWork.old.js
+54
-4
@@ -7,9 +7,11 @@
7
* @flow
8
*/
9
10
-import type {Fiber} from './ReactInternalTypes';
10
+import type {ReactContext} from 'shared/ReactTypes';
11
+import type {Fiber, FiberRoot} from './ReactInternalTypes';
12
import type {Lanes} from './ReactFiberLane.old';
13
import type {SuspenseState} from './ReactFiberSuspenseComponent.old';
14
+import type {Cache, SpawnedCachePool} from './ReactFiberCacheComponent.old';
15
16
import {resetWorkInProgressVersions as resetMutableSourceWorkInProgressVersions} from './ReactMutableSource.old';
17
import {
@@ -22,12 +24,14 @@ import {
24
SuspenseListComponent,
25
OffscreenComponent,
26
LegacyHiddenComponent,
27
+ CacheComponent,
28
} from './ReactWorkTags';
29
import {DidCapture, NoFlags, ShouldCapture} from './ReactFiberFlags';
30
import {NoMode, ProfileMode} from './ReactTypeOfMode';
31
import {
32
enableSuspenseServerRenderer,
33
enableProfilerTimer,
34
+ enableCache,
35
} from 'shared/ReactFeatureFlags';
36
37
import {popHostContainer, popHostContext} from './ReactFiberHostContext.old';
@@ -40,6 +44,11 @@ import {
44
} from './ReactFiberContext.old';
45
import {popProvider} from './ReactFiberNewContext.old';
46
import {popRenderLanes} from './ReactFiberWorkLoop.old';
47
+import {
48
+ popCacheProvider,
49
+ popRootCachePool,
50
+ popCachePool,
51
+} from './ReactFiberCacheComponent.old';
52
import {transferActualDuration} from './ReactProfilerTimer.old';
53
54
import invariant from 'shared/invariant';
@@ -65,6 +74,13 @@ function unwindWork(workInProgress: Fiber, renderLanes: Lanes) {
74
return null;
75
}
76
case HostRoot: {
77
+ if (enableCache) {
78
+ const root: FiberRoot = workInProgress.stateNode;
79
+ popRootCachePool(root, renderLanes);
80
+
81
+ const cache: Cache = workInProgress.memoizedState.cache;
82
+ popCacheProvider(workInProgress, cache);
83
+ }
84
popHostContainer(workInProgress);
85
popTopLevelLegacyContextObject(workInProgress);
86
resetMutableSourceWorkInProgressVersions();
@@ -120,18 +136,31 @@ function unwindWork(workInProgress: Fiber, renderLanes: Lanes) {
136
popHostContainer(workInProgress);
137
return null;
138
case ContextProvider:
123
- popProvider(workInProgress);
139
+ const context: ReactContext<any> = workInProgress.type._context;
140
+ popProvider(context, workInProgress);
141
return null;
142
case OffscreenComponent:
143
case LegacyHiddenComponent:
144
popRenderLanes(workInProgress);
145
+ if (enableCache) {
146
+ const spawnedCachePool: SpawnedCachePool | null = (workInProgress.updateQueue: any);
147
+ if (spawnedCachePool !== null) {
148
+ popCachePool(workInProgress);
149
+ }
150
+ }
151
+ return null;
152
+ case CacheComponent:
153
+ if (enableCache) {
154
+ const cache: Cache = workInProgress.memoizedState.cache;
155
+ popCacheProvider(workInProgress, cache);
156
+ }
157
return null;
158
default:
159
return null;
160
}
161
}
162
134
-function unwindInterruptedWork(interruptedWork: Fiber) {
163
+function unwindInterruptedWork(interruptedWork: Fiber, renderLanes: Lanes) {
164
switch (interruptedWork.tag) {
165
case ClassComponent: {
166
const childContextTypes = interruptedWork.type.childContextTypes;
@@ -141,6 +170,13 @@ function unwindInterruptedWork(interruptedWork: Fiber) {
170
break;
171
}
172
case HostRoot: {
173
+ if (enableCache) {
174
+ const root: FiberRoot = interruptedWork.stateNode;
175
+ popRootCachePool(root, renderLanes);
176
+
177
+ const cache: Cache = interruptedWork.memoizedState.cache;
178
+ popCacheProvider(interruptedWork, cache);
179
+ }
180
popHostContainer(interruptedWork);
181
popTopLevelLegacyContextObject(interruptedWork);
182
resetMutableSourceWorkInProgressVersions();
@@ -160,11 +196,25 @@ function unwindInterruptedWork(interruptedWork: Fiber) {
196
popSuspenseContext(interruptedWork);
197
break;
198
case ContextProvider:
163
- popProvider(interruptedWork);
199
+ const context: ReactContext<any> = interruptedWork.type._context;
200
+ popProvider(context, interruptedWork);
201
break;
202
case OffscreenComponent:
203
case LegacyHiddenComponent:
204
popRenderLanes(interruptedWork);
205
+ if (enableCache) {
206
+ const spawnedCachePool: SpawnedCachePool | null = (interruptedWork.updateQueue: any);
207
+ if (spawnedCachePool !== null) {
208
+ popCachePool(interruptedWork);
209
+ }
210
+ }
211
+
212
+ break;
213
+ case CacheComponent:
214
+ if (enableCache) {
215
+ const cache: Cache = interruptedWork.memoizedState.cache;
216
+ popCacheProvider(interruptedWork, cache);
217
+ }
218
break;
219
default:
220
break;
packages/react-reconciler/src/ReactFiberWorkLoop.new.js
+9
-4
@@ -531,7 +531,7 @@ export function scheduleUpdateOnFiber(
531
fiber: Fiber,
532
lane: Lane,
533
eventTime: number,
534
-) {
534
+): FiberRoot | null {
535
checkForNestedUpdates();
536
warnAboutRenderPhaseUpdatesInDEV(fiber);
537
@@ -653,6 +653,8 @@ export function scheduleUpdateOnFiber(
653
// the same root, then it's not a huge deal, we just might batch more stuff
654
// together more than necessary.
655
mostRecentlyUpdatedRoot = root;
656
+
657
+ return root;
658
}
659
660
// This is split into a separate function so we can mark a fiber with pending
@@ -1371,7 +1373,7 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes) {
1373
if (workInProgress !== null) {
1374
let interruptedWork = workInProgress.return;
1375
while (interruptedWork !== null) {
1374
- unwindInterruptedWork(interruptedWork);
1376
+ unwindInterruptedWork(interruptedWork, workInProgressRootRenderLanes);
1377
interruptedWork = interruptedWork.return;
1378
}
1379
}
@@ -2303,7 +2305,10 @@ function commitBeforeMutationEffects() {
2305
}
2306
}
2307
2306
-function commitMutationEffects(root: FiberRoot, renderPriorityLevel) {
2308
+function commitMutationEffects(
2309
+ root: FiberRoot,
2310
+ renderPriorityLevel: ReactPriorityLevel,
2311
+) {
2312
// TODO: Should probably move the bulk of this function to commitWork.
2313
while (nextEffect !== null) {
2314
setCurrentDebugFiberInDEV(nextEffect);
@@ -3047,7 +3052,7 @@ if (__DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback) {
3052
// same fiber again.
3053
3054
// Unwind the failed stack frame
3050
- unwindInterruptedWork(unitOfWork);
3055
+ unwindInterruptedWork(unitOfWork, workInProgressRootRenderLanes);
3056
3057
// Restore the original properties of the fiber.
3058
assignFiberPropertiesInDEV(unitOfWork, originalWorkInProgressCopy);
packages/react-reconciler/src/ReactFiberWorkLoop.old.js
+9
-4
@@ -531,7 +531,7 @@ export function scheduleUpdateOnFiber(
531
fiber: Fiber,
532
lane: Lane,
533
eventTime: number,
534
-) {
534
+): FiberRoot | null {
535
checkForNestedUpdates();
536
warnAboutRenderPhaseUpdatesInDEV(fiber);
537
@@ -653,6 +653,8 @@ export function scheduleUpdateOnFiber(
653
// the same root, then it's not a huge deal, we just might batch more stuff
654
// together more than necessary.
655
mostRecentlyUpdatedRoot = root;
656
+
657
+ return root;
658
}
659
660
// This is split into a separate function so we can mark a fiber with pending
@@ -1371,7 +1373,7 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes) {
1373
if (workInProgress !== null) {
1374
let interruptedWork = workInProgress.return;
1375
while (interruptedWork !== null) {
1374
- unwindInterruptedWork(interruptedWork);
1376
+ unwindInterruptedWork(interruptedWork, workInProgressRootRenderLanes);
1377
interruptedWork = interruptedWork.return;
1378
}
1379
}
@@ -2303,7 +2305,10 @@ function commitBeforeMutationEffects() {
2305
}
2306
}
2307
2306
-function commitMutationEffects(root: FiberRoot, renderPriorityLevel) {
2308
+function commitMutationEffects(
2309
+ root: FiberRoot,
2310
+ renderPriorityLevel: ReactPriorityLevel,
2311
+) {
2312
// TODO: Should probably move the bulk of this function to commitWork.
2313
while (nextEffect !== null) {
2314
setCurrentDebugFiberInDEV(nextEffect);
@@ -3047,7 +3052,7 @@ if (__DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback) {
3052
// same fiber again.
3053
3054
// Unwind the failed stack frame
3050
- unwindInterruptedWork(unitOfWork);
3055
+ unwindInterruptedWork(unitOfWork, workInProgressRootRenderLanes);
3056
3057
// Restore the original properties of the fiber.
3058
assignFiberPropertiesInDEV(unitOfWork, originalWorkInProgressCopy);
packages/react-reconciler/src/ReactInternalTypes.js
+7
-1
@@ -25,6 +25,7 @@ import type {RootTag} from './ReactRootTags';
25
import type {TimeoutHandle, NoTimeout} from './ReactFiberHostConfig';
26
import type {Wakeable} from 'shared/ReactTypes';
27
import type {Interaction} from 'scheduler/src/Tracing';
28
+import type {Cache} from './ReactFiberCacheComponent.old';
29
30
// Unwind Circular: moved from ReactFiberHooks.old
31
export type HookType =
@@ -41,7 +42,8 @@ export type HookType =
42
| 'useDeferredValue'
43
| 'useTransition'
44
| 'useMutableSource'
44
- | 'useOpaqueIdentifier';
45
+ | 'useOpaqueIdentifier'
46
+ | 'useCacheRefresh';
47
48
export type ReactPriorityLevel = 99 | 98 | 97 | 96 | 95 | 90;
49
@@ -235,6 +237,9 @@ type BaseFiberRootProperties = {|
237
238
entangledLanes: Lanes,
239
entanglements: LaneMap<Lanes>,
240
+
241
+ pooledCache: Cache | null,
242
+ pooledCacheLanes: Lanes,
243
|};
244
245
// The following attributes are only used by interaction tracing builds.
@@ -314,6 +319,7 @@ export type Dispatcher = {|
319
subscribe: MutableSourceSubscribeFn<Source, Snapshot>,
320
): Snapshot,
321
useOpaqueIdentifier(): any,
322
+ useCacheRefresh?: () => <T>(?() => T, ?T) => void,
323
324
unstable_isNewReconciler?: boolean,
325
|};
packages/react-reconciler/src/ReactWorkTags.js
+3
-1
@@ -31,7 +31,8 @@ export type WorkTag =
31
| 20
32
| 21
33
| 22
34
- | 23;
34
+ | 23
35
+ | 24;
36
37
export const FunctionComponent = 0;
38
export const ClassComponent = 1;
@@ -57,3 +58,4 @@ export const FundamentalComponent = 20;
58
export const ScopeComponent = 21;
59
export const OffscreenComponent = 22;
60
export const LegacyHiddenComponent = 23;
61
+export const CacheComponent = 24;
packages/react-reconciler/src/__tests__/ReactCache-test.js
new
+916
@@ -0,0 +1,916 @@
1
+let React;
2
+let ReactNoop;
3
+let Cache;
4
+let getCacheForType;
5
+let Scheduler;
6
+let Suspense;
7
+let useCacheRefresh;
8
+let startTransition;
9
+let useState;
10
+
11
+let textService;
12
+let textServiceVersion;
13
+
14
+describe('ReactCache', () => {
15
+ beforeEach(() => {
16
+ jest.resetModules();
17
+
18
+ React = require('react');
19
+ ReactNoop = require('react-noop-renderer');
20
+ Cache = React.unstable_Cache;
21
+ Scheduler = require('scheduler');
22
+ Suspense = React.Suspense;
23
+ getCacheForType = React.unstable_getCacheForType;
24
+ useCacheRefresh = React.unstable_useCacheRefresh;
25
+ startTransition = React.unstable_startTransition;
26
+ useState = React.useState;
27
+
28
+ // Represents some data service that returns text. It likely has additional
29
+ // caching layers, like a CDN or the local browser cache. It can be mutated
30
+ // or emptied independently of the React cache.
31
+ textService = new Map();
32
+ textServiceVersion = 1;
33
+ });
34
+
35
+ function createTextCache() {
36
+ return new Map();
37
+ }
38
+
39
+ function readText(text) {
40
+ const textCache = getCacheForType(createTextCache);
41
+ const record = textCache.get(text);
42
+ if (record !== undefined) {
43
+ switch (record.status) {
44
+ case 'pending':
45
+ throw record.value;
46
+ case 'rejected':
47
+ throw record.value;
48
+ case 'resolved':
49
+ return record.value;
50
+ }
51
+ } else {
52
+ Scheduler.unstable_yieldValue(`Cache miss! [${text}]`);
53
+
54
+ let request = textService.get(text);
55
+ if (request === undefined) {
56
+ let resolve;
57
+ let reject;
58
+ request = new Promise((res, rej) => {
59
+ resolve = res;
60
+ reject = rej;
61
+ });
62
+ request.resolve = resolve;
63
+ request.reject = reject;
64
+
65
+ // Add the request to a backing cache. This may outlive the lifetime
66
+ // of the component that is currently reading the data.
67
+ textService.set(text, request);
68
+ }
69
+
70
+ const thenable = request.then(
71
+ value => {
72
+ if (newRecord.status === 'pending') {
73
+ newRecord.status = 'resolved';
74
+ newRecord.value = value;
75
+ }
76
+ },
77
+ error => {
78
+ if (newRecord.status === 'pending') {
79
+ newRecord.status = 'rejected';
80
+ newRecord.value = error;
81
+ }
82
+ },
83
+ );
84
+
85
+ const newRecord = {
86
+ ping: null,
87
+ status: 'pending',
88
+ value: thenable,
89
+ };
90
+ textCache.set(text, newRecord);
91
+
92
+ throw thenable;
93
+ }
94
+ }
95
+
96
+ function mutateRemoteTextService() {
97
+ textService = new Map();
98
+ textServiceVersion++;
99
+ }
100
+
101
+ function resolveText(text) {
102
+ const request = textService.get(text);
103
+ if (request !== undefined) {
104
+ request.resolve(textServiceVersion);
105
+ return request;
106
+ } else {
107
+ const newRequest = Promise.resolve(textServiceVersion);
108
+ newRequest.resolve = newRequest.reject = () => {};
109
+ textService.set(text, newRequest);
110
+ return newRequest;
111
+ }
112
+ }
113
+
114
+ function Text({text}) {
115
+ Scheduler.unstable_yieldValue(text);
116
+ return text;
117
+ }
118
+
119
+ function AsyncText({text, showVersion}) {
120
+ const version = readText(text);
121
+ const fullText = showVersion ? `${text} [v${version}]` : text;
122
+ Scheduler.unstable_yieldValue(fullText);
123
+ return fullText;
124
+ }
125
+
126
+ // @gate experimental
127
+ test('render Cache component', async () => {
128
+ const root = ReactNoop.createRoot();
129
+ await ReactNoop.act(async () => {
130
+ root.render(<Cache>Hi</Cache>);
131
+ });
132
+ expect(root).toMatchRenderedOutput('Hi');
133
+ });
134
+
135
+ // @gate experimental
136
+ test('mount new data', async () => {
137
+ const root = ReactNoop.createRoot();
138
+ await ReactNoop.act(async () => {
139
+ root.render(
140
+ <Cache>
141
+ <Suspense fallback={<Text text="Loading..." />}>
142
+ <AsyncText text="A" />
143
+ </Suspense>
144
+ </Cache>,
145
+ );
146
+ });
147
+ expect(Scheduler).toHaveYielded(['Cache miss! [A]', 'Loading...']);
148
+ expect(root).toMatchRenderedOutput('Loading...');
149
+
150
+ await ReactNoop.act(async () => {
151
+ await resolveText('A');
152
+ });
153
+ expect(Scheduler).toHaveYielded(['A']);
154
+ expect(root).toMatchRenderedOutput('A');
155
+ });
156
+
157
+ // @gate experimental
158
+ test('root acts as implicit cache boundary', async () => {
159
+ const root = ReactNoop.createRoot();
160
+ await ReactNoop.act(async () => {
161
+ root.render(
162
+ <Suspense fallback={<Text text="Loading..." />}>
163
+ <AsyncText text="A" />
164
+ </Suspense>,
165
+ );
166
+ });
167
+ expect(Scheduler).toHaveYielded(['Cache miss! [A]', 'Loading...']);
168
+ expect(root).toMatchRenderedOutput('Loading...');
169
+
170
+ await ReactNoop.act(async () => {
171
+ await resolveText('A');
172
+ });
173
+ expect(Scheduler).toHaveYielded(['A']);
174
+ expect(root).toMatchRenderedOutput('A');
175
+ });
176
+
177
+ // @gate experimental
178
+ test('multiple new Cache boundaries in the same update share the same, fresh cache', async () => {
179
+ function App({text}) {
180
+ return (
181
+ <>
182
+ <Cache>
183
+ <Suspense fallback={<Text text="Loading..." />}>
184
+ <AsyncText text="A" />
185
+ </Suspense>
186
+ </Cache>
187
+ <Cache>
188
+ <Suspense fallback={<Text text="Loading..." />}>
189
+ <AsyncText text="A" />
190
+ </Suspense>
191
+ </Cache>
192
+ </>
193
+ );
194
+ }
195
+
196
+ const root = ReactNoop.createRoot();
197
+ await ReactNoop.act(async () => {
198
+ root.render(<App showMore={false} />);
199
+ });
200
+ // Even though there are two new <Cache /> trees, they should share the same
201
+ // data cache. So there should be only a single cache miss for A.
202
+ expect(Scheduler).toHaveYielded([
203
+ 'Cache miss! [A]',
204
+ 'Loading...',
205
+ 'Loading...',
206
+ ]);
207
+ expect(root).toMatchRenderedOutput('Loading...Loading...');
208
+
209
+ await ReactNoop.act(async () => {
210
+ await resolveText('A');
211
+ });
212
+ expect(Scheduler).toHaveYielded(['A', 'A']);
213
+ expect(root).toMatchRenderedOutput('AA');
214
+ });
215
+
216
+ // @gate experimental
217
+ test(
218
+ 'nested cache boundaries share the same cache as the root during ' +
219
+ 'the initial render',
220
+ async () => {
221
+ function App() {
222
+ return (
223
+ <Suspense fallback={<Text text="Loading..." />}>
224
+ <AsyncText text="A" />
225
+ <Cache>
226
+ <AsyncText text="A" />
227
+ </Cache>
228
+ </Suspense>
229
+ );
230
+ }
231
+
232
+ const root = ReactNoop.createRoot();
233
+ await ReactNoop.act(async () => {
234
+ root.render(<App />);
235
+ });
236
+ // Even though there are two new <Cache /> trees, they should share the same
237
+ // data cache. So there should be only a single cache miss for A.
238
+ expect(Scheduler).toHaveYielded(['Cache miss! [A]', 'Loading...']);
239
+ expect(root).toMatchRenderedOutput('Loading...');
240
+
241
+ await ReactNoop.act(async () => {
242
+ await resolveText('A');
243
+ });
244
+ expect(Scheduler).toHaveYielded(['A', 'A']);
245
+ expect(root).toMatchRenderedOutput('AA');
246
+ },
247
+ );
248
+
249
+ // @gate experimental
250
+ test('new content inside an existing Cache boundary should re-use already cached data', async () => {
251
+ function App({showMore}) {
252
+ return (
253
+ <Cache>
254
+ <Suspense fallback={<Text text="Loading..." />}>
255
+ <AsyncText showVersion={true} text="A" />
256
+ </Suspense>
257
+ {showMore ? (
258
+ <Suspense fallback={<Text text="Loading..." />}>
259
+ <AsyncText showVersion={true} text="A" />
260
+ </Suspense>
261
+ ) : null}
262
+ </Cache>
263
+ );
264
+ }
265
+
266
+ const root = ReactNoop.createRoot();
267
+ await ReactNoop.act(async () => {
268
+ await resolveText('A');
269
+ root.render(<App showMore={false} />);
270
+ });
271
+ expect(Scheduler).toHaveYielded([
272
+ 'Cache miss! [A]',
273
+ 'Loading...',
274
+ 'A [v1]',
275
+ ]);
276
+ expect(root).toMatchRenderedOutput('A [v1]');
277
+
278
+ // Simulate a server mutation.
279
+ mutateRemoteTextService();
280
+
281
+ // Add a new cache boundary
282
+ await ReactNoop.act(async () => {
283
+ await resolveText('A');
284
+ root.render(<App showMore={true} />);
285
+ });
286
+ expect(Scheduler).toHaveYielded([
287
+ 'A [v1]',
288
+ // New tree should use already cached data
289
+ 'A [v1]',
290
+ ]);
291
+ expect(root).toMatchRenderedOutput('A [v1]A [v1]');
292
+ });
293
+
294
+ // @gate experimental
295
+ test('a new Cache boundary uses fresh cache', async () => {
296
+ // The only difference from the previous test is that the "Show More"
297
+ // content is wrapped in a nested <Cache /> boundary
298
+ function App({showMore}) {
299
+ return (
300
+ <Cache>
301
+ <Suspense fallback={<Text text="Loading..." />}>
302
+ <AsyncText showVersion={true} text="A" />
303
+ </Suspense>
304
+ {showMore ? (
305
+ <Cache>
306
+ <Suspense fallback={<Text text="Loading..." />}>
307
+ <AsyncText showVersion={true} text="A" />
308
+ </Suspense>
309
+ </Cache>
310
+ ) : null}
311
+ </Cache>
312
+ );
313
+ }
314
+
315
+ const root = ReactNoop.createRoot();
316
+ await ReactNoop.act(async () => {
317
+ await resolveText('A');
318
+ root.render(<App showMore={false} />);
319
+ });
320
+ expect(Scheduler).toHaveYielded([
321
+ 'Cache miss! [A]',
322
+ 'Loading...',
323
+ 'A [v1]',
324
+ ]);
325
+ expect(root).toMatchRenderedOutput('A [v1]');
326
+
327
+ // Simulate a server mutation.
328
+ mutateRemoteTextService();
329
+
330
+ // Add a new cache boundary
331
+ await ReactNoop.act(async () => {
332
+ await resolveText('A');
333
+ root.render(<App showMore={true} />);
334
+ });
335
+ expect(Scheduler).toHaveYielded([
336
+ 'A [v1]',
337
+ // New tree should load fresh data.
338
+ 'Cache miss! [A]',
339
+ 'Loading...',
340
+ 'A [v2]',
341
+ ]);
342
+ expect(root).toMatchRenderedOutput('A [v1]A [v2]');
343
+ });
344
+
345
+ // @gate experimental
346
+ test('inner content uses same cache as shell if spawned by the same transition', async () => {
347
+ const root = ReactNoop.createRoot();
348
+
349
+ function App() {
350
+ return (
351
+ <Cache>
352
+ <Suspense fallback={<Text text="Loading shell..." />}>
353
+ {/* The shell reads A */}
354
+ <Shell>
355
+ {/* The inner content reads both A and B */}
356
+ <Suspense fallback={<Text text="Loading content..." />}>
357
+ <Cache>
358
+ <Content />
359
+ </Cache>
360
+ </Suspense>
361
+ </Shell>
362
+ </Suspense>
363
+ </Cache>
364
+ );
365
+ }
366
+
367
+ function Shell({children}) {
368
+ readText('A');
369
+ return (
370
+ <>
371
+ <div>
372
+ <Text text="Shell" />
373
+ </div>
374
+ <div>{children}</div>
375
+ </>
376
+ );
377
+ }
378
+
379
+ function Content() {
380
+ readText('A');
381
+ readText('B');
382
+ return <Text text="Content" />;
383
+ }
384
+
385
+ await ReactNoop.act(async () => {
386
+ root.render(<App />);
387
+ });
388
+ expect(Scheduler).toHaveYielded(['Cache miss! [A]', 'Loading shell...']);
389
+ expect(root).toMatchRenderedOutput('Loading shell...');
390
+
391
+ await ReactNoop.act(async () => {
392
+ await resolveText('A');
393
+ });
394
+ expect(Scheduler).toHaveYielded([
395
+ 'Shell',
396
+ // There's a cache miss for B, because it hasn't been read yet. But not
397
+ // A, because it was cached when we rendered the shell.
398
+ 'Cache miss! [B]',
399
+ 'Loading content...',
400
+ ]);
401
+ expect(root).toMatchRenderedOutput(
402
+ <>
403
+ <div>Shell</div>
404
+ <div>Loading content...</div>
405
+ </>,
406
+ );
407
+
408
+ await ReactNoop.act(async () => {
409
+ await resolveText('B');
410
+ });
411
+ expect(Scheduler).toHaveYielded(['Content']);
412
+ expect(root).toMatchRenderedOutput(
413
+ <>
414
+ <div>Shell</div>
415
+ <div>Content</div>
416
+ </>,
417
+ );
418
+ });
419
+
420
+ // @gate experimental
421
+ test('refresh a cache', async () => {
422
+ let refresh;
423
+ function App() {
424
+ refresh = useCacheRefresh();
425
+ return <AsyncText showVersion={true} text="A" />;
426
+ }
427
+
428
+ // Mount initial data
429
+ const root = ReactNoop.createRoot();
430
+ await ReactNoop.act(async () => {
431
+ root.render(
432
+ <Cache>
433
+ <Suspense fallback={<Text text="Loading..." />}>
434
+ <App />
435
+ </Suspense>
436
+ </Cache>,
437
+ );
438
+ });
439
+ expect(Scheduler).toHaveYielded(['Cache miss! [A]', 'Loading...']);
440
+ expect(root).toMatchRenderedOutput('Loading...');
441
+
442
+ await ReactNoop.act(async () => {
443
+ await resolveText('A');
444
+ });
445
+ expect(Scheduler).toHaveYielded(['A [v1]']);
446
+ expect(root).toMatchRenderedOutput('A [v1]');
447
+
448
+ // Mutate the text service, then refresh for new data.
449
+ mutateRemoteTextService();
450
+ await ReactNoop.act(async () => {
451
+ startTransition(() => refresh());
452
+ });
453
+ expect(Scheduler).toHaveYielded(['Cache miss! [A]', 'Loading...']);
454
+ expect(root).toMatchRenderedOutput('A [v1]');
455
+
456
+ await ReactNoop.act(async () => {
457
+ await resolveText('A');
458
+ });
459
+ // Note that the version has updated
460
+ expect(Scheduler).toHaveYielded(['A [v2]']);
461
+ expect(root).toMatchRenderedOutput('A [v2]');
462
+ });
463
+
464
+ // @gate experimental
465
+ test('refresh the root cache', async () => {
466
+ let refresh;
467
+ function App() {
468
+ refresh = useCacheRefresh();
469
+ return <AsyncText showVersion={true} text="A" />;
470
+ }
471
+
472
+ // Mount initial data
473
+ const root = ReactNoop.createRoot();
474
+ await ReactNoop.act(async () => {
475
+ root.render(
476
+ <Suspense fallback={<Text text="Loading..." />}>
477
+ <App />
478
+ </Suspense>,
479
+ );
480
+ });
481
+ expect(Scheduler).toHaveYielded(['Cache miss! [A]', 'Loading...']);
482
+ expect(root).toMatchRenderedOutput('Loading...');
483
+
484
+ await ReactNoop.act(async () => {
485
+ await resolveText('A');
486
+ });
487
+ expect(Scheduler).toHaveYielded(['A [v1]']);
488
+ expect(root).toMatchRenderedOutput('A [v1]');
489
+
490
+ // Mutate the text service, then refresh for new data.
491
+ mutateRemoteTextService();
492
+ await ReactNoop.act(async () => {
493
+ startTransition(() => refresh());
494
+ });
495
+ expect(Scheduler).toHaveYielded(['Cache miss! [A]', 'Loading...']);
496
+ expect(root).toMatchRenderedOutput('A [v1]');
497
+
498
+ await ReactNoop.act(async () => {
499
+ await resolveText('A');
500
+ });
501
+ // Note that the version has updated
502
+ expect(Scheduler).toHaveYielded(['A [v2]']);
503
+ expect(root).toMatchRenderedOutput('A [v2]');
504
+ });
505
+
506
+ // @gate experimental
507
+ test('refresh a cache with seed data', async () => {
508
+ let refresh;
509
+ function App() {
510
+ refresh = useCacheRefresh();
511
+ return <AsyncText showVersion={true} text="A" />;
512
+ }
513
+
514
+ // Mount initial data
515
+ const root = ReactNoop.createRoot();
516
+ await ReactNoop.act(async () => {
517
+ root.render(
518
+ <Cache>
519
+ <Suspense fallback={<Text text="Loading..." />}>
520
+ <App />
521
+ </Suspense>
522
+ </Cache>,
523
+ );
524
+ });
525
+ expect(Scheduler).toHaveYielded(['Cache miss! [A]', 'Loading...']);
526
+ expect(root).toMatchRenderedOutput('Loading...');
527
+
528
+ await ReactNoop.act(async () => {
529
+ await resolveText('A');
530
+ });
531
+ expect(Scheduler).toHaveYielded(['A [v1]']);
532
+ expect(root).toMatchRenderedOutput('A [v1]');
533
+
534
+ // Mutate the text service, then refresh for new data.
535
+ mutateRemoteTextService();
536
+ await ReactNoop.act(async () => {
537
+ // Refresh the cache with seeded data, like you would receive from a
538
+ // server mutation.
539
+ // TODO: Seeding multiple typed caches. Should work by calling `refresh`
540
+ // multiple times with different key/value pairs
541
+ const seededCache = new Map();
542
+ seededCache.set('A', {
543
+ ping: null,
544
+ status: 'resolved',
545
+ value: textServiceVersion,
546
+ });
547
+ startTransition(() => refresh(createTextCache, seededCache));
548
+ });
549
+ // The root should re-render without a cache miss.
550
+ expect(Scheduler).toHaveYielded(['A [v2]']);
551
+ expect(root).toMatchRenderedOutput('A [v2]');
552
+ });
553
+
554
+ // @gate experimental
555
+ test('refreshing a parent cache also refreshes its children', async () => {
556
+ let refreshShell;
557
+ function RefreshShell() {
558
+ refreshShell = useCacheRefresh();
559
+ return null;
560
+ }
561
+
562
+ function App({showMore}) {
563
+ return (
564
+ <Cache>
565
+ <RefreshShell />
566
+ <Suspense fallback={<Text text="Loading..." />}>
567
+ <AsyncText showVersion={true} text="A" />
568
+ </Suspense>
569
+ {showMore ? (
570
+ <Cache>
571
+ <Suspense fallback={<Text text="Loading..." />}>
572
+ <AsyncText showVersion={true} text="A" />
573
+ </Suspense>
574
+ </Cache>
575
+ ) : null}
576
+ </Cache>
577
+ );
578
+ }
579
+
580
+ const root = ReactNoop.createRoot();
581
+ await ReactNoop.act(async () => {
582
+ await resolveText('A');
583
+ root.render(<App showMore={false} />);
584
+ });
585
+ expect(Scheduler).toHaveYielded([
586
+ 'Cache miss! [A]',
587
+ 'Loading...',
588
+ 'A [v1]',
589
+ ]);
590
+ expect(root).toMatchRenderedOutput('A [v1]');
591
+
592
+ // Simulate a server mutation.
593
+ mutateRemoteTextService();
594
+
595
+ // Add a new cache boundary
596
+ await ReactNoop.act(async () => {
597
+ await resolveText('A');
598
+ root.render(<App showMore={true} />);
599
+ });
600
+ expect(Scheduler).toHaveYielded([
601
+ 'A [v1]',
602
+ // New tree should load fresh data.
603
+ 'Cache miss! [A]',
604
+ 'Loading...',
605
+ 'A [v2]',
606
+ ]);
607
+ expect(root).toMatchRenderedOutput('A [v1]A [v2]');
608
+
609
+ // Now refresh the shell. This should also cause the "Show More" contents to
610
+ // refresh, since its cache is nested inside the outer one.
611
+ mutateRemoteTextService();
612
+ await ReactNoop.act(async () => {
613
+ startTransition(() => refreshShell());
614
+ });
615
+ expect(Scheduler).toHaveYielded([
616
+ 'Cache miss! [A]',
617
+ 'Loading...',
618
+ 'Loading...',
619
+ ]);
620
+ expect(root).toMatchRenderedOutput('A [v1]A [v2]');
621
+
622
+ await ReactNoop.act(async () => {
623
+ await resolveText('A');
624
+ });
625
+ expect(Scheduler).toHaveYielded(['A [v3]', 'A [v3]']);
626
+ expect(root).toMatchRenderedOutput('A [v3]A [v3]');
627
+ });
628
+
629
+ // @gate experimental
630
+ test(
631
+ 'refreshing a cache boundary does not refresh the other boundaries ' +
632
+ 'that mounted at the same time (i.e. the ones that share the same cache)',
633
+ async () => {
634
+ let refreshFirstBoundary;
635
+ function RefreshFirstBoundary() {
636
+ refreshFirstBoundary = useCacheRefresh();
637
+ return null;
638
+ }
639
+
640
+ function App({showMore}) {
641
+ return showMore ? (
642
+ <>
643
+ <Cache>
644
+ <Suspense fallback={<Text text="Loading..." />}>
645
+ <RefreshFirstBoundary />
646
+ <AsyncText showVersion={true} text="A" />
647
+ </Suspense>
648
+ </Cache>
649
+ <Cache>
650
+ <Suspense fallback={<Text text="Loading..." />}>
651
+ <AsyncText showVersion={true} text="A" />
652
+ </Suspense>
653
+ </Cache>
654
+ </>
655
+ ) : null;
656
+ }
657
+
658
+ // First mount the initial shell without the nested boundaries. This is
659
+ // necessary for this test because we want the two inner boundaries to be
660
+ // treated like sibling providers that happen to share an underlying
661
+ // cache, as opposed to consumers of the root-level cache.
662
+ const root = ReactNoop.createRoot();
663
+ await ReactNoop.act(async () => {
664
+ root.render(<App showMore={false} />);
665
+ });
666
+
667
+ // Now reveal the boundaries. In a real app this would be a navigation.
668
+ await ReactNoop.act(async () => {
669
+ root.render(<App showMore={true} />);
670
+ });
671
+
672
+ // Even though there are two new <Cache /> trees, they should share the same
673
+ // data cache. So there should be only a single cache miss for A.
674
+ expect(Scheduler).toHaveYielded([
675
+ 'Cache miss! [A]',
676
+ 'Loading...',
677
+ 'Loading...',
678
+ ]);
679
+ expect(root).toMatchRenderedOutput('Loading...Loading...');
680
+
681
+ await ReactNoop.act(async () => {
682
+ await resolveText('A');
683
+ });
684
+ expect(Scheduler).toHaveYielded(['A [v1]', 'A [v1]']);
685
+ expect(root).toMatchRenderedOutput('A [v1]A [v1]');
686
+
687
+ // Refresh the first boundary. It should not refresh the second boundary,
688
+ // even though they previously shared the same underlying cache.
689
+ mutateRemoteTextService();
690
+ await ReactNoop.act(async () => {
691
+ await refreshFirstBoundary();
692
+ });
693
+ expect(Scheduler).toHaveYielded(['Cache miss! [A]', 'Loading...']);
694
+
695
+ await ReactNoop.act(async () => {
696
+ await resolveText('A');
697
+ });
698
+ expect(Scheduler).toHaveYielded(['A [v2]']);
699
+ expect(root).toMatchRenderedOutput('A [v2]A [v1]');
700
+ },
701
+ );
702
+
703
+ // @gate experimental
704
+ test(
705
+ 'mount a new Cache boundary in a sibling while simultaneously ' +
706
+ 'resolving a Suspense boundary',
707
+ async () => {
708
+ function App({showMore}) {
709
+ return (
710
+ <>
711
+ {showMore ? (
712
+ <Suspense fallback={<Text text="Loading..." />}>
713
+ <Cache>
714
+ <AsyncText showVersion={true} text="A" />
715
+ </Cache>
716
+ </Suspense>
717
+ ) : null}
718
+ <Suspense fallback={<Text text="Loading..." />}>
719
+ <Cache>
720
+ {' '}
721
+ <AsyncText showVersion={true} text="A" />{' '}
722
+ <AsyncText showVersion={true} text="B" />
723
+ </Cache>
724
+ </Suspense>
725
+ </>
726
+ );
727
+ }
728
+
729
+ const root = ReactNoop.createRoot();
730
+ await ReactNoop.act(async () => {
731
+ root.render(<App showMore={false} />);
732
+ });
733
+ expect(Scheduler).toHaveYielded([
734
+ 'Cache miss! [A]',
735
+ 'Cache miss! [B]',
736
+ 'Loading...',
737
+ ]);
738
+
739
+ await ReactNoop.act(async () => {
740
+ // This will resolve the content in the first cache
741
+ resolveText('A');
742
+ resolveText('B');
743
+ // Now let's simulate a mutation
744
+ mutateRemoteTextService();
745
+ // And mount the second tree, which includes new content
746
+ root.render(<App showMore={true} />);
747
+ });
748
+ expect(Scheduler).toHaveYielded([
749
+ // The new tree should use a fresh cache
750
+ 'Cache miss! [A]',
751
+ 'Loading...',
752
+ // The other tree uses the cached responses. This demonstrates that the
753
+ // requests are not dropped.
754
+ 'A [v1]',
755
+ 'B [v1]',
756
+ ]);
757
+
758
+ // Now resolve the second tree
759
+ await ReactNoop.act(async () => {
760
+ resolveText('A');
761
+ });
762
+ expect(Scheduler).toHaveYielded(['A [v2]']);
763
+ expect(root).toMatchRenderedOutput('A [v2] A [v1] B [v1]');
764
+ },
765
+ );
766
+
767
+ // @gate experimental
768
+ test('cache pool is cleared once transitions that depend on it commit their shell', async () => {
769
+ function Child({text}) {
770
+ return (
771
+ <Cache>
772
+ <AsyncText text={text} />
773
+ </Cache>
774
+ );
775
+ }
776
+
777
+ const root = ReactNoop.createRoot();
778
+ await ReactNoop.act(async () => {
779
+ root.render(
780
+ <Suspense fallback={<Text text="Loading..." />}>(empty)</Suspense>,
781
+ );
782
+ });
783
+ expect(Scheduler).toHaveYielded([]);
784
+ expect(root).toMatchRenderedOutput('(empty)');
785
+
786
+ await ReactNoop.act(async () => {
787
+ startTransition(() => {
788
+ root.render(
789
+ <Suspense fallback={<Text text="Loading..." />}>
790
+ <Child text="A" />
791
+ </Suspense>,
792
+ );
793
+ });
794
+ });
795
+ expect(Scheduler).toHaveYielded(['Cache miss! [A]', 'Loading...']);
796
+ expect(root).toMatchRenderedOutput('(empty)');
797
+
798
+ await ReactNoop.act(async () => {
799
+ startTransition(() => {
800
+ root.render(
801
+ <Suspense fallback={<Text text="Loading..." />}>
802
+ <Child text="A" />
803
+ <Child text="A" />
804
+ </Suspense>,
805
+ );
806
+ });
807
+ });
808
+ expect(Scheduler).toHaveYielded([
809
+ // No cache miss, because it uses the pooled cache
810
+ 'Loading...',
811
+ ]);
812
+ expect(root).toMatchRenderedOutput('(empty)');
813
+
814
+ // Resolve the request
815
+ await ReactNoop.act(async () => {
816
+ await resolveText('A');
817
+ });
818
+ expect(Scheduler).toHaveYielded(['A', 'A']);
819
+ expect(root).toMatchRenderedOutput('AA');
820
+
821
+ // Now do another transition
822
+ await ReactNoop.act(async () => {
823
+ startTransition(() => {
824
+ root.render(
825
+ <Suspense fallback={<Text text="Loading..." />}>
826
+ <Child text="A" />
827
+ <Child text="A" />
828
+ <Child text="A" />
829
+ </Suspense>,
830
+ );
831
+ });
832
+ });
833
+ expect(Scheduler).toHaveYielded([
834
+ // First two children use the old cache because they already finished
835
+ 'A',
836
+ 'A',
837
+ // The new child uses a fresh cache
838
+ 'Cache miss! [A]',
839
+ 'Loading...',
840
+ 'A',
841
+ 'A',
842
+ 'A',
843
+ ]);
844
+ expect(root).toMatchRenderedOutput('AAA');
845
+ });
846
+
847
+ // @gate experimental
848
+ test('cache pool is not cleared by arbitrary commits', async () => {
849
+ function App() {
850
+ return (
851
+ <>
852
+ <ShowMore />
853
+ <Unrelated />
854
+ </>
855
+ );
856
+ }
857
+
858
+ let showMore;
859
+ function ShowMore() {
860
+ const [shouldShow, _showMore] = useState(false);
861
+ showMore = () => _showMore(true);
862
+ return (
863
+ <>
864
+ <Suspense fallback={<Text text="Loading..." />}>
865
+ {shouldShow ? (
866
+ <Cache>
867
+ <AsyncText text="A" />
868
+ </Cache>
869
+ ) : null}
870
+ </Suspense>
871
+ </>
872
+ );
873
+ }
874
+
875
+ let updateUnrelated;
876
+ function Unrelated() {
877
+ const [count, _updateUnrelated] = useState(0);
878
+ updateUnrelated = _updateUnrelated;
879
+ return <Text text={count + ''} />;
880
+ }
881
+
882
+ const root = ReactNoop.createRoot();
883
+ await ReactNoop.act(async () => {
884
+ root.render(<App showMore={false} />);
885
+ });
886
+ expect(Scheduler).toHaveYielded(['0']);
887
+ expect(root).toMatchRenderedOutput('0');
888
+
889
+ await ReactNoop.act(async () => {
890
+ startTransition(() => {
891
+ showMore();
892
+ });
893
+ });
894
+ expect(Scheduler).toHaveYielded(['Cache miss! [A]', 'Loading...']);
895
+ expect(root).toMatchRenderedOutput('0');
896
+
897
+ await ReactNoop.act(async () => {
898
+ updateUnrelated(1);
899
+ });
900
+ expect(Scheduler).toHaveYielded([
901
+ '1',
902
+
903
+ // Happens to re-render the fallback. Doesn't need to, but not relevant
904
+ // to this test.
905
+ 'Loading...',
906
+ ]);
907
+ expect(root).toMatchRenderedOutput('1');
908
+
909
+ await ReactNoop.act(async () => {
910
+ resolveText('A');
911
+ mutateRemoteTextService();
912
+ });
913
+ expect(Scheduler).toHaveYielded(['A']);
914
+ expect(root).toMatchRenderedOutput('A1');
915
+ });
916
+});
packages/react-server/src/ReactFlightServer.js
+10
@@ -758,6 +758,13 @@ function unsupportedHook(): void {
758
invariant(false, 'This Hook is not supported in Server Components.');
759
}
760
761
+function unsupportedRefresh(): void {
762
+ invariant(
763
+ currentCache,
764
+ 'Refreshing the cache is not supported in Server Components.',
765
+ );
766
+}
767
+
768
let currentCache: Map<Function, mixed> | null = null;
769
770
const Dispatcher: DispatcherType = {
@@ -797,4 +804,7 @@ const Dispatcher: DispatcherType = {
804
useEffect: (unsupportedHook: any),
805
useOpaqueIdentifier: (unsupportedHook: any),
806
useMutableSource: (unsupportedHook: any),
807
+ useCacheRefresh(): <T>(?() => T, ?T) => void {
808
+ return unsupportedRefresh;
809
+ },
810
};
packages/react-suspense-test-utils/src/ReactSuspenseTestUtils.js
+1
@@ -44,6 +44,7 @@ export function waitForSuspense<T>(fn: () => T): Promise<T> {
44
useTransition: unsupported,
45
useOpaqueIdentifier: unsupported,
46
useMutableSource: unsupported,
47
+ useCacheRefresh: unsupported,
48
};
49
// Not using async/await because we don't compile it.
50
return new Promise((resolve, reject) => {
packages/react/index.classic.fb.js
+2
@@ -51,6 +51,8 @@ export {
51
SuspenseList,
52
SuspenseList as unstable_SuspenseList,
53
unstable_getCacheForType,
54
+ unstable_Cache,
55
+ unstable_useCacheRefresh,
56
// enableScopeAPI
57
unstable_Scope,
58
unstable_useOpaqueIdentifier,
packages/react/index.experimental.js
+2
@@ -46,6 +46,8 @@ export {
46
SuspenseList as unstable_SuspenseList,
47
unstable_useOpaqueIdentifier,
48
unstable_getCacheForType,
49
+ unstable_Cache,
50
+ unstable_useCacheRefresh,
51
// enableDebugTracing
52
unstable_DebugTracingMode,
53
} from './src/React';
packages/react/index.js
+2
@@ -83,4 +83,6 @@ export {
83
unstable_Scope,
84
unstable_useOpaqueIdentifier,
85
unstable_getCacheForType,
86
+ unstable_Cache,
87
+ unstable_useCacheRefresh,
88
} from './src/React';
packages/react/index.modern.fb.js
+2
@@ -50,6 +50,8 @@ export {
50
SuspenseList,
51
SuspenseList as unstable_SuspenseList,
52
unstable_getCacheForType,
53
+ unstable_Cache,
54
+ unstable_useCacheRefresh,
55
// enableScopeAPI
56
unstable_Scope,
57
unstable_useOpaqueIdentifier,
packages/react/src/React.js
+4
@@ -17,6 +17,7 @@ import {
17
REACT_SUSPENSE_LIST_TYPE,
18
REACT_LEGACY_HIDDEN_TYPE,
19
REACT_SCOPE_TYPE,
20
+ REACT_CACHE_TYPE,
21
} from 'shared/ReactSymbols';
22
23
import {Component, PureComponent} from './ReactBaseClasses';
@@ -48,6 +49,7 @@ import {
49
useTransition,
50
useDeferredValue,
51
useOpaqueIdentifier,
52
+ useCacheRefresh,
53
} from './ReactHooks';
54
import {
55
createElementWithValidation,
@@ -112,6 +114,8 @@ export {
114
REACT_SUSPENSE_LIST_TYPE as SuspenseList,
115
REACT_LEGACY_HIDDEN_TYPE as unstable_LegacyHidden,
116
getCacheForType as unstable_getCacheForType,
117
+ useCacheRefresh as unstable_useCacheRefresh,
118
+ REACT_CACHE_TYPE as unstable_Cache,
119
// enableFundamentalAPI
120
createFundamental as unstable_createFundamental,
121
// enableScopeAPI
packages/react/src/ReactHooks.js
+6
@@ -180,3 +180,9 @@ export function useMutableSource<Source, Snapshot>(
180
const dispatcher = resolveDispatcher();
181
return dispatcher.useMutableSource(source, getSnapshot, subscribe);
182
}
183
+
184
+export function useCacheRefresh(): <T>(?() => T, ?T) => void {
185
+ const dispatcher = resolveDispatcher();
186
+ // $FlowFixMe This is unstable, thus optional
187
+ return dispatcher.useCacheRefresh();
188
+}
packages/shared/ReactSymbols.js
+2
@@ -31,6 +31,7 @@ export let REACT_OPAQUE_ID_TYPE = 0xeae0;
31
export let REACT_DEBUG_TRACING_MODE_TYPE = 0xeae1;
32
export let REACT_OFFSCREEN_TYPE = 0xeae2;
33
export let REACT_LEGACY_HIDDEN_TYPE = 0xeae3;
34
+export let REACT_CACHE_TYPE = 0xeae4;
35
36
if (typeof Symbol === 'function' && Symbol.for) {
37
const symbolFor = Symbol.for;
@@ -52,6 +53,7 @@ if (typeof Symbol === 'function' && Symbol.for) {
53
REACT_DEBUG_TRACING_MODE_TYPE = symbolFor('react.debug_trace_mode');
54
REACT_OFFSCREEN_TYPE = symbolFor('react.offscreen');
55
REACT_LEGACY_HIDDEN_TYPE = symbolFor('react.legacy_hidden');
56
+ REACT_CACHE_TYPE = symbolFor('react.cache');
57
}
58
59
const MAYBE_ITERATOR_SYMBOL = typeof Symbol === 'function' && Symbol.iterator;
packages/shared/getComponentName.js
+3
@@ -21,6 +21,7 @@ import {
21
REACT_SUSPENSE_TYPE,
22
REACT_SUSPENSE_LIST_TYPE,
23
REACT_LAZY_TYPE,
24
+ REACT_CACHE_TYPE,
25
} from 'shared/ReactSymbols';
26
import type {ReactContext, ReactProviderType} from 'shared/ReactTypes';
27
@@ -72,6 +73,8 @@ function getComponentName(type: mixed): string | null {
73
return 'Suspense';
74
case REACT_SUSPENSE_LIST_TYPE:
75
return 'SuspenseList';
76
+ case REACT_CACHE_TYPE:
77
+ return 'Cache';
78
}
79
if (typeof type === 'object') {
80
switch (type.$$typeof) {
packages/shared/isValidElementType.js
+4
-2
@@ -22,8 +22,9 @@ import {
22
REACT_FUNDAMENTAL_TYPE,
23
REACT_SCOPE_TYPE,
24
REACT_LEGACY_HIDDEN_TYPE,
25
+ REACT_CACHE_TYPE,
26
} from 'shared/ReactSymbols';
26
-import {enableScopeAPI} from './ReactFeatureFlags';
27
+import {enableScopeAPI, enableCache} from './ReactFeatureFlags';
28
29
let REACT_MODULE_REFERENCE: number | Symbol = 0;
30
if (typeof Symbol === 'function') {
@@ -44,7 +45,8 @@ export default function isValidElementType(type: mixed) {
45
type === REACT_SUSPENSE_TYPE ||
46
type === REACT_SUSPENSE_LIST_TYPE ||
47
type === REACT_LEGACY_HIDDEN_TYPE ||
47
- (enableScopeAPI && type === REACT_SCOPE_TYPE)
48
+ (enableScopeAPI && type === REACT_SCOPE_TYPE) ||
49
+ (enableCache && type === REACT_CACHE_TYPE)
50
) {
51
return true;
52
}
scripts/error-codes/codes.json
+2
-1
@@ -371,5 +371,6 @@
371
"380": "Reading the cache is only supported while rendering.",
372
"381": "This feature is not supported by ReactSuspenseTestUtils.",
373
"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.",
374
- "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."
374
+ "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": "Refreshing the cache is not supported in Server Components."
376
}