Skip to content

Commit

Permalink
feat(useMutation): add hook context
Browse files Browse the repository at this point in the history
  • Loading branch information
Elise Patrikainen authored and Elise Patrikainen committed Mar 16, 2024
1 parent c44af13 commit 0894a81
Show file tree
Hide file tree
Showing 4 changed files with 36 additions and 29 deletions.
1 change: 0 additions & 1 deletion playground/src/components/TheSlideOver.vue
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@ const open = defineModel<boolean>('open', { default: false })
<button
type="button"
class="relative text-gray-400 bg-white rounded-md hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
@click="open = false"
>
<span class="absolute -inset-2.5" />
<span class="sr-only">Close panel</span>
Expand Down
4 changes: 3 additions & 1 deletion playground/src/pages/ecom/item/[id].vue
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,11 @@ const { mutate: bookProduct } = useMutation({
mutation: (product: ProductListItem) => changeProductAvailability(product, undefined, 1500),
// NOTE: the optimistic update only works if there are no parallele updates
onMutate: (product) => {
const context = { previousAvailability: product.availability }
itemAvailability.value = product.availability - 1
return context
},
onError() {
onError({ context }) {

Check failure on line 35 in playground/src/pages/ecom/item/[id].vue

View workflow job for this annotation

GitHub Actions / build

'context' is defined but never used. Allowed unused args must match /^_/u
itemAvailability.value = item.value?.availability
},
onSuccess(data) {
Expand Down
33 changes: 15 additions & 18 deletions src/use-mutation.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,36 +58,33 @@ describe('useMutation', () => {
})

it('invokes the `onMutate` hook', async () => {
let foo
const onMutate = vi.fn(() => 0)
const { wrapper } = mountSimple({
mutation: async (arg: number) => {
mutation: async (arg1: number, arg2: number) => {
await delay(0)
return arg + 42
return arg1 + arg2
},
onMutate: arg => foo = arg,
onMutate,
})

expect(foo).toBeUndefined()
wrapper.vm.mutate(24)
expect(foo).toBe(24)
expect(onMutate).not.toHaveBeenCalled()
wrapper.vm.mutate(24, 42)
expect(onMutate).toHaveBeenCalledWith(24, 42)
})

it('invokes the `onError` hook', async () => {
let foo
const onError = vi.fn(() => 0)
const { wrapper } = mountSimple({
mutation: async () => {
mutation: async (arg1: number, arg2: number) => {
await delay(0)
throw new Error('bar')
},
onError(err) {
foo = err.message
throw new Error(String(arg1 + arg2))
},
onError,
})

expect(wrapper.vm.mutate()).rejects.toThrow()
expect(foo).toBeUndefined()
expect(wrapper.vm.mutate(24, 42)).rejects.toThrow()
expect(onError).not.toHaveBeenCalled()
await runTimers()
expect(foo).toBe('bar')
expect(onError).toHaveBeenCalledWith(24, 42)
})

it('invokes the `onSuccess` hook', async () => {
Expand Down Expand Up @@ -124,7 +121,7 @@ describe('useMutation', () => {
const { wrapper } = mountSimple({
mutation: async () => {
await delay(0)
throw new Error('bar')
throw new Error('foobar')
},
onSettled() {
foo = 24
Expand Down
27 changes: 18 additions & 9 deletions src/use-mutation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ type _MutationKeys<TParams extends readonly any[], TResult> =
export interface UseMutationOptions<
TResult = unknown,
TParams extends readonly unknown[] = readonly [],
TError = ErrorDefault,
TContext = any, // TODO: type as `unknown`
> {
/**
* The key of the mutation. If the mutation is successful, it will invalidate the query with the same key and refetch it
Expand All @@ -26,22 +28,25 @@ export interface UseMutationOptions<
/**
* Hook to execute a callback when the mutation is triggered
*/
onMutate?: (...args: TParams) => void
onMutate?: (...args: TParams) => Promise<TContext | void> | TContext | void
// onMutate?: (...args: TParams) => TContext

/**
* Hook to execute a callback in case of error
*/
onError?: (error: any) => void
onError?: (context: { error: TError, args: TParams, context: TContext }) => Promise<TContext | void> | TContext | void
// TODO: check that eh contact is well not obligatoire
// onError?: (context: { error: TError, args: TParams } & TContext) => Promise<TContext | void> | TContext | void

/**
* Hook to execute a callback in case of error
*/
onSuccess?: (result: TResult) => void
onSuccess?: (context: { result: TResult, args: TParams, context: TContext }) => Promise<TContext | void> | TContext | void

/**
* Hook to execute a callback in case of error
*/
onSettled?: () => void
onSettled?: (context: { result: TResult, error: TError, args: TParams, context: TContext }) => void

// TODO: invalidate options exact, refetch, etc
}
Expand Down Expand Up @@ -91,6 +96,7 @@ export function useMutation<
TResult,
TParams extends readonly unknown[] = readonly [],
TError = ErrorDefault,
TContext = unknown,
>(
options: UseMutationOptions<TResult, TParams>,
): UseMutationReturn<TResult, TParams, TError> {
Expand All @@ -99,22 +105,24 @@ export function useMutation<
const status = shallowRef<UseQueryStatus>('pending')
const data = shallowRef<TResult>()
const error = shallowRef<TError | null>(null)
let hookContext: TContext

// a pending promise allows us to discard previous ongoing requests
let pendingPromise: Promise<TResult> | null = null
function mutate(...args: TParams) {
// NOTE: do a mutation context?
async function mutate(...args: TParams) {
status.value = 'loading'

if (options.onMutate) {
options.onMutate(...args)
hookContext = options.onMutate(...args)
}

// TODO: AbortSignal that is aborted when the mutation is called again so we can throw in pending
const promise = (pendingPromise = options
.mutation(...args)
.then((_data) => {
if (options.onSuccess) {
options.onSuccess(_data)
options.onSuccess({ result: _data, args, context: hookContext })
}
if (pendingPromise === promise) {
data.value = _data
Expand All @@ -139,13 +147,14 @@ export function useMutation<
status.value = 'error'
}
if (options.onError) {
options.onError(_error)
options.onError({ error: error.value, args, context: hookContext })
}
throw _error
})
.finally(async () => {
if (options.onSettled) {
options.onSettled()
// TODO: TS
options.onSettled({ result: data.value, error: error.value, args, context: hookContext })
}
}))

Expand Down

0 comments on commit 0894a81

Please sign in to comment.