diff --git a/packages/react-dom/src/server/ReactPartialRendererHooks.js b/packages/react-dom/src/server/ReactPartialRendererHooks.js index 3a543aa337b6c..7759cc62c83bd 100644 --- a/packages/react-dom/src/server/ReactPartialRendererHooks.js +++ b/packages/react-dom/src/server/ReactPartialRendererHooks.js @@ -489,6 +489,10 @@ function useOpaqueIdentifier(): OpaqueIDType { ); } +function useRefresh(): () => void { + invariant(false, 'Not implemented.'); +} + function noop(): void {} export let currentPartialRenderer: PartialRenderer = (null: any); @@ -520,4 +524,5 @@ export const Dispatcher: DispatcherType = { if (enableCache) { Dispatcher.getCacheForType = getCacheForType; + Dispatcher.useRefresh = useRefresh; } diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.new.js b/packages/react-reconciler/src/ReactFiberBeginWork.new.js index d96637f1feb56..b4448d0b775f8 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.new.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.new.js @@ -133,6 +133,7 @@ import { isSuspenseInstanceFallback, registerSuspenseInstanceRetry, supportsHydration, + isPrimaryRenderer, } from './ReactFiberHostConfig'; import type {SuspenseInstance} from './ReactFiberHostConfig'; import {shouldSuspend} from './ReactFiberReconciler'; @@ -151,6 +152,7 @@ import {findFirstSuspended} from './ReactFiberSuspenseComponent.new'; import { pushProvider, propagateContextChange, + propagateCacheRefresh, readContext, prepareToReadContext, calculateChangedBits, @@ -662,22 +664,82 @@ function updateCacheComponent( return null; } - const root = getWorkInProgressRoot(); - invariant( - root !== null, - 'Expected a work-in-progress root. This is a bug in React. Please ' + - 'file an issue.', - ); + // Read directly from the context. We don't set up a context dependency + // because the propagation function automatically includes CacheComponents in + // its search. + const parentCache: Cache | null = isPrimaryRenderer + ? CacheContext._currentValue + : CacheContext._currentValue2; - const cache: Cache = - current === null - ? requestFreshCache(root, renderLanes) - : current.memoizedState; - - // TODO: Propagate changes, once refreshing exists. - pushProvider(workInProgress, CacheContext, cache); + let ownCache: Cache | null = null; + if (parentCache !== null && parentCache.providers === null) { + // The parent boundary also has a new cache. We're either inside a new tree, + // or there was a refresh. In both cases, we should use the parent cache. + ownCache = null; + } else { + if (current === null) { + // This is a newly mounted component. Request a fresh cache. + const root = getWorkInProgressRoot(); + invariant( + root !== null, + 'Expected a work-in-progress root. This is a bug in React. Please ' + + 'file an issue.', + ); + const freshCache = requestFreshCache(root, renderLanes); + // This may be the same as the parent cache, like if the current render + // spawned from a previous render that already committed. Otherwise, this + // is the root of a cache consistency boundary. + if (freshCache !== parentCache) { + ownCache = freshCache; + pushProvider(workInProgress, CacheContext, freshCache); + // No need to propagate the refresh, because this is a new tree. + } else { + // Use the parent cache + ownCache = null; + } + } else { + // This component already mounted. + if (includesSomeLane(renderLanes, updateLanes)) { + // A refresh was scheduled. + const root = getWorkInProgressRoot(); + invariant( + root !== null, + 'Expected a work-in-progress root. This is a bug in React. Please ' + + 'file an issue.', + ); + const freshCache = requestFreshCache(root, renderLanes); + if (freshCache !== parentCache) { + ownCache = freshCache; + pushProvider(workInProgress, CacheContext, freshCache); + // Refreshes propagate through the entire subtree. The refreshed cache + // will override nested caches. + propagateCacheRefresh(workInProgress, renderLanes); + } else { + // The fresh cache is the same as the parent cache. I think this + // unreachable in practice, because this means the parent cache was + // refreshed in the same render. So we would have already handled this + // in the earlier branch, where we check if the parent is new. + ownCache = null; + } + } else { + // Reuse the memoized cache. + const prevCache: Cache | null = current.memoizedState; + if (prevCache !== null) { + ownCache = prevCache; + // There was no refresh, so no need to propagate to nested boundaries. + pushProvider(workInProgress, CacheContext, prevCache); + } else { + ownCache = null; + } + } + } + } - workInProgress.memoizedState = cache; + // If this CacheComponent is the root of its tree, then `memoizedState` will + // point to a cache object. Otherwise, a null state indicates that this + // CacheComponent inherits from a parent boundary. We can use this to infer + // whether to push/pop the cache context. + workInProgress.memoizedState = ownCache; const nextChildren = workInProgress.pendingProps.children; reconcileChildren(current, workInProgress, nextChildren, renderLanes); @@ -3273,8 +3335,10 @@ function beginWork( } case CacheComponent: { if (enableCache) { - const cache: Cache = current.memoizedState; - pushProvider(workInProgress, CacheContext, cache); + const ownCache: Cache | null = workInProgress.memoizedState; + if (ownCache !== null) { + pushProvider(workInProgress, CacheContext, ownCache); + } } break; } diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.old.js b/packages/react-reconciler/src/ReactFiberBeginWork.old.js index 6f212abd26b13..11b915d24bc01 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.old.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.old.js @@ -133,6 +133,7 @@ import { isSuspenseInstanceFallback, registerSuspenseInstanceRetry, supportsHydration, + isPrimaryRenderer, } from './ReactFiberHostConfig'; import type {SuspenseInstance} from './ReactFiberHostConfig'; import {shouldSuspend} from './ReactFiberReconciler'; @@ -151,6 +152,7 @@ import {findFirstSuspended} from './ReactFiberSuspenseComponent.old'; import { pushProvider, propagateContextChange, + propagateCacheRefresh, readContext, prepareToReadContext, calculateChangedBits, @@ -662,22 +664,82 @@ function updateCacheComponent( return null; } - const root = getWorkInProgressRoot(); - invariant( - root !== null, - 'Expected a work-in-progress root. This is a bug in React. Please ' + - 'file an issue.', - ); + // Read directly from the context. We don't set up a context dependency + // because the propagation function automatically includes CacheComponents in + // its search. + const parentCache: Cache | null = isPrimaryRenderer + ? CacheContext._currentValue + : CacheContext._currentValue2; - const cache: Cache = - current === null - ? requestFreshCache(root, renderLanes) - : current.memoizedState; - - // TODO: Propagate changes, once refreshing exists. - pushProvider(workInProgress, CacheContext, cache); + let ownCache: Cache | null = null; + if (parentCache !== null && parentCache.providers === null) { + // The parent boundary also has a new cache. We're either inside a new tree, + // or there was a refresh. In both cases, we should use the parent cache. + ownCache = null; + } else { + if (current === null) { + // This is a newly mounted component. Request a fresh cache. + const root = getWorkInProgressRoot(); + invariant( + root !== null, + 'Expected a work-in-progress root. This is a bug in React. Please ' + + 'file an issue.', + ); + const freshCache = requestFreshCache(root, renderLanes); + // This may be the same as the parent cache, like if the current render + // spawned from a previous render that already committed. Otherwise, this + // is the root of a cache consistency boundary. + if (freshCache !== parentCache) { + ownCache = freshCache; + pushProvider(workInProgress, CacheContext, freshCache); + // No need to propagate the refresh, because this is a new tree. + } else { + // Use the parent cache + ownCache = null; + } + } else { + // This component already mounted. + if (includesSomeLane(renderLanes, updateLanes)) { + // A refresh was scheduled. + const root = getWorkInProgressRoot(); + invariant( + root !== null, + 'Expected a work-in-progress root. This is a bug in React. Please ' + + 'file an issue.', + ); + const freshCache = requestFreshCache(root, renderLanes); + if (freshCache !== parentCache) { + ownCache = freshCache; + pushProvider(workInProgress, CacheContext, freshCache); + // Refreshes propagate through the entire subtree. The refreshed cache + // will override nested caches. + propagateCacheRefresh(workInProgress, renderLanes); + } else { + // The fresh cache is the same as the parent cache. I think this + // unreachable in practice, because this means the parent cache was + // refreshed in the same render. So we would have already handled this + // in the earlier branch, where we check if the parent is new. + ownCache = null; + } + } else { + // Reuse the memoized cache. + const prevCache: Cache | null = current.memoizedState; + if (prevCache !== null) { + ownCache = prevCache; + // There was no refresh, so no need to propagate to nested boundaries. + pushProvider(workInProgress, CacheContext, prevCache); + } else { + ownCache = null; + } + } + } + } - workInProgress.memoizedState = cache; + // If this CacheComponent is the root of its tree, then `memoizedState` will + // point to a cache object. Otherwise, a null state indicates that this + // CacheComponent inherits from a parent boundary. We can use this to infer + // whether to push/pop the cache context. + workInProgress.memoizedState = ownCache; const nextChildren = workInProgress.pendingProps.children; reconcileChildren(current, workInProgress, nextChildren, renderLanes); @@ -3273,8 +3335,10 @@ function beginWork( } case CacheComponent: { if (enableCache) { - const cache: Cache = current.memoizedState; - pushProvider(workInProgress, CacheContext, cache); + const ownCache: Cache | null = workInProgress.memoizedState; + if (ownCache !== null) { + pushProvider(workInProgress, CacheContext, ownCache); + } } break; } diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.new.js b/packages/react-reconciler/src/ReactFiberCommitWork.new.js index 57a99b23aaf0f..28f2f2a2be3be 100644 --- a/packages/react-reconciler/src/ReactFiberCommitWork.new.js +++ b/packages/react-reconciler/src/ReactFiberCommitWork.new.js @@ -36,6 +36,7 @@ import { enableFundamentalAPI, enableSuspenseCallback, enableScopeAPI, + enableCache, } from 'shared/ReactFeatureFlags'; import { FunctionComponent, @@ -793,6 +794,30 @@ function commitLifeCycles( case OffscreenComponent: case LegacyHiddenComponent: return; + case CacheComponent: { + if (enableCache) { + if (current !== null) { + const oldCache: Cache | null = current.memoizedState; + if (oldCache !== null) { + const oldCacheProviders = oldCache.providers; + if (oldCacheProviders) { + oldCacheProviders.delete(current); + oldCacheProviders.delete(finishedWork); + } + } + } + const newCache: Cache | null = finishedWork.memoizedState; + if (newCache !== null) { + const newCacheProviders = newCache.providers; + if (newCacheProviders === null) { + newCache.providers = new Set([finishedWork]); + } else { + newCacheProviders.add(finishedWork); + } + } + } + return; + } } invariant( false, diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.old.js b/packages/react-reconciler/src/ReactFiberCommitWork.old.js index 168696e71f516..d46759f7c9334 100644 --- a/packages/react-reconciler/src/ReactFiberCommitWork.old.js +++ b/packages/react-reconciler/src/ReactFiberCommitWork.old.js @@ -37,6 +37,7 @@ import { enableSuspenseCallback, enableScopeAPI, enableDoubleInvokingEffects, + enableCache, } from 'shared/ReactFeatureFlags'; import { FunctionComponent, @@ -794,6 +795,30 @@ function commitLifeCycles( case OffscreenComponent: case LegacyHiddenComponent: return; + case CacheComponent: { + if (enableCache) { + if (current !== null) { + const oldCache: Cache | null = current.memoizedState; + if (oldCache !== null) { + const oldCacheProviders = oldCache.providers; + if (oldCacheProviders) { + oldCacheProviders.delete(current); + oldCacheProviders.delete(finishedWork); + } + } + } + const newCache: Cache | null = finishedWork.memoizedState; + if (newCache !== null) { + const newCacheProviders = newCache.providers; + if (newCacheProviders === null) { + newCache.providers = new Set([finishedWork]); + } else { + newCacheProviders.add(finishedWork); + } + } + } + return; + } } invariant( false, diff --git a/packages/react-reconciler/src/ReactFiberCompleteWork.new.js b/packages/react-reconciler/src/ReactFiberCompleteWork.new.js index 458bb7df21307..3aa000e4ca810 100644 --- a/packages/react-reconciler/src/ReactFiberCompleteWork.new.js +++ b/packages/react-reconciler/src/ReactFiberCompleteWork.new.js @@ -1488,7 +1488,28 @@ function completeWork( } case CacheComponent: { if (enableCache) { - popProvider(CacheContext, workInProgress); + // If the cache provided by this boundary has changed, schedule an + // effect to add this component to the cache's providers, and to remove + // it from the old cache. + // TODO: Schedule for Passive phase + const ownCache: Cache | null = workInProgress.memoizedState; + if (current === null) { + if (ownCache !== null) { + // This is a cache provider. + popProvider(CacheContext, workInProgress); + // Set up a refresh subscription. + workInProgress.flags |= Update; + } + } else { + if (ownCache !== null) { + // This is a cache provider. + popProvider(CacheContext, workInProgress); + } + if (ownCache !== current.memoizedState) { + // Cache changed. Create or update a refresh subscription. + workInProgress.flags |= Update; + } + } bubbleProperties(workInProgress); return null; } diff --git a/packages/react-reconciler/src/ReactFiberCompleteWork.old.js b/packages/react-reconciler/src/ReactFiberCompleteWork.old.js index f3c3efafcb9e0..b20910ddc5ece 100644 --- a/packages/react-reconciler/src/ReactFiberCompleteWork.old.js +++ b/packages/react-reconciler/src/ReactFiberCompleteWork.old.js @@ -1488,7 +1488,28 @@ function completeWork( } case CacheComponent: { if (enableCache) { - popProvider(CacheContext, workInProgress); + // If the cache provided by this boundary has changed, schedule an + // effect to add this component to the cache's providers, and to remove + // it from the old cache. + // TODO: Schedule for Passive phase + const ownCache: Cache | null = workInProgress.memoizedState; + if (current === null) { + if (ownCache !== null) { + // This is a cache provider. + popProvider(CacheContext, workInProgress); + // Set up a refresh subscription. + workInProgress.flags |= Update; + } + } else { + if (ownCache !== null) { + // This is a cache provider. + popProvider(CacheContext, workInProgress); + } + if (ownCache !== current.memoizedState) { + // Cache changed. Create or update a refresh subscription. + workInProgress.flags |= Update; + } + } bubbleProperties(workInProgress); return null; } diff --git a/packages/react-reconciler/src/ReactFiberHooks.new.js b/packages/react-reconciler/src/ReactFiberHooks.new.js index a50527e120901..25aa64015c473 100644 --- a/packages/react-reconciler/src/ReactFiberHooks.new.js +++ b/packages/react-reconciler/src/ReactFiberHooks.new.js @@ -19,6 +19,7 @@ import type {HookFlags} from './ReactHookEffectTags'; import type {ReactPriorityLevel} from './ReactInternalTypes'; import type {FiberRoot} from './ReactInternalTypes'; import type {OpaqueIDType} from './ReactFiberHostConfig'; +import type {Cache} from './ReactFiberCacheComponent'; import ReactSharedInternals from 'shared/ReactSharedInternals'; import { @@ -1707,6 +1708,43 @@ function rerenderOpaqueIdentifier(): OpaqueIDType | void { return id; } +function mountRefresh() { + const cache: Cache | null = readContext(CacheContext); + return mountCallback(refreshCache.bind(null, cache), [cache]); +} + +function updateRefresh() { + const cache: Cache | null = readContext(CacheContext); + return updateCallback(refreshCache.bind(null, cache), [cache]); +} + +function refreshCache(cache: Cache | null) { + if (cache !== null) { + const providers = cache.providers; + if (providers !== null) { + providers.forEach(scheduleCacheRefresh); + } + } else { + // TODO: Warn if cache is null? + } +} + +function scheduleCacheRefresh(cacheComponentFiber: Fiber) { + // Inlined startTransition + // TODO: Maybe we shouldn't automatically give this transition priority. Are + // there valid use cases for a high-pri refresh? Like if the content is + // super stale and you want to immediately hide it. + const prevTransition = ReactCurrentBatchConfig.transition; + ReactCurrentBatchConfig.transition = 1; + try { + const eventTime = requestEventTime(); + const lane = requestUpdateLane(cacheComponentFiber); + scheduleUpdateOnFiber(cacheComponentFiber, lane, eventTime); + } finally { + ReactCurrentBatchConfig.transition = prevTransition; + } +} + function dispatchAction( fiber: Fiber, queue: UpdateQueue, @@ -1818,7 +1856,7 @@ function dispatchAction( } function getCacheForType(resourceType: () => T): T { - const cache = readContext(CacheContext); + const cache: Cache | null = readContext(CacheContext); invariant( cache !== null, 'Tried to fetch data, but no cache was found. To fix, wrap your ' + @@ -1866,6 +1904,7 @@ export const ContextOnlyDispatcher: Dispatcher = { }; if (enableCache) { (ContextOnlyDispatcher: Dispatcher).getCacheForType = getCacheForType; + (ContextOnlyDispatcher: Dispatcher).useRefresh = throwInvalidHookError; } const HooksDispatcherOnMount: Dispatcher = { @@ -1890,6 +1929,7 @@ const HooksDispatcherOnMount: Dispatcher = { }; if (enableCache) { (HooksDispatcherOnMount: Dispatcher).getCacheForType = getCacheForType; + (HooksDispatcherOnMount: Dispatcher).useRefresh = mountRefresh; } const HooksDispatcherOnUpdate: Dispatcher = { @@ -1914,6 +1954,7 @@ const HooksDispatcherOnUpdate: Dispatcher = { }; if (enableCache) { (HooksDispatcherOnUpdate: Dispatcher).getCacheForType = getCacheForType; + (HooksDispatcherOnUpdate: Dispatcher).useRefresh = updateRefresh; } const HooksDispatcherOnRerender: Dispatcher = { @@ -1938,6 +1979,7 @@ const HooksDispatcherOnRerender: Dispatcher = { }; if (enableCache) { (HooksDispatcherOnRerender: Dispatcher).getCacheForType = getCacheForType; + (HooksDispatcherOnRerender: Dispatcher).useRefresh = updateRefresh; } let HooksDispatcherOnMountInDEV: Dispatcher | null = null; @@ -2095,6 +2137,11 @@ if (__DEV__) { }; if (enableCache) { (HooksDispatcherOnMountInDEV: Dispatcher).getCacheForType = getCacheForType; + (HooksDispatcherOnMountInDEV: Dispatcher).useRefresh = function useRefresh() { + currentHookNameInDev = 'useRefresh'; + mountHookTypesDev(); + return mountRefresh(); + }; } HooksDispatcherOnMountWithHookTypesInDEV = { @@ -2220,6 +2267,11 @@ if (__DEV__) { }; if (enableCache) { (HooksDispatcherOnMountWithHookTypesInDEV: Dispatcher).getCacheForType = getCacheForType; + (HooksDispatcherOnMountWithHookTypesInDEV: Dispatcher).useRefresh = function useRefresh() { + currentHookNameInDev = 'useRefresh'; + updateHookTypesDev(); + return mountRefresh(); + }; } HooksDispatcherOnUpdateInDEV = { @@ -2345,6 +2397,11 @@ if (__DEV__) { }; if (enableCache) { (HooksDispatcherOnUpdateInDEV: Dispatcher).getCacheForType = getCacheForType; + (HooksDispatcherOnUpdateInDEV: Dispatcher).useRefresh = function useRefresh() { + currentHookNameInDev = 'useRefresh'; + updateHookTypesDev(); + return updateRefresh(); + }; } HooksDispatcherOnRerenderInDEV = { @@ -2471,6 +2528,11 @@ if (__DEV__) { }; if (enableCache) { (HooksDispatcherOnRerenderInDEV: Dispatcher).getCacheForType = getCacheForType; + (HooksDispatcherOnRerenderInDEV: Dispatcher).useRefresh = function useRefresh() { + currentHookNameInDev = 'useRefresh'; + updateHookTypesDev(); + return updateRefresh(); + }; } InvalidNestedHooksDispatcherOnMountInDEV = { @@ -2611,6 +2673,11 @@ if (__DEV__) { }; if (enableCache) { (InvalidNestedHooksDispatcherOnMountInDEV: Dispatcher).getCacheForType = getCacheForType; + (InvalidNestedHooksDispatcherOnMountInDEV: Dispatcher).useRefresh = function useRefresh() { + currentHookNameInDev = 'useRefresh'; + updateHookTypesDev(); + return mountRefresh(); + }; } InvalidNestedHooksDispatcherOnUpdateInDEV = { @@ -2751,6 +2818,11 @@ if (__DEV__) { }; if (enableCache) { (InvalidNestedHooksDispatcherOnUpdateInDEV: Dispatcher).getCacheForType = getCacheForType; + (InvalidNestedHooksDispatcherOnUpdateInDEV: Dispatcher).useRefresh = function useRefresh() { + currentHookNameInDev = 'useRefresh'; + updateHookTypesDev(); + return updateRefresh(); + }; } InvalidNestedHooksDispatcherOnRerenderInDEV = { @@ -2892,5 +2964,10 @@ if (__DEV__) { }; if (enableCache) { (InvalidNestedHooksDispatcherOnRerenderInDEV: Dispatcher).getCacheForType = getCacheForType; + (InvalidNestedHooksDispatcherOnRerenderInDEV: Dispatcher).useRefresh = function useRefresh() { + currentHookNameInDev = 'useRefresh'; + updateHookTypesDev(); + return updateRefresh(); + }; } } diff --git a/packages/react-reconciler/src/ReactFiberHooks.old.js b/packages/react-reconciler/src/ReactFiberHooks.old.js index 28e0222a66664..eecbffc1e7306 100644 --- a/packages/react-reconciler/src/ReactFiberHooks.old.js +++ b/packages/react-reconciler/src/ReactFiberHooks.old.js @@ -19,6 +19,7 @@ import type {HookFlags} from './ReactHookEffectTags'; import type {ReactPriorityLevel} from './ReactInternalTypes'; import type {FiberRoot} from './ReactInternalTypes'; import type {OpaqueIDType} from './ReactFiberHostConfig'; +import type {Cache} from './ReactFiberCacheComponent'; import ReactSharedInternals from 'shared/ReactSharedInternals'; import { @@ -1778,6 +1779,43 @@ function rerenderOpaqueIdentifier(): OpaqueIDType | void { return id; } +function mountRefresh() { + const cache: Cache | null = readContext(CacheContext); + return mountCallback(refreshCache.bind(null, cache), [cache]); +} + +function updateRefresh() { + const cache: Cache | null = readContext(CacheContext); + return updateCallback(refreshCache.bind(null, cache), [cache]); +} + +function refreshCache(cache: Cache | null) { + if (cache !== null) { + const providers = cache.providers; + if (providers !== null) { + providers.forEach(scheduleCacheRefresh); + } + } else { + // TODO: Warn if cache is null? + } +} + +function scheduleCacheRefresh(cacheComponentFiber: Fiber) { + // Inlined startTransition + // TODO: Maybe we shouldn't automatically give this transition priority. Are + // there valid use cases for a high-pri refresh? Like if the content is + // super stale and you want to immediately hide it. + const prevTransition = ReactCurrentBatchConfig.transition; + ReactCurrentBatchConfig.transition = 1; + try { + const eventTime = requestEventTime(); + const lane = requestUpdateLane(cacheComponentFiber); + scheduleUpdateOnFiber(cacheComponentFiber, lane, eventTime); + } finally { + ReactCurrentBatchConfig.transition = prevTransition; + } +} + function dispatchAction( fiber: Fiber, queue: UpdateQueue, @@ -1889,7 +1927,7 @@ function dispatchAction( } function getCacheForType(resourceType: () => T): T { - const cache = readContext(CacheContext); + const cache: Cache | null = readContext(CacheContext); invariant( cache !== null, 'Tried to fetch data, but no cache was found. To fix, wrap your ' + @@ -1937,6 +1975,7 @@ export const ContextOnlyDispatcher: Dispatcher = { }; if (enableCache) { (ContextOnlyDispatcher: Dispatcher).getCacheForType = getCacheForType; + (ContextOnlyDispatcher: Dispatcher).useRefresh = throwInvalidHookError; } const HooksDispatcherOnMount: Dispatcher = { @@ -1961,6 +2000,7 @@ const HooksDispatcherOnMount: Dispatcher = { }; if (enableCache) { (HooksDispatcherOnMount: Dispatcher).getCacheForType = getCacheForType; + (HooksDispatcherOnMount: Dispatcher).useRefresh = mountRefresh; } const HooksDispatcherOnUpdate: Dispatcher = { @@ -1985,6 +2025,7 @@ const HooksDispatcherOnUpdate: Dispatcher = { }; if (enableCache) { (HooksDispatcherOnUpdate: Dispatcher).getCacheForType = getCacheForType; + (HooksDispatcherOnUpdate: Dispatcher).useRefresh = updateRefresh; } const HooksDispatcherOnRerender: Dispatcher = { @@ -2009,6 +2050,7 @@ const HooksDispatcherOnRerender: Dispatcher = { }; if (enableCache) { (HooksDispatcherOnRerender: Dispatcher).getCacheForType = getCacheForType; + (HooksDispatcherOnRerender: Dispatcher).useRefresh = updateRefresh; } let HooksDispatcherOnMountInDEV: Dispatcher | null = null; @@ -2166,6 +2208,11 @@ if (__DEV__) { }; if (enableCache) { (HooksDispatcherOnMountInDEV: Dispatcher).getCacheForType = getCacheForType; + (HooksDispatcherOnMountInDEV: Dispatcher).useRefresh = function useRefresh() { + currentHookNameInDev = 'useRefresh'; + mountHookTypesDev(); + return mountRefresh(); + }; } HooksDispatcherOnMountWithHookTypesInDEV = { @@ -2291,6 +2338,11 @@ if (__DEV__) { }; if (enableCache) { (HooksDispatcherOnMountWithHookTypesInDEV: Dispatcher).getCacheForType = getCacheForType; + (HooksDispatcherOnMountWithHookTypesInDEV: Dispatcher).useRefresh = function useRefresh() { + currentHookNameInDev = 'useRefresh'; + updateHookTypesDev(); + return mountRefresh(); + }; } HooksDispatcherOnUpdateInDEV = { @@ -2416,6 +2468,11 @@ if (__DEV__) { }; if (enableCache) { (HooksDispatcherOnUpdateInDEV: Dispatcher).getCacheForType = getCacheForType; + (HooksDispatcherOnUpdateInDEV: Dispatcher).useRefresh = function useRefresh() { + currentHookNameInDev = 'useRefresh'; + updateHookTypesDev(); + return updateRefresh(); + }; } HooksDispatcherOnRerenderInDEV = { @@ -2542,6 +2599,11 @@ if (__DEV__) { }; if (enableCache) { (HooksDispatcherOnRerenderInDEV: Dispatcher).getCacheForType = getCacheForType; + (HooksDispatcherOnRerenderInDEV: Dispatcher).useRefresh = function useRefresh() { + currentHookNameInDev = 'useRefresh'; + updateHookTypesDev(); + return updateRefresh(); + }; } InvalidNestedHooksDispatcherOnMountInDEV = { @@ -2682,6 +2744,11 @@ if (__DEV__) { }; if (enableCache) { (InvalidNestedHooksDispatcherOnMountInDEV: Dispatcher).getCacheForType = getCacheForType; + (InvalidNestedHooksDispatcherOnMountInDEV: Dispatcher).useRefresh = function useRefresh() { + currentHookNameInDev = 'useRefresh'; + updateHookTypesDev(); + return mountRefresh(); + }; } InvalidNestedHooksDispatcherOnUpdateInDEV = { @@ -2822,6 +2889,11 @@ if (__DEV__) { }; if (enableCache) { (InvalidNestedHooksDispatcherOnUpdateInDEV: Dispatcher).getCacheForType = getCacheForType; + (InvalidNestedHooksDispatcherOnUpdateInDEV: Dispatcher).useRefresh = function useRefresh() { + currentHookNameInDev = 'useRefresh'; + updateHookTypesDev(); + return updateRefresh(); + }; } InvalidNestedHooksDispatcherOnRerenderInDEV = { @@ -2963,5 +3035,10 @@ if (__DEV__) { }; if (enableCache) { (InvalidNestedHooksDispatcherOnRerenderInDEV: Dispatcher).getCacheForType = getCacheForType; + (InvalidNestedHooksDispatcherOnRerenderInDEV: Dispatcher).useRefresh = function useRefresh() { + currentHookNameInDev = 'useRefresh'; + updateHookTypesDev(); + return updateRefresh(); + }; } } diff --git a/packages/react-reconciler/src/ReactFiberLane.new.js b/packages/react-reconciler/src/ReactFiberLane.new.js index 19f156fa50d3b..02c1bf0f55448 100644 --- a/packages/react-reconciler/src/ReactFiberLane.new.js +++ b/packages/react-reconciler/src/ReactFiberLane.new.js @@ -885,19 +885,21 @@ export function getWorkInProgressCache( root: FiberRoot, renderLanes: Lanes, ): Cache | null { - // TODO: There should be a primary render lane, and we should use whatever - // cache is associated with that one. - const caches = root.caches; - if (caches !== null) { - let lanes = renderLanes; - while (lanes > 0) { - const lane = getHighestPriorityLanes(lanes); - const index = laneToIndex(lane); - const inProgressCache: Cache | null = caches[index]; - if (inProgressCache !== null) { - return inProgressCache; + if (enableCache) { + // TODO: There should be a primary render lane, and we should use whatever + // cache is associated with that one. + const caches = root.caches; + if (caches !== null) { + let lanes = renderLanes; + while (lanes > 0) { + const lane = getHighestPriorityLanes(lanes); + const index = laneToIndex(lane); + const inProgressCache: Cache | null = caches[index]; + if (inProgressCache !== null) { + return inProgressCache; + } + lanes &= ~lane; } - lanes &= ~lane; } } return null; diff --git a/packages/react-reconciler/src/ReactFiberLane.old.js b/packages/react-reconciler/src/ReactFiberLane.old.js index 020d71696170a..82b1056259b47 100644 --- a/packages/react-reconciler/src/ReactFiberLane.old.js +++ b/packages/react-reconciler/src/ReactFiberLane.old.js @@ -885,19 +885,21 @@ export function getWorkInProgressCache( root: FiberRoot, renderLanes: Lanes, ): Cache | null { - // TODO: There should be a primary render lane, and we should use whatever - // cache is associated with that one. - const caches = root.caches; - if (caches !== null) { - let lanes = renderLanes; - while (lanes > 0) { - const lane = getHighestPriorityLanes(lanes); - const index = laneToIndex(lane); - const inProgressCache: Cache | null = caches[index]; - if (inProgressCache !== null) { - return inProgressCache; + if (enableCache) { + // TODO: There should be a primary render lane, and we should use whatever + // cache is associated with that one. + const caches = root.caches; + if (caches !== null) { + let lanes = renderLanes; + while (lanes > 0) { + const lane = getHighestPriorityLanes(lanes); + const index = laneToIndex(lane); + const inProgressCache: Cache | null = caches[index]; + if (inProgressCache !== null) { + return inProgressCache; + } + lanes &= ~lane; } - lanes &= ~lane; } } return null; diff --git a/packages/react-reconciler/src/ReactFiberNewContext.new.js b/packages/react-reconciler/src/ReactFiberNewContext.new.js index 584e2ff43b5cc..7bb83518eb8fd 100644 --- a/packages/react-reconciler/src/ReactFiberNewContext.new.js +++ b/packages/react-reconciler/src/ReactFiberNewContext.new.js @@ -19,6 +19,7 @@ import { ContextProvider, ClassComponent, DehydratedFragment, + CacheComponent, } from './ReactWorkTags'; import { NoLanes, @@ -33,7 +34,11 @@ import invariant from 'shared/invariant'; import is from 'shared/objectIs'; import {createUpdate, enqueueUpdate, ForceUpdate} from './ReactUpdateQueue.new'; import {markWorkInProgressReceivedUpdate} from './ReactFiberBeginWork.new'; -import {enableSuspenseServerRenderer} from 'shared/ReactFeatureFlags'; +import {CacheContext} from './ReactFiberCacheComponent'; +import { + enableSuspenseServerRenderer, + enableCache, +} from 'shared/ReactFeatureFlags'; const valueCursor: StackCursor = createCursor(null); @@ -296,6 +301,114 @@ export function propagateContextChange( } } +export function propagateCacheRefresh( + workInProgress: Fiber, + renderLanes: Lanes, +): void { + if (!enableCache) { + return; + } + + let fiber = workInProgress.child; + if (fiber !== null) { + // Set the return pointer of the child to the work-in-progress fiber. + fiber.return = workInProgress; + } + while (fiber !== null) { + let nextFiber; + + // Visit this fiber. + const list = fiber.dependencies; + if (list !== null) { + nextFiber = fiber.child; + + let dependency = list.firstContext; + while (dependency !== null) { + // Check if the context matches. + if (dependency.context === CacheContext) { + // Match! Schedule an update on this fiber. + + if (fiber.tag === ClassComponent) { + // Schedule a force update on the work-in-progress. + const update = createUpdate( + NoTimestamp, + pickArbitraryLane(renderLanes), + ); + update.tag = ForceUpdate; + // TODO: Because we don't have a work-in-progress, this will add the + // update to the current fiber, too, which means it will persist even if + // this render is thrown away. Since it's a race condition, not sure it's + // worth fixing. + enqueueUpdate(fiber, update); + } + fiber.lanes = mergeLanes(fiber.lanes, renderLanes); + const alternate = fiber.alternate; + if (alternate !== null) { + alternate.lanes = mergeLanes(alternate.lanes, renderLanes); + } + scheduleWorkOnParentPath(fiber.return, renderLanes); + + // Mark the updated lanes on the list, too. + list.lanes = mergeLanes(list.lanes, renderLanes); + + // Since we already found a match, we can stop traversing the + // dependency list. + break; + } + dependency = dependency.next; + } + } else if (fiber.tag === CacheComponent) { + const nestedCache = fiber.memoizedState; + if (nestedCache !== null) { + // Found a nested cache boundary with its own cache. The parent refresh + // should override it. Mark it with an update. + fiber.lanes = mergeLanes(fiber.lanes, renderLanes); + const alternate = fiber.alternate; + if (alternate !== null) { + alternate.lanes = mergeLanes(alternate.lanes, renderLanes); + } + scheduleWorkOnParentPath(fiber.return, renderLanes); + } + + // Unlike propagateContextChange, we don't stop traversing when we reach a + // nested cache boundary; refreshes propagate through the entire subtree. + // The refreshed cache will override nested caches. + // + // We also don't need to do anything special with DehydratedFragments, + // since the Fast Boot renderer is not allowed to fetch data. + nextFiber = fiber.child; + } else { + // Traverse down. + nextFiber = fiber.child; + } + + if (nextFiber !== null) { + // Set the return pointer of the child to the work-in-progress fiber. + nextFiber.return = fiber; + } else { + // No child. Traverse to next sibling. + nextFiber = fiber; + while (nextFiber !== null) { + if (nextFiber === workInProgress) { + // We're back to the root of this subtree. Exit. + nextFiber = null; + break; + } + const sibling = nextFiber.sibling; + if (sibling !== null) { + // Set the return pointer of the sibling to the work-in-progress fiber. + sibling.return = nextFiber.return; + nextFiber = sibling; + break; + } + // No more siblings. Traverse up. + nextFiber = nextFiber.return; + } + } + fiber = nextFiber; + } +} + export function prepareToReadContext( workInProgress: Fiber, renderLanes: Lanes, diff --git a/packages/react-reconciler/src/ReactFiberNewContext.old.js b/packages/react-reconciler/src/ReactFiberNewContext.old.js index da4859f0be800..6901a1e28ae42 100644 --- a/packages/react-reconciler/src/ReactFiberNewContext.old.js +++ b/packages/react-reconciler/src/ReactFiberNewContext.old.js @@ -19,6 +19,7 @@ import { ContextProvider, ClassComponent, DehydratedFragment, + CacheComponent, } from './ReactWorkTags'; import { NoLanes, @@ -33,7 +34,11 @@ import invariant from 'shared/invariant'; import is from 'shared/objectIs'; import {createUpdate, enqueueUpdate, ForceUpdate} from './ReactUpdateQueue.old'; import {markWorkInProgressReceivedUpdate} from './ReactFiberBeginWork.old'; -import {enableSuspenseServerRenderer} from 'shared/ReactFeatureFlags'; +import {CacheContext} from './ReactFiberCacheComponent'; +import { + enableSuspenseServerRenderer, + enableCache, +} from 'shared/ReactFeatureFlags'; const valueCursor: StackCursor = createCursor(null); @@ -296,6 +301,114 @@ export function propagateContextChange( } } +export function propagateCacheRefresh( + workInProgress: Fiber, + renderLanes: Lanes, +): void { + if (!enableCache) { + return; + } + + let fiber = workInProgress.child; + if (fiber !== null) { + // Set the return pointer of the child to the work-in-progress fiber. + fiber.return = workInProgress; + } + while (fiber !== null) { + let nextFiber; + + // Visit this fiber. + const list = fiber.dependencies; + if (list !== null) { + nextFiber = fiber.child; + + let dependency = list.firstContext; + while (dependency !== null) { + // Check if the context matches. + if (dependency.context === CacheContext) { + // Match! Schedule an update on this fiber. + + if (fiber.tag === ClassComponent) { + // Schedule a force update on the work-in-progress. + const update = createUpdate( + NoTimestamp, + pickArbitraryLane(renderLanes), + ); + update.tag = ForceUpdate; + // TODO: Because we don't have a work-in-progress, this will add the + // update to the current fiber, too, which means it will persist even if + // this render is thrown away. Since it's a race condition, not sure it's + // worth fixing. + enqueueUpdate(fiber, update); + } + fiber.lanes = mergeLanes(fiber.lanes, renderLanes); + const alternate = fiber.alternate; + if (alternate !== null) { + alternate.lanes = mergeLanes(alternate.lanes, renderLanes); + } + scheduleWorkOnParentPath(fiber.return, renderLanes); + + // Mark the updated lanes on the list, too. + list.lanes = mergeLanes(list.lanes, renderLanes); + + // Since we already found a match, we can stop traversing the + // dependency list. + break; + } + dependency = dependency.next; + } + } else if (fiber.tag === CacheComponent) { + const nestedCache = fiber.memoizedState; + if (nestedCache !== null) { + // Found a nested cache boundary with its own cache. The parent refresh + // should override it. Mark it with an update. + fiber.lanes = mergeLanes(fiber.lanes, renderLanes); + const alternate = fiber.alternate; + if (alternate !== null) { + alternate.lanes = mergeLanes(alternate.lanes, renderLanes); + } + scheduleWorkOnParentPath(fiber.return, renderLanes); + } + + // Unlike propagateContextChange, we don't stop traversing when we reach a + // nested cache boundary; refreshes propagate through the entire subtree. + // The refreshed cache will override nested caches. + // + // We also don't need to do anything special with DehydratedFragments, + // since the Fast Boot renderer is not allowed to fetch data. + nextFiber = fiber.child; + } else { + // Traverse down. + nextFiber = fiber.child; + } + + if (nextFiber !== null) { + // Set the return pointer of the child to the work-in-progress fiber. + nextFiber.return = fiber; + } else { + // No child. Traverse to next sibling. + nextFiber = fiber; + while (nextFiber !== null) { + if (nextFiber === workInProgress) { + // We're back to the root of this subtree. Exit. + nextFiber = null; + break; + } + const sibling = nextFiber.sibling; + if (sibling !== null) { + // Set the return pointer of the sibling to the work-in-progress fiber. + sibling.return = nextFiber.return; + nextFiber = sibling; + break; + } + // No more siblings. Traverse up. + nextFiber = nextFiber.return; + } + } + fiber = nextFiber; + } +} + export function prepareToReadContext( workInProgress: Fiber, renderLanes: Lanes, diff --git a/packages/react-reconciler/src/ReactFiberUnwindWork.new.js b/packages/react-reconciler/src/ReactFiberUnwindWork.new.js index a394e385e1af9..eb480177eb570 100644 --- a/packages/react-reconciler/src/ReactFiberUnwindWork.new.js +++ b/packages/react-reconciler/src/ReactFiberUnwindWork.new.js @@ -133,7 +133,10 @@ function unwindWork(workInProgress: Fiber, renderLanes: Lanes) { return null; case CacheComponent: if (enableCache) { - popProvider(CacheContext, workInProgress); + const ownCache: Cache | null = workInProgress.memoizedState; + if (ownCache !== null) { + popProvider(CacheContext, workInProgress); + } } return null; default: @@ -179,7 +182,10 @@ function unwindInterruptedWork(interruptedWork: Fiber) { break; case CacheComponent: if (enableCache) { - popProvider(CacheContext, interruptedWork); + const ownCache: Cache | null = interruptedWork.memoizedState; + if (ownCache !== null) { + popProvider(CacheContext, interruptedWork); + } } break; default: diff --git a/packages/react-reconciler/src/ReactFiberUnwindWork.old.js b/packages/react-reconciler/src/ReactFiberUnwindWork.old.js index 11ec5fb1dc720..6827ac7ccf0ac 100644 --- a/packages/react-reconciler/src/ReactFiberUnwindWork.old.js +++ b/packages/react-reconciler/src/ReactFiberUnwindWork.old.js @@ -133,7 +133,10 @@ function unwindWork(workInProgress: Fiber, renderLanes: Lanes) { return null; case CacheComponent: if (enableCache) { - popProvider(CacheContext, workInProgress); + const ownCache: Cache | null = workInProgress.memoizedState; + if (ownCache !== null) { + popProvider(CacheContext, workInProgress); + } } return null; default: @@ -179,7 +182,10 @@ function unwindInterruptedWork(interruptedWork: Fiber) { break; case CacheComponent: if (enableCache) { - popProvider(CacheContext, interruptedWork); + const ownCache: Cache | null = interruptedWork.memoizedState; + if (ownCache !== null) { + popProvider(CacheContext, interruptedWork); + } } break; default: diff --git a/packages/react-reconciler/src/ReactInternalTypes.js b/packages/react-reconciler/src/ReactInternalTypes.js index b6651e92671ae..d73e9e5e74b1f 100644 --- a/packages/react-reconciler/src/ReactInternalTypes.js +++ b/packages/react-reconciler/src/ReactInternalTypes.js @@ -42,7 +42,8 @@ export type HookType = | 'useDeferredValue' | 'useTransition' | 'useMutableSource' - | 'useOpaqueIdentifier'; + | 'useOpaqueIdentifier' + | 'useRefresh'; export type ReactPriorityLevel = 99 | 98 | 97 | 96 | 95 | 90; @@ -318,6 +319,7 @@ export type Dispatcher = {| subscribe: MutableSourceSubscribeFn, ): Snapshot, useOpaqueIdentifier(): any, + useRefresh?: () => () => void, unstable_isNewReconciler?: boolean, |}; diff --git a/packages/react-reconciler/src/__tests__/ReactCache-test.js b/packages/react-reconciler/src/__tests__/ReactCache-test.js index c0cad03c04c3e..b101cff3702c6 100644 --- a/packages/react-reconciler/src/__tests__/ReactCache-test.js +++ b/packages/react-reconciler/src/__tests__/ReactCache-test.js @@ -4,6 +4,8 @@ let Cache; let getCacheForType; let Scheduler; let Suspense; +let useRefresh; + let textService; let textServiceVersion; @@ -17,6 +19,7 @@ describe('ReactCache', () => { Scheduler = require('scheduler'); Suspense = React.Suspense; getCacheForType = React.unstable_getCacheForType; + useRefresh = React.unstable_useRefresh; // Represents some data service that returns text. It likely has additional // caching layers, like a CDN or the local browser cache. It can be mutated @@ -356,4 +359,191 @@ describe('ReactCache', () => { , ); }); + + // @gate experimental + test('refresh a cache', async () => { + let refresh; + function App() { + refresh = useRefresh(); + return ; + } + + // Mount initial data + const root = ReactNoop.createRoot(); + await ReactNoop.act(async () => { + root.render( + + }> + + + , + ); + }); + expect(Scheduler).toHaveYielded(['Cache miss! [A]', 'Loading...']); + expect(root).toMatchRenderedOutput('Loading...'); + + await ReactNoop.act(async () => { + await resolveText('A'); + }); + expect(Scheduler).toHaveYielded(['A [v1]']); + expect(root).toMatchRenderedOutput('A [v1]'); + + // Mutate the text service, then refresh for new data. + mutateRemoteTextService(); + await ReactNoop.act(async () => { + await refresh(); + }); + expect(Scheduler).toHaveYielded(['Cache miss! [A]', 'Loading...']); + expect(root).toMatchRenderedOutput('A [v1]'); + + await ReactNoop.act(async () => { + await resolveText('A'); + }); + // Note that the version has updated + expect(Scheduler).toHaveYielded(['A [v2]']); + expect(root).toMatchRenderedOutput('A [v2]'); + }); + + // @gate experimental + test('refreshing a parent cache also refreshes its children', async () => { + let refreshShell; + function RefreshShell() { + refreshShell = useRefresh(); + return null; + } + + function App({showMore}) { + return ( + + + }> + + + {showMore ? ( + + }> + + + + ) : null} + + ); + } + + const root = ReactNoop.createRoot(); + await ReactNoop.act(async () => { + await resolveText('A'); + root.render(); + }); + expect(Scheduler).toHaveYielded([ + 'Cache miss! [A]', + 'Loading...', + 'A [v1]', + ]); + expect(root).toMatchRenderedOutput('A [v1]'); + + // Simulate a server mutation. + mutateRemoteTextService(); + + // Add a new cache boundary + await ReactNoop.act(async () => { + await resolveText('A'); + root.render(); + }); + expect(Scheduler).toHaveYielded([ + 'A [v1]', + // New tree should load fresh data. + 'Cache miss! [A]', + 'Loading...', + 'A [v2]', + ]); + expect(root).toMatchRenderedOutput('A [v1]A [v2]'); + + // Now refresh the shell. This should also cause the "Show More" contents to + // refresh, since its cache is nested inside the outer one. + mutateRemoteTextService(); + await ReactNoop.act(async () => { + refreshShell(); + }); + expect(Scheduler).toHaveYielded([ + 'Cache miss! [A]', + 'Loading...', + 'Loading...', + ]); + expect(root).toMatchRenderedOutput('A [v1]A [v2]'); + + await ReactNoop.act(async () => { + await resolveText('A'); + }); + expect(Scheduler).toHaveYielded(['A [v3]', 'A [v3]']); + expect(root).toMatchRenderedOutput('A [v3]A [v3]'); + }); + + // @gate experimental + test( + 'refreshing a cache boundary also refreshes the other boundaries ' + + 'that mounted at the same time (i.e. the ones that share the same cache)', + async () => { + let refreshFirstBoundary; + function RefreshFirstBoundary() { + refreshFirstBoundary = useRefresh(); + return null; + } + + function App({text}) { + return ( + <> + + }> + + + + + + }> + + + + + ); + } + + const root = ReactNoop.createRoot(); + await ReactNoop.act(async () => { + root.render(); + }); + // Even though there are two new trees, they should share the same + // data cache. So there should be only a single cache miss for A. + expect(Scheduler).toHaveYielded([ + 'Cache miss! [A]', + 'Loading...', + 'Loading...', + ]); + expect(root).toMatchRenderedOutput('Loading...Loading...'); + + await ReactNoop.act(async () => { + await resolveText('A'); + }); + expect(Scheduler).toHaveYielded(['A [v1]', 'A [v1]']); + expect(root).toMatchRenderedOutput('A [v1]A [v1]'); + + // Refresh the first boundary. It should also refresh the second boundary, + // since they appeared at the same time. + mutateRemoteTextService(); + await ReactNoop.act(async () => { + await refreshFirstBoundary(); + }); + expect(Scheduler).toHaveYielded([ + 'Cache miss! [A]', + 'Loading...', + 'Loading...', + ]); + + await ReactNoop.act(async () => { + await resolveText('A'); + }); + expect(Scheduler).toHaveYielded(['A [v2]', 'A [v2]']); + expect(root).toMatchRenderedOutput('A [v2]A [v2]'); + }, + ); }); diff --git a/packages/react-server/src/ReactFlightServer.js b/packages/react-server/src/ReactFlightServer.js index d6717a0830760..0f8170852f035 100644 --- a/packages/react-server/src/ReactFlightServer.js +++ b/packages/react-server/src/ReactFlightServer.js @@ -758,6 +758,13 @@ function unsupportedHook(): void { invariant(false, 'This Hook is not supported in Server Components.'); } +function unsupportedRefresh(): void { + invariant( + currentCache, + 'Refreshing the cache is not supported in Server Components.', + ); +} + let currentCache: Map | null = null; const Dispatcher: DispatcherType = { @@ -797,4 +804,7 @@ const Dispatcher: DispatcherType = { useEffect: (unsupportedHook: any), useOpaqueIdentifier: (unsupportedHook: any), useMutableSource: (unsupportedHook: any), + useRefresh(): () => void { + return unsupportedRefresh; + }, }; diff --git a/packages/react-suspense-test-utils/src/ReactSuspenseTestUtils.js b/packages/react-suspense-test-utils/src/ReactSuspenseTestUtils.js index a58c6cae824c0..37ecb3a1c3c6a 100644 --- a/packages/react-suspense-test-utils/src/ReactSuspenseTestUtils.js +++ b/packages/react-suspense-test-utils/src/ReactSuspenseTestUtils.js @@ -44,6 +44,7 @@ export function waitForSuspense(fn: () => T): Promise { useTransition: unsupported, useOpaqueIdentifier: unsupported, useMutableSource: unsupported, + useRefresh: unsupported, }; // Not using async/await because we don't compile it. return new Promise((resolve, reject) => { diff --git a/packages/react/index.classic.fb.js b/packages/react/index.classic.fb.js index 4beccf1dfbca0..79bb0696ec1e0 100644 --- a/packages/react/index.classic.fb.js +++ b/packages/react/index.classic.fb.js @@ -52,6 +52,7 @@ export { SuspenseList as unstable_SuspenseList, unstable_getCacheForType, unstable_Cache, + unstable_useRefresh, // enableScopeAPI unstable_Scope, unstable_useOpaqueIdentifier, diff --git a/packages/react/index.experimental.js b/packages/react/index.experimental.js index 8c4d97eb8e746..43c3f08eb000d 100644 --- a/packages/react/index.experimental.js +++ b/packages/react/index.experimental.js @@ -47,6 +47,7 @@ export { unstable_useOpaqueIdentifier, unstable_getCacheForType, unstable_Cache, + unstable_useRefresh, // enableDebugTracing unstable_DebugTracingMode, } from './src/React'; diff --git a/packages/react/index.js b/packages/react/index.js index 7f0173bd69e1c..fb074650a0852 100644 --- a/packages/react/index.js +++ b/packages/react/index.js @@ -84,4 +84,5 @@ export { unstable_useOpaqueIdentifier, unstable_getCacheForType, unstable_Cache, + unstable_useRefresh, } from './src/React'; diff --git a/packages/react/index.modern.fb.js b/packages/react/index.modern.fb.js index 982388dd2d645..1ab2bdd13dbe0 100644 --- a/packages/react/index.modern.fb.js +++ b/packages/react/index.modern.fb.js @@ -51,6 +51,7 @@ export { SuspenseList as unstable_SuspenseList, unstable_getCacheForType, unstable_Cache, + unstable_useRefresh, // enableScopeAPI unstable_Scope, unstable_useOpaqueIdentifier, diff --git a/packages/react/src/React.js b/packages/react/src/React.js index 3382743b8ccd0..9567ff7dc3b22 100644 --- a/packages/react/src/React.js +++ b/packages/react/src/React.js @@ -49,6 +49,7 @@ import { useTransition, useDeferredValue, useOpaqueIdentifier, + useRefresh, } from './ReactHooks'; import { createElementWithValidation, @@ -113,6 +114,7 @@ export { REACT_SUSPENSE_LIST_TYPE as SuspenseList, REACT_LEGACY_HIDDEN_TYPE as unstable_LegacyHidden, getCacheForType as unstable_getCacheForType, + useRefresh as unstable_useRefresh, REACT_CACHE_TYPE as unstable_Cache, // enableFundamentalAPI createFundamental as unstable_createFundamental, diff --git a/packages/react/src/ReactHooks.js b/packages/react/src/ReactHooks.js index 1020efa74cb96..0f34b6405a29b 100644 --- a/packages/react/src/ReactHooks.js +++ b/packages/react/src/ReactHooks.js @@ -180,3 +180,9 @@ export function useMutableSource( const dispatcher = resolveDispatcher(); return dispatcher.useMutableSource(source, getSnapshot, subscribe); } + +export function useRefresh(): () => void { + const dispatcher = resolveDispatcher(); + // $FlowFixMe This is unstable, thus optional + return dispatcher.useRefresh(); +} diff --git a/packages/react/unstable-shared-subset.experimental.js b/packages/react/unstable-shared-subset.experimental.js index 890066957e383..bd2e1cd77c25d 100644 --- a/packages/react/unstable-shared-subset.experimental.js +++ b/packages/react/unstable-shared-subset.experimental.js @@ -33,6 +33,7 @@ export { SuspenseList as unstable_SuspenseList, unstable_useOpaqueIdentifier, unstable_getCacheForType, + unstable_useRefresh, // enableDebugTracing unstable_DebugTracingMode, } from './src/React'; diff --git a/scripts/error-codes/codes.json b/scripts/error-codes/codes.json index 6941f92df2770..028fca5bdc513 100644 --- a/scripts/error-codes/codes.json +++ b/scripts/error-codes/codes.json @@ -372,5 +372,6 @@ "381": "This feature is not supported by ReactSuspenseTestUtils.", "382": "This query has received more parameters than the last time the same query was used. Always pass the exact number of parameters that the query needs.", "383": "This query has received fewer parameters than the last time the same query was used. Always pass the exact number of parameters that the query needs.", - "384": "Tried to fetch data, but no cache was found. To fix, wrap your component in a boundary. It doesn't need to be a direct parent; it can be anywhere in the ancestor path" + "384": "Tried to fetch data, but no cache was found. To fix, wrap your component in a boundary. It doesn't need to be a direct parent; it can be anywhere in the ancestor path", + "385": "Refreshing the cache is not supported in Server Components." }