与 queries 不同,mutations 通常用于创建/更新/删除数据或执行服务器端副作用。为此,TanStack Query 导出 injectMutation 函数。
这是一个向服务器添加新 todo 的 mutation 示例
@Component({
template: `
<div>
@if (mutation.isPending()) {
<span>Adding todo...</span>
} @else if (mutation.isError()) {
<div>An error occurred: {{ mutation.error()?.message }}</div>
} @else if (mutation.isSuccess()) {
<div>Todo added!</div>
}
<button (click)="mutation.mutate(1)">Create Todo</button>
</div>
`,
})
export class TodosComponent {
todoService = inject(TodoService)
mutation = injectMutation(() => ({
mutationFn: (todoId: number) =>
lastValueFrom(this.todoService.create(todoId)),
}))
}
@Component({
template: `
<div>
@if (mutation.isPending()) {
<span>Adding todo...</span>
} @else if (mutation.isError()) {
<div>An error occurred: {{ mutation.error()?.message }}</div>
} @else if (mutation.isSuccess()) {
<div>Todo added!</div>
}
<button (click)="mutation.mutate(1)">Create Todo</button>
</div>
`,
})
export class TodosComponent {
todoService = inject(TodoService)
mutation = injectMutation(() => ({
mutationFn: (todoId: number) =>
lastValueFrom(this.todoService.create(todoId)),
}))
}
一个 mutation 在任何给定时刻只能处于以下状态之一
除了这些主要状态之外,还可以根据 mutation 的状态获得更多信息
在上面的示例中,您还看到可以通过使用单个变量或对象调用 mutate 函数,将变量传递给 mutation 函数。
即使只有变量,mutations 也没有那么特别,但是当与 onSuccess 选项,Query Client 的 invalidateQueries 方法 和 Query Client 的 setQueryData 方法 一起使用时,mutations 将成为一个非常强大的工具。
有时您可能需要清除 mutation 请求的 error 或 data。为此,您可以使用 reset 函数来处理此问题
@Component({
standalone: true,
selector: 'todo-item',
imports: [ReactiveFormsModule],
template: `
<form [formGroup]="todoForm" (ngSubmit)="onCreateTodo()">
@if (mutation.error()) {
<h5 (click)="mutation.reset()">{{ mutation.error() }}</h5>
}
<input type="text" formControlName="title" />
<br />
<button type="submit">Create Todo</button>
</form>
`,
})
export class TodosComponent {
mutation = injectMutation(() => ({
mutationFn: createTodo,
}))
fb = inject(NonNullableFormBuilder)
todoForm = this.fb.group({
title: this.fb.control('', {
validators: [Validators.required],
}),
})
title = toSignal(this.todoForm.controls.title.valueChanges, {
initialValue: '',
})
onCreateTodo = () => {
this.mutation.mutate(this.title())
}
}
@Component({
standalone: true,
selector: 'todo-item',
imports: [ReactiveFormsModule],
template: `
<form [formGroup]="todoForm" (ngSubmit)="onCreateTodo()">
@if (mutation.error()) {
<h5 (click)="mutation.reset()">{{ mutation.error() }}</h5>
}
<input type="text" formControlName="title" />
<br />
<button type="submit">Create Todo</button>
</form>
`,
})
export class TodosComponent {
mutation = injectMutation(() => ({
mutationFn: createTodo,
}))
fb = inject(NonNullableFormBuilder)
todoForm = this.fb.group({
title: this.fb.control('', {
validators: [Validators.required],
}),
})
title = toSignal(this.todoForm.controls.title.valueChanges, {
initialValue: '',
})
onCreateTodo = () => {
this.mutation.mutate(this.title())
}
}
injectMutation 附带一些辅助选项,允许在 mutation 生命周期的任何阶段快速轻松地产生副作用。 这些对于 在 mutations 后使 queries 失效和重新获取 甚至 乐观更新 都非常方便
mutation = injectMutation(() => ({
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!
},
}))
mutation = injectMutation(() => ({
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 时,它将首先被等待,然后再调用下一个回调
mutation = injectMutation(() => ({
mutationFn: addTodo,
onSuccess: async () => {
console.log("I'm first!")
},
onSettled: async () => {
console.log("I'm second!")
},
}))
mutation = injectMutation(() => ({
mutationFn: addTodo,
onSuccess: async () => {
console.log("I'm first!")
},
onSettled: async () => {
console.log("I'm second!")
},
}))
您可能会发现您希望在调用 mutate 时,触发超出 injectMutation 上定义的其他回调。 这可用于触发组件特定的副作用。为此,您可以在 mutation 变量之后向 mutate 函数提供任何相同的回调选项。 支持的选项包括:onSuccess、onError 和 onSettled。 请记住,如果您的组件在 mutation 完成之前被销毁,则这些额外的回调将不会运行。
mutation = injectMutation(() => ({
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
},
}))
mutation.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!
},
})
mutation = injectMutation(() => ({
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
},
}))
mutation.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 回调时,连续 mutations 的处理方式略有不同。 当传递给 mutate 函数时,它们将仅触发一次,并且仅在组件仍然处于活动状态时触发。 这是因为每次调用 mutate 函数时,mutation 观察器都会被移除并重新订阅。 相反,injectMutation 处理程序会为每个 mutate 调用执行。
请注意,很可能传递给 injectMutation 的 mutationFn 是异步的。 在这种情况下,mutations 完成的顺序可能与 mutate 函数调用的顺序不同。
export class Example {
mutation = injectMutation(() => ({
mutationFn: addTodo,
onSuccess: (data, variables, context) => {
// Will be called 3 times
},
}))
doMutations() {
;['Todo 1', 'Todo 2', 'Todo 3'].forEach((todo) => {
this.mutation.mutate(todo, {
onSuccess: (data, variables, context) => {
// Will execute only once, for the last mutation (Todo 3),
// regardless which mutation resolves first
},
})
})
}
}
export class Example {
mutation = injectMutation(() => ({
mutationFn: addTodo,
onSuccess: (data, variables, context) => {
// Will be called 3 times
},
}))
doMutations() {
;['Todo 1', 'Todo 2', 'Todo 3'].forEach((todo) => {
this.mutation.mutate(todo, {
onSuccess: (data, variables, context) => {
// Will execute only once, for the last mutation (Todo 3),
// regardless which mutation resolves first
},
})
})
}
}
使用 mutateAsync 而不是 mutate 以获得一个 promise,该 promise 将在成功时解析,或在错误时抛出错误。 例如,这可以用于组合副作用。
mutation = injectMutation(() => ({ mutationFn: addTodo }))
try {
const todo = await mutation.mutateAsync(todo)
console.log(todo)
} catch (error) {
console.error(error)
} finally {
console.log('done')
}
mutation = injectMutation(() => ({ mutationFn: addTodo }))
try {
const todo = await mutation.mutateAsync(todo)
console.log(todo)
} catch (error) {
console.error(error)
} finally {
console.log('done')
}
默认情况下,TanStack Query 不会在错误时重试 mutation,但可以通过 retry 选项实现
mutation = injectMutation(() => ({
mutationFn: addTodo,
retry: 3,
}))
mutation = injectMutation(() => ({
mutationFn: addTodo,
retry: 3,
}))
如果 mutations 因设备离线而失败,则当设备重新连接时,它们将按相同顺序重试。
如果需要,可以将 Mutations 持久化到存储,并在稍后恢复。 这可以使用 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,
})
class someComponent {
// Start mutation in some component:
mutation = injectMutation(() => ({ mutationKey: ['addTodo'] }))
someMethod() {
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,
})
class someComponent {
// Start mutation in some component:
mutation = injectMutation(() => ({ mutationKey: ['addTodo'] }))
someMethod() {
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 插件 持久化离线 mutations,则除非您提供默认的 mutation 函数,否则在重新加载页面时无法恢复 mutations。
这是一个技术限制。 当持久化到外部存储时,仅持久化 mutations 的状态,因为函数无法序列化。 水合后,触发 mutation 的组件可能未初始化,因此调用 resumePausedMutations 可能会产生错误:No mutationFn found。
我们还有一个广泛的 离线示例,涵盖了 queries 和 mutations。
默认情况下,所有 mutations 都是并行运行的 - 即使您多次调用同一 mutation 的 .mutate()。 可以为 Mutations 提供带有 id 的 scope 以避免这种情况。 所有具有相同 scope.id 的 mutations 将串行运行,这意味着当它们被触发时,如果该 scope 已经有一个 mutation 正在进行中,它们将以 isPaused: true 状态启动。 它们将被放入队列中,并在队列中的时间到来时自动恢复。
const mutation = injectMutation({
mutationFn: addTodo,
scope: {
id: 'todo',
},
})
const mutation = injectMutation({
mutationFn: addTodo,
scope: {
id: 'todo',
},
})