Framework
Version

Mutationen

Im Gegensatz zu Queries werden Mutationen typischerweise verwendet, um Daten zu erstellen/aktualisieren/löschen oder serverseitige Effekte auszuführen. Zu diesem Zweck exportiert TanStack Query die Funktion injectMutation.

Hier ist ein Beispiel für eine Mutation, die einen neuen Todo-Eintrag auf dem Server hinzufügt

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

Eine Mutation kann sich zu jedem gegebenen Zeitpunkt nur in einem der folgenden Zustände befinden

  • isIdle oder status === 'idle' - Die Mutation ist derzeit inaktiv oder im Anfangszustand/zurückgesetzt
  • isPending oder status === 'pending' - Die Mutation wird gerade ausgeführt
  • isError oder status === 'error' - Bei der Mutation ist ein Fehler aufgetreten
  • isSuccess oder status === 'success' - Die Mutation war erfolgreich und die Mutationsdaten sind verfügbar

Über diese Hauptzustände hinaus sind je nach Zustand der Mutation weitere Informationen verfügbar

  • error - Wenn sich die Mutation im Zustand error befindet, ist der Fehler über die Eigenschaft error verfügbar.
  • data - Wenn sich die Mutation im Zustand success befindet, sind die Daten über die Eigenschaft data verfügbar.

Im obigen Beispiel haben Sie auch gesehen, dass Sie Variablen an Ihre Mutationsfunktion übergeben können, indem Sie die Funktion mutate mit **einer einzelnen Variablen oder einem Objekt** aufrufen.

Auch mit nur Variablen sind Mutationen nicht so besonders, aber wenn sie mit der Option onSuccess, der Methode invalidateQueries des Query Clients und der Methode setQueryData des Query Clients verwendet werden, werden Mutationen zu einem sehr mächtigen Werkzeug.

Zurücksetzen des Mutationszustands

Manchmal müssen Sie den error oder die data einer Mutationsanfrage löschen. Dazu können Sie die Funktion reset verwenden.

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

Mutations-Seiteneffekte

injectMutation verfügt über einige Hilfsoptionen, die schnelle und einfache Nebeneffekte in jeder Phase des Mutationslebenszyklus ermöglichen. Diese sind sowohl für das Invalidieren und erneute Abrufen von Queries nach Mutationen als auch für optimistische Updates praktisch.

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

Wenn Sie in einer der Callback-Funktionen ein Promise zurückgeben, wird dieses zuerst abgewartet, bevor der nächste Callback aufgerufen wird.

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

Sie werden feststellen, dass Sie zusätzliche Callbacks auslösen möchten, die über die auf injectMutation definierten hinausgehen, wenn Sie mutate aufrufen. Dies kann verwendet werden, um komponentenspezifische Nebeneffekte auszulösen. Dazu können Sie beliebige der gleichen Callback-Optionen an die Funktion mutate übergeben, nachdem Ihre Mutationsvariable. Unterstützte Optionen sind: onSuccess, onError und onSettled. Bitte beachten Sie, dass diese zusätzlichen Callbacks nicht ausgeführt werden, wenn Ihre Komponente zerstört wird, *bevor* die Mutation abgeschlossen ist.

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

Aufeinanderfolgende Mutationen

Bei sequenziellen Mutationen gibt es einen kleinen Unterschied bei der Behandlung der Callbacks onSuccess, onError und onSettled. Wenn sie an die Funktion mutate übergeben werden, werden sie nur *einmal* und nur dann ausgelöst, wenn die Komponente noch aktiv ist. Dies liegt daran, dass der Mutationsbeobachter jedes Mal entfernt und neu abonniert wird, wenn die Funktion mutate aufgerufen wird. Im Gegensatz dazu werden die Handler von injectMutation für jeden Aufruf von mutate ausgeführt.

Beachten Sie, dass die an injectMutation übergebene mutationFn höchstwahrscheinlich asynchron ist. In diesem Fall kann die Reihenfolge, in der Mutationen erfüllt werden, von der Reihenfolge der Aufrufe der Funktion mutate abweichen.

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

Verwenden Sie mutateAsync anstelle von mutate, um ein Promise zu erhalten, das bei Erfolg aufgelöst oder bei einem Fehler einen Fehler auslöst. Dies kann beispielsweise zur Zusammensetzung von Seiteneffekten verwendet werden.

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

Wiederholung

Standardmäßig versucht TanStack Query nicht, eine Mutation bei einem Fehler erneut auszuführen. Mit der Option retry ist dies jedoch möglich.

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

Wenn Mutationen fehlschlagen, weil das Gerät offline ist, werden sie in derselben Reihenfolge wiederholt, wenn das Gerät wieder online geht.

Mutations speichern

Mutationen können bei Bedarf in einem Speicher persistiert und zu einem späteren Zeitpunkt wieder aufgenommen werden. Dies kann mit den Hydrationsfunktionen erfolgen.

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

Offline-Mutationen persistieren

Wenn Sie Offline-Mutationen mit dem Plugin persistQueryClient beibehalten, können Mutationen beim Neuladen der Seite nicht fortgesetzt werden, es sei denn, Sie geben eine Standard-Mutationsfunktion an.

Dies ist eine technische Einschränkung. Wenn Sie in externen Speicher persistieren, wird nur der Zustand von Mutationen gespeichert, da Funktionen nicht serialisiert werden können. Nach der Hydration ist die Komponente, die die Mutation auslöst, möglicherweise noch nicht initialisiert, sodass der Aufruf von resumePausedMutations möglicherweise einen Fehler liefert: No mutationFn found.

Wir haben auch ein umfassendes Offline-Beispiel, das sowohl Queries als auch Mutationen abdeckt.

Mutationsbereiche

Standardmäßig laufen alle Mutationen parallel – auch wenn Sie .mutate() derselben Mutation mehrmals aufrufen. Mutationen kann ein scope mit einer id zugewiesen werden, um dies zu vermeiden. Alle Mutationen mit derselben scope.id werden seriell ausgeführt, was bedeutet, dass sie beim Auslösen in einem Zustand isPaused: true beginnen, wenn bereits eine Mutation für diesen Bereich aktiv ist. Sie werden in eine Warteschlange gestellt und automatisch fortgesetzt, sobald ihre Zeit in der Warteschlange gekommen ist.

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