Skip to content

Commit 81299ff

Browse files
committed
fix: re-add useMutation callback composition
1 parent 83a1ba6 commit 81299ff

File tree

6 files changed

+92
-34
lines changed

6 files changed

+92
-34
lines changed

docs/src/pages/guides/migrating-to-react-query-3.md

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -190,9 +190,6 @@ try {
190190
}
191191
```
192192
193-
Callbacks passed to the `mutate` or `mutateAsync` functions will now override the callbacks defined on `useMutation`.
194-
The `mutateAsync` function can be used to compose side effects.
195-
196193
### Query object syntax
197194
198195
The object syntax has been collapsed:

docs/src/pages/guides/mutations.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -149,30 +149,30 @@ useMutation(addTodo, {
149149
})
150150
```
151151

152-
You might find that you want to **trigger different callbacks** then the ones defined on `useMutation` when calling `mutate`. To do that, you can provide any of the same callback options to the `mutate` function after your mutation variable. Supported overrides include: `onSuccess`, `onError` and `onSettled`.
152+
You might find that you want to **trigger additional callbacks** then the ones defined on `useMutation` when calling `mutate`. This can be used to trigger component specific side effects. To do that, you can provide any of the same callback options to the `mutate` function after your mutation variable. Supported overrides include: `onSuccess`, `onError` and `onSettled`.
153153

154154
```js
155155
useMutation(addTodo, {
156156
onSuccess: (data, variables, context) => {
157-
// I will not fire
157+
// I will fire first
158158
},
159159
onError: (error, variables, context) => {
160-
// I will not fire
160+
// I will fire first
161161
},
162162
onSettled: (data, error, variables, context) => {
163-
// I will not fire
163+
// I will fire first
164164
},
165165
})
166166

167167
mutate(todo, {
168168
onSuccess: (data, variables, context) => {
169-
// I will fire instead!
169+
// I will fire second!
170170
},
171171
onError: (error, variables, context) => {
172-
// I will fire instead!
172+
// I will fire second!
173173
},
174174
onSettled: (data, error, variables, context) => {
175-
// I will fire instead!
175+
// I will fire second!
176176
},
177177
})
178178
```

src/core/mutation.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ interface SetStateAction<TData, TError, TVariables, TContext> {
6464
state: MutationState<TData, TError, TVariables, TContext>
6565
}
6666

67-
type Action<TData, TError, TVariables, TContext> =
67+
export type Action<TData, TError, TVariables, TContext> =
6868
| ContinueAction
6969
| ErrorAction<TError>
7070
| FailedAction
@@ -230,7 +230,7 @@ export class Mutation<
230230

231231
notifyManager.batch(() => {
232232
this.observers.forEach(observer => {
233-
observer.onMutationUpdate()
233+
observer.onMutationUpdate(action)
234234
})
235235
this.mutationCache.notify(this)
236236
})

src/core/mutationObserver.ts

Lines changed: 61 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { getDefaultState, Mutation } from './mutation'
1+
import { Action, getDefaultState, Mutation } from './mutation'
22
import { notifyManager } from './notifyManager'
33
import type { QueryClient } from './queryClient'
44
import { Subscribable } from './subscribable'
@@ -15,6 +15,12 @@ type MutationObserverListener<TData, TError, TVariables, TContext> = (
1515
result: MutationObserverResult<TData, TError, TVariables, TContext>
1616
) => void
1717

18+
interface NotifyOptions {
19+
listeners?: boolean
20+
onError?: boolean
21+
onSuccess?: boolean
22+
}
23+
1824
// CLASS
1925

2026
export class MutationObserver<
@@ -35,6 +41,7 @@ export class MutationObserver<
3541
TContext
3642
>
3743
private currentMutation?: Mutation<TData, TError, TVariables, TContext>
44+
private mutateOptions?: MutateOptions<TData, TError, TVariables, TContext>
3845

3946
constructor(
4047
client: QueryClient,
@@ -65,9 +72,21 @@ export class MutationObserver<
6572
}
6673
}
6774

68-
onMutationUpdate(): void {
75+
onMutationUpdate(action: Action<TData, TError, TVariables, TContext>): void {
6976
this.updateResult()
70-
this.notify()
77+
78+
// Determine which callbacks to trigger
79+
const notifyOptions: NotifyOptions = {
80+
listeners: true,
81+
}
82+
83+
if (action.type === 'success') {
84+
notifyOptions.onSuccess = true
85+
} else if (action.type === 'error') {
86+
notifyOptions.onError = true
87+
}
88+
89+
this.notify(notifyOptions)
7190
}
7291

7392
getCurrentResult(): MutationObserverResult<
@@ -82,20 +101,21 @@ export class MutationObserver<
82101
reset(): void {
83102
this.currentMutation = undefined
84103
this.updateResult()
85-
this.notify()
104+
this.notify({ listeners: true })
86105
}
87106

88107
mutate(
89108
variables?: TVariables,
90109
options?: MutateOptions<TData, TError, TVariables, TContext>
91110
): Promise<TData> {
111+
this.mutateOptions = options
112+
92113
if (this.currentMutation) {
93114
this.currentMutation.removeObserver(this)
94115
}
95116

96117
this.currentMutation = this.client.getMutationCache().build(this.client, {
97118
...this.options,
98-
...options,
99119
variables: variables ?? this.options.variables,
100120
})
101121

@@ -117,11 +137,43 @@ export class MutationObserver<
117137
}
118138
}
119139

120-
private notify() {
140+
private notify(options: NotifyOptions) {
121141
notifyManager.batch(() => {
122-
this.listeners.forEach(listener => {
123-
listener(this.currentResult)
124-
})
142+
// First trigger the mutate callbacks
143+
if (this.mutateOptions) {
144+
if (options.onSuccess) {
145+
this.mutateOptions.onSuccess?.(
146+
this.currentResult.data!,
147+
this.currentResult.variables!,
148+
this.currentResult.context
149+
)
150+
this.mutateOptions.onSettled?.(
151+
this.currentResult.data!,
152+
null,
153+
this.currentResult.variables!,
154+
this.currentResult.context
155+
)
156+
} else if (options.onError) {
157+
this.mutateOptions.onError?.(
158+
this.currentResult.error!,
159+
this.currentResult.variables!,
160+
this.currentResult.context
161+
)
162+
this.mutateOptions.onSettled?.(
163+
undefined,
164+
this.currentResult.error,
165+
this.currentResult.variables!,
166+
this.currentResult.context
167+
)
168+
}
169+
}
170+
171+
// Then trigger the listeners
172+
if (options.listeners) {
173+
this.listeners.forEach(listener => {
174+
listener(this.currentResult)
175+
})
176+
}
125177
})
126178
}
127179
}

src/core/types.ts

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -69,30 +69,23 @@ export interface QueryOptions<
6969
getNextPageParam?: GetNextPageParamFunction<TQueryFnData>
7070
}
7171

72-
export interface FetchQueryOptions<
73-
TData = unknown,
74-
TError = unknown,
75-
TQueryFnData = TData
76-
> extends QueryOptions<TData, TError, TQueryFnData> {
77-
/**
78-
* The time in milliseconds after data is considered stale.
79-
* If set to `Infinity`, the data will never be considered stale.
80-
*/
81-
staleTime?: number
82-
}
83-
8472
export interface QueryObserverOptions<
8573
TData = unknown,
8674
TError = unknown,
8775
TQueryFnData = TData,
8876
TQueryData = TQueryFnData
89-
> extends FetchQueryOptions<TData, TError, TQueryFnData> {
77+
> extends QueryOptions<TData, TError, TQueryFnData> {
9078
/**
9179
* Set this to `false` to disable automatic refetching when the query mounts or changes query keys.
9280
* To refetch the query, use the `refetch` method returned from the `useQuery` instance.
9381
* Defaults to `true`.
9482
*/
9583
enabled?: boolean
84+
/**
85+
* The time in milliseconds after data is considered stale.
86+
* If set to `Infinity`, the data will never be considered stale.
87+
*/
88+
staleTime?: number
9689
/**
9790
* If set to a number, the query will continuously refetch at this frequency in milliseconds.
9891
* Defaults to `false`.
@@ -186,6 +179,18 @@ export interface InfiniteQueryObserverOptions<
186179
InfiniteData<TQueryData>
187180
> {}
188181

182+
export interface FetchQueryOptions<
183+
TData = unknown,
184+
TError = unknown,
185+
TQueryFnData = TData
186+
> extends QueryOptions<TData, TError, TQueryFnData> {
187+
/**
188+
* The time in milliseconds after data is considered stale.
189+
* If the data is fresh it will be returned from the cache.
190+
*/
191+
staleTime?: number
192+
}
193+
189194
export interface ResultOptions {
190195
throwOnError?: boolean
191196
}

src/react/tests/useMutation.test.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,8 @@ describe('useMutation', () => {
247247
await sleep(100)
248248

249249
expect(callbacks).toEqual([
250+
'useMutation.onSuccess',
251+
'useMutation.onSettled',
250252
'mutateAsync.onSuccess',
251253
'mutateAsync.onSettled',
252254
'mutateAsync.result:todo',
@@ -296,6 +298,8 @@ describe('useMutation', () => {
296298
await sleep(100)
297299

298300
expect(callbacks).toEqual([
301+
'useMutation.onError',
302+
'useMutation.onSettled',
299303
'mutateAsync.onError',
300304
'mutateAsync.onSettled',
301305
'mutateAsync.error:oops',

0 commit comments

Comments
 (0)