Skip to content

Commit

Permalink
feat: offline mutations
Browse files Browse the repository at this point in the history
optimistically set paused state depending on if we can fetch or not to avoid an intermediate state where we are loading but not paused
  • Loading branch information
TkDodo committed Dec 4, 2021
1 parent 0b7d6ef commit beb39f2
Show file tree
Hide file tree
Showing 2 changed files with 97 additions and 2 deletions.
4 changes: 2 additions & 2 deletions src/core/mutation.ts
Expand Up @@ -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
Expand Down Expand Up @@ -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,
}
Expand Down
95 changes: 95 additions & 0 deletions src/reactjs/tests/useMutation.test.tsx
Expand Up @@ -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 (
<div>
<button onClick={() => mutation.mutate('todo')}>mutate</button>
<div>
data: {mutation.data ?? 'null'}, status: {mutation.status},
isPaused: {String(mutation.isPaused)}
</div>
</div>
)
}

const rendered = renderWithClient(queryClient, <Page />)

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<string> = []

function Page() {
const mutation = useMutation(async (_text: string) => {
count++
await sleep(10)
return count
})

states.push(`${mutation.status}, ${mutation.isPaused}`)

return (
<div>
<button onClick={() => mutation.mutate('todo')}>mutate</button>
<div>
data: {mutation.data ?? 'null'}, status: {mutation.status},
isPaused: {String(mutation.isPaused)}
</div>
</div>
)
}

const rendered = renderWithClient(queryClient, <Page />)

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)
Expand Down

0 comments on commit beb39f2

Please sign in to comment.