Skip to content

Commit b2072a6

Browse files
committed
add helpers for mutation builder
1 parent 9179031 commit b2072a6

File tree

12 files changed

+128
-19
lines changed

12 files changed

+128
-19
lines changed

examples/vite/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
"postinstall": "msw init"
1111
},
1212
"dependencies": {
13+
"@tanstack/react-query-devtools": "^5.66.9",
1314
"react": "^19.0.0",
1415
"react-dom": "^19.0.0",
1516
"react-query-builder": "*"

examples/vite/src/App.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@ function App() {
6767
const deletePost = deletePostMutation.useMutation();
6868
const reset = resetMutation.useMutation();
6969

70+
const deleteMutations = deletePostMutation.useAllMutations();
71+
7072
const [refresh] = useOperateOnTags({ tags: ['refreshable'], operation: 'refetch' });
7173

7274
if (postId) return <PostPage postId={postId} onBack={() => setPostId(null)} />;
@@ -106,6 +108,10 @@ function App() {
106108
Delete
107109
</button>
108110

111+
{deleteMutations.getMutation({ params: { id: post.id } })?.error && (
112+
<span style={{ color: 'red' }}>Error deleting post</span>
113+
)}
114+
109115
<p>{post.body}</p>
110116
</div>
111117
))}

examples/vite/src/client.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export const queryClient = new QueryClient({
55
defaultOptions: {
66
queries: {
77
staleTime: 1000 * 60,
8+
refetchOnWindowFocus: false,
89
},
910
},
1011
mutationCache: new BuilderMutationCache(

examples/vite/src/main.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@ import { createRoot } from 'react-dom/client';
22
import App from './App';
33
import './index.css';
44
import { QueryClientProvider } from '@tanstack/react-query';
5+
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
56
import { queryClient } from './client';
67

78
const app = (
89
<QueryClientProvider client={queryClient}>
910
<App />
11+
<ReactQueryDevtools initialIsOpen={false} />
1012
</QueryClientProvider>
1113
);
1214

examples/vite/src/mocks.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,9 @@ const handlers = [
167167
http.delete(`${baseUrl}/posts/:id`, async (req) => {
168168
await delay(1000);
169169
const { id } = req.params;
170+
if (id === '7' && Math.random() > 0.1)
171+
return HttpResponse.json({ error: '[Mock] Failed to delete' }, { status: 500 });
172+
170173
const postIndex = posts.findIndex((post) => post.id === Number(id));
171174
if (postIndex === -1) return HttpResponse.json({ error: 'Not found' }, { status: 404 });
172175

package-lock.json

Lines changed: 28 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/builder/MutationBuilder.ts

Lines changed: 52 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,21 @@ import {
22
MutationFilters,
33
MutationFunction,
44
MutationKey,
5+
MutationState,
56
QueryClient,
67
UseMutationOptions,
78
useIsMutating,
89
useMutation,
10+
useMutationState,
911
useQueryClient,
1012
} from '@tanstack/react-query';
13+
import { useStableCallback } from '../hooks/useStableCallback';
1114
import { mergeTagOptions } from '../tags/mergeTagOptions';
1215
import { QueryInvalidatesMetadata } from '../tags/types';
1316
import { QueryBuilder } from './QueryBuilder';
1417
import { BuilderMergeVarsFn, BuilderQueryFn, SetDataType, SetErrorType } from './types';
1518
import { AppendVarsType, BuilderTypeTemplate } from './types';
16-
import { mergeMutationOptions, mergeVars } from './utils';
19+
import { areKeysEqual, mergeMutationOptions, mergeVars } from './utils';
1720

1821
function getRandomKey() {
1922
return Math.random().toString(36).substring(7);
@@ -58,9 +61,8 @@ export class MutationBuilderFrozen<T extends BuilderTypeTemplate> {
5861
};
5962
};
6063

61-
getMutationKey: (vars: T['vars']) => MutationKey = (vars) => {
62-
const mergedVars = this.mergeVars([this.config.vars, vars]);
63-
return [this.mutationKeyPrefix, mergedVars];
64+
getMutationKey: () => MutationKey = () => {
65+
return [this.mutationKeyPrefix];
6466
};
6567

6668
getMutationOptions: (
@@ -69,6 +71,7 @@ export class MutationBuilderFrozen<T extends BuilderTypeTemplate> {
6971
) => UseMutationOptions<T['data'], T['error'], T['vars']> = (queryClient, opts) => {
7072
return mergeMutationOptions([
7173
{
74+
mutationKey: this.getMutationKey(),
7275
mutationFn: this.getMutationFn(queryClient, opts?.meta),
7376
meta: {
7477
invalidates: this.config.invalidates,
@@ -81,6 +84,22 @@ export class MutationBuilderFrozen<T extends BuilderTypeTemplate> {
8184
]);
8285
};
8386

87+
getMutationFilters: (
88+
vars?: T['vars'],
89+
filters?: MutationFilters<T['data'], T['error'], T['vars']>,
90+
) => MutationFilters<T['data'], T['error'], T['vars']> = (vars, filters) => {
91+
return {
92+
mutationKey: this.getMutationKey(),
93+
...filters,
94+
predicate: (m) => {
95+
if (filters?.predicate && !filters.predicate(m)) return false;
96+
if (vars == null) return true;
97+
if (!m.state.variables) return false;
98+
return areKeysEqual([m.state.variables], [vars]);
99+
},
100+
};
101+
};
102+
84103
useMutation: (
85104
opts?: MutationBuilderConfig<T>['options'],
86105
) => ReturnType<typeof useMutation<T['data'], T['error'], T['vars']>> = (opts) => {
@@ -92,7 +111,27 @@ export class MutationBuilderFrozen<T extends BuilderTypeTemplate> {
92111
vars,
93112
filters,
94113
) => {
95-
return useIsMutating({ mutationKey: this.getMutationKey(vars), ...filters }, this.config.queryClient);
114+
return useIsMutating(this.getMutationFilters(vars, filters), this.config.queryClient);
115+
};
116+
117+
useAllMutations: (filters?: MutationFilters<T['data'], T['error'], T['vars']>) => MutationStateHelper<T> = (
118+
filters,
119+
) => {
120+
const list = useMutationState({ filters: this.getMutationFilters(undefined, filters) }, this.config.queryClient);
121+
122+
const getMutation: MutationStateHelper<T>['getMutation'] = useStableCallback(
123+
(vars, predicate?: (mutation: MutationState<T['data'], T['error'], T['vars']>) => boolean) =>
124+
list.findLast((m) => areKeysEqual([m.variables], [vars]) && (!predicate || predicate(m))),
125+
);
126+
127+
return { list, getMutation };
128+
};
129+
130+
useMutationState: (
131+
vars: T['vars'],
132+
filters?: MutationFilters<T['data'], T['error'], T['vars']>,
133+
) => MutationState<T['data'], T['error'], T['vars']> | undefined = (vars, filters) => {
134+
return useMutationState({ filters: this.getMutationFilters(vars, filters) }, this.config.queryClient)[0];
96135
};
97136

98137
private _client?: MutationBuilderClient<T>;
@@ -101,14 +140,16 @@ export class MutationBuilderFrozen<T extends BuilderTypeTemplate> {
101140
}
102141
}
103142

104-
class MutationBuilderClient<
105-
T extends BuilderTypeTemplate,
106-
TFilters = MutationFilters<T['data'], T['error'], T['vars']>,
107-
> {
143+
export type MutationStateHelper<T extends BuilderTypeTemplate> = {
144+
list: MutationState<T['data'], T['error'], T['vars']>[];
145+
getMutation(vars: T['vars']): MutationState<T['data'], T['error'], T['vars']> | undefined;
146+
};
147+
148+
class MutationBuilderClient<T extends BuilderTypeTemplate> {
108149
constructor(private builder: MutationBuilderFrozen<T>) {}
109150

110-
readonly isMutating = (vars: T['vars'], filters?: TFilters) =>
111-
this.builder.config.queryClient?.isMutating({ mutationKey: this.builder.getMutationKey(vars), ...filters });
151+
readonly isMutating = (vars: T['vars'], filters?: MutationFilters<T['data'], T['error'], T['vars']>) =>
152+
this.builder.config.queryClient?.isMutating(this.builder.getMutationFilters(vars, filters));
112153
}
113154

114155
export class MutationBuilder<T extends BuilderTypeTemplate = BuilderTypeTemplate> extends MutationBuilderFrozen<T> {

src/builder/utils.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
1-
import type { DefaultError, QueryKey, UseMutationOptions, UseQueryOptions } from '@tanstack/react-query';
1+
import {
2+
DefaultError,
3+
MutationKey,
4+
QueryKey,
5+
UseMutationOptions,
6+
UseQueryOptions,
7+
hashKey,
8+
} from '@tanstack/react-query';
29
import { httpRequest } from '../http/request';
310
import { mergeQueryEnabled } from '../tags/mergeQueryEnabled';
411
import { mergeTagOptions } from '../tags/mergeTagOptions';
@@ -90,3 +97,11 @@ export function createHttpQueryFn<T extends HttpBuilderTypeTemplate>(
9097
return httpRequest<T['data']>({ ...(mergedVars as any), meta, signal });
9198
};
9299
}
100+
101+
export function areKeysEqual(
102+
a: QueryKey | MutationKey,
103+
b: QueryKey | MutationKey,
104+
hashFn: typeof hashKey = hashKey,
105+
): boolean {
106+
return hashFn(a) === hashFn(b);
107+
}

src/tags/cache.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,12 @@ export class BuilderMutationCache extends MutationCache {
2626
const queryClient = getQueryClient();
2727

2828
const data = !vars || typeof vars !== 'object' ? undefined : Reflect.get(vars, 'body');
29-
const updates = resolveTags({ client: queryClient, tags: mutation.meta?.optimisticUpdates, vars, data });
29+
const optUpdates = resolveTags({ client: queryClient, tags: mutation.meta?.optimisticUpdates, vars, data });
3030
const ctx: QueryTagContext<unknown> = { client: queryClient, vars, data };
31-
const undos = updateTags({ queryClient, tags: updates, ctx, optimistic: true });
31+
const undos = updateTags({ queryClient, tags: optUpdates, ctx, optimistic: true });
3232
if (undos.length) mutationContexts.set(mutation.mutationId, { undos });
3333

34-
const tags = updates.filter(
34+
const tags = optUpdates.filter(
3535
(tag) => typeof tag !== 'object' || ['pre', 'both'].includes(tag.invalidate || 'both'),
3636
);
3737

src/tags/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,8 @@ export type QueryTagsMetadata<TVars = void, TData = unknown, TErr = unknown> = {
6363
* Provides tags for the query, which can be used by mutations to invalidate or optimistically update the query data.
6464
*/
6565
tags?: QueryTagOption<TVars, TData, TErr>;
66+
67+
updated?: 'optimistic' | 'pessimistic' | 'undone';
6668
};
6769

6870
export type QueryInvalidatesMetadata<TVars = void, TData = unknown, TErr = unknown> = {

0 commit comments

Comments
 (0)