diff --git a/packages/react-dom/src/server/ReactPartialRendererHooks.js b/packages/react-dom/src/server/ReactPartialRendererHooks.js index 7759cc62c83bd..087e81b0e7683 100644 --- a/packages/react-dom/src/server/ReactPartialRendererHooks.js +++ b/packages/react-dom/src/server/ReactPartialRendererHooks.js @@ -489,7 +489,7 @@ function useOpaqueIdentifier(): OpaqueIDType { ); } -function useRefresh(): () => void { +function useRefresh(): (?() => T, ?T) => void { invariant(false, 'Not implemented.'); } diff --git a/packages/react-reconciler/src/ReactFiberHooks.new.js b/packages/react-reconciler/src/ReactFiberHooks.new.js index 25aa64015c473..c2c1098363301 100644 --- a/packages/react-reconciler/src/ReactFiberHooks.new.js +++ b/packages/react-reconciler/src/ReactFiberHooks.new.js @@ -45,6 +45,7 @@ import { setCurrentUpdateLanePriority, higherLanePriority, DefaultLanePriority, + transferCacheToSpawnedLane, } from './ReactFiberLane.new'; import {readContext} from './ReactFiberNewContext.new'; import { @@ -1718,28 +1719,46 @@ function updateRefresh() { return updateCallback(refreshCache.bind(null, cache), [cache]); } -function refreshCache(cache: Cache | null) { +function refreshCache(cache: Cache | null, seedKey: ?() => T, seedValue: T) { if (cache !== null) { const providers = cache.providers; if (providers !== null) { - providers.forEach(scheduleCacheRefresh); + let seededCache = null; + if (seedKey !== null && seedKey !== undefined) { + // TODO: Warn if wrong type + seededCache = { + providers: null, + data: new Map([[seedKey, seedValue]]), + }; + } + providers.forEach(provider => + scheduleCacheRefresh(provider, seededCache), + ); } } else { // TODO: Warn if cache is null? } } -function scheduleCacheRefresh(cacheComponentFiber: Fiber) { +function scheduleCacheRefresh( + cacheComponentFiber: Fiber, + seededCache: Cache | null, +) { // 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; + // TODO: Do we really need the try/finally? I don't think any of these + // functions would ever throw unless there's an internal error. try { const eventTime = requestEventTime(); const lane = requestUpdateLane(cacheComponentFiber); - scheduleUpdateOnFiber(cacheComponentFiber, lane, eventTime); + const root = scheduleUpdateOnFiber(cacheComponentFiber, lane, eventTime); + if (seededCache !== null && root !== null) { + transferCacheToSpawnedLane(root, seededCache, lane); + } } finally { ReactCurrentBatchConfig.transition = prevTransition; } diff --git a/packages/react-reconciler/src/ReactFiberHooks.old.js b/packages/react-reconciler/src/ReactFiberHooks.old.js index eecbffc1e7306..b962af97401ed 100644 --- a/packages/react-reconciler/src/ReactFiberHooks.old.js +++ b/packages/react-reconciler/src/ReactFiberHooks.old.js @@ -51,6 +51,7 @@ import { setCurrentUpdateLanePriority, higherLanePriority, DefaultLanePriority, + transferCacheToSpawnedLane, } from './ReactFiberLane.old'; import {readContext} from './ReactFiberNewContext.old'; import { @@ -1789,28 +1790,46 @@ function updateRefresh() { return updateCallback(refreshCache.bind(null, cache), [cache]); } -function refreshCache(cache: Cache | null) { +function refreshCache(cache: Cache | null, seedKey: ?() => T, seedValue: T) { if (cache !== null) { const providers = cache.providers; if (providers !== null) { - providers.forEach(scheduleCacheRefresh); + let seededCache = null; + if (seedKey !== null && seedKey !== undefined) { + // TODO: Warn if wrong type + seededCache = { + providers: null, + data: new Map([[seedKey, seedValue]]), + }; + } + providers.forEach(provider => + scheduleCacheRefresh(provider, seededCache), + ); } } else { // TODO: Warn if cache is null? } } -function scheduleCacheRefresh(cacheComponentFiber: Fiber) { +function scheduleCacheRefresh( + cacheComponentFiber: Fiber, + seededCache: Cache | null, +) { // 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; + // TODO: Do we really need the try/finally? I don't think any of these + // functions would ever throw unless there's an internal error. try { const eventTime = requestEventTime(); const lane = requestUpdateLane(cacheComponentFiber); - scheduleUpdateOnFiber(cacheComponentFiber, lane, eventTime); + const root = scheduleUpdateOnFiber(cacheComponentFiber, lane, eventTime); + if (seededCache !== null && root !== null) { + transferCacheToSpawnedLane(root, seededCache, lane); + } } finally { ReactCurrentBatchConfig.transition = prevTransition; } diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.new.js b/packages/react-reconciler/src/ReactFiberWorkLoop.new.js index 348c49bda97fb..8c4eae29ee2ae 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.new.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.new.js @@ -527,7 +527,7 @@ export function scheduleUpdateOnFiber( fiber: Fiber, lane: Lane, eventTime: number, -) { +): FiberRoot | null { checkForNestedUpdates(); warnAboutRenderPhaseUpdatesInDEV(fiber); @@ -649,6 +649,8 @@ export function scheduleUpdateOnFiber( // the same root, then it's not a huge deal, we just might batch more stuff // together more than necessary. mostRecentlyUpdatedRoot = root; + + return root; } // This is split into a separate function so we can mark a fiber with pending diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.old.js b/packages/react-reconciler/src/ReactFiberWorkLoop.old.js index 63e0771ed973c..5cc02e4c494ee 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.old.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.old.js @@ -535,7 +535,7 @@ export function scheduleUpdateOnFiber( fiber: Fiber, lane: Lane, eventTime: number, -) { +): FiberRoot | null { checkForNestedUpdates(); warnAboutRenderPhaseUpdatesInDEV(fiber); @@ -657,6 +657,8 @@ export function scheduleUpdateOnFiber( // the same root, then it's not a huge deal, we just might batch more stuff // together more than necessary. mostRecentlyUpdatedRoot = root; + + return root; } // This is split into a separate function so we can mark a fiber with pending diff --git a/packages/react-reconciler/src/ReactInternalTypes.js b/packages/react-reconciler/src/ReactInternalTypes.js index d73e9e5e74b1f..ff04c72d6c762 100644 --- a/packages/react-reconciler/src/ReactInternalTypes.js +++ b/packages/react-reconciler/src/ReactInternalTypes.js @@ -319,7 +319,7 @@ export type Dispatcher = {| subscribe: MutableSourceSubscribeFn, ): Snapshot, useOpaqueIdentifier(): any, - useRefresh?: () => () => void, + useRefresh?: () => (?() => T, ?T) => 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 338de647ce49a..63d283a6a9748 100644 --- a/packages/react-reconciler/src/__tests__/ReactCache-test.js +++ b/packages/react-reconciler/src/__tests__/ReactCache-test.js @@ -38,7 +38,7 @@ describe('ReactCache', () => { if (record !== undefined) { switch (record.status) { case 'pending': - throw record.thenable; + throw record.value; case 'rejected': throw record.value; case 'resolved': @@ -404,6 +404,52 @@ describe('ReactCache', () => { expect(root).toMatchRenderedOutput('A [v2]'); }); + // @gate experimental + test('refresh a cache with seed data', 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 () => { + // Refresh the cache with seeded data, like you would receive from a + // server mutation. + const seededCache = new Map(); + seededCache.set('A', { + ping: null, + status: 'resolved', + value: textServiceVersion, + }); + refresh(createTextCache, seededCache); + }); + // The root should re-render without a cache miss. + expect(Scheduler).toHaveYielded(['A [v2]']); + expect(root).toMatchRenderedOutput('A [v2]'); + }); + // @gate experimental test('refreshing a parent cache also refreshes its children', async () => { let refreshShell; diff --git a/packages/react-server/src/ReactFlightServer.js b/packages/react-server/src/ReactFlightServer.js index 0f8170852f035..acc446dd14df3 100644 --- a/packages/react-server/src/ReactFlightServer.js +++ b/packages/react-server/src/ReactFlightServer.js @@ -804,7 +804,7 @@ const Dispatcher: DispatcherType = { useEffect: (unsupportedHook: any), useOpaqueIdentifier: (unsupportedHook: any), useMutableSource: (unsupportedHook: any), - useRefresh(): () => void { + useRefresh(): (?() => T, ?T) => void { return unsupportedRefresh; }, }; diff --git a/packages/react/src/ReactHooks.js b/packages/react/src/ReactHooks.js index 0f34b6405a29b..9a087af4a1f1d 100644 --- a/packages/react/src/ReactHooks.js +++ b/packages/react/src/ReactHooks.js @@ -181,7 +181,7 @@ export function useMutableSource( return dispatcher.useMutableSource(source, getSnapshot, subscribe); } -export function useRefresh(): () => void { +export function useRefresh(): (?() => T, ?T) => void { const dispatcher = resolveDispatcher(); // $FlowFixMe This is unstable, thus optional return dispatcher.useRefresh();