与查询不同,突变通常用于创建/更新/删除数据或执行服务器端副作用。为此,TanStack Query 导出了一个 injectMutation 函数。
这是一个将新待办事项添加到服务器的突变示例
@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)),
}))
}
突变在任何给定时刻只能处于以下状态之一
除了这些主要状态之外,根据突变的状态,还可以获得更多信息
在上面的示例中,您还看到可以通过使用 mutate 函数并传入**单个变量或对象**来将变量传递给您的突变函数。
即使只有变量,突变本身也并不特殊,但当与 onSuccess 选项、Query Client 的 invalidateQueries 方法 和 Query Client 的 setQueryData 方法 一起使用时,突变就会成为一个非常强大的工具。
有时您需要清除突变请求的 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 = 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 时,它将首先被 await,然后才会调用下一个回调。
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 上定义的那些。这可以用来触发组件特定的副作用。要做到这一点,您可以将与 injectMutation 相同的任何回调选项传递给 mutate 函数(在您的突变变量之后)。支持的选项包括:onSuccess、onError 和 onSettled。请注意,如果您的组件在突变完成之前被销毁,这些额外的回调将不会运行。
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 回调时,存在细微的差别。当传递给 mutate 函数时,它们将只触发一次,并且仅在组件仍然活动时触发。这是因为每次调用 mutate 函数时,突变观察者都会被移除并重新订阅。相反,injectMutation 处理程序对每次 mutate 调用都会执行。
请注意,传递给 injectMutation 的 mutationFn 很可能是异步的。在这种情况下,突变完成的顺序可能与 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。例如,这可用于组合副作用。
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 不会在错误时重试突变,但可以使用 retry 选项进行重试。
mutation = injectMutation(() => ({
mutationFn: addTodo,
retry: 3,
}))
mutation = injectMutation(() => ({
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,
})
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 插件 持久化离线突变,当页面重新加载时,突变将无法恢复,除非您提供一个默认的突变函数。
这是一个技术限制。当持久化到外部存储时,只能持久化突变的状态,因为函数无法被序列化。在水合之后,触发突变的组件可能尚未初始化,因此调用 resumePausedMutations 可能会产生错误:No mutationFn found。
我们还有一个详细的 离线示例,涵盖了查询和突变。
默认情况下,所有突变都会并行运行 - 即使您多次调用同一个突变的 .mutate()。可以通过为突变提供一个带有 id 的 scope 来避免这种情况。具有相同 scope.id 的所有突变将按顺序运行,这意味着当它们被触发时,如果该范围已有一个突变正在进行中,它们将处于 isPaused: true 状态。它们将被放入队列,并在轮到它们时自动恢复。
const mutation = injectMutation({
mutationFn: addTodo,
scope: {
id: 'todo',
},
})
const mutation = injectMutation({
mutationFn: addTodo,
scope: {
id: 'todo',
},
})