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
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
Über diese Hauptzustände hinaus sind je nach Zustand der Mutation weitere Informationen 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 mutate Funktion ist eine asynchrone Funktion. Das bedeutet, dass Sie sie in Solid 16 und früheren Versionen nicht direkt in einem Event-Callback verwenden können. Wenn Sie das Event in onSubmit abrufen müssen, müssen Sie mutate in eine andere Funktion einschließen. Dies liegt am Solid Event Pooling.
// This will not work in Solid 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 Solid 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>
}
Manchmal müssen Sie den error oder die data einer Mutationsanfrage löschen. Dazu können Sie die Funktion reset verwenden.
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>
)
}
useMutation 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 Abfragen nach Mutationen als auch für optimistische Updates nützlich.
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.
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 beim Aufrufen von mutate zusätzliche Callbacks auslösen, die über die auf useMutation definierten hinausgehen. Dies kann zum Auslösen von komponenten-spezifischen Nebeneffekten verwendet werden. Dazu können Sie nach Ihrer Mutationsvariable beliebige der gleichen Callback-Optionen an die Funktion mutate übergeben. Unterstützte Optionen sind: onSuccess, onError und onSettled. Bitte beachten Sie, dass diese zusätzlichen Callbacks nicht ausgeführt werden, wenn Ihre Komponente de-mountet wird, bevor die Mutation abgeschlossen ist.
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!
},
})
Bei aufeinanderfolgenden Mutationen gibt es einen geringfügigen Unterschied in der Behandlung von onSuccess, onError und onSettled Callbacks. Wenn diese an die Funktion mutate übergeben werden, werden sie nur *einmal* und nur dann ausgeführt, 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.
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
},
})
})
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.
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')
}
Standardmäßig versucht TanStack Query nicht, eine Mutation bei einem Fehler erneut auszuführen. Mit der Option retry ist dies jedoch möglich.
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.
Mutationen können bei Bedarf in einem Speicher persistiert und zu einem späteren Zeitpunkt wieder aufgenommen werden. Dies kann mit den Hydrationsfunktionen erfolgen.
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()
Wenn Sie Offline-Mutationen mit dem persistQueryClient Plugin beibehalten, können Mutationen nach einem Seiten-Reload nicht fortgesetzt 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.
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 umfangreiches Offline-Beispiel, das sowohl Abfragen als auch Mutationen abdeckt.
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.
const mutation = useMutation(() => {
mutationFn: addTodo,
scope: {
id: 'todo',
},
})
const mutation = useMutation(() => {
mutationFn: addTodo,
scope: {
id: 'todo',
},
})
Weitere Informationen zu Mutationen finden Sie unter #12: Mastering Mutations in Solid Query aus den Community Resources.