Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 71 additions & 0 deletions src/lib/queries/mutationOptions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import type { UseMutationOptions } from "@tanstack/react-query"
import type { Observable } from "rxjs"

/**
* Resolves the public `mutationFn` option — an Observable or a function
* returning one — to its source Observable for the given variables.
*/
export function resolveMutationFnSource<TData, TVariables>(
mutationFn:
| ((variables: TVariables) => Observable<TData>)
| Observable<TData>,
variables: TVariables,
) {
return typeof mutationFn === "function" ? mutationFn(variables) : mutationFn
}

type MutationCallbacks<TData, TError, TVariables, TOnMutateResult> = Pick<
UseMutationOptions<TData, TError, TVariables, TOnMutateResult>,
"onMutate" | "onSuccess" | "onError" | "onSettled"
>

/**
* Same callbacks, but keyed as required so that spreading the adapted
* callbacks after `...options` overrides (rather than unions with) the
* raw-variables callbacks still present in `options`.
*/
type AdaptedMutationCallbacks<TData, TError, TVariables, TOnMutateResult> = {
[K in "onMutate" | "onSuccess" | "onError" | "onSettled"]:
| MutationCallbacks<
TData,
TError,
{ variables: TVariables },
TOnMutateResult
>[K]
| undefined
}

/**
* Hooks built on top of `useMutation$` (`useSwitchMutation$`,
* `useConcatMutation$`) run their inner mutation with the user variables
* wrapped in an envelope (`{ variables, ... }`). This adapts the user-facing
* callbacks, which expect the raw variables, to that envelope.
*/
export function adaptCallbacksToWrappedVariables<
TData,
TError,
TVariables,
TOnMutateResult,
>({
onMutate,
onSuccess,
onError,
onSettled,
}: MutationCallbacks<
TData,
TError,
TVariables,
TOnMutateResult
>): AdaptedMutationCallbacks<TData, TError, TVariables, TOnMutateResult> {
return {
onMutate: onMutate
? ({ variables }, ...rest) => onMutate(variables, ...rest)
: undefined,
onSuccess: (data, { variables }, ...rest) =>
onSuccess?.(data, variables, ...rest),
onError: (error, { variables }, ...rest) =>
onError?.(error, variables, ...rest),
onSettled: (data, error, { variables }, ...rest) =>
onSettled?.(data, error, variables, ...rest),
}
}
27 changes: 7 additions & 20 deletions src/lib/queries/useConcatMutation$.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ import {
type Subject,
switchMap,
} from "rxjs"
import {
adaptCallbacksToWrappedVariables,
resolveMutationFnSource,
} from "./mutationOptions"
import { type UseMutation$Options, useMutation$ } from "./useMutation$"

export function useConcatMutation$<
Expand All @@ -21,10 +25,7 @@ export function useConcatMutation$<
TVariables = void,
TContext = unknown,
>(
{
onMutate,
...options
}: UseMutation$Options<TData | null, TError, TVariables, TContext> & {
options: UseMutation$Options<TData | null, TError, TVariables, TContext> & {
mutationKey: MutationKey
},
queryClient?: QueryClient,
Expand All @@ -41,23 +42,9 @@ export function useConcatMutation$<
>(
{
...options,
onMutate: onMutate
? ({ variables }, ...rest) => onMutate(variables, ...rest)
: undefined,
onSuccess(data, { variables }, ...rest) {
return options.onSuccess?.(data, variables, ...rest)
},
onError(error, { variables }, ...rest) {
return options.onError?.(error, variables, ...rest)
},
onSettled(data, error, { variables }, ...rest) {
return options.onSettled?.(data, error, variables, ...rest)
},
...adaptCallbacksToWrappedVariables(options),
mutationFn: ({ ready$, variables }) => {
const source =
typeof options.mutationFn === "function"
? options.mutationFn(variables)
: options.mutationFn
const source = resolveMutationFnSource(options.mutationFn, variables)

return ready$.pipe(
filter((isReady) => isReady),
Expand Down
6 changes: 2 additions & 4 deletions src/lib/queries/useMutation$.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
import { useEffect } from "react"
import { BehaviorSubject, type Observable, take } from "rxjs"
import { useConstant } from "../utils/react/useConstant"
import { resolveMutationFnSource } from "./mutationOptions"

export type UseMutation$Options<
TData = unknown,
Expand Down Expand Up @@ -53,10 +54,7 @@ export function useMutation$<
let lastData: { value: TData } | undefined

return new Promise<TData>((resolve, reject) => {
const source =
typeof options.mutationFn === "function"
? options.mutationFn(variables)
: options.mutationFn
const source = resolveMutationFnSource(options.mutationFn, variables)

source.pipe(take(1)).subscribe({
next: (data) => {
Expand Down
27 changes: 6 additions & 21 deletions src/lib/queries/useSwitchMutation$.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ import {
tap,
} from "rxjs"
import { useRefOnce } from "../utils"
import {
adaptCallbacksToWrappedVariables,
resolveMutationFnSource,
} from "./mutationOptions"
import { type UseMutation$Options, useMutation$ } from "./useMutation$"

export class SwitchMutationCancelError extends Error {
Expand All @@ -26,9 +30,6 @@ export function useSwitchMutation$<
>(
{
mutationFn,
onMutate,
onError,
onSettled,
...options
}: UseMutation$Options<TData | null, TError, TVariables, TOnMutateResult>,
queryClient?: QueryClient,
Expand Down Expand Up @@ -56,10 +57,7 @@ export function useSwitchMutation$<
throw new SwitchMutationCancelError()
}

const source =
typeof mutationFn === "function"
? mutationFn(variables)
: mutationFn
const source = resolveMutationFnSource(mutationFn, variables)

/**
* `defaultIfEmpty` must sit on the source itself: the abort stream
Expand All @@ -79,20 +77,7 @@ export function useSwitchMutation$<
},
[mutationFn],
),
onMutate: onMutate
? ({ variables }, ...rest) => {
return onMutate(variables, ...rest)
}
: undefined,
onSuccess: (data, { variables }, ...rest) => {
return options.onSuccess?.(data, variables, ...rest)
},
onError: (error, { variables }, ...rest) => {
return onError?.(error, variables, ...rest)
},
onSettled: (data, error, { variables }, ...rest) => {
return onSettled?.(data, error, variables, ...rest)
},
...adaptCallbacksToWrappedVariables(options),
},
queryClient,
)
Expand Down