Skip to content

Commit 38d32cc

Browse files
committed
fix tag based invalidation by storing tags in query client
1 parent 04135b6 commit 38d32cc

File tree

10 files changed

+42
-38
lines changed

10 files changed

+42
-38
lines changed

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,4 +38,4 @@ yarn-error.log*
3838
next-env.d.ts
3939

4040
# vite/vitest
41-
tsconfig.vitest-temp.json
41+
*.vitest-temp.json

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ It uses the builder pattern, the best pattern that works with complex Typescript
3232
## TODO
3333

3434
- Strong typed customizable tags
35+
- Infinite queries
3536

3637
## Examples
3738

src/builder/QueryBuilder.ts

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import {
1717
useSuspenseQuery,
1818
} from '@tanstack/react-query';
1919
import { operateOnTags } from '../tags/operateOnTags';
20-
import { QueryTagCache, QueryTagObject, QueryTagOption } from '../tags/types';
20+
import { QueryTagOption } from '../tags/types';
2121
import { FunctionType } from '../types/utils';
2222
import { MutationBuilder } from './MutationBuilder';
2323
import { MiddlewareFn, createMiddlewareFunction } from './createMiddlewareFunction';
@@ -54,7 +54,7 @@ export class QueryBuilderFrozen<T extends BuilderTypeTemplate> {
5454

5555
getQueryFn: () => QueryFunction<T['data'], [T['vars']]> = () => {
5656
return ({ client, meta, queryKey, signal, pageParam }) => {
57-
return this.config.queryFn({ client, meta, queryKey, signal, pageParam });
57+
return this.config.queryFn({ client, meta, queryKey, signal, pageParam, originalQueryKey: queryKey });
5858
};
5959
};
6060

@@ -229,9 +229,6 @@ export class QueryBuilder<T extends BuilderTypeTemplate = BuilderTypeTemplate> e
229229
static tagCacheId = 0;
230230

231231
withTags(...tags: QueryTagOption<T['vars'], T['data'], T['error']>[]): this {
232-
const tagCache: QueryTagCache = { tags: [] };
233-
const setTags = (tags: QueryTagObject[]) => (tagCache.tags = tags);
234-
235232
if (this.config.queryClient) {
236233
this.config.syncChannel?.addEventListener('message', (event) => {
237234
const { type, data } = event.data;
@@ -242,9 +239,7 @@ export class QueryBuilder<T extends BuilderTypeTemplate = BuilderTypeTemplate> e
242239
});
243240
}
244241

245-
return this.withMiddleware(createTagMiddleware<T>(tags, setTags), {
246-
options: { meta: { tagCaches: { [QueryBuilder.tagCacheId++]: tagCache } } },
247-
}) as unknown as this;
242+
return this.withMiddleware(createTagMiddleware<T>(tags, QueryBuilder.tagCacheId++)) as unknown as this;
248243
}
249244

250245
freeze(): QueryBuilderFrozen<T> {

src/builder/createMiddlewareFunction.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
1-
import { QueryFunction, QueryFunctionContext } from '@tanstack/react-query';
21
import { MutationBuilderConfig } from './MutationBuilder';
32
import { QueryBuilderConfig } from './QueryBuilder';
4-
import { BuilderQueryFn, BuilderTypeTemplate } from './types';
3+
import type { BuilderQueryContext, BuilderQueryFn, BuilderTypeTemplate, SetAllTypes } from './types';
54

65
export type MiddlewareFn<TVars, TData, TError, TOriginalTemplate extends BuilderTypeTemplate> = (
76
context: MiddlewareContext<TVars>,
@@ -10,15 +9,15 @@ export type MiddlewareFn<TVars, TData, TError, TOriginalTemplate extends Builder
109
config: QueryBuilderConfig<TOriginalTemplate> | MutationBuilderConfig<TOriginalTemplate>,
1110
) => TData | Promise<TData>;
1211

13-
export type MiddlewareContext<TVars> = QueryFunctionContext<[TVars]> & {
12+
export type MiddlewareContext<TVars> = BuilderQueryContext<TVars> & {
1413
vars: TVars;
1514
};
1615

1716
export type MiddlewareNextFn<T extends BuilderTypeTemplate> = (
1817
context: Omit<MiddlewareContext<T['vars']>, 'queryKey'>,
1918
) => Promise<T['data']>;
2019

21-
const createMiddlewareContext = <TVars>(context: QueryFunctionContext<[TVars]>): MiddlewareContext<TVars> => {
20+
const createMiddlewareContext = <TVars>(context: BuilderQueryContext<TVars>): MiddlewareContext<TVars> => {
2221
return {
2322
...context,
2423
vars: context.queryKey[0],
@@ -33,7 +32,7 @@ export const createMiddlewareFunction = <TVars, TData, TError, TOriginalTemplate
3332
originalFn: BuilderQueryFn<TOriginalTemplate>,
3433
middleware: MiddlewareFn<TVars, TData, TError, TOriginalTemplate>,
3534
config: QueryBuilderConfig<TOriginalTemplate> | MutationBuilderConfig<TOriginalTemplate>,
36-
): QueryFunction<TData, [TVars]> => {
35+
): BuilderQueryFn<SetAllTypes<TOriginalTemplate, TData, TError, TVars, true>> => {
3736
return async (context) =>
3837
middleware(
3938
createMiddlewareContext<TVars>(context),

src/builder/createTagMiddleware.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
1+
import { hashKey } from '@tanstack/react-query';
12
import { resolveTags } from '../tags/resolveTags';
2-
import { QueryTagObject, QueryTagOption } from '../tags/types';
3+
import { QueryTagCache, QueryTagObject, QueryTagOption } from '../tags/types';
34
import { MiddlewareFn } from './createMiddlewareFunction';
45
import { BuilderTypeTemplate } from './types';
56

67
type CreateTagMiddleware = <T extends BuilderTypeTemplate>(
78
tags: QueryTagOption[],
8-
setTags: (tags: QueryTagObject[]) => void,
9+
cacheId: string | number,
910
) => MiddlewareFn<T['vars'], T['data'], T['error'], T>;
1011

11-
export const createTagMiddleware: CreateTagMiddleware = (tags, setTags) =>
12+
export const createTagMiddleware: CreateTagMiddleware = (tags, cacheId) =>
1213
async function tagMiddlware(ctx, next) {
1314
let resolvedTags: QueryTagObject[] = [];
1415

@@ -23,6 +24,9 @@ export const createTagMiddleware: CreateTagMiddleware = (tags, setTags) =>
2324

2425
throw error;
2526
} finally {
26-
setTags(resolvedTags);
27+
const hash = hashKey(ctx.originalQueryKey);
28+
const tagCache = ((ctx.client as any).tagCache ??= {}) as QueryTagCache;
29+
const tags = (tagCache[hash] ??= {});
30+
tags[cacheId] = resolvedTags;
2731
}
2832
};

src/builder/types.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { QueryFunction, UseQueryResult } from '@tanstack/react-query';
1+
import type { QueryFunctionContext, UseQueryResult } from '@tanstack/react-query';
22
import type { RequestError } from '../http/errors';
33
import type { HttpRequestOptions } from '../http/types';
44
import type { Prettify } from '../types/utils';
@@ -34,7 +34,13 @@ export type BuilderQueriesResult<T extends BuilderTypeTemplate> = QueriesResultI
3434
queryMap: QueriesResultMapType<T>;
3535
};
3636

37-
export type BuilderQueryFn<T extends BuilderTypeTemplate> = QueryFunction<T['data'], [T['vars']]>;
37+
export type BuilderQueryContext<TVars> = QueryFunctionContext<[TVars], never> & {
38+
originalQueryKey: unknown[];
39+
};
40+
41+
export type BuilderQueryFn<T extends BuilderTypeTemplate> = (
42+
context: BuilderQueryContext<T['vars']>,
43+
) => T['data'] | Promise<T['data']>;
3844

3945
export type SetDataType<T extends BuilderTypeTemplate, TData> = Prettify<Omit<T, 'data'> & { data: TData }>;
4046

src/builder/utils.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,7 @@ export function mergeQueryOptions<
3636
for (const { enabled, meta, ...opt } of filtered) {
3737
Object.assign(opts, opt);
3838
opts.enabled = mergeQueryEnabled([opts.enabled, enabled]) as TOpt['enabled'];
39-
opts.meta = {
40-
...opts.meta,
41-
...meta,
42-
tagCaches: { ...opts.meta?.tagCaches!, ...meta?.tagCaches! },
43-
};
39+
opts.meta = { ...opts.meta, ...meta };
4440
}
4541

4642
return opts;

src/tags/operateOnTags.ts

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1-
import type {
2-
InvalidateOptions,
3-
InvalidateQueryFilters,
4-
Query,
5-
QueryClient,
6-
QueryFilters,
1+
import {
2+
type InvalidateOptions,
3+
type InvalidateQueryFilters,
4+
type Query,
5+
type QueryClient,
6+
type QueryFilters,
7+
hashKey,
78
} from '@tanstack/react-query';
8-
import type { QueryTag, QueryTagCache } from './types';
9+
import type { QueryTag } from './types';
910

1011
/**
1112
* Based on the behavior described in https://redux-toolkit.js.org/rtk-query/usage/automated-refetching#tag-invalidation-behavior
@@ -24,9 +25,11 @@ function tagMatchesTag(queryTag: QueryTag, comparedTag: QueryTag) {
2425
return cId === qId;
2526
}
2627

27-
export function queryMatchesTag(query: Query, tag: QueryTag) {
28-
const tagCaches = query.meta?.tagCaches as Record<string, QueryTagCache>;
29-
const tagsInMeta = Object.values(tagCaches)?.flatMap((t) => t.tags || []);
28+
export function queryMatchesTag(queryClient: QueryClient, query: Query, tag: QueryTag) {
29+
const hash = hashKey(query.queryKey);
30+
const tagCaches = (queryClient as any)?.tagCache?.[hash] as Record<string, QueryTag[]>;
31+
if (!tagCaches) return false;
32+
const tagsInMeta = Object.values(tagCaches)?.flatMap((t) => t || []);
3033
if (tagsInMeta?.length) return tagsInMeta.some((t: QueryTag) => tagMatchesTag(t, tag));
3134
return false;
3235
}
@@ -57,7 +60,7 @@ export function operateOnTags(
5760
...(operation === 'refetch' && { type: 'active' }),
5861
...filters,
5962
predicate: (query) =>
60-
tags.some((tag) => queryMatchesTag(query, tag)) && (!filters?.predicate || filters.predicate(query)),
63+
tags.some((tag) => queryMatchesTag(queryClient, query, tag)) && (!filters?.predicate || filters.predicate(query)),
6164
};
6265

6366
if (operation === 'refetch') return queryClient.refetchQueries(filtersObj, options);

src/tags/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,4 +81,4 @@ export type QueryUpdateTag<TVars = unknown, TData = unknown, TErr = unknown, TTa
8181
QueryUpdateTagObject<TVars, TData, TErr, TTarget>
8282
>;
8383

84-
export type QueryTagCache = { tags: QueryTagObject[] };
84+
export type QueryTagCache = Record<string | number, Record<string, QueryTagObject[]>>;

src/tags/updateTags.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ export function updateTags({
2929
const tag = tags[i];
3030

3131
const list = queryClient.getQueryCache().findAll({
32-
predicate: (query) => queryMatchesTag(query, tag),
32+
predicate: (query) => queryMatchesTag(queryClient, query, tag),
3333
type: 'all',
3434
});
3535

0 commit comments

Comments
 (0)