Skip to content

Commit

Permalink
Refresh with seeded data
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
acdlite committed Dec 14, 2020
1 parent ea5478c commit 0581bdf
Show file tree
Hide file tree
Showing 9 changed files with 103 additions and 15 deletions.
2 changes: 1 addition & 1 deletion packages/react-dom/src/server/ReactPartialRendererHooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -489,7 +489,7 @@ function useOpaqueIdentifier(): OpaqueIDType {
);
}

function useRefresh(): () => void {
function useRefresh(): <T>(?() => T, ?T) => void {
invariant(false, 'Not implemented.');
}

Expand Down
27 changes: 23 additions & 4 deletions packages/react-reconciler/src/ReactFiberHooks.new.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import {
setCurrentUpdateLanePriority,
higherLanePriority,
DefaultLanePriority,
transferCacheToSpawnedLane,
} from './ReactFiberLane.new';
import {readContext} from './ReactFiberNewContext.new';
import {
Expand Down Expand Up @@ -1718,28 +1719,46 @@ function updateRefresh() {
return updateCallback(refreshCache.bind(null, cache), [cache]);
}

function refreshCache(cache: Cache | null) {
function refreshCache<T>(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;
}
Expand Down
27 changes: 23 additions & 4 deletions packages/react-reconciler/src/ReactFiberHooks.old.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ import {
setCurrentUpdateLanePriority,
higherLanePriority,
DefaultLanePriority,
transferCacheToSpawnedLane,
} from './ReactFiberLane.old';
import {readContext} from './ReactFiberNewContext.old';
import {
Expand Down Expand Up @@ -1789,28 +1790,46 @@ function updateRefresh() {
return updateCallback(refreshCache.bind(null, cache), [cache]);
}

function refreshCache(cache: Cache | null) {
function refreshCache<T>(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;
}
Expand Down
4 changes: 3 additions & 1 deletion packages/react-reconciler/src/ReactFiberWorkLoop.new.js
Original file line number Diff line number Diff line change
Expand Up @@ -527,7 +527,7 @@ export function scheduleUpdateOnFiber(
fiber: Fiber,
lane: Lane,
eventTime: number,
) {
): FiberRoot | null {
checkForNestedUpdates();
warnAboutRenderPhaseUpdatesInDEV(fiber);

Expand Down Expand Up @@ -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
Expand Down
4 changes: 3 additions & 1 deletion packages/react-reconciler/src/ReactFiberWorkLoop.old.js
Original file line number Diff line number Diff line change
Expand Up @@ -535,7 +535,7 @@ export function scheduleUpdateOnFiber(
fiber: Fiber,
lane: Lane,
eventTime: number,
) {
): FiberRoot | null {
checkForNestedUpdates();
warnAboutRenderPhaseUpdatesInDEV(fiber);

Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion packages/react-reconciler/src/ReactInternalTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -319,7 +319,7 @@ export type Dispatcher = {|
subscribe: MutableSourceSubscribeFn<Source, Snapshot>,
): Snapshot,
useOpaqueIdentifier(): any,
useRefresh?: () => () => void,
useRefresh?: () => <T>(?() => T, ?T) => void,

unstable_isNewReconciler?: boolean,
|};
48 changes: 47 additions & 1 deletion packages/react-reconciler/src/__tests__/ReactCache-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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':
Expand Down Expand Up @@ -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 <AsyncText showVersion={true} text="A" />;
}

// Mount initial data
const root = ReactNoop.createRoot();
await ReactNoop.act(async () => {
root.render(
<Cache>
<Suspense fallback={<Text text="Loading..." />}>
<App />
</Suspense>
</Cache>,
);
});
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;
Expand Down
2 changes: 1 addition & 1 deletion packages/react-server/src/ReactFlightServer.js
Original file line number Diff line number Diff line change
Expand Up @@ -804,7 +804,7 @@ const Dispatcher: DispatcherType = {
useEffect: (unsupportedHook: any),
useOpaqueIdentifier: (unsupportedHook: any),
useMutableSource: (unsupportedHook: any),
useRefresh(): () => void {
useRefresh(): <T>(?() => T, ?T) => void {
return unsupportedRefresh;
},
};
2 changes: 1 addition & 1 deletion packages/react/src/ReactHooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ export function useMutableSource<Source, Snapshot>(
return dispatcher.useMutableSource(source, getSnapshot, subscribe);
}

export function useRefresh(): () => void {
export function useRefresh(): <T>(?() => T, ?T) => void {
const dispatcher = resolveDispatcher();
// $FlowFixMe This is unstable, thus optional
return dispatcher.useRefresh();
Expand Down

0 comments on commit 0581bdf

Please sign in to comment.