框架
版本

Mutations

与 queries 不同,mutations 通常用于创建/更新/删除数据或执行服务器端副作用。为此,TanStack Query 导出 injectMutation 函数。

这是一个向服务器添加新 todo 的 mutation 示例

angular-ts
@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 在任何给定时刻只能处于以下状态之一

  • isIdlestatus === 'idle' - Mutation 当前处于空闲状态或新的/重置状态
  • isPendingstatus === 'pending' - Mutation 当前正在运行
  • isErrorstatus === 'error' - Mutation 遇到错误
  • isSuccessstatus === 'success' - Mutation 成功,并且 mutation 数据可用

除了这些主要状态之外,还可以根据 mutation 的状态获得更多信息

  • error - 如果 mutation 处于 error 状态,则可以通过 error 属性获得错误信息。
  • data - 如果 mutation 处于 success 状态,则可以通过 data 属性获得数据。

在上面的示例中,您还看到可以通过使用单个变量或对象调用 mutate 函数,将变量传递给 mutation 函数。

即使只有变量,mutations 也没有那么特别,但是当与 onSuccess 选项,Query Client 的 invalidateQueries 方法Query Client 的 setQueryData 方法 一起使用时,mutations 将成为一个非常强大的工具。

重置 Mutation 状态

有时您可能需要清除 mutation 请求的 errordata。为此,您可以使用 reset 函数来处理此问题

angular-ts
@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())
  }
}

Mutation 副作用

injectMutation 附带一些辅助选项,允许在 mutation 生命周期的任何阶段快速轻松地产生副作用。 这些对于 在 mutations 后使 queries 失效和重新获取 甚至 乐观更新 都非常方便

ts
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 时,它将首先被等待,然后再调用下一个回调

ts
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 函数提供任何相同的回调选项。 支持的选项包括:onSuccessonErroronSettled。 请记住,如果您的组件在 mutation 完成之前被销毁,则这些额外的回调将不会运行。

ts
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!
  },
})

连续 mutations

在处理 onSuccessonErroronSettled 回调时,连续 mutations 的处理方式略有不同。 当传递给 mutate 函数时,它们将仅触发一次,并且仅在组件仍然处于活动状态时触发。 这是因为每次调用 mutate 函数时,mutation 观察器都会被移除并重新订阅。 相反,injectMutation 处理程序会为每个 mutate 调用执行。

请注意,很可能传递给 injectMutationmutationFn 是异步的。 在这种情况下,mutations 完成的顺序可能与 mutate 函数调用的顺序不同。

ts
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
        },
      })
    })
  }
}

Promises

使用 mutateAsync 而不是 mutate 以获得一个 promise,该 promise 将在成功时解析,或在错误时抛出错误。 例如,这可以用于组合副作用。

ts
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')
}

Retry

默认情况下,TanStack Query 不会在错误时重试 mutation,但可以通过 retry 选项实现

ts
mutation = injectMutation(() => ({
  mutationFn: addTodo,
  retry: 3,
}))
mutation = injectMutation(() => ({
  mutationFn: addTodo,
  retry: 3,
}))

如果 mutations 因设备离线而失败,则当设备重新连接时,它们将按相同顺序重试。

持久化 mutations

如果需要,可以将 Mutations 持久化到存储,并在稍后恢复。 这可以使用 hydration 函数来完成

ts
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()

持久化离线 mutations

如果您使用 persistQueryClient 插件 持久化离线 mutations,则除非您提供默认的 mutation 函数,否则在重新加载页面时无法恢复 mutations。

这是一个技术限制。 当持久化到外部存储时,仅持久化 mutations 的状态,因为函数无法序列化。 水合后,触发 mutation 的组件可能未初始化,因此调用 resumePausedMutations 可能会产生错误:No mutationFn found

我们还有一个广泛的 离线示例,涵盖了 queries 和 mutations。

Mutation Scopes

默认情况下,所有 mutations 都是并行运行的 - 即使您多次调用同一 mutation 的 .mutate()。 可以为 Mutations 提供带有 idscope 以避免这种情况。 所有具有相同 scope.id 的 mutations 将串行运行,这意味着当它们被触发时,如果该 scope 已经有一个 mutation 正在进行中,它们将以 isPaused: true 状态启动。 它们将被放入队列中,并在队列中的时间到来时自动恢复。

tsx
const mutation = injectMutation({
  mutationFn: addTodo,
  scope: {
    id: 'todo',
  },
})
const mutation = injectMutation({
  mutationFn: addTodo,
  scope: {
    id: 'todo',
  },
})