snapshot of https://github.com/TanStack/query.git (main, 2026-09-16; blobs > 131072B omitted)
Some checks failed
autofix.ci / autofix (push) Failing after 4s
Release / Release (push) Has been skipped
GitHub Actions Security Analysis / Run zizmor (push) Failing after 2s

This commit is contained in:
upstream-mirror
2026-09-16 01:13:08 +05:00
commit 3a013ccdfe
3570 changed files with 490194 additions and 0 deletions

View File

@@ -0,0 +1,30 @@
<script lang="ts">
import type { QueryClient } from '@tanstack/query-core'
import { createMutation, setQueryClientContext } from '../../src/index.js'
import { sleep } from '@tanstack/query-test-utils'
type Props = {
queryClient: QueryClient
onSuccessPerCall: (...args: Array<unknown>) => void
}
const { queryClient, onSuccessPerCall }: Props = $props()
setQueryClientContext(queryClient)
const mutation = createMutation(() => ({
mutationFn: (text: string) => sleep(10).then(() => text),
}))
</script>
<button
onclick={() => mutation.mutate('Todo 1', { onSuccess: onSuccessPerCall })}
>
mutate1
</button>
<button
onclick={() => mutation.mutate('Todo 2', { onSuccess: onSuccessPerCall })}
>
mutate2
</button>
<div>data: {mutation.data ?? 'null'}, status: {mutation.status}</div>

View File

@@ -0,0 +1,24 @@
<script lang="ts">
import type { QueryClient } from '@tanstack/query-core'
import { createMutation, setQueryClientContext } from '../../src/index.js'
type Props = {
queryClient: QueryClient
mutationFn: (value: { count: number }) => Promise<{ count: number }>
}
let { queryClient, mutationFn }: Props = $props()
let count = $state(0)
setQueryClientContext(queryClient)
const mutation = createMutation(() => ({ mutationFn }))
</script>
<button onclick={() => mutation.mutate({ count: ++count })}>Mutate</button>
<div>Data: {mutation.data?.count ?? 'undefined'}</div>
<div>Status: {mutation.status}</div>
<div>Failure Count: {mutation.failureCount}</div>
<div>Failure Reason: {mutation.failureReason ?? 'undefined'}</div>

View File

@@ -0,0 +1,23 @@
<script lang="ts">
import type { QueryClient, QueryKey } from '@tanstack/query-core'
import { createMutation, setQueryClientContext } from '../../src/index.js'
import { sleep } from '@tanstack/query-test-utils'
type Props = {
queryClient: QueryClient
queryKey: QueryKey
}
const { queryClient, queryKey }: Props = $props()
setQueryClientContext(queryClient)
const mutation = createMutation(() => ({
mutationFn: () => sleep(10).then(() => 'mutated'),
onSuccess: (_data, _variables, _onMutateResult, context) => {
context.client.invalidateQueries({ queryKey })
},
}))
</script>
<button onclick={() => mutation.mutate()}>Mutate</button>

View File

@@ -0,0 +1,23 @@
<script lang="ts">
import type { QueryClient, QueryKey } from '@tanstack/query-core'
import { createMutation, setQueryClientContext } from '../../src/index.js'
import { sleep } from '@tanstack/query-test-utils'
type Props = {
queryClient: QueryClient
queryKey: QueryKey
}
const { queryClient, queryKey }: Props = $props()
setQueryClientContext(queryClient)
const mutation = createMutation(() => ({
mutationFn: (_text: string, context) =>
sleep(10).then(() => context.client.getQueryData(queryKey)),
}))
</script>
<button onclick={() => mutation.mutate('todo')}>Mutate</button>
<div>data: {mutation.data}</div>

View File

@@ -0,0 +1,38 @@
<script lang="ts">
import type { QueryClient } from '@tanstack/query-core'
import { createMutation, setQueryClientContext } from '../../src/index.js'
import { sleep } from '@tanstack/query-test-utils'
type Props = {
queryClient: QueryClient
queryKey: Array<string>
shouldSucceed: boolean
}
const { queryClient, queryKey, shouldSucceed }: Props = $props()
setQueryClientContext(queryClient)
const mutation = createMutation(() => ({
mutationFn: (newTodo: string) =>
shouldSucceed
? sleep(10).then(() => newTodo)
: sleep(10).then(() => Promise.reject(new Error('Some error'))),
onMutate: async (newTodo: string) => {
await queryClient.cancelQueries({ queryKey })
const previousTodos = queryClient.getQueryData<Array<string>>(queryKey)
queryClient.setQueryData<Array<string>>(queryKey, (old) => [
...(old ?? []),
newTodo,
])
return { previousTodos }
},
onError: (_err, _newTodo, onMutateResult) => {
queryClient.setQueryData(queryKey, onMutateResult?.previousTodos)
},
}))
</script>
<button onclick={() => mutation.mutate('Todo 2')}>add</button>

