与查询不同,突变通常用于创建/更新/删除数据或执行服务器端副作用。为此,TanStack Query 导出了 useMutation Hook。
这是一个向服务器添加新 todo 的突变示例
<script setup>
import { useMutation } from '@tanstack/vue-query'
const { isPending, isError, error, isSuccess, mutate } = useMutation({
mutationFn: (newTodo) => axios.post('/todos', newTodo),
})
function addTodo() {
mutate({ id: new Date(), title: 'Do Laundry' })
}
</script>
<template>
<span v-if="isPending">Adding todo...</span>
<span v-else-if="isError">An error occurred: {{ error.message }}</span>
<span v-else-if="isSuccess">Todo added!</span>
<button @click="addTodo">Create Todo</button>
</template>
<script setup>
import { useMutation } from '@tanstack/vue-query'
const { isPending, isError, error, isSuccess, mutate } = useMutation({
mutationFn: (newTodo) => axios.post('/todos', newTodo),
})
function addTodo() {
mutate({ id: new Date(), title: 'Do Laundry' })
}
</script>
<template>
<span v-if="isPending">Adding todo...</span>
<span v-else-if="isError">An error occurred: {{ error.message }}</span>
<span v-else-if="isSuccess">Todo added!</span>
<button @click="addTodo">Create Todo</button>
</template>
一个突变在任何给定时刻只能处于以下状态之一
除了这些主要状态之外,根据突变的状态,还可以获得更多信息
在上面的示例中,您还看到可以通过使用 mutate 函数和单个变量或对象调用,将变量传递给突变函数。
即使只有变量,突变也不是那么特殊,但是当与 onSuccess 选项、Query Client 的 invalidateQueries 方法和 Query Client 的 setQueryData 方法一起使用时,突变将成为一个非常强大的工具。
有时您可能需要清除突变请求的 error 或 data。为此,您可以使用 reset 函数来处理。
<script>
import { useMutation } from '@tanstack/vue-query'
const { error, mutate, reset } = useMutation({
mutationFn: (newTodo) => axios.post('/todos', newTodo),
})
function addTodo() {
mutate({ id: new Date(), title: 'Do Laundry' })
}
</script>
<template>
<span v-else-if="error">
<span>An error occurred: {{ error.message }}</span>
<button @click="reset">Reset error</button>
</span>
<button @click="addTodo">Create Todo</button>
</template>
<script>
import { useMutation } from '@tanstack/vue-query'
const { error, mutate, reset } = useMutation({
mutationFn: (newTodo) => axios.post('/todos', newTodo),
})
function addTodo() {
mutate({ id: new Date(), title: 'Do Laundry' })
}
</script>
<template>
<span v-else-if="error">
<span>An error occurred: {{ error.message }}</span>
<button @click="reset">Reset error</button>
</span>
<button @click="addTodo">Create Todo</button>
</template>
useMutation 提供了一些辅助选项,这些选项允许在突变生命周期的任何阶段快速轻松地实现副作用。这些选项对于在突变后使查询失效和重新获取查询,甚至 乐观更新都非常方便。
useMutation({
mutationFn: addTodo,
onMutate: (variables) => {
// A mutation is about to happen!
// Optionally return a context containing data to use when for example rolling back
return { id: 1 }
},
onError: (error, variables, context) => {
// An error happened!
console.log(`rolling back optimistic update with id ${context.id}`)
},
onSuccess: (data, variables, context) => {
// Boom baby!
},
onSettled: (data, error, variables, context) => {
// Error or success... doesn't matter!
},
})
useMutation({
mutationFn: addTodo,
onMutate: (variables) => {
// A mutation is about to happen!
// Optionally return a context containing data to use when for example rolling back
return { id: 1 }
},
onError: (error, variables, context) => {
// An error happened!
console.log(`rolling back optimistic update with id ${context.id}`)
},
onSuccess: (data, variables, context) => {
// Boom baby!
},
onSettled: (data, error, variables, context) => {
// Error or success... doesn't matter!
},
})
当在任何回调函数中返回 promise 时,它将首先被等待,然后再调用下一个回调
useMutation({
mutationFn: addTodo,
onSuccess: async () => {
console.log("I'm first!")
},
onSettled: async () => {
console.log("I'm second!")
},
})
useMutation({
mutationFn: addTodo,
onSuccess: async () => {
console.log("I'm first!")
},
onSettled: async () => {
console.log("I'm second!")
},
})
您可能会发现,在调用 mutate 时,除了 useMutation 上定义的那些回调之外,您还想触发其他回调。这可以用于触发组件特定的副作用。为此,您可以在突变变量之后向 mutate 函数提供任何相同的回调选项。支持的选项包括:onSuccess、onError 和 onSettled。请记住,如果您的组件在突变完成之前卸载,这些额外的回调将不会运行。
useMutation({
mutationFn: addTodo,
onSuccess: (data, variables, context) => {
// I will fire first
},
onError: (error, variables, context) => {
// I will fire first
},
onSettled: (data, error, variables, context) => {
// I will fire first
},
})
mutate(todo, {
onSuccess: (data, variables, context) => {
// I will fire second!
},
onError: (error, variables, context) => {
// I will fire second!
},
onSettled: (data, error, variables, context) => {
// I will fire second!
},
})
useMutation({
mutationFn: addTodo,
onSuccess: (data, variables, context) => {
// I will fire first
},
onError: (error, variables, context) => {
// I will fire first
},
onSettled: (data, error, variables, context) => {
// I will fire first
},
})
mutate(todo, {
onSuccess: (data, variables, context) => {
// I will fire second!
},
onError: (error, variables, context) => {
// I will fire second!
},
onSettled: (data, error, variables, context) => {
// I will fire second!
},
})
在处理连续突变的 onSuccess、onError 和 onSettled 回调时,存在细微差异。当传递给 mutate 函数时,它们将仅触发一次,并且仅当组件仍然挂载时才会触发。这是因为每次调用 mutate 函数时,突变观察器都会被移除和重新订阅。相反,useMutation 处理程序为每个 mutate 调用执行。
请注意,传递给 useMutation 的 mutationFn 很可能是异步的。在这种情况下,突变完成的顺序可能与 mutate 函数调用的顺序不同。
useMutation({
mutationFn: addTodo,
onSuccess: (data, variables, context) => {
// Will be called 3 times
},
})
const todos = ['Todo 1', 'Todo 2', 'Todo 3']
todos.forEach((todo) => {
mutate(todo, {
onSuccess: (data, variables, context) => {
// Will execute only once, for the last mutation (Todo 3),
// regardless which mutation resolves first
},
})
})
useMutation({
mutationFn: addTodo,
onSuccess: (data, variables, context) => {
// Will be called 3 times
},
})
const todos = ['Todo 1', 'Todo 2', 'Todo 3']
todos.forEach((todo) => {
mutate(todo, {
onSuccess: (data, variables, context) => {
// Will execute only once, for the last mutation (Todo 3),
// regardless which mutation resolves first
},
})
})
使用 mutateAsync 而不是 mutate 来获取 promise,它将在成功时 resolve,或在出现错误时抛出错误。例如,这可以用于组合副作用。
const mutation = useMutation({ mutationFn: addTodo })
try {
const todo = await mutation.mutateAsync(todo)
console.log(todo)
} catch (error) {
console.error(error)
} finally {
console.log('done')
}
const mutation = useMutation({ mutationFn: addTodo })
try {
const todo = await mutation.mutateAsync(todo)
console.log(todo)
} catch (error) {
console.error(error)
} finally {
console.log('done')
}
默认情况下,TanStack Query 不会在错误时重试突变,但可以通过 retry 选项实现
const mutation = useMutation({
mutationFn: addTodo,
retry: 3,
})
const mutation = useMutation({
mutationFn: addTodo,
retry: 3,
})
如果突变因设备离线而失败,则当设备重新连接时,它们将按相同顺序重试。
如果需要,可以将突变持久化到存储中,并在稍后恢复。这可以通过 hydration 函数完成
const queryClient = new QueryClient()
// Define the "addTodo" mutation
queryClient.setMutationDefaults(['addTodo'], {
mutationFn: addTodo,
onMutate: async (variables) => {
// Cancel current queries for the todos list
await queryClient.cancelQueries({ queryKey: ['todos'] })
// Create optimistic todo
const optimisticTodo = { id: uuid(), title: variables.title }
// Add optimistic todo to todos list
queryClient.setQueryData(['todos'], (old) => [...old, optimisticTodo])
// Return context with the optimistic todo
return { optimisticTodo }
},
onSuccess: (result, variables, context) => {
// Replace optimistic todo in the todos list with the result
queryClient.setQueryData(['todos'], (old) =>
old.map((todo) =>
todo.id === context.optimisticTodo.id ? result : todo,
),
)
},
onError: (error, variables, context) => {
// Remove optimistic todo from the todos list
queryClient.setQueryData(['todos'], (old) =>
old.filter((todo) => todo.id !== context.optimisticTodo.id),
)
},
retry: 3,
})
// Start mutation in some component:
const mutation = useMutation({ mutationKey: ['addTodo'] })
mutation.mutate({ title: 'title' })
// If the mutation has been paused because the device is for example offline,
// Then the paused mutation can be dehydrated when the application quits:
const state = dehydrate(queryClient)
// The mutation can then be hydrated again when the application is started:
hydrate(queryClient, state)
// Resume the paused mutations:
queryClient.resumePausedMutations()
const queryClient = new QueryClient()
// Define the "addTodo" mutation
queryClient.setMutationDefaults(['addTodo'], {
mutationFn: addTodo,
onMutate: async (variables) => {
// Cancel current queries for the todos list
await queryClient.cancelQueries({ queryKey: ['todos'] })
// Create optimistic todo
const optimisticTodo = { id: uuid(), title: variables.title }
// Add optimistic todo to todos list
queryClient.setQueryData(['todos'], (old) => [...old, optimisticTodo])
// Return context with the optimistic todo
return { optimisticTodo }
},
onSuccess: (result, variables, context) => {
// Replace optimistic todo in the todos list with the result
queryClient.setQueryData(['todos'], (old) =>
old.map((todo) =>
todo.id === context.optimisticTodo.id ? result : todo,
),
)
},
onError: (error, variables, context) => {
// Remove optimistic todo from the todos list
queryClient.setQueryData(['todos'], (old) =>
old.filter((todo) => todo.id !== context.optimisticTodo.id),
)
},
retry: 3,
})
// Start mutation in some component:
const mutation = useMutation({ mutationKey: ['addTodo'] })
mutation.mutate({ title: 'title' })
// If the mutation has been paused because the device is for example offline,
// Then the paused mutation can be dehydrated when the application quits:
const state = dehydrate(queryClient)
// The mutation can then be hydrated again when the application is started:
hydrate(queryClient, state)
// Resume the paused mutations:
queryClient.resumePausedMutations()
如果您使用 persistQueryClient 插件持久化离线突变,则在页面重新加载时,除非您提供默认的突变函数,否则无法恢复突变。
这是一个技术限制。当持久化到外部存储时,由于函数无法序列化,因此仅持久化突变的状态。在 hydration 之后,触发突变的组件可能未挂载,因此调用 resumePausedMutations 可能会产生错误:No mutationFn found。
const client = new QueryClient({
defaultOptions: {
queries: {
gcTime: 1000 * 60 * 60 * 24, // 24 hours
},
},
})
// we need a default mutation function so that paused mutations can resume after a page reload
queryClient.setMutationDefaults({
mutationKey: ['todos'],
mutationFn: ({ id, data }) => {
return api.updateTodo(id, data)
},
})
const vueQueryOptions: VueQueryPluginOptions = {
queryClient: client,
clientPersister: (queryClient) => {
return persistQueryClient({
queryClient,
persister: createSyncStoragePersister({ storage: localStorage }),
})
},
clientPersisterOnSuccess: (queryClient) => {
queryClient.resumePausedMutations()
},
}
createApp(App).use(VueQueryPlugin, vueQueryOptions).mount('#app')
const client = new QueryClient({
defaultOptions: {
queries: {
gcTime: 1000 * 60 * 60 * 24, // 24 hours
},
},
})
// we need a default mutation function so that paused mutations can resume after a page reload
queryClient.setMutationDefaults({
mutationKey: ['todos'],
mutationFn: ({ id, data }) => {
return api.updateTodo(id, data)
},
})
const vueQueryOptions: VueQueryPluginOptions = {
queryClient: client,
clientPersister: (queryClient) => {
return persistQueryClient({
queryClient,
persister: createSyncStoragePersister({ storage: localStorage }),
})
},
clientPersisterOnSuccess: (queryClient) => {
queryClient.resumePausedMutations()
},
}
createApp(App).use(VueQueryPlugin, vueQueryOptions).mount('#app')
我们还有一个广泛的 离线示例,涵盖了查询和突变。
默认情况下,所有突变都并行运行 - 即使您多次调用同一突变的 .mutate()。可以为突变提供带有 id 的作用域以避免这种情况。所有具有相同 scope.id 的突变将串行运行,这意味着当它们被触发时,如果该作用域已经有正在进行的突变,它们将以 isPaused: true 状态启动。它们将被放入队列中,并在队列中的时间到了后自动恢复。
const mutation = useMutation({
mutationFn: addTodo,
scope: {
id: 'todo',
},
})
const mutation = useMutation({
mutationFn: addTodo,
scope: {
id: 'todo',
},
})