Framework
Version

Mutationen

Im Gegensatz zu Queries werden Mutationen typischerweise zum Erstellen/Aktualisieren/Löschen von Daten oder zum Ausführen von serverseitigen Effekten verwendet. Zu diesem Zweck exportiert TanStack Query den Hook useMutation.

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

vue
<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>

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.

vue
<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>

Mutations-Seiteneffekte

useMutation bietet einige Hilfsoptionen, die schnelle und einfache Nebeneffekte in jeder Phase des Mutationslebenszyklus ermöglichen. Diese sind nützlich für das Invalidieren und erneute Abrufen von Queries nach Mutationen und sogar für optimistische Updates.

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

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

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

Möglicherweise möchten Sie zusätzliche Rückrufe auslösen, die über die auf useMutation 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 Rückrufe nicht ausgeführt werden, wenn Ihre Komponente unmountet wird, bevor die Mutation abgeschlossen ist.

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

Aufeinanderfolgende Mutationen

Es gibt einen geringfügigen Unterschied in der Handhabung von onSuccess, onError und onSettled Rückrufen bei aufeinanderfolgenden Mutationen. Wenn sie an die Funktion mutate übergeben werden, werden sie nur einmal und nur dann ausgelöst, wenn die Komponente noch gemountet ist. Dies liegt daran, dass der Mutationsbeobachter bei jedem Aufruf der Funktion mutate entfernt und neu abonniert wird. Im Gegensatz dazu werden die Handler von useMutation für jeden Aufruf von mutate ausgeführt.

Beachten Sie, dass die an useMutation ü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.

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

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.

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

Wiederholung

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

tsx
const mutation = useMutation({
  mutationFn: addTodo,
  retry: 3,
})
const mutation = useMutation({
  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.

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

Offline-Mutationen persistieren

Wenn Sie Offline-Mutationen mit dem persistQueryClient Plugin speichern, 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 in einen externen Speicher persistiert wird, wird nur der Zustand von Mutationen persistiert, da Funktionen nicht serialisiert werden können. Nach der Hydration ist die Komponente, die die Mutation auslöst, möglicherweise nicht gemountet, sodass der Aufruf von resumePausedMutations einen Fehler zurückgeben kann: No mutationFn found.

js
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: createAsyncStoragePersister({ 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: createAsyncStoragePersister({ storage: localStorage }),
    })
  },
  clientPersisterOnSuccess: (queryClient) => {
    queryClient.resumePausedMutations()
  },
}

createApp(App).use(VueQueryPlugin, vueQueryOptions).mount('#app')

Wir haben auch ein ausführliches 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 = useMutation({
  mutationFn: addTodo,
  scope: {
    id: 'todo',
  },
})
const mutation = useMutation({
  mutationFn: addTodo,
  scope: {
    id: 'todo',
  },
})