From beb39f2179a96e42666a09e1eb5218ae978dcecb Mon Sep 17 00:00:00 2001 From: Dominik Dorfmeister Date: Sat, 4 Dec 2021 14:56:30 +0100 Subject: [PATCH] feat: offline mutations optimistically set paused state depending on if we can fetch or not to avoid an intermediate state where we are loading but not paused --- src/core/mutation.ts | 4 +- src/reactjs/tests/useMutation.test.tsx | 95 ++++++++++++++++++++++++++ 2 files changed, 97 insertions(+), 2 deletions(-) diff --git a/src/core/mutation.ts b/src/core/mutation.ts index aeb1942a98..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 @@ -330,7 +330,7 @@ export class Mutation< context: action.context, data: undefined, error: null, - isPaused: false, + isPaused: !canFetch(this.options.networkMode), status: 'loading', variables: action.variables, } 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)