import * as React from 'react' import { MutationInfo, MutationInfoOptions, getDefaultState, } from '../vanilla/mutationInfo' import { UNDEFINED } from '../vanilla/utils' import { useQueryClient } from './QueryClientProvider' import { shouldThrowError } from './utils' export interface UseMutationOptions< TData = unknown, TVars = void, TError = Error > extends Omit, 'defaulted'> { throwOnError?: boolean | ((error: TError) => boolean) } export interface TriggerOptions< TData = unknown, TVars = void, TError = Error > extends Pick< MutationInfoOptions, 'onError' | 'onSettled' | 'onSuccess' > {} export type TriggerFn = ( variables: TVars extends void ? void | TVars : TVars, mutateOptions?: TriggerOptions ) => Promise export type UseMutationResult< TData = unknown, TVars = void, TError = Error > = { data?: TData error: TError | null variables?: TVars isMutating: boolean trigger: TriggerFn reset: () => void } export const useMutation = ( mutaionOptions: UseMutationOptions ): UseMutationResult => { const client = useQueryClient() const currentMutationInfoRef = React.useRef>() const options = client.defaultMutationOptions(mutaionOptions) const optionsRef = React.useRef(options) optionsRef.current = options React.useEffect(() => { currentMutationInfoRef.current?.setOptions(options) }) const [, rerender] = React.useReducer(count => ++count, 0) const [trigger, reset, cleanup] = React.useMemo(() => { let unsubscribe: (() => void) | undefined const cleanup = () => { unsubscribe?.() unsubscribe = UNDEFINED currentMutationInfoRef.current = UNDEFINED } const trigger: TriggerFn = ( variables, mutateOptions ) => { cleanup() const currentMutationInfo = (currentMutationInfoRef.current = client .getMutationCache() .build(client, optionsRef.current)) unsubscribe = currentMutationInfo.subscribe(rerender) return currentMutationInfo.trigger(variables as TVars).then( data => { mutateOptions?.onSuccess?.(data, variables as TVars, currentMutationInfo) mutateOptions?.onSettled?.(data, null, variables as TVars, currentMutationInfo) return data }, error => { mutateOptions?.onError?.(error, variables as TVars, currentMutationInfo) mutateOptions?.onSettled?.( UNDEFINED, error, variables as TVars, currentMutationInfo ) throw error } ) } const reset = () => { cleanup() rerender() } return [trigger, reset, cleanup] }, [client]) // Cleanup on unmount React.useEffect(() => cleanup, [cleanup]) const state = currentMutationInfoRef.current?.state ?? getDefaultState() // Throw error if needed if (state.error && shouldThrowError(options.throwOnError, [state.error])) { throw state.error } return { data: state.data, error: state.error, variables: state.variables, isMutating: state.status === 'mutating', trigger, reset, } }