Skip to content

Commit 306a4f3

Browse files
committed
refactor: deprecate onMutate in favor of onBeforeMutate
People sometimes think onMutate triggers after the mutation and try to invalidate queries there. Adding _before_ makes it clearer.
1 parent 29e1210 commit 306a4f3

5 files changed

+142
-70
lines changed

Diff for: src/mutation-options.ts

+37-16
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,16 @@ export interface UseMutationOptionsGlobal {
1313
* passed to `mutation`, `onSuccess`, `onError` and `onSettled`. If it
1414
* returns a promise, it will be awaited before running `mutation`.
1515
*/
16+
onBeforeMutate?: (
17+
/**
18+
* The variables passed to the mutation.
19+
*/
20+
vars: unknown,
21+
) => _Awaitable<UseMutationGlobalContext | undefined | void | null>
22+
23+
/**
24+
* @deprecated Use {@link onBeforeMutate} instead.
25+
*/
1626
onMutate?: (
1727
/**
1828
* The variables passed to the mutation.
@@ -33,7 +43,7 @@ export interface UseMutationOptionsGlobal {
3343
*/
3444
vars: unknown,
3545
/**
36-
* The merged context from `onMutate` and the global context.
46+
* The merged context from `onBeforeMutate` and the global context.
3747
*/
3848
context: UseMutationGlobalContext,
3949
) => unknown
@@ -51,13 +61,13 @@ export interface UseMutationOptionsGlobal {
5161
*/
5262
vars: unknown,
5363
/**
54-
* The merged context from `onMutate` and the global context. Properties returned by `onMutate` can be `undefined`
55-
* if `onMutate` throws.
64+
* The merged context from `onBeforeMutate` and the global context. Properties returned by `onBeforeMutate` can be `undefined`
65+
* if `onBeforeMutate` throws.
5666
*/
5767
context:
5868
| Partial<Record<keyof UseMutationGlobalContext, never>>
5969
// this is the success case where everything is defined
60-
// undefined if global onMutate throws
70+
// undefined if global onBeforeMutate throws
6171
| UseMutationGlobalContext,
6272
) => unknown
6373

@@ -78,13 +88,13 @@ export interface UseMutationOptionsGlobal {
7888
*/
7989
vars: unknown,
8090
/**
81-
* The merged context from `onMutate` and the global context. Properties returned by `onMutate` can be `undefined`
82-
* if `onMutate` throws.
91+
* The merged context from `onBeforeMutate` and the global context. Properties are `undefined`
92+
* if `onBeforeMutate` throws.
8393
*/
8494
context:
8595
| Partial<Record<keyof UseMutationGlobalContext, never>>
8696
// this is the success case where everything is defined
87-
// undefined if global onMutate throws
97+
// undefined if global onBeforeMutate throws
8898
| UseMutationGlobalContext,
8999
) => unknown
90100

@@ -126,6 +136,17 @@ export interface UseMutationOptions<
126136
*/
127137
key?: _MutationKey<NoInfer<TVars>>
128138

139+
/**
140+
* @deprecated Use {@link onBeforeMutate} instead.
141+
*/
142+
onMutate?: (
143+
/**
144+
* The variables passed to the mutation.
145+
*/
146+
vars: NoInfer<TVars>,
147+
context: UseMutationGlobalContext,
148+
) => _Awaitable<TContext | undefined | void | null>
149+
129150
/**
130151
* Runs before the mutation is executed. **It should be placed before `mutation()` for `context` to be inferred**. It
131152
* can return a value that will be passed to `mutation`, `onSuccess`, `onError` and `onSettled`. If it returns a
@@ -136,7 +157,7 @@ export interface UseMutationOptions<
136157
* useMutation({
137158
* // must appear before `mutation` for `{ foo: string }` to be inferred
138159
* // within `mutation`
139-
* onMutate() {
160+
* onBeforeMutate() {
140161
* return { foo: 'bar' }
141162
* },
142163
* mutation: (id: number, { foo }) => {
@@ -149,7 +170,7 @@ export interface UseMutationOptions<
149170
* })
150171
* ```
151172
*/
152-
onMutate?: (
173+
onBeforeMutate?: (
153174
/**
154175
* The variables passed to the mutation.
155176
*/
@@ -170,7 +191,7 @@ export interface UseMutationOptions<
170191
*/
171192
vars: NoInfer<TVars>,
172193
/**
173-
* The merged context from `onMutate` and the global context.
194+
* The merged context from `onBeforeMutate` and the global context.
174195
*/
175196
context: UseMutationGlobalContext & _ReduceContext<NoInfer<TContext>>,
176197
) => unknown
@@ -188,14 +209,14 @@ export interface UseMutationOptions<
188209
*/
189210
vars: NoInfer<TVars>,
190211
/**
191-
* The merged context from `onMutate` and the global context. Properties returned by `onMutate` can be `undefined`
192-
* if `onMutate` throws.
212+
* The merged context from `onBeforeMutate` and the global context. Properties returned by `onBeforeMutate` can be `undefined`
213+
* if `onBeforeMutate` throws.
193214
*/
194215
context:
195216
| (Partial<Record<keyof UseMutationGlobalContext, never>> &
196217
Partial<Record<keyof _ReduceContext<NoInfer<TContext>>, never>>)
197218
// this is the success case where everything is defined
198-
// undefined if global onMutate throws
219+
// undefined if global onBeforeMutate throws
199220
| (UseMutationGlobalContext & _ReduceContext<NoInfer<TContext>>),
200221
) => unknown
201222

@@ -216,14 +237,14 @@ export interface UseMutationOptions<
216237
*/
217238
vars: NoInfer<TVars>,
218239
/**
219-
* The merged context from `onMutate` and the global context. Properties returned by `onMutate` can be `undefined`
220-
* if `onMutate` throws.
240+
* The merged context from `onBeforeMutate` and the global context. Properties returned by `onBeforeMutate` can be `undefined`
241+
* if `onBeforeMutate` throws.
221242
*/
222243
context:
223244
| (Partial<Record<keyof UseMutationGlobalContext, never>> &
224245
Partial<Record<keyof _ReduceContext<NoInfer<TContext>>, never>>)
225246
// this is the success case where everything is defined
226-
// undefined if global onMutate throws
247+
// undefined if global onBeforeMutate throws
227248
| (UseMutationGlobalContext & _ReduceContext<NoInfer<TContext>>),
228249
) => unknown
229250
}

Diff for: src/mutation-store.ts

+26-8
Original file line numberDiff line numberDiff line change
@@ -367,7 +367,7 @@ export const useMutationCache = /* @__PURE__ */ defineStore(MUTATION_STORE_ID, (
367367
let currentData: TResult | undefined
368368
let currentError: TError | undefined
369369
type OnMutateContext = Parameters<
370-
Required<UseMutationOptions<TResult, TVars, TError, TContext>>['onMutate']
370+
Required<UseMutationOptions<TResult, TVars, TError, TContext>>['onBeforeMutate']
371371
>['1']
372372
type OnSuccessContext = Parameters<
373373
Required<UseMutationOptions<TResult, TVars, TError, TContext>>['onSuccess']
@@ -378,24 +378,42 @@ export const useMutationCache = /* @__PURE__ */ defineStore(MUTATION_STORE_ID, (
378378

379379
let context: OnMutateContext | OnErrorContext | OnSuccessContext = {}
380380

381+
if (process.env.NODE_ENV !== 'production') {
382+
if (globalOptions.onMutate || options.onMutate) {
383+
console.warn(
384+
`[@pinia/colada] The "onMutate" option is deprecated. Use "onBeforeMutate" instead. It will be removed on the next version.`,
385+
)
386+
if (
387+
(globalOptions.onMutate && globalOptions.onBeforeMutate)
388+
|| (options.onMutate && options.onBeforeMutate)
389+
) {
390+
console.warn(
391+
`[@pinia/colada] You are using both "onMutate" and "onBeforeMutate" but only "onMutate" is ran. Use only "onBeforeMutate" instead.`,
392+
)
393+
}
394+
}
395+
}
396+
381397
try {
382-
const globalOnMutateContext = globalOptions.onMutate?.(vars)
398+
const globalOnMutateContext
399+
= globalOptions.onMutate?.(vars) || globalOptions.onBeforeMutate?.(vars)
383400

384401
context
385402
= (globalOnMutateContext instanceof Promise
386403
? await globalOnMutateContext
387404
: globalOnMutateContext) || {}
388405

389-
const onMutateContext = (await options.onMutate?.(
390-
vars,
391-
context,
392-
// NOTE: the cast makes it easier to write without extra code. It's safe because { ...null, ...undefined } works and TContext must be a Record<any, any>
393-
)) as _ReduceContext<TContext>
406+
const onBeforeMutateContext = (await (options.onMutate?.(vars, context)
407+
|| options.onBeforeMutate?.(
408+
vars,
409+
context,
410+
// NOTE: the cast makes it easier to write without extra code. It's safe because { ...null, ...undefined } works and TContext must be a Record<any, any>
411+
))) as _ReduceContext<TContext>
394412

395413
// we set the context here so it can be used by other hooks
396414
context = {
397415
...context,
398-
...onMutateContext,
416+
...onBeforeMutateContext,
399417
// NOTE: needed for onSuccess cast
400418
} satisfies OnSuccessContext
401419

Diff for: src/use-mutation.spec.ts

+62-29
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,24 @@ describe('useMutation', () => {
122122
})
123123

124124
describe('hooks', () => {
125-
it('invokes the "onMutate" hook before mutating', async () => {
125+
it('invokes the "onBeforeMutate" hook before mutating', async () => {
126+
const onBeforeMutate = vi.fn()
127+
const { wrapper } = mountSimple({
128+
mutation: async ({ a, b }: { a: number, b: number }) => {
129+
return a + b
130+
},
131+
onBeforeMutate,
132+
})
133+
expect(onBeforeMutate).not.toHaveBeenCalled()
134+
wrapper.vm.mutate({ a: 24, b: 42 })
135+
expect(onBeforeMutate).toHaveBeenCalledTimes(1)
136+
expect(onBeforeMutate).toHaveBeenLastCalledWith({ a: 24, b: 42 }, expect.objectContaining({}))
137+
wrapper.vm.mutateAsync({ a: 0, b: 1 })
138+
expect(onBeforeMutate).toHaveBeenCalledTimes(2)
139+
expect(onBeforeMutate).toHaveBeenLastCalledWith({ a: 0, b: 1 }, expect.objectContaining({}))
140+
})
141+
142+
it('works with the deprecated local "onMutate"', async () => {
126143
const onMutate = vi.fn()
127144
const { wrapper } = mountSimple({
128145
mutation: async ({ a, b }: { a: number, b: number }) => {
@@ -137,6 +154,22 @@ describe('useMutation', () => {
137154
wrapper.vm.mutateAsync({ a: 0, b: 1 })
138155
expect(onMutate).toHaveBeenCalledTimes(2)
139156
expect(onMutate).toHaveBeenLastCalledWith({ a: 0, b: 1 }, expect.objectContaining({}))
157+
expect('"onMutate" option is deprecated').toHaveBeenWarned()
158+
})
159+
160+
it('warns about misusing deprecated "onMutate"', async () => {
161+
const onMutate = vi.fn()
162+
const onBeforeMutate = vi.fn()
163+
const { wrapper } = mountSimple({
164+
mutation: async ({ a, b }: { a: number, b: number }) => {
165+
return a + b
166+
},
167+
onMutate,
168+
onBeforeMutate,
169+
})
170+
wrapper.vm.mutate({ a: 24, b: 42 })
171+
expect('"onMutate" option is deprecated').toHaveBeenWarned()
172+
expect('Use only "onBeforeMutate"').toHaveBeenWarned()
140173
})
141174

142175
it('invokes the "onError" hook if mutation throws', async () => {
@@ -154,36 +187,36 @@ describe('useMutation', () => {
154187
expect(onError).toHaveBeenCalledWith(new Error('24'), 24, expect.objectContaining({}))
155188
})
156189

157-
it('invokes the "onError" hook if onMutate throws', async () => {
190+
it('invokes the "onError" hook if onBeforeMutate throws', async () => {
158191
const onError = vi.fn()
159192
const { wrapper } = mountSimple({
160-
onMutate() {
161-
throw new Error('onMutate')
193+
onBeforeMutate() {
194+
throw new Error('onBeforeMutate')
162195
},
163196
onError,
164197
})
165198

166199
wrapper.vm.mutate()
167200
await flushPromises()
168201
expect(onError).toHaveBeenCalledWith(
169-
new Error('onMutate'),
202+
new Error('onBeforeMutate'),
170203
undefined,
171204
expect.objectContaining({}),
172205
)
173206
})
174207

175-
it('passes the returned value from onMutate to onError', async () => {
208+
it('passes the returned value from onBeforeMutate to onError', async () => {
176209
const onError = vi.fn()
177210
const { wrapper, mutation } = mountSimple({
178-
onMutate: () => ({ foo: 'bar' }),
211+
onBeforeMutate: () => ({ foo: 'bar' }),
179212
onError,
180213
})
181214

182-
mutation.mockRejectedValueOnce(new Error('onMutate'))
215+
mutation.mockRejectedValueOnce(new Error('onBeforeMutate'))
183216
wrapper.vm.mutate()
184217
await flushPromises()
185218
expect(onError).toHaveBeenCalledWith(
186-
new Error('onMutate'),
219+
new Error('onBeforeMutate'),
187220
undefined,
188221
expect.objectContaining({ foo: 'bar' }),
189222
)
@@ -205,12 +238,12 @@ describe('useMutation', () => {
205238
expect(wrapper.vm.error).toEqual(null)
206239
})
207240

208-
it('awaits the "onMutate" hook before mutation', async () => {
209-
const onMutate = vi.fn(async () => delay(10))
210-
const { wrapper, mutation } = mountSimple({ onMutate })
241+
it('awaits the "onBeforeMutate" hook before mutation', async () => {
242+
const onBeforeMutate = vi.fn(async () => delay(10))
243+
const { wrapper, mutation } = mountSimple({ onBeforeMutate })
211244

212245
wrapper.vm.mutate()
213-
expect(onMutate).toHaveBeenCalled()
246+
expect(onBeforeMutate).toHaveBeenCalled()
214247
expect(mutation).not.toHaveBeenCalled()
215248
vi.advanceTimersByTime(10)
216249
expect(mutation).not.toHaveBeenCalled()
@@ -249,14 +282,14 @@ describe('useMutation', () => {
249282
expect(wrapper.vm.error).toEqual(new Error('onSuccess'))
250283
})
251284

252-
it('sets the error if "onMutate" throws', async () => {
253-
const onMutate = vi.fn().mockRejectedValueOnce(new Error('onMutate'))
254-
const { wrapper } = mountSimple({ onMutate })
285+
it('sets the error if "onBeforeMutate" throws', async () => {
286+
const onBeforeMutate = vi.fn().mockRejectedValueOnce(new Error('onBeforeMutate'))
287+
const { wrapper } = mountSimple({ onBeforeMutate })
255288

256289
wrapper.vm.mutate()
257290
await flushPromises()
258-
expect(onMutate).toHaveBeenCalled()
259-
expect(wrapper.vm.error).toEqual(new Error('onMutate'))
291+
expect(onBeforeMutate).toHaveBeenCalled()
292+
expect(wrapper.vm.error).toEqual(new Error('onBeforeMutate'))
260293
})
261294

262295
describe('invokes the "onSettled" hook', () => {
@@ -296,37 +329,37 @@ describe('useMutation', () => {
296329
})
297330
})
298331

299-
it('triggers global onMutate', async () => {
300-
const onMutate = vi.fn()
332+
it('triggers global onBeforeMutate', async () => {
333+
const onBeforeMutate = vi.fn()
301334
const { wrapper } = mountSimple(
302335
{},
303336
{
304-
plugins: [createPinia(), [PiniaColada, { mutationOptions: { onMutate } }]],
337+
plugins: [createPinia(), [PiniaColada, { mutationOptions: { onBeforeMutate } }]],
305338
},
306339
)
307340

308-
expect(onMutate).toHaveBeenCalledTimes(0)
341+
expect(onBeforeMutate).toHaveBeenCalledTimes(0)
309342
wrapper.vm.mutate()
310343
// no need since it's synchronous
311344
// await flushPromises()
312-
expect(onMutate).toHaveBeenCalledTimes(1)
313-
expect(onMutate).toHaveBeenCalledWith(undefined)
345+
expect(onBeforeMutate).toHaveBeenCalledTimes(1)
346+
expect(onBeforeMutate).toHaveBeenCalledWith(undefined)
314347
})
315348

316-
it('local onMutate receives global onMutate result', async () => {
317-
const onMutate = vi.fn(() => ({ foo: 'bar' }))
349+
it('local onBeforeMutate receives global onBeforeMutate result', async () => {
350+
const onBeforeMutate = vi.fn(() => ({ foo: 'bar' }))
318351
const { wrapper } = mountSimple(
319-
{ onMutate },
352+
{ onBeforeMutate },
320353
{
321354
plugins: [
322355
createPinia(),
323-
[PiniaColada, { mutationOptions: { onMutate: () => ({ global: true }) } }],
356+
[PiniaColada, { mutationOptions: { onBeforeMutate: () => ({ global: true }) } }],
324357
],
325358
},
326359
)
327360

328361
wrapper.vm.mutate()
329-
expect(onMutate).toHaveBeenCalledWith(undefined, { global: true })
362+
expect(onBeforeMutate).toHaveBeenCalledWith(undefined, { global: true })
330363
})
331364

332365
it('triggers global onSuccess', async () => {

0 commit comments

Comments
 (0)