Skip to content

Commit

Permalink
feat: allow updating cache entries using a predicate function
Browse files Browse the repository at this point in the history
  • Loading branch information
swain committed Dec 11, 2024
1 parent 520cf2f commit bf00472
Show file tree
Hide file tree
Showing 4 changed files with 88 additions and 24 deletions.
56 changes: 37 additions & 19 deletions src/cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,8 @@ import { QueryClient, QueryFilters } from '@tanstack/react-query';
// eslint-disable-next-line no-restricted-imports
import { isEqual } from 'lodash';
import { produce } from 'immer';
import {
CacheUtils,
EndpointInvalidationMap,
RequestPayloadOf,
RoughEndpoints,
} from './types';
import { InternalQueryKey, isInternalQueryKey } from './util';
import { CacheUtils, EndpointInvalidationMap, RoughEndpoints } from './types';
import { createQueryKey, isInternalQueryKey } from './util';

const createQueryFilterFromSpec = <Endpoints extends RoughEndpoints>(
endpoints: EndpointInvalidationMap<Endpoints>,
Expand Down Expand Up @@ -47,23 +42,43 @@ export const INFINITE_QUERY_KEY = 'infinite' as const;

export const createCacheUtils = <Endpoints extends RoughEndpoints>(
client: QueryClient,
makeQueryKey: <Route extends keyof Endpoints & string>(
route: Route,
payload: RequestPayloadOf<Endpoints, Route>,
) => InternalQueryKey,
name: string,
): CacheUtils<Endpoints> => {
const updateCache: (
keyPrefix?: typeof INFINITE_QUERY_KEY,
) => CacheUtils<Endpoints>['updateCache'] =
(keyPrefix) => (route, payload, updater) => {
client.setQueryData<Endpoints[typeof route]['Response']>(
[keyPrefix, makeQueryKey(route, payload)].filter(Boolean),
(keyPrefix) => (route, payloadOrPredicate, updater) => {
client.setQueriesData<Endpoints[typeof route]['Response']>(
typeof payloadOrPredicate === 'function'
? {
predicate: ({ queryKey }) => {
/* istanbul ignore next */
if (keyPrefix && queryKey[0] !== keyPrefix) {
/* istanbul ignore next */
return false;
}
const payloadInKey = keyPrefix ? queryKey[1] : queryKey[0];

return (
isInternalQueryKey(payloadInKey) &&
payloadInKey.name === name &&
payloadInKey.route === route &&
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore These types are a bit finicky. Override it.
payloadOrPredicate(payloadInKey.payload)
);
},
}
: {
queryKey: [
keyPrefix,
createQueryKey(name, route, payloadOrPredicate),
].filter(Boolean),
exact: true,
},
typeof updater !== 'function'
? updater
: (current) => {
if (current === undefined) {
return;
}
return produce(
current,
// @ts-expect-error TypeScript incorrectly thinks that `updater`
Expand All @@ -84,9 +99,12 @@ export const createCacheUtils = <Endpoints extends RoughEndpoints>(
updateCache: updateCache(),
updateInfiniteCache: updateCache(INFINITE_QUERY_KEY),
getQueryData: (route, payload) =>
client.getQueryData([makeQueryKey(route, payload)]),
client.getQueryData([createQueryKey(name, route, payload)]),
getInfiniteQueryData: (route, payload) =>
client.getQueryData([INFINITE_QUERY_KEY, makeQueryKey(route, payload)]),
client.getQueryData([
INFINITE_QUERY_KEY,
createQueryKey(name, route, payload),
]),
getQueriesData: (route) =>
client
.getQueriesData(createQueryFilterFromSpec({ [route]: 'all' }))
Expand Down
44 changes: 44 additions & 0 deletions src/hooks.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1557,6 +1557,50 @@ describe('useAPICache', () => {

expect(updateFn).not.toHaveBeenCalled();
});

it('allows updating using a predicate', async () => {
const updater =
method === 'updateCache'
? () => ({ message: 'Samwise Gamgee' })
: () => ({
pages: [{ items: [{ message: 'Samwise Gamgee' }] }],
pageParams: [],
});

const screen = render(() => (
<TestComponent
getRenderData={config.getRenderData}
onPress={(cache) => {
const updateMethod = cache[method];

updateMethod(
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
config.route,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
(payload) => payload.filter === '',
updater,
);
}}
/>
));

await screen.findByText('Response: Frodo Baggins');

expect(client.request).toHaveBeenCalledTimes(1);

TestingLibrary.fireEvent.click(screen.getByText('Update Cache'));

// The update does not happen immediately.
await TestingLibrary.waitFor(() => {
expect(screen.getByTestId('render-data').textContent).toStrictEqual(
'Response: Samwise Gamgee',
);
});

expect(client.request).toHaveBeenCalledTimes(1);
});
});
});
});
Expand Down
4 changes: 1 addition & 3 deletions src/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,9 +181,7 @@ export const createAPIHooks = <Endpoints extends RoughEndpoints>({

useAPICache: () => {
const client = useQueryClient();
return createCacheUtils(client, (route, payload) =>
createQueryKey(name, route, payload),
);
return createCacheUtils(client, name);
},
};
};
8 changes: 6 additions & 2 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,13 +140,17 @@ export type CacheUtils<Endpoints extends RoughEndpoints> = {

updateCache: <Route extends keyof Endpoints & string>(
route: Route,
payload: RequestPayloadOf<Endpoints, Route>,
payloadOrPredicate:
| RequestPayloadOf<Endpoints, Route>
| ((payload: RequestPayloadOf<Endpoints, Route>) => boolean),
updater: CacheUpdate<Endpoints[Route]['Response']>,
) => void;

updateInfiniteCache: <Route extends keyof Endpoints & string>(
route: Route,
payload: RequestPayloadOf<Endpoints, Route>,
payloadOrPredicate:
| RequestPayloadOf<Endpoints, Route>
| ((payload: RequestPayloadOf<Endpoints, Route>) => boolean),
updater: CacheUpdate<InfiniteData<Endpoints[Route]['Response']>>,
) => void;

Expand Down

0 comments on commit bf00472

Please sign in to comment.