import { doRetry, GQlessClient, GQlessError, RetryOptions } from 'gqless'; import { Dispatch, useCallback, useMemo, useReducer, useRef } from 'react'; import { OnErrorHandler, useDeferDispatch, useSuspensePromise, } from '../common'; import { ReactClientOptionsWithDefaults } from '../utils'; export interface UseMutationOptions { noCache?: boolean; onCompleted?: (data: TData) => void; onError?: OnErrorHandler; /** * Retry behaviour * * @default false */ retry?: RetryOptions; /** * Refetch specific queries after mutation completion. * * You can give functions or parts of the schema to be refetched */ refetchQueries?: unknown[]; /** * Await refetch resolutions before calling the mutation actually complete */ awaitRefetchQueries?: boolean; /** * Enable suspense behavior */ suspense?: boolean; /** * Activate special handling of non-serializable variables, * for example, files uploading * * @default false */ nonSerializableVariables?: boolean; } export interface UseMutationState { data: TData | undefined; error?: GQlessError; isLoading: boolean; } type UseMutationReducerAction = | { type: 'success'; data: TData } | { type: 'failure'; error: GQlessError } | { type: 'loading' }; function UseMutationReducer( state: UseMutationState, action: UseMutationReducerAction ): UseMutationState { switch (action.type) { case 'loading': { if (state.isLoading) return state; return { data: state.data, isLoading: true, }; } case 'success': { return { data: action.data, isLoading: false, }; } case 'failure': { return { data: state.data, isLoading: false, error: action.error, }; } } } function InitUseMutationReducer(): UseMutationState { return { data: undefined, isLoading: false, }; } export interface UseMutation< GeneratedSchema extends { mutation: object; } > { ( mutationFn?: (mutation: GeneratedSchema['mutation'], args: TArgs) => TData, options?: UseMutationOptions ): readonly [ ( ...opts: undefined extends TArgs ? [ { fn?: ( mutation: GeneratedSchema['mutation'], args: TArgs ) => TData; args?: TArgs; }? ] : [ { fn?: ( mutation: GeneratedSchema['mutation'], args: TArgs ) => TData; args: TArgs; } ] ) => Promise, UseMutationState ]; } export function createUseMutation< GeneratedSchema extends { mutation: object; query: object; subscription: object; } >( client: GQlessClient, { defaults: { mutationSuspense: defaultSuspense }, }: ReactClientOptionsWithDefaults ) { const { resolved, refetch } = client; const clientMutation: GeneratedSchema['mutation'] = client.mutation; const useMutation: UseMutation = function useMutation< TData, TArgs = undefined >( mutationFn?: (mutation: typeof clientMutation, args: TArgs) => TData, opts: UseMutationOptions = {} ): readonly [ ({ fn: fnArg, args, }?: { fn?: (mutation: GeneratedSchema['mutation'], args: TArgs) => TData; args?: TArgs; }) => Promise, UseMutationState ] { const optsRef = useRef(opts); optsRef.current = Object.assign({}, opts); optsRef.current.suspense ??= defaultSuspense; const setSuspensePromise = useSuspensePromise(optsRef); const [state, dispatchReducer] = useReducer( UseMutationReducer, undefined, InitUseMutationReducer ) as [UseMutationState, Dispatch>]; const dispatch = useDeferDispatch(dispatchReducer); const fnRef = useRef(mutationFn); fnRef.current = mutationFn; const callRefetchQueries = useCallback((): Promise | void => { const { refetchQueries, awaitRefetchQueries } = optsRef.current; if (refetchQueries?.length) { const refetchPromise = Promise.all( refetchQueries.map((v) => refetch(v)) ).catch((err) => { dispatch({ type: 'failure', error: GQlessError.create(err, useMutation), }); }); if (awaitRefetchQueries) return refetchPromise; } }, [optsRef, dispatch]); const mutate = useCallback( function mutateFn({ fn: fnArg, args, }: { fn?: typeof mutationFn; args?: any } = {}) { dispatch({ type: 'loading' }); const refFn = fnRef.current; const functionResolve = fnArg ? () => fnArg(clientMutation, args) : refFn ? () => refFn(clientMutation, args) : (() => { throw new GQlessError( 'You have to specify a function to be resolved', { caller: mutateFn, } ); })(); return resolved(functionResolve, { noCache: optsRef.current.noCache, refetch: true, nonSerializableVariables: optsRef.current.nonSerializableVariables, }).then( async (data) => { const refetchingQueries = callRefetchQueries(); if (refetchingQueries) await refetchingQueries; optsRef.current.onCompleted?.(data); dispatch({ type: 'success', data, }); return data; }, (err: unknown) => { const error = GQlessError.create(err, useMutation); optsRef.current.onError?.(error); dispatch({ type: 'failure', error, }); throw error; } ); }, [optsRef, fnRef, dispatch, callRefetchQueries] ); const { retry = false } = opts; return useMemo(() => { const fn: typeof mutate = retry ? (...args: any[]) => { const promise = mutate(...args).catch((err) => { doRetry(retry, { onRetry: () => { const promise = mutate(...args).then(() => {}); setSuspensePromise(promise); return promise; }, }); throw err; }); setSuspensePromise(promise); return promise; } : mutate; return [fn, state]; }, [state, mutate, retry, optsRef, setSuspensePromise]); }; return useMutation; }