diff --git a/src/core/mutation.ts b/src/core/mutation.ts index ee5d778bf1..37c514a917 100644 --- a/src/core/mutation.ts +++ b/src/core/mutation.ts @@ -4,7 +4,7 @@ import type { MutationObserver } from './mutationObserver' import { getLogger } from './logger' import { notifyManager } from './notifyManager' import { Removable } from './removable' -import { Retryer } from './retryer' +import { canFetch, Retryer } from './retryer' import { noop } from './utils' // TYPES @@ -291,7 +291,7 @@ export class Mutation< } private dispatch(action: Action): void { - this.state = reducer(this.state, action) + this.state = this.reducer(action) notifyManager.batch(() => { this.observers.forEach(observer => { @@ -304,6 +304,62 @@ export class Mutation< }) }) } + + private reducer( + action: Action + ): MutationState { + switch (action.type) { + case 'failed': + return { + ...this.state, + failureCount: this.state.failureCount + 1, + } + case 'pause': + return { + ...this.state, + isPaused: true, + } + case 'continue': + return { + ...this.state, + isPaused: false, + } + case 'loading': + return { + ...this.state, + context: action.context, + data: undefined, + error: null, + isPaused: !canFetch(this.options.networkMode), + status: 'loading', + variables: action.variables, + } + case 'success': + return { + ...this.state, + data: action.data, + error: null, + status: 'success', + isPaused: false, + } + case 'error': + return { + ...this.state, + data: undefined, + error: action.error, + failureCount: this.state.failureCount + 1, + isPaused: false, + status: 'error', + } + case 'setState': + return { + ...this.state, + ...action.state, + } + default: + return this.state + } + } } export function getDefaultState< @@ -322,60 +378,3 @@ export function getDefaultState< variables: undefined, } } - -function reducer( - state: MutationState, - action: Action -): MutationState { - switch (action.type) { - case 'failed': - return { - ...state, - failureCount: state.failureCount + 1, - } - case 'pause': - return { - ...state, - isPaused: true, - } - case 'continue': - return { - ...state, - isPaused: false, - } - case 'loading': - return { - ...state, - context: action.context, - data: undefined, - error: null, - isPaused: false, - status: 'loading', - variables: action.variables, - } - case 'success': - return { - ...state, - data: action.data, - error: null, - status: 'success', - isPaused: false, - } - case 'error': - return { - ...state, - data: undefined, - error: action.error, - failureCount: state.failureCount + 1, - isPaused: false, - status: 'error', - } - case 'setState': - return { - ...state, - ...action.state, - } - default: - return state - } -} diff --git a/src/reactjs/tests/useMutation.test.tsx b/src/reactjs/tests/useMutation.test.tsx index 799be48033..cae6d06d4f 100644 --- a/src/reactjs/tests/useMutation.test.tsx +++ b/src/reactjs/tests/useMutation.test.tsx @@ -445,6 +445,101 @@ describe('useMutation', () => { onlineMock.mockRestore() }) + it('should call onMutate even if paused', async () => { + const onlineMock = mockNavigatorOnLine(false) + const onMutate = jest.fn() + let count = 0 + + function Page() { + const mutation = useMutation( + async (_text: string) => { + count++ + await sleep(10) + return count + }, + { + onMutate, + } + ) + + return ( +
+ +
+ data: {mutation.data ?? 'null'}, status: {mutation.status}, + isPaused: {String(mutation.isPaused)} +
+
+ ) + } + + const rendered = renderWithClient(queryClient, ) + + await rendered.findByText('data: null, status: idle, isPaused: false') + + rendered.getByRole('button', { name: /mutate/i }).click() + + await rendered.findByText('data: null, status: loading, isPaused: true') + + expect(onMutate).toHaveBeenCalledTimes(1) + expect(onMutate).toHaveBeenCalledWith('todo') + + onlineMock.mockReturnValue(true) + window.dispatchEvent(new Event('online')) + + await rendered.findByText('data: 1, status: success, isPaused: false') + + expect(onMutate).toHaveBeenCalledTimes(1) + expect(count).toBe(1) + + onlineMock.mockRestore() + }) + + it('should optimistically go to paused state if offline', async () => { + const onlineMock = mockNavigatorOnLine(false) + let count = 0 + const states: Array = [] + + function Page() { + const mutation = useMutation(async (_text: string) => { + count++ + await sleep(10) + return count + }) + + states.push(`${mutation.status}, ${mutation.isPaused}`) + + return ( +
+ +
+ data: {mutation.data ?? 'null'}, status: {mutation.status}, + isPaused: {String(mutation.isPaused)} +
+
+ ) + } + + const rendered = renderWithClient(queryClient, ) + + await rendered.findByText('data: null, status: idle, isPaused: false') + + rendered.getByRole('button', { name: /mutate/i }).click() + + await rendered.findByText('data: null, status: loading, isPaused: true') + + // no intermediate 'loading, false' state is expected because we don't start mutating! + expect(states[0]).toBe('idle, false') + expect(states[1]).toBe('loading, true') + + onlineMock.mockReturnValue(true) + window.dispatchEvent(new Event('online')) + + await rendered.findByText('data: 1, status: success, isPaused: false') + + onlineMock.mockRestore() + }) + it('should be able to retry a mutation when online', async () => { const consoleMock = mockConsoleError() const onlineMock = mockNavigatorOnLine(false)