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

tsx
function App() {
  const mutation = useMutation({
    mutationFn: (newTodo) => {
      return axios.post('/todos', newTodo)
    },
  })

  return (
    <div>
      {mutation.isPending ? (
        'Adding todo...'
      ) : (
        <>
          {mutation.isError ? (
            <div>An error occurred: {mutation.error.message}</div>
          ) : null}

          {mutation.isSuccess ? <div>Todo added!</div> : null}

          <button
            onClick={() => {
              mutation.mutate({ id: new Date(), title: 'Do Laundry' })
            }}
          >
            Create Todo
          </button>
        </>
      )}
    </div>
  )
}
function App() {
  const mutation = useMutation({
    mutationFn: (newTodo) => {
      return axios.post('/todos', newTodo)
    },
  })

  return (
    <div>
      {mutation.isPending ? (
        'Adding todo...'
      ) : (
        <>
          {mutation.isError ? (
            <div>An error occurred: {mutation.error.message}</div>
          ) : null}

          {mutation.isSuccess ? <div>Todo added!</div> : null}

          <button
            onClick={() => {
              mutation.mutate({ id: new Date(), title: 'Do Laundry' })
            }}
          >
            Create Todo
          </button>
        </>
      )}
    </div>
  )
}

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.

WICHTIG: Die Funktion mutate ist eine asynchrone Funktion, was bedeutet, dass Sie sie in React 16 und früher nicht direkt in einem Event-Callback verwenden können. Wenn Sie das Event in onSubmit aufrufen müssen, müssen Sie mutate in eine andere Funktion einschließen. Dies liegt am React Event Pooling.

tsx
// This will not work in React 16 and earlier
const CreateTodo = () => {
  const mutation = useMutation({
    mutationFn: (event) => {
      event.preventDefault()
      return fetch('/api', new FormData(event.target))
    },
  })

  return <form onSubmit={mutation.mutate}>...</form>
}

// This will work
const CreateTodo = () => {
  const mutation = useMutation({
    mutationFn: (formData) => {
      return fetch('/api', formData)
    },
  })
  const onSubmit = (event) => {
    event.preventDefault()
    mutation.mutate(new FormData(event.target))
  }

  return <form onSubmit={onSubmit}>...</form>
}
// This will not work in React 16 and earlier
const CreateTodo = () => {
  const mutation = useMutation({
    mutationFn: (event) => {
      event.preventDefault()
      return fetch('/api', new FormData(event.target))
    },
  })

  return <form onSubmit={mutation.mutate}>...</form>
}

// This will work
const CreateTodo = () => {
  const mutation = useMutation({
    mutationFn: (formData) => {
      return fetch('/api', formData)
    },
  })
  const onSubmit = (event) => {
    event.preventDefault()
    mutation.mutate(new FormData(event.target))
  }

  return <form onSubmit={onSubmit}>...</form>
}

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.

tsx
const CreateTodo = () => {
  const [title, setTitle] = useState('')
  const mutation = useMutation({ mutationFn: createTodo })

  const onCreateTodo = (e) => {
    e.preventDefault()
    mutation.mutate({ title })
  }

  return (
    <form onSubmit={onCreateTodo}>
      {mutation.error && (
        <h5 onClick={() => mutation.reset()}>{mutation.error}</h5>
      )}
      <input
        type="text"
        value={title}
        onChange={(e) => setTitle(e.target.value)}
      />
      <br />
      <button type="submit">Create Todo</button>
    </form>
  )
}
const CreateTodo = () => {
  const [title, setTitle] = useState('')
  const mutation = useMutation({ mutationFn: createTodo })

  const onCreateTodo = (e) => {
    e.preventDefault()
    mutation.mutate({ title })
  }

  return (
    <form onSubmit={onCreateTodo}>
      {mutation.error && (
        <h5 onClick={() => mutation.reset()}>{mutation.error}</h5>
      )}
      <input
        type="text"
        value={title}
        onChange={(e) => setTitle(e.target.value)}
      />
      <br />
      <button type="submit">Create Todo</button>
    </form>
  )
}

Mutations-Seiteneffekte

useMutation bietet einige Hilfsoptionen, die schnelle und einfache Seiteneffekte in jeder Phase des Mutationslebenszyklus ermöglichen. Diese sind nützlich sowohl für das Invalidieren und erneute Abrufen von Queries nach Mutationen als auch 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!")
  },
})

Es kann vorkommen, dass Sie **zusätzliche Callbacks** auslösen möchten, die über die auf useMutation definierten hinausgehen, wenn Sie mutate aufrufen. Dies kann verwendet werden, um komponentenspezifische Seiteneffekte auszulösen. Dazu können Sie beliebige der gleichen Callback-Optionen an die Funktion mutate nach Ihrer Mutationsvariablen übergeben. Unterstützte Optionen sind: onSuccess, onError und onSettled. Bitte beachten Sie, dass diese zusätzlichen Callbacks nicht ausgeführt werden, wenn Ihre Komponente unmounted 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 Behandlung von onSuccess, onError und onSettled Callbacks bei aufeinanderfolgenden Mutationen. Wenn sie an die Funktion mutate übergeben werden, werden sie nur *einmal* aufgerufen und nur, wenn die Komponente noch gemountet ist. Dies liegt daran, dass der Mutations-Observer 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 persistieren, können Mutationen nach einem Seiten-Reload nicht wiederhergestellt werden, es sei denn, Sie stellen eine Standard-Mutationsfunktion bereit.

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.

tsx
const persister = createSyncStoragePersister({
  storage: window.localStorage,
})
const queryClient = 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(['todos'], {
  mutationFn: ({ id, data }) => {
    return api.updateTodo(id, data)
  },
})

export default function App() {
  return (
    <PersistQueryClientProvider
      client={queryClient}
      persistOptions={{ persister }}
      onSuccess={() => {
        // resume mutations after initial restore from localStorage was successful
        queryClient.resumePausedMutations()
      }}
    >
      <RestOfTheApp />
    </PersistQueryClientProvider>
  )
}
const persister = createSyncStoragePersister({
  storage: window.localStorage,
})
const queryClient = 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(['todos'], {
  mutationFn: ({ id, data }) => {
    return api.updateTodo(id, data)
  },
})

export default function App() {
  return (
    <PersistQueryClientProvider
      client={queryClient}
      persistOptions={{ persister }}
      onSuccess={() => {
        // resume mutations after initial restore from localStorage was successful
        queryClient.resumePausedMutations()
      }}
    >
      <RestOfTheApp />
    </PersistQueryClientProvider>
  )
}

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

Weiterführende Lektüre

Für weitere Informationen zu Mutationen schauen Sie sich #12: Mastering Mutations in React Query in den Community-Ressourcen an.