diff --git a/src/core/mutation.ts b/src/core/mutation.ts index 2db6a609bf..3880d75cba 100644 --- a/src/core/mutation.ts +++ b/src/core/mutation.ts @@ -160,35 +160,7 @@ export class Mutation< return this.execute() } - execute(): Promise { - let data: TData - - const restored = this.state.status === 'loading' - - let promise = Promise.resolve() - - if (!restored) { - this.dispatch({ type: 'loading', variables: this.options.variables! }) - promise = promise - .then(() => { - // Notify cache callback - this.mutationCache.config.onMutate?.( - this.state.variables, - this as Mutation - ) - }) - .then(() => this.options.onMutate?.(this.state.variables!)) - .then(context => { - if (context !== this.state.context) { - this.dispatch({ - type: 'loading', - context, - variables: this.state.variables, - }) - } - }) - } - + async execute(): Promise { const executeMutation = () => { this.retryer = createRetryer({ fn: () => { @@ -214,38 +186,51 @@ export class Mutation< return this.retryer.promise } - return promise - .then(executeMutation) - .then(result => { - data = result + const restored = this.state.status === 'loading' + try { + if (!restored) { + this.dispatch({ type: 'loading', variables: this.options.variables! }) // Notify cache callback - this.mutationCache.config.onSuccess?.( - data, + this.mutationCache.config.onMutate?.( this.state.variables, - this.state.context, this as Mutation ) - }) - .then(() => - this.options.onSuccess?.( - data, - this.state.variables!, - this.state.context! - ) + const context = await this.options.onMutate?.(this.state.variables!) + if (context !== this.state.context) { + this.dispatch({ + type: 'loading', + context, + variables: this.state.variables, + }) + } + } + const data = await executeMutation() + + // Notify cache callback + this.mutationCache.config.onSuccess?.( + data, + this.state.variables, + this.state.context, + this as Mutation ) - .then(() => - this.options.onSettled?.( - data, - null, - this.state.variables!, - this.state.context - ) + + await this.options.onSuccess?.( + data, + this.state.variables!, + this.state.context! ) - .then(() => { - this.dispatch({ type: 'success', data }) - return data - }) - .catch(error => { + + await this.options.onSettled?.( + data, + null, + this.state.variables!, + this.state.context + ) + + this.dispatch({ type: 'success', data }) + return data + } catch (error) { + try { // Notify cache callback this.mutationCache.config.onError?.( error, @@ -258,27 +243,23 @@ export class Mutation< this.logger.error(error) } - return Promise.resolve() - .then(() => - this.options.onError?.( - error, - this.state.variables!, - this.state.context - ) - ) - .then(() => - this.options.onSettled?.( - undefined, - error, - this.state.variables!, - this.state.context - ) - ) - .then(() => { - this.dispatch({ type: 'error', error }) - throw error - }) - }) + await this.options.onError?.( + error as TError, + this.state.variables!, + this.state.context + ) + + await this.options.onSettled?.( + undefined, + error as TError, + this.state.variables!, + this.state.context + ) + throw error + } finally { + this.dispatch({ type: 'error', error: error as TError }) + } + } } private dispatch(action: Action): void { diff --git a/src/reactjs/tests/useMutation.test.tsx b/src/reactjs/tests/useMutation.test.tsx index 8bf5e06b33..24b9f41c2c 100644 --- a/src/reactjs/tests/useMutation.test.tsx +++ b/src/reactjs/tests/useMutation.test.tsx @@ -855,4 +855,115 @@ describe('useMutation', () => { undefined ) }) + + test('should go to error state if onSuccess callback errors', async () => { + const error = new Error('error from onSuccess') + const onError = jest.fn() + + function Page() { + const mutation = useMutation( + async (_text: string) => { + await sleep(10) + return 'result' + }, + { + onSuccess: () => Promise.reject(error), + onError, + } + ) + + return ( +
+ +
status: {mutation.status}
+
+ ) + } + + const rendered = renderWithClient(queryClient, ) + + await rendered.findByText('status: idle') + + rendered.getByRole('button', { name: /mutate/i }).click() + + await rendered.findByText('status: error') + + expect(onError).toHaveBeenCalledWith(error, 'todo', undefined) + }) + + test('should go to error state if onError callback errors', async () => { + const error = new Error('error from onError') + const mutateFnError = new Error('mutateFnError') + + function Page() { + const mutation = useMutation( + async (_text: string) => { + await sleep(10) + throw mutateFnError + }, + { + onError: () => Promise.reject(error), + } + ) + + return ( +
+ +
+ error:{' '} + {mutation.error instanceof Error ? mutation.error.message : 'null'}, + status: {mutation.status} +
+
+ ) + } + + const rendered = renderWithClient(queryClient, ) + + await rendered.findByText('error: null, status: idle') + + rendered.getByRole('button', { name: /mutate/i }).click() + + await rendered.findByText('error: mutateFnError, status: error') + }) + + test('should go to error state if onSettled callback errors', async () => { + const error = new Error('error from onSettled') + const mutateFnError = new Error('mutateFnError') + const onError = jest.fn() + + function Page() { + const mutation = useMutation( + async (_text: string) => { + await sleep(10) + throw mutateFnError + }, + { + onSettled: () => Promise.reject(error), + onError, + } + ) + + return ( +
+ +
+ error:{' '} + {mutation.error instanceof Error ? mutation.error.message : 'null'}, + status: {mutation.status} +
+
+ ) + } + + const rendered = renderWithClient(queryClient, ) + + await rendered.findByText('error: null, status: idle') + + rendered.getByRole('button', { name: /mutate/i }).click() + + await rendered.findByText('error: mutateFnError, status: error') + + expect(onError).toHaveBeenCalledWith(mutateFnError, 'todo', undefined) + }) })