View File

@@ -0,0 +1,58 @@
<script lang="ts">
import type { QueryClient } from '@tanstack/query-core'
import { createMutation, setQueryClientContext } from '../../src/index.js'
import { sleep } from '@tanstack/query-test-utils'
type Props = {
queryClient: QueryClient
strategy: 'all' | 'allSettled'
failOn?: string
}
const { queryClient, strategy, failOn }: Props = $props()
let result = $state('idle')
setQueryClientContext(queryClient)
const mutation = createMutation(() => ({
mutationFn: (file: string) =>
sleep(10).then(() => {
if (file === failOn) {
throw new Error('upload failed')
}
return `uploaded: ${file}`
}),
retry: false,
}))
async function uploadAll() {
if (strategy === 'all') {
try {
const results = await Promise.all([
mutation.mutateAsync('file1'),
mutation.mutateAsync('file2'),
mutation.mutateAsync('file3'),
])
result = results.join(', ')
} catch (error) {
result = `error: ${(error as Error).message}`
}
} else {
const results = await Promise.allSettled([
mutation.mutateAsync('file1'),
mutation.mutateAsync('file2'),
mutation.mutateAsync('file3'),
])
result = results
.map((r) =>
r.status === 'fulfilled' ? r.value : `error: ${r.reason.message}`,
)
.join(', ')
}
}
</script>
<button onclick={uploadAll}>upload all</button>
<div>result: {result}</div>

View File

@@ -0,0 +1,24 @@
<script lang="ts">
import type { QueryClient } from '@tanstack/query-core'
import { createMutation, setQueryClientContext } from '../../src/index.js'
import { sleep } from '@tanstack/query-test-utils'
type Props = {
queryClient: QueryClient
perCallOnSuccess: (...args: Array<unknown>) => void
}
const { queryClient, perCallOnSuccess }: Props = $props()
setQueryClientContext(queryClient)
const mutation = createMutation(() => ({
mutationFn: (text: string) => sleep(10).then(() => text),
}))
</script>
<button
onclick={() => mutation.mutate('todo', { onSuccess: perCallOnSuccess })}
>
Mutate
</button>

View File

@@ -0,0 +1,23 @@
<script lang="ts">
import type { QueryClient } from '@tanstack/query-core'
import { createMutation, setQueryClientContext } from '../../src/index.js'
import { sleep } from '@tanstack/query-test-utils'
type Props = {
queryClient: QueryClient
}
let { queryClient }: Props = $props()
setQueryClientContext(queryClient)
const mutation = createMutation(() => ({
mutationFn: () =>
sleep(10).then(() => Promise.reject(new Error('Expected mock error'))),
}))
</script>
<button onclick={() => mutation.reset()}>Reset</button>
<button onclick={() => mutation.mutate()}>Mutate</button>
<div>Error: {mutation.error?.message ?? 'undefined'}</div>

View File

@@ -0,0 +1,31 @@
<script lang="ts">
import type { QueryClient } from '@tanstack/query-core'
import { createMutation, setQueryClientContext } from '../../src/index.js'
import { sleep } from '@tanstack/query-test-utils'
type Props = {
queryClient: QueryClient
onSuccessMock: (data: number) => void
onSettledMock: (data: number | undefined) => void
}
const { queryClient, onSettledMock, onSuccessMock }: Props = $props()
let count = $state(0)
setQueryClientContext(queryClient)
const mutation = createMutation(() => ({
mutationFn: (vars: { count: number }) => sleep(10).then(() => vars.count),
onSuccess: (data) => {
onSuccessMock(data)
},
onSettled: (data) => {
onSettledMock(data)
},
}))
</script>
<button onclick={() => mutation.mutate({ count: ++count })}>Mutate</button>
<div>Count: {count}</div>

View File

@@ -0,0 +1,24 @@
<script lang="ts">
import type { MutationKey, QueryClient } from '@tanstack/query-core'
import { createMutation, setQueryClientContext } from '../../src/index.js'
import { sleep } from '@tanstack/query-test-utils'
type Props = {
queryClient: QueryClient
mutationKey?: MutationKey
onSuccessMock: (...args: Array<unknown>) => void
}
const { queryClient, mutationKey, onSuccessMock }: Props = $props()
setQueryClientContext(queryClient)
const mutation = createMutation(() => ({
mutationKey,
mutationFn: (text: string) => sleep(10).then(() => text.toUpperCase()),
onMutate: (text: string) => ({ startedWith: text }),
onSuccess: onSuccessMock,
}))
</script>
<button onclick={() => mutation.mutate('todo')}>Mutate</button>

View File

@@ -0,0 +1,706 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { flushSync } from 'svelte'
import { fireEvent, render } from '@testing-library/svelte'
import { QueryClient, noop } from '@tanstack/query-core'
import { queryKey, sleep } from '@tanstack/query-test-utils'
import { createMutation } from '../../src/index.js'
import { withEffectRoot } from '../utils.svelte.js'
import Reset from './Reset.svelte'
import Success from './Success.svelte'
import Failure from './Failure.svelte'
import OptimisticUpdate from './OptimisticUpdate.svelte'
import SuccessContext from './SuccessContext.svelte'
import InvalidateFromContext from './InvalidateFromContext.svelte'
import PerCallSuccess from './PerCallSuccess.svelte'
import MutationFnContext from './MutationFnContext.svelte'
import ParallelMutateAsync from './ParallelMutateAsync.svelte'
import ConcurrentMutate from './ConcurrentMutate.svelte'
describe('createMutation', () => {
let queryClient: QueryClient
beforeEach(() => {
vi.useFakeTimers()
queryClient = new QueryClient()
})
afterEach(() => {
queryClient.clear()
vi.useRealTimers()
})
it('should be able to reset `error`', async () => {
const rendered = render(Reset, {
props: { queryClient },
})
expect(rendered.queryByText('Error: undefined')).toBeInTheDocument()
fireEvent.click(rendered.getByRole('button', { name: /Mutate/i }))
await vi.advanceTimersByTimeAsync(11)
expect(rendered.getByText('Error: Expected mock error')).toBeInTheDocument()
fireEvent.click(rendered.getByRole('button', { name: /Reset/i }))
await vi.advanceTimersByTimeAsync(11)
expect(rendered.getByText('Error: undefined')).toBeInTheDocument()
})
it(
'should call mutate callbacks when createMutation has no callbacks',
withEffectRoot(async () => {
const callbacks: Array<string> = []
const mutation = createMutation(
() => ({
mutationFn: (text: string) => sleep(10).then(() => text),
}),
() => queryClient,
)
mutation.mutate('todo', {
onSuccess: () => callbacks.push('mutate.onSuccess'),
onSettled: () => callbacks.push('mutate.onSettled'),
})
await vi.advanceTimersByTimeAsync(10)
expect(callbacks).toEqual(['mutate.onSuccess', 'mutate.onSettled'])
}),
)
it(
'should call mutate error callbacks when createMutation has no callbacks',
withEffectRoot(async () => {
const callbacks: Array<string> = []
const mutation = createMutation(
() => ({
mutationFn: (_text: string) =>
sleep(10).then(() => Promise.reject(new Error('oops'))),
}),
() => queryClient,
)
mutation.mutate('todo', {
onError: () => callbacks.push('mutate.onError'),
onSettled: () => callbacks.push('mutate.onSettled'),
})
await vi.advanceTimersByTimeAsync(10)
expect(callbacks).toEqual(['mutate.onError', 'mutate.onSettled'])
}),
)
it(
'should call only mutate onSuccess when createMutation has no callbacks',
withEffectRoot(async () => {
const callbacks: Array<string> = []
const mutation = createMutation(
() => ({
mutationFn: (text: string) => sleep(10).then(() => text),
}),
() => queryClient,
)
mutation.mutate('todo', {
onSuccess: () => callbacks.push('mutate.onSuccess'),
})
await vi.advanceTimersByTimeAsync(10)
expect(callbacks).toEqual(['mutate.onSuccess'])
}),
)
it(
'should call only mutate onError when createMutation has no callbacks',
withEffectRoot(async () => {
const callbacks: Array<string> = []
const mutation = createMutation(
() => ({
mutationFn: (_text: string) =>
sleep(10).then(() => Promise.reject(new Error('oops'))),
}),
() => queryClient,
)
mutation.mutate('todo', {
onError: () => callbacks.push('mutate.onError'),
})
await vi.advanceTimersByTimeAsync(10)
expect(callbacks).toEqual(['mutate.onError'])
}),
)
it(
'should call only mutate onSettled when createMutation has no callbacks',
withEffectRoot(async () => {
const callbacks: Array<string> = []
const mutation = createMutation(
() => ({
mutationFn: (text: string) => sleep(10).then(() => text),
}),
() => queryClient,
)
mutation.mutate('todo', {
onSettled: () => callbacks.push('mutate.onSettled'),
})
await vi.advanceTimersByTimeAsync(10)
expect(callbacks).toEqual(['mutate.onSettled'])
}),
)
it(
'should call mutateAsync callbacks when createMutation has no callbacks',
withEffectRoot(async () => {
const callbacks: Array<string> = []
const mutation = createMutation(
() => ({
mutationFn: (text: string) => sleep(10).then(() => text),
}),
() => queryClient,
)
mutation.mutateAsync('todo', {
onSuccess: () => callbacks.push('mutateAsync.onSuccess'),
onSettled: () => callbacks.push('mutateAsync.onSettled'),
})
await vi.advanceTimersByTimeAsync(10)
expect(callbacks).toEqual([
'mutateAsync.onSuccess',
'mutateAsync.onSettled',
])
}),
)
it(
'should call mutateAsync error callbacks when createMutation has no callbacks',
withEffectRoot(async () => {
const callbacks: Array<string> = []
const mutation = createMutation(
() => ({
mutationFn: (_text: string) =>
sleep(10).then(() => Promise.reject(new Error('oops'))),
}),
() => queryClient,
)
mutation
.mutateAsync('todo', {
onError: () => callbacks.push('mutateAsync.onError'),
onSettled: () => callbacks.push('mutateAsync.onSettled'),
})
.catch(noop)
await vi.advanceTimersByTimeAsync(10)
expect(callbacks).toEqual([
'mutateAsync.onError',
'mutateAsync.onSettled',
])
}),
)
it(
'should call only mutateAsync onSuccess when createMutation has no callbacks',
withEffectRoot(async () => {
const callbacks: Array<string> = []
const mutation = createMutation(
() => ({
mutationFn: (text: string) => sleep(10).then(() => text),
}),
() => queryClient,
)
mutation.mutateAsync('todo', {
onSuccess: () => callbacks.push('mutateAsync.onSuccess'),
})
await vi.advanceTimersByTimeAsync(10)
expect(callbacks).toEqual(['mutateAsync.onSuccess'])
}),
)
it(
'should call only mutateAsync onError when createMutation has no callbacks',
withEffectRoot(async () => {
const callbacks: Array<string> = []
const mutation = createMutation(
() => ({
mutationFn: (_text: string) =>
sleep(10).then(() => Promise.reject(new Error('oops'))),
}),
() => queryClient,
)
mutation
.mutateAsync('todo', {
onError: () => callbacks.push('mutateAsync.onError'),
})
.catch(noop)
await vi.advanceTimersByTimeAsync(10)
expect(callbacks).toEqual(['mutateAsync.onError'])
}),
)
it(
'should call only mutateAsync onSettled when createMutation has no callbacks',
withEffectRoot(async () => {
const callbacks: Array<string> = []
const mutation = createMutation(
() => ({
mutationFn: (text: string) => sleep(10).then(() => text),
}),
() => queryClient,
)
mutation.mutateAsync('todo', {
onSettled: () => callbacks.push('mutateAsync.onSettled'),
})
await vi.advanceTimersByTimeAsync(10)
expect(callbacks).toEqual(['mutateAsync.onSettled'])
}),
)
it('should be able to call `onSuccess` and `onSettled` after each successful mutate', async () => {
const onSuccessMock = vi.fn()
const onSettledMock = vi.fn()
const rendered = render(Success, {
props: {
queryClient,
onSuccessMock,
onSettledMock,
},
})
expect(rendered.queryByText('Count: 0')).toBeInTheDocument()
fireEvent.click(rendered.getByRole('button', { name: /Mutate/i }))
fireEvent.click(rendered.getByRole('button', { name: /Mutate/i }))
fireEvent.click(rendered.getByRole('button', { name: /Mutate/i }))
await vi.advanceTimersByTimeAsync(11)
expect(rendered.queryByText('Count: 3')).toBeInTheDocument()
expect(onSuccessMock).toHaveBeenCalledTimes(3)
expect(onSuccessMock).toHaveBeenNthCalledWith(1, 1)
expect(onSuccessMock).toHaveBeenNthCalledWith(2, 2)
expect(onSuccessMock).toHaveBeenNthCalledWith(3, 3)
expect(onSettledMock).toHaveBeenCalledTimes(3)
expect(onSettledMock).toHaveBeenNthCalledWith(1, 1)
expect(onSettledMock).toHaveBeenNthCalledWith(2, 2)
expect(onSettledMock).toHaveBeenNthCalledWith(3, 3)
})
it('should set correct values for `failureReason` and `failureCount` on multiple mutate calls', async () => {
type Value = { count: number }
const mutationFn = vi.fn<(value: Value) => Promise<Value>>()
mutationFn.mockImplementationOnce(() =>
sleep(20).then(() => Promise.reject(`Expected mock error`)),
)
mutationFn.mockImplementation((value) => sleep(10).then(() => value))
const rendered = render(Failure, {
props: {
queryClient,
mutationFn,
},
})
expect(rendered.queryByText('Data: undefined')).toBeInTheDocument()
fireEvent.click(rendered.getByRole('button', { name: /Mutate/i }))
expect(rendered.getByText('Data: undefined')).toBeInTheDocument()
await vi.advanceTimersByTimeAsync(21)
expect(rendered.getByText('Status: error')).toBeInTheDocument()
expect(rendered.getByText('Failure Count: 1')).toBeInTheDocument()
expect(
rendered.getByText('Failure Reason: Expected mock error'),
).toBeInTheDocument()
fireEvent.click(rendered.getByRole('button', { name: /Mutate/i }))
await vi.advanceTimersByTimeAsync(0)
expect(rendered.getByText('Status: pending')).toBeInTheDocument()
await vi.advanceTimersByTimeAsync(11)
expect(rendered.getByText('Status: success')).toBeInTheDocument()
expect(rendered.getByText('Data: 2')).toBeInTheDocument()
expect(rendered.getByText('Failure Count: 0')).toBeInTheDocument()
expect(rendered.getByText('Failure Reason: undefined')).toBeInTheDocument()
})
it(
'should be able to call `onSuccess` callback after successful mutateAsync',
withEffectRoot(async () => {
const callbacks: Array<string> = []
const mutation = createMutation(
() => ({
mutationFn: (text: string) => sleep(10).then(() => text),
onSuccess: () => callbacks.push('useMutation.onSuccess'),
}),
() => queryClient,
)
mutation.mutateAsync('todo', {
onSuccess: () => callbacks.push('mutateAsync.onSuccess'),
})
await vi.advanceTimersByTimeAsync(10)
expect(callbacks).toEqual([
'useMutation.onSuccess',
'mutateAsync.onSuccess',
])
}),
)
it(
'should be able to call `onError` callback after failed mutateAsync',
withEffectRoot(async () => {
const callbacks: Array<string> = []
const mutation = createMutation(
() => ({
mutationFn: (_text: string) =>
sleep(10).then(() => Promise.reject(new Error('oops'))),
onError: () => callbacks.push('useMutation.onError'),
}),
() => queryClient,
)
mutation
.mutateAsync('todo', {
onError: () => callbacks.push('mutateAsync.onError'),
})
.catch(noop)
await vi.advanceTimersByTimeAsync(10)
expect(callbacks).toEqual(['useMutation.onError', 'mutateAsync.onError'])
}),
)
it(
'should be able to call `onSettled` callback after mutateAsync',
withEffectRoot(async () => {
const callbacks: Array<string> = []
const mutation = createMutation(
() => ({
mutationFn: (text: string) => sleep(10).then(() => text),
onSettled: () => callbacks.push('useMutation.onSettled'),
}),
() => queryClient,
)
mutation.mutateAsync('todo', {
onSettled: () => callbacks.push('mutateAsync.onSettled'),
})
await vi.advanceTimersByTimeAsync(10)
expect(callbacks).toEqual([
'useMutation.onSettled',
'mutateAsync.onSettled',
])
}),
)
it(
'should be able to override the useMutation success callbacks',
withEffectRoot(async () => {
const callbacks: Array<string> = []
const mutation = createMutation(
() => ({
mutationFn: (text: string) => sleep(10).then(() => text),
onSuccess: () =>
sleep(10).then(() => callbacks.push('useMutation.onSuccess')),
onSettled: () =>
sleep(10).then(() => callbacks.push('useMutation.onSettled')),
}),
() => queryClient,
)
mutation
.mutateAsync('todo', {
onSuccess: () => callbacks.push('mutateAsync.onSuccess'),
onSettled: () => callbacks.push('mutateAsync.onSettled'),
})
.then((result) => callbacks.push(`mutateAsync.result:${result}`))
await vi.advanceTimersByTimeAsync(30)
expect(callbacks).toEqual([
'useMutation.onSuccess',
'useMutation.onSettled',
'mutateAsync.onSuccess',
'mutateAsync.onSettled',
'mutateAsync.result:todo',
])
}),
)
it(
'should be able to override the error callbacks when using mutateAsync',
withEffectRoot(async () => {
const callbacks: Array<string> = []
const mutation = createMutation(
() => ({
mutationFn: (_text: string) =>
sleep(10).then(() => Promise.reject(new Error('oops'))),
onError: () =>
sleep(10).then(() => callbacks.push('useMutation.onError')),
onSettled: () =>
sleep(10).then(() => callbacks.push('useMutation.onSettled')),
}),
() => queryClient,
)
mutation
.mutateAsync('todo', {
onError: () => callbacks.push('mutateAsync.onError'),
onSettled: () => callbacks.push('mutateAsync.onSettled'),
})
.catch((error) =>
callbacks.push(`mutateAsync.error:${(error as Error).message}`),
)
await vi.advanceTimersByTimeAsync(30)
expect(callbacks).toEqual([
'useMutation.onError',
'useMutation.onSettled',
'mutateAsync.onError',
'mutateAsync.onSettled',
'mutateAsync.error:oops',
])
}),
)
it(
'should recreate observer when queryClient changes',
withEffectRoot(async () => {
const queryClient1 = new QueryClient()
const queryClient2 = new QueryClient()
let activeClient = $state(queryClient1)
const mutation = createMutation(
() => ({
mutationFn: (params: string) => sleep(10).then(() => params),
}),
() => activeClient,
)
mutation.mutate('first')
await vi.advanceTimersByTimeAsync(11)
expect(mutation.status).toBe('success')
expect(mutation.data).toBe('first')
activeClient = queryClient2
flushSync()
expect(mutation.status).toBe('idle')
expect(mutation.data).toBeUndefined()
}),
)
it('should update the cache in onMutate and roll back via onMutateResult in onError', async () => {
const key = queryKey()
queryClient.setQueryData<Array<string>>(key, ['Todo 1'])
const rendered = render(OptimisticUpdate, {
props: { queryClient, queryKey: key, shouldSucceed: false },
})
fireEvent.click(rendered.getByRole('button', { name: /add/i }))
// The optimistic value lands after onMutate's first await, so flush
// microtasks before asserting.
await vi.advanceTimersByTimeAsync(0)
expect(queryClient.getQueryData(key)).toEqual(['Todo 1', 'Todo 2'])
await vi.advanceTimersByTimeAsync(11)
expect(queryClient.getQueryData(key)).toEqual(['Todo 1'])
})
it('should keep the optimistic update in place when the mutation succeeds', async () => {
const key = queryKey()
queryClient.setQueryData<Array<string>>(key, ['Todo 1'])
const rendered = render(OptimisticUpdate, {
props: { queryClient, queryKey: key, shouldSucceed: true },
})
fireEvent.click(rendered.getByRole('button', { name: /add/i }))
await vi.advanceTimersByTimeAsync(11)
expect(queryClient.getQueryData(key)).toEqual(['Todo 1', 'Todo 2'])
})
it('should pass a non-undefined onMutateResult alongside context to onSuccess', async () => {
const onSuccessMock = vi.fn()
const rendered = render(SuccessContext, {
props: { queryClient, onSuccessMock },
})
fireEvent.click(rendered.getByRole('button', { name: /Mutate/i }))
await vi.advanceTimersByTimeAsync(10)
expect(onSuccessMock).toHaveBeenCalledTimes(1)
const [data, variables, onMutateResult, context] =
onSuccessMock.mock.calls[0]!
expect(data).toBe('TODO')
expect(variables).toBe('todo')
expect(onMutateResult).toEqual({ startedWith: 'todo' })
expect(context.client).toBe(queryClient)
expect(context.meta).toBeUndefined()
expect(context.mutationKey).toBeUndefined()
})
it('should include mutationKey in the context passed to hook-level callbacks', async () => {
const onSuccessMock = vi.fn()
const rendered = render(SuccessContext, {
props: { queryClient, mutationKey: ['todos', 'add'], onSuccessMock },
})
fireEvent.click(rendered.getByRole('button', { name: /Mutate/i }))
await vi.advanceTimersByTimeAsync(10)
expect(onSuccessMock).toHaveBeenCalledTimes(1)
expect(onSuccessMock.mock.calls[0]?.[3].mutationKey).toEqual([
'todos',
'add',
])
})
it('should give mutationFn the same QueryClient instance via context', async () => {
const key = queryKey()
queryClient.setQueryData(key, 'tag-from-this-client')
const rendered = render(MutationFnContext, {
props: { queryClient, queryKey: key },
})
fireEvent.click(rendered.getByRole('button', { name: /Mutate/i }))
await vi.advanceTimersByTimeAsync(11)
expect(rendered.getByText('data: tag-from-this-client')).toBeInTheDocument()
})
it('should let onSuccess invalidate queries via context.client without a useQueryClient() closure', async () => {
const key = queryKey()
queryClient.setQueryData(key, 'data')
const rendered = render(InvalidateFromContext, {
props: { queryClient, queryKey: key },
})
expect(queryClient.getQueryState(key)?.isInvalidated).toBe(false)
fireEvent.click(rendered.getByRole('button', { name: /Mutate/i }))
await vi.advanceTimersByTimeAsync(10)
expect(queryClient.getQueryState(key)?.isInvalidated).toBe(true)
})
it('should give a per-call onSuccess the same QueryClient instance via context', async () => {
const perCallOnSuccess = vi.fn()
const rendered = render(PerCallSuccess, {
props: { queryClient, perCallOnSuccess },
})
fireEvent.click(rendered.getByRole('button', { name: /Mutate/i }))
await vi.advanceTimersByTimeAsync(10)
expect(perCallOnSuccess).toHaveBeenCalledTimes(1)
expect(perCallOnSuccess.mock.calls[0]?.[3].client).toBe(queryClient)
})
it('should be able to run multiple mutateAsync calls in parallel with Promise.all', async () => {
const rendered = render(ParallelMutateAsync, {
props: { queryClient, strategy: 'all' },
})
fireEvent.click(rendered.getByRole('button', { name: /upload all/i }))
await vi.advanceTimersByTimeAsync(11)
expect(
rendered.getByText(
'result: uploaded: file1, uploaded: file2, uploaded: file3',
),
).toBeInTheDocument()
})
it('should handle Promise.all rejection when one parallel mutateAsync call fails', async () => {
const rendered = render(ParallelMutateAsync, {
props: { queryClient, strategy: 'all', failOn: 'file2' },
})
fireEvent.click(rendered.getByRole('button', { name: /upload all/i }))
await vi.advanceTimersByTimeAsync(11)
expect(
rendered.getByText('result: error: upload failed'),
).toBeInTheDocument()
})
it('should handle partial failure in parallel mutateAsync calls with Promise.allSettled', async () => {
const rendered = render(ParallelMutateAsync, {
props: { queryClient, strategy: 'allSettled', failOn: 'file2' },
})
fireEvent.click(rendered.getByRole('button', { name: /upload all/i }))
await vi.advanceTimersByTimeAsync(11)
expect(
rendered.getByText(
'result: uploaded: file1, error: upload failed, uploaded: file3',
),
).toBeInTheDocument()
})
it('should only fire the per-call onSuccess for the last mutate() call', async () => {
const onSuccessPerCall = vi.fn()
const rendered = render(ConcurrentMutate, {
props: { queryClient, onSuccessPerCall },
})
expect(rendered.getByText('data: null, status: idle')).toBeInTheDocument()
fireEvent.click(rendered.getByRole('button', { name: /mutate1/i }))
fireEvent.click(rendered.getByRole('button', { name: /mutate2/i }))
await vi.advanceTimersByTimeAsync(11)
expect(
rendered.getByText('data: Todo 2, status: success'),
).toBeInTheDocument()
expect(onSuccessPerCall).toHaveBeenCalledTimes(1)
expect(onSuccessPerCall).toHaveBeenCalledWith(
'Todo 2',
'Todo 2',
undefined,
expect.anything(),
)
})
})

View File

@@ -0,0 +1,203 @@
import { describe, expectTypeOf, it } from 'vitest'
import { QueryClient } from '@tanstack/query-core'
import { createMutation } from '../../src/index.js'
import type {
DefaultError,
MutationFunctionContext,
MutationKey,
} from '@tanstack/query-core'
import type { CreateMutationResult } from '../../src/types.js'
describe('createMutation', () => {
it('should infer TData from mutationFn return type', () => {
const mutation = createMutation(() => ({
mutationFn: () => Promise.resolve('data'),
}))
expectTypeOf(mutation.data).toEqualTypeOf<string | undefined>()
expectTypeOf(mutation.error).toEqualTypeOf<DefaultError | null>()
})
it('should infer TVariables from mutationFn parameter', () => {
const mutation = createMutation(() => ({
mutationFn: (vars: { id: string }) => Promise.resolve(vars.id),
}))
expectTypeOf(mutation.mutate).toBeCallableWith({ id: '1' })
expectTypeOf(mutation.data).toEqualTypeOf<string | undefined>()
})
it('should infer TOnMutateResult from onMutate return type', () => {
createMutation(() => ({
mutationFn: () => Promise.resolve('data'),
onMutate: () => {
return { token: 'abc' }
},
onSuccess: (_data, _variables, onMutateResult) => {
expectTypeOf(onMutateResult).toEqualTypeOf<{ token: string }>()
},
onError: (_error, _variables, onMutateResult) => {
expectTypeOf(onMutateResult).toEqualTypeOf<
{ token: string } | undefined
>()
},
}))
})
it('should allow explicit generic types', () => {
const mutation = createMutation<string, Error, { id: number }>(() => ({
mutationFn: (vars) => {
expectTypeOf(vars).toEqualTypeOf<{ id: number }>()
return Promise.resolve('result')
},
}))
expectTypeOf(mutation.data).toEqualTypeOf<string | undefined>()
expectTypeOf(mutation.error).toEqualTypeOf<Error | null>()
})
it('should return correct CreateMutationResult type', () => {
const mutation = createMutation(() => ({
mutationFn: () => Promise.resolve(42),
}))
expectTypeOf(mutation).toEqualTypeOf<
CreateMutationResult<number, DefaultError, void, unknown>
>()
})
it('should type mutateAsync with correct return type', () => {
const mutation = createMutation(() => ({
mutationFn: (id: string) => Promise.resolve(id.length),
}))
expectTypeOf(mutation.mutateAsync).toBeCallableWith('test')
expectTypeOf(mutation.mutateAsync('test')).toEqualTypeOf<Promise<number>>()
})
it('should default TVariables to void when mutationFn has no parameters', () => {
const mutation = createMutation(() => ({
mutationFn: () => Promise.resolve('data'),
}))
expectTypeOf(mutation.mutate).toBeCallableWith()
})
it('should allow calling mutate with no arguments when TVariables is optional undefinable', () => {
const mutation = createMutation(() => ({
mutationFn: (_variables: number | undefined) => Promise.resolve(1),
}))
expectTypeOf(mutation.mutate).toBeCallableWith()
expectTypeOf(mutation.mutateAsync).toBeCallableWith()
})
it('should infer custom TError type', () => {
class CustomError extends Error {
code: number
constructor(code: number) {
super()
this.code = code
}
}
const mutation = createMutation<string, CustomError>(() => ({
mutationFn: () => Promise.resolve('data'),
}))
expectTypeOf(mutation.error).toEqualTypeOf<CustomError | null>()
expectTypeOf(mutation.data).toEqualTypeOf<string | undefined>()
})
it('should infer types for onSettled callback', () => {
createMutation(() => ({
mutationFn: () => Promise.resolve(42),
onSettled: (data, error, _variables, _onMutateResult) => {
expectTypeOf(data).toEqualTypeOf<number | undefined>()
expectTypeOf(error).toEqualTypeOf<DefaultError | null>()
},
}))
})
it('should infer custom TError in onError callback', () => {
class CustomError extends Error {
code: number
constructor(code: number) {
super()
this.code = code
}
}
createMutation<string, CustomError>(() => ({
mutationFn: () => Promise.resolve('data'),
onError: (error) => {
expectTypeOf(error).toEqualTypeOf<CustomError>()
},
}))
})
it('should accept queryClient as second argument', () => {
const queryClient = new QueryClient()
const mutation = createMutation(
() => ({
mutationFn: () => Promise.resolve('data'),
}),
() => queryClient,
)
expectTypeOf(mutation.data).toEqualTypeOf<string | undefined>()
})
it('should type context as the last argument for mutationFn and every hook-level callback', () => {
createMutation(() => ({
mutationFn: (_variables, context) => {
expectTypeOf(context).toEqualTypeOf<MutationFunctionContext>()
expectTypeOf(context.client).toEqualTypeOf<QueryClient>()
return Promise.resolve('data')
},
onMutate: (_variables, context) => {
expectTypeOf(context).toEqualTypeOf<MutationFunctionContext>()
},
onSuccess: (_data, _variables, _onMutateResult, context) => {
expectTypeOf(context).toEqualTypeOf<MutationFunctionContext>()
},
onError: (_error, _variables, _onMutateResult, context) => {
expectTypeOf(context).toEqualTypeOf<MutationFunctionContext>()
},
onSettled: (_data, _error, _variables, _onMutateResult, context) => {
expectTypeOf(context).toEqualTypeOf<MutationFunctionContext>()
},
}))
})
it('should type context as the last argument for every per-call mutate option', () => {
const mutation = createMutation(() => ({
mutationFn: () => Promise.resolve('data'),
}))
mutation.mutate(undefined, {
onSuccess: (_data, _variables, _onMutateResult, context) => {
expectTypeOf(context).toEqualTypeOf<MutationFunctionContext>()
},
onError: (_error, _variables, _onMutateResult, context) => {
expectTypeOf(context).toEqualTypeOf<MutationFunctionContext>()
},
onSettled: (_data, _error, _variables, _onMutateResult, context) => {
expectTypeOf(context).toEqualTypeOf<MutationFunctionContext>()
},
})
})
it('should type context.mutationKey as MutationKey', () => {
createMutation(() => ({
mutationKey: ['todos', 'add'] as const,
mutationFn: () => Promise.resolve('data'),
onSuccess: (_data, _variables, _onMutateResult, context) => {
expectTypeOf(context.mutationKey).toEqualTypeOf<
MutationKey | undefined
>()
},
}))
})
})