Skip to content

Commit

Permalink
feat(mutation): allow passing the context to mutation
Browse files Browse the repository at this point in the history
  • Loading branch information
posva committed Mar 18, 2024
1 parent 5c97b69 commit b9acca0
Show file tree
Hide file tree
Showing 3 changed files with 51 additions and 11 deletions.
17 changes: 16 additions & 1 deletion src/use-mutation.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,17 +68,25 @@ it('can infer the data from the mutation', () => {

it('can infer the context from sync onMutate', () => {
useMutation({
mutation: () => Promise.resolve(42),
onMutate() {
return { foo: 'bar' }
},
mutation: async (vars: number, context) => {
expectTypeOf(vars).toBeNumber()
expectTypeOf(context).not.toBeAny()
expectTypeOf(context).toEqualTypeOf<{ foo: string }>()
return 42
},
onSuccess(context) {
expectTypeOf(context).not.toBeAny()
expectTypeOf(context).toMatchTypeOf<{ foo: string }>()
},
onError(context) {
expectTypeOf(context).not.toBeAny()
expectTypeOf(context).toMatchTypeOf<{ foo: string }>()
},
onSettled(context) {
expectTypeOf(context).not.toBeAny()
expectTypeOf(context).toMatchTypeOf<{ foo: string }>()
},
})
Expand All @@ -101,6 +109,7 @@ it('can return undefined in onMutate', () => {
return undefined
},
onSuccess(context) {
expectTypeOf(context).not.toBeAny()
expectTypeOf(context).toMatchTypeOf<{
data: number
}>()
Expand All @@ -124,12 +133,15 @@ it('can infer the context from async onMutate', () => {
return { foo: 'bar' }
},
onSuccess(context) {
expectTypeOf(context).not.toBeAny()
expectTypeOf(context).toMatchTypeOf<{ foo: string }>()
},
onError(context) {
expectTypeOf(context).not.toBeAny()
expectTypeOf(context).toMatchTypeOf<{ foo: string }>()
},
onSettled(context) {
expectTypeOf(context).not.toBeAny()
expectTypeOf(context).toMatchTypeOf<{ foo: string }>()
},
})
Expand All @@ -143,18 +155,21 @@ it('can infer a context of void', () => {
},

onSuccess(context) {
expectTypeOf(context).not.toBeAny()
expectTypeOf(context).toMatchTypeOf<{
data: number
vars: undefined | void
}>()
},
onError(context) {
expectTypeOf(context).not.toBeAny()
expectTypeOf(context).toMatchTypeOf<{
error: unknown
vars: undefined | void
}>()
},
onSettled(context) {
expectTypeOf(context).not.toBeAny()
expectTypeOf(context).toMatchTypeOf<{
data: number | undefined
error: unknown | null
Expand Down
30 changes: 23 additions & 7 deletions src/use-mutation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export interface UseMutationOptions<
/**
* The key of the mutation. If the mutation is successful, it will invalidate the query with the same key and refetch it
*/
mutation: (vars: TVars) => Promise<TResult>
mutation: (vars: TVars, context: NoInfer<TContext>) => Promise<TResult>

// TODO: move this to a plugin that calls invalidateEntry()
/**
Expand All @@ -33,27 +33,43 @@ export interface UseMutationOptions<
keys?: _MutationKeys<TVars, TResult>

/**
* Hook to execute a callback when the mutation is triggered
* Runs before the mutation is executed. **It should be placed before `mutation()` for `context` to be inferred**. It
* can return a value that will be passed to `mutation`, `onSuccess`, `onError` and `onSettled`.
*
* @example
* ```ts
* useMutation({
* onMutate() {
* return { foo: 'bar' }
* },
* mutation: (id: number, { foo }) => {
* console.log(foo) // bar
* return fetch(`/api/todos/${id}`)
* },
* onSuccess(context) {
* console.log(context.foo) // bar
* },
* })
* ```
*/
onMutate?: (vars: TVars) => _Awaitable<TContext>

/**
* Hook to execute a callback in case of error
* Runs if the mutation encounters an error.
*/
onError?: (
context: { error: TError, vars: TVars } & _ReduceContext<TContext>,
) => unknown
// onError?: (context: { error: TError, vars: TParams } & TContext) => Promise<TContext | void> | TContext | void

/**
* Hook to execute a callback in case of error
* Runs if the mutation is successful.
*/
onSuccess?: (
context: { data: TResult, vars: TVars } & _ReduceContext<TContext>,
) => unknown

/**
* Hook to execute a callback in case of error
* Runs after the mutation is settled, regardless of the result.
*/
onSettled?: (
context: {
Expand Down Expand Up @@ -147,7 +163,7 @@ export function useMutation<
// 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>
context = (await options.onMutate?.(vars)) as _ReduceContext<TContext>

const newData = (currentData = await options.mutation(vars))
const newData = (currentData = await options.mutation(vars, context as TContext))

await options.onSuccess?.({ data: newData, vars, ...context })

Expand Down
15 changes: 12 additions & 3 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export function useEventListener<E extends keyof WindowEventMap>(
target: Window,
event: E,
listener: (this: Window, ev: WindowEventMap[E]) => any,
options?: boolean | AddEventListenerOptions
options?: boolean | AddEventListenerOptions,
): void

/**
Expand All @@ -27,7 +27,7 @@ export function useEventListener<E extends keyof DocumentEventMap>(
target: Document,
event: E,
listener: (this: Document, ev: DocumentEventMap[E]) => any,
options?: boolean | AddEventListenerOptions
options?: boolean | AddEventListenerOptions,
): void

export function useEventListener(
Expand Down Expand Up @@ -90,10 +90,19 @@ export function stringifyFlatObject(obj: _ObjectFlat | _JSONPrimitive): string {
: String(obj)
}

/**
* Merges two types when the second one can be null | undefined. Allows to safely use the returned type for { ...a,
* ...undefined, ...null }
* @internal
*/
export type _MergeObjects<Obj, MaybeNull> = MaybeNull extends undefined | null
? Obj
: _Simplify<Obj & MaybeNull>

/**
* @internal
*/
export const noop = () => {}
export const noop = () => { }

/**
* Creates a delayed computed ref from an existing ref, computed, or getter. Use this to delay a loading state (`isFetching`, `isLoading`) to avoid flickering.
Expand Down

0 comments on commit b9acca0

Please sign in to comment.