Skip to content

Commit 0581bdf

Browse files
committed
Refresh with seeded data
Usually, when performing a server mutation, the response includes an updated version of the mutated data. This avoids an extra roundtrip, and because of eventual consistency, it also guarantees that we reload with the freshest possible data. If we didn't seed with the mutation response, and instead refetched with a separate GET request, we might receive stale data as the mutation propagates through the data layer. Not all refreshes are the result of a mutation, though, so the seed is not required.
1 parent ea5478c commit 0581bdf

File tree

9 files changed

+103
-15
lines changed

9 files changed

+103
-15
lines changed

packages/react-dom/src/server/ReactPartialRendererHooks.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -489,7 +489,7 @@ function useOpaqueIdentifier(): OpaqueIDType {
489489
);
490490
}
491491

492-
function useRefresh(): () => void {
492+
function useRefresh(): <T>(?() => T, ?T) => void {
493493
invariant(false, 'Not implemented.');
494494
}
495495

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

+23-4
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ import {
4545
setCurrentUpdateLanePriority,
4646
higherLanePriority,
4747
DefaultLanePriority,
48+
transferCacheToSpawnedLane,
4849
} from './ReactFiberLane.new';
4950
import {readContext} from './ReactFiberNewContext.new';
5051
import {
@@ -1718,28 +1719,46 @@ function updateRefresh() {
17181719
return updateCallback(refreshCache.bind(null, cache), [cache]);
17191720
}
17201721

1721-
function refreshCache(cache: Cache | null) {
1722+
function refreshCache<T>(cache: Cache | null, seedKey: ?() => T, seedValue: T) {
17221723
if (cache !== null) {
17231724
const providers = cache.providers;
17241725
if (providers !== null) {
1725-
providers.forEach(scheduleCacheRefresh);
1726+
let seededCache = null;
1727+
if (seedKey !== null && seedKey !== undefined) {
1728+
// TODO: Warn if wrong type
1729+
seededCache = {
1730+
providers: null,
1731+
data: new Map([[seedKey, seedValue]]),
1732+
};
1733+
}
1734+
providers.forEach(provider =>
1735+
scheduleCacheRefresh(provider, seededCache),
1736+
);
17261737
}
17271738
} else {
17281739
// TODO: Warn if cache is null?
17291740
}
17301741
}
17311742

1732-
function scheduleCacheRefresh(cacheComponentFiber: Fiber) {
1743+
function scheduleCacheRefresh(
1744+
cacheComponentFiber: Fiber,
1745+
seededCache: Cache | null,
1746+
) {
17331747
// Inlined startTransition
17341748
// TODO: Maybe we shouldn't automatically give this transition priority. Are
17351749
// there valid use cases for a high-pri refresh? Like if the content is
17361750
// super stale and you want to immediately hide it.
17371751
const prevTransition = ReactCurrentBatchConfig.transition;
17381752
ReactCurrentBatchConfig.transition = 1;
1753+
// TODO: Do we really need the try/finally? I don't think any of these
1754+
// functions would ever throw unless there's an internal error.
17391755
try {
17401756
const eventTime = requestEventTime();
17411757
const lane = requestUpdateLane(cacheComponentFiber);
1742-
scheduleUpdateOnFiber(cacheComponentFiber, lane, eventTime);
1758+
const root = scheduleUpdateOnFiber(cacheComponentFiber, lane, eventTime);
1759+
if (seededCache !== null && root !== null) {
1760+
transferCacheToSpawnedLane(root, seededCache, lane);
1761+
}
17431762
} finally {
17441763
ReactCurrentBatchConfig.transition = prevTransition;
17451764
}

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

+23-4
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ import {
5151
setCurrentUpdateLanePriority,
5252
higherLanePriority,
5353
DefaultLanePriority,
54+
transferCacheToSpawnedLane,
5455
} from './ReactFiberLane.old';
5556
import {readContext} from './ReactFiberNewContext.old';
5657
import {
@@ -1789,28 +1790,46 @@ function updateRefresh() {
17891790
return updateCallback(refreshCache.bind(null, cache), [cache]);
17901791
}
17911792

1792-
function refreshCache(cache: Cache | null) {
1793+
function refreshCache<T>(cache: Cache | null, seedKey: ?() => T, seedValue: T) {
17931794
if (cache !== null) {
17941795
const providers = cache.providers;
17951796
if (providers !== null) {
1796-
providers.forEach(scheduleCacheRefresh);
1797+
let seededCache = null;
1798+
if (seedKey !== null && seedKey !== undefined) {
1799+
// TODO: Warn if wrong type
1800+
seededCache = {
1801+
providers: null,
1802+
data: new Map([[seedKey, seedValue]]),
1803+
};
1804+
}
1805+
providers.forEach(provider =>
1806+
scheduleCacheRefresh(provider, seededCache),
1807+
);
17971808
}
17981809
} else {
17991810
// TODO: Warn if cache is null?
18001811
}
18011812
}
18021813

1803-
function scheduleCacheRefresh(cacheComponentFiber: Fiber) {
1814+
function scheduleCacheRefresh(
1815+
cacheComponentFiber: Fiber,
1816+
seededCache: Cache | null,
1817+
) {
18041818
// Inlined startTransition
18051819
// TODO: Maybe we shouldn't automatically give this transition priority. Are
18061820
// there valid use cases for a high-pri refresh? Like if the content is
18071821
// super stale and you want to immediately hide it.
18081822
const prevTransition = ReactCurrentBatchConfig.transition;
18091823
ReactCurrentBatchConfig.transition = 1;
1824+
// TODO: Do we really need the try/finally? I don't think any of these
1825+
// functions would ever throw unless there's an internal error.
18101826
try {
18111827
const eventTime = requestEventTime();
18121828
const lane = requestUpdateLane(cacheComponentFiber);
1813-
scheduleUpdateOnFiber(cacheComponentFiber, lane, eventTime);
1829+
const root = scheduleUpdateOnFiber(cacheComponentFiber, lane, eventTime);
1830+
if (seededCache !== null && root !== null) {
1831+
transferCacheToSpawnedLane(root, seededCache, lane);
1832+
}
18141833
} finally {
18151834
ReactCurrentBatchConfig.transition = prevTransition;
18161835
}

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

+3-1
Original file line numberDiff line numberDiff line change
@@ -527,7 +527,7 @@ export function scheduleUpdateOnFiber(
527527
fiber: Fiber,
528528
lane: Lane,
529529
eventTime: number,
530-
) {
530+
): FiberRoot | null {
531531
checkForNestedUpdates();
532532
warnAboutRenderPhaseUpdatesInDEV(fiber);
533533

@@ -649,6 +649,8 @@ export function scheduleUpdateOnFiber(
649649
// the same root, then it's not a huge deal, we just might batch more stuff
650650
// together more than necessary.
651651
mostRecentlyUpdatedRoot = root;
652+
653+
return root;
652654
}
653655

654656
// This is split into a separate function so we can mark a fiber with pending

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

+3-1
Original file line numberDiff line numberDiff line change
@@ -535,7 +535,7 @@ export function scheduleUpdateOnFiber(
535535
fiber: Fiber,
536536
lane: Lane,
537537
eventTime: number,
538-
) {
538+
): FiberRoot | null {
539539
checkForNestedUpdates();
540540
warnAboutRenderPhaseUpdatesInDEV(fiber);
541541

@@ -657,6 +657,8 @@ export function scheduleUpdateOnFiber(
657657
// the same root, then it's not a huge deal, we just might batch more stuff
658658
// together more than necessary.
659659
mostRecentlyUpdatedRoot = root;
660+
661+
return root;
660662
}
661663

662664
// This is split into a separate function so we can mark a fiber with pending

packages/react-reconciler/src/ReactInternalTypes.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -319,7 +319,7 @@ export type Dispatcher = {|
319319
subscribe: MutableSourceSubscribeFn<Source, Snapshot>,
320320
): Snapshot,
321321
useOpaqueIdentifier(): any,
322-
useRefresh?: () => () => void,
322+
useRefresh?: () => <T>(?() => T, ?T) => void,
323323

324324
unstable_isNewReconciler?: boolean,
325325
|};

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

+47-1
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ describe('ReactCache', () => {
3838
if (record !== undefined) {
3939
switch (record.status) {
4040
case 'pending':
41-
throw record.thenable;
41+
throw record.value;
4242
case 'rejected':
4343
throw record.value;
4444
case 'resolved':
@@ -404,6 +404,52 @@ describe('ReactCache', () => {
404404
expect(root).toMatchRenderedOutput('A [v2]');
405405
});
406406

407+
// @gate experimental
408+
test('refresh a cache with seed data', async () => {
409+
let refresh;
410+
function App() {
411+
refresh = useRefresh();
412+
return <AsyncText showVersion={true} text="A" />;
413+
}
414+
415+
// Mount initial data
416+
const root = ReactNoop.createRoot();
417+
await ReactNoop.act(async () => {
418+
root.render(
419+
<Cache>
420+
<Suspense fallback={<Text text="Loading..." />}>
421+
<App />
422+
</Suspense>
423+
</Cache>,
424+
);
425+
});
426+
expect(Scheduler).toHaveYielded(['Cache miss! [A]', 'Loading...']);
427+
expect(root).toMatchRenderedOutput('Loading...');
428+
429+
await ReactNoop.act(async () => {
430+
await resolveText('A');
431+
});
432+
expect(Scheduler).toHaveYielded(['A [v1]']);
433+
expect(root).toMatchRenderedOutput('A [v1]');
434+
435+
// Mutate the text service, then refresh for new data.
436+
mutateRemoteTextService();
437+
await ReactNoop.act(async () => {
438+
// Refresh the cache with seeded data, like you would receive from a
439+
// server mutation.
440+
const seededCache = new Map();
441+
seededCache.set('A', {
442+
ping: null,
443+
status: 'resolved',
444+
value: textServiceVersion,
445+
});
446+
refresh(createTextCache, seededCache);
447+
});
448+
// The root should re-render without a cache miss.
449+
expect(Scheduler).toHaveYielded(['A [v2]']);
450+
expect(root).toMatchRenderedOutput('A [v2]');
451+
});
452+
407453
// @gate experimental
408454
test('refreshing a parent cache also refreshes its children', async () => {
409455
let refreshShell;

packages/react-server/src/ReactFlightServer.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -804,7 +804,7 @@ const Dispatcher: DispatcherType = {
804804
useEffect: (unsupportedHook: any),
805805
useOpaqueIdentifier: (unsupportedHook: any),
806806
useMutableSource: (unsupportedHook: any),
807-
useRefresh(): () => void {
807+
useRefresh(): <T>(?() => T, ?T) => void {
808808
return unsupportedRefresh;
809809
},
810810
};

packages/react/src/ReactHooks.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,7 @@ export function useMutableSource<Source, Snapshot>(
181181
return dispatcher.useMutableSource(source, getSnapshot, subscribe);
182182
}
183183

184-
export function useRefresh(): () => void {
184+
export function useRefresh(): <T>(?() => T, ?T) => void {
185185
const dispatcher = resolveDispatcher();
186186
// $FlowFixMe This is unstable, thus optional
187187
return dispatcher.useRefresh();

0 commit comments

Comments
 (0)