Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: mutate callbacks called if mutation started after unmount #4848

Merged
merged 6 commits into from Jan 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/query-core/src/mutationObserver.ts
Expand Up @@ -166,7 +166,7 @@ export class MutationObserver<
private notify(options: NotifyOptions) {
notifyManager.batch(() => {
// First trigger the mutate callbacks
if (this.mutateOptions) {
if (this.mutateOptions && this.hasListeners()) {
if (options.onSuccess) {
this.mutateOptions.onSuccess?.(
this.currentResult.data!,
Expand Down
67 changes: 29 additions & 38 deletions packages/query-core/src/tests/mutations.test.tsx
Expand Up @@ -308,49 +308,24 @@ describe('mutations', () => {
expect(onSettled).toHaveBeenCalled()
})

test('setState should update the mutation state', async () => {
const mutation = new MutationObserver(queryClient, {
mutationFn: async () => {
return 'update'
},
onMutate: (text) => text,
})
await mutation.mutate()
expect(mutation.getCurrentResult().data).toEqual('update')

// Force setState usage
// because no use case has been found using mutation.setState
const currentMutation = mutation['currentMutation']
currentMutation?.setState({
context: undefined,
variables: undefined,
data: 'new',
error: undefined,
failureCount: 0,
failureReason: null,
isPaused: false,
status: 'success',
})
test('addObserver should not add an existing observer', async () => {
const mutationCache = queryClient.getMutationCache()
const observer = new MutationObserver(queryClient, {})
const currentMutation = mutationCache.build(queryClient, {})

expect(mutation.getCurrentResult().data).toEqual('new')
})
const fn = jest.fn()

test('addObserver should not add an existing observer', async () => {
const mutation = new MutationObserver(queryClient, {
mutationFn: async () => {
return 'update'
},
onMutate: (text) => text,
const unsubscribe = mutationCache.subscribe((event) => {
fn(event.type)
})
await mutation.mutate()

// Force addObserver usage to add an existing observer
// because no use case has been found
const currentMutation = mutation['currentMutation']!
expect(currentMutation['observers'].length).toEqual(1)
currentMutation.addObserver(mutation)
currentMutation.addObserver(observer)
currentMutation.addObserver(observer)

expect(currentMutation['observers'].length).toEqual(1)
expect(fn).toHaveBeenCalledTimes(1)
expect(fn).toHaveBeenCalledWith('observerAdded')

unsubscribe()
})

test('mutate should throw an error if no mutationFn found', async () => {
Expand All @@ -367,4 +342,20 @@ describe('mutations', () => {
}
expect(error).toEqual('No mutationFn found')
})

test('mutate update the mutation state even without an active subscription', async () => {
const onSuccess = jest.fn()
const onSettled = jest.fn()

const mutation = new MutationObserver(queryClient, {
mutationFn: async () => {
return 'update'
},
})

await mutation.mutate(undefined, { onSuccess, onSettled })
expect(mutation.getCurrentResult().data).toEqual('update')
expect(onSuccess).not.toHaveBeenCalled()
expect(onSettled).not.toHaveBeenCalled()
})
})
62 changes: 59 additions & 3 deletions packages/react-query/src/__tests__/useMutation.test.tsx
Expand Up @@ -1044,7 +1044,7 @@ describe('useMutation', () => {
)
})

test('should go to error state if onSuccess callback errors', async () => {
it('should go to error state if onSuccess callback errors', async () => {
const error = new Error('error from onSuccess')
const onError = jest.fn()

Expand Down Expand Up @@ -1079,7 +1079,7 @@ describe('useMutation', () => {
expect(onError).toHaveBeenCalledWith(error, 'todo', undefined)
})

test('should go to error state if onError callback errors', async () => {
it('should go to error state if onError callback errors', async () => {
const error = new Error('error from onError')
const mutateFnError = new Error('mutateFnError')

Expand Down Expand Up @@ -1115,7 +1115,7 @@ describe('useMutation', () => {
await rendered.findByText('error: mutateFnError, status: error')
})

test('should go to error state if onSettled callback errors', async () => {
it('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()
Expand Down Expand Up @@ -1154,4 +1154,60 @@ describe('useMutation', () => {

expect(onError).toHaveBeenCalledWith(mutateFnError, 'todo', undefined)
})

it('should not call mutate callbacks for mutations started after unmount', async () => {
const onSuccessMutate = jest.fn()
const onSuccessUseMutation = jest.fn()
const onSettledMutate = jest.fn()
const onSettledUseMutation = jest.fn()

function Page() {
const [show, setShow] = React.useState(true)
return (
<div>
<button onClick={() => setShow(false)}>hide</button>
{show && <Component />}
</div>
)
}

function Component() {
const mutation = useMutation({
mutationFn: async (text: string) => {
await sleep(10)
TkDodo marked this conversation as resolved.
Show resolved Hide resolved
return text
},
onSuccess: onSuccessUseMutation,
onSettled: onSettledUseMutation,
})

return (
<div>
<button
onClick={() => {
setActTimeout(() => {
mutation.mutate('todo', {
onSuccess: onSuccessMutate,
onSettled: onSettledMutate,
})
}, 10)
}}
>
mutate
</button>
</div>
)
}

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

fireEvent.click(rendered.getByRole('button', { name: /mutate/i }))
fireEvent.click(rendered.getByRole('button', { name: /hide/i }))

await waitFor(() => expect(onSuccessUseMutation).toHaveBeenCalledTimes(1))
await waitFor(() => expect(onSettledUseMutation).toHaveBeenCalledTimes(1))

expect(onSuccessMutate).toHaveBeenCalledTimes(0)
expect(onSettledMutate).toHaveBeenCalledTimes(0)
})
})