Skip to content

Commit beb39f2

Browse files
committed
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
1 parent 0b7d6ef commit beb39f2

File tree

2 files changed

+97
-2
lines changed

2 files changed

+97
-2
lines changed

src/core/mutation.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import type { MutationObserver } from './mutationObserver'
44
import { getLogger } from './logger'
55
import { notifyManager } from './notifyManager'
66
import { Removable } from './removable'
7-
import { Retryer } from './retryer'
7+
import { canFetch, Retryer } from './retryer'
88
import { noop } from './utils'
99

1010
// TYPES
@@ -330,7 +330,7 @@ export class Mutation<
330330
context: action.context,
331331
data: undefined,
332332
error: null,
333-
isPaused: false,
333+
isPaused: !canFetch(this.options.networkMode),
334334
status: 'loading',
335335
variables: action.variables,
336336
}

src/reactjs/tests/useMutation.test.tsx

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -445,6 +445,101 @@ describe('useMutation', () => {
445445
onlineMock.mockRestore()
446446
})
447447

448+
it('should call onMutate even if paused', async () => {
449+
const onlineMock = mockNavigatorOnLine(false)
450+
const onMutate = jest.fn()
451+
let count = 0
452+
453+
function Page() {
454+
const mutation = useMutation(
455+
async (_text: string) => {
456+
count++
457+
await sleep(10)
458+
return count
459+
},
460+
{
461+
onMutate,
462+
}
463+
)
464+
465+
return (
466+
<div>
467+
<button onClick={() => mutation.mutate('todo')}>mutate</button>
468+
<div>
469+
data: {mutation.data ?? 'null'}, status: {mutation.status},
470+
isPaused: {String(mutation.isPaused)}
471+
</div>
472+
</div>
473+
)
474+
}
475+
476+
const rendered = renderWithClient(queryClient, <Page />)
477+
478+
await rendered.findByText('data: null, status: idle, isPaused: false')
479+
480+
rendered.getByRole('button', { name: /mutate/i }).click()
481+
482+
await rendered.findByText('data: null, status: loading, isPaused: true')
483+
484+
expect(onMutate).toHaveBeenCalledTimes(1)
485+
expect(onMutate).toHaveBeenCalledWith('todo')
486+
487+
onlineMock.mockReturnValue(true)
488+
window.dispatchEvent(new Event('online'))
489+
490+
await rendered.findByText('data: 1, status: success, isPaused: false')
491+
492+
expect(onMutate).toHaveBeenCalledTimes(1)
493+
expect(count).toBe(1)
494+
495+
onlineMock.mockRestore()
496+
})
497+
498+
it('should optimistically go to paused state if offline', async () => {
499+
const onlineMock = mockNavigatorOnLine(false)
500+
let count = 0
501+
const states: Array<string> = []
502+
503+
function Page() {
504+
const mutation = useMutation(async (_text: string) => {
505+
count++
506+
await sleep(10)
507+
return count
508+
})
509+
510+
states.push(`${mutation.status}, ${mutation.isPaused}`)
511+
512+
return (
513+
<div>
514+
<button onClick={() => mutation.mutate('todo')}>mutate</button>
515+
<div>
516+
data: {mutation.data ?? 'null'}, status: {mutation.status},
517+
isPaused: {String(mutation.isPaused)}
518+
</div>
519+
</div>
520+
)
521+
}
522+
523+
const rendered = renderWithClient(queryClient, <Page />)
524+
525+
await rendered.findByText('data: null, status: idle, isPaused: false')
526+
527+
rendered.getByRole('button', { name: /mutate/i }).click()
528+
529+
await rendered.findByText('data: null, status: loading, isPaused: true')
530+
531+
// no intermediate 'loading, false' state is expected because we don't start mutating!
532+
expect(states[0]).toBe('idle, false')
533+
expect(states[1]).toBe('loading, true')
534+
535+
onlineMock.mockReturnValue(true)
536+
window.dispatchEvent(new Event('online'))
537+
538+
await rendered.findByText('data: 1, status: success, isPaused: false')
539+
540+
onlineMock.mockRestore()
541+
})
542+
448543
it('should be able to retry a mutation when online', async () => {
449544
const consoleMock = mockConsoleError()
450545
const onlineMock = mockNavigatorOnLine(false)

0 commit comments

Comments
 (0)