@samitouri / QOS-React-2 / commits / efc57e5cbb

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 }