Unlike queries, mutations are typically used to create/update/delete data or perform server side-effects. For this purpose, TanStack Query exports a useMutation
hook.
Here's an example of a mutation that adds a new todo to the server:
1function App() {
2 const mutation = useMutation({
3 mutationFn: (newTodo) => {
4 return axios.post('/todos', newTodo)
5 },
6 })
7
8 return (
9 <div>
10 {mutation.isPending ? (
11 'Adding todo...'
12 ) : (
13 <>
14 {mutation.isError ? (
15 <div>An error occurred: {mutation.error.message}</div>
16 ) : null}
17
18 {mutation.isSuccess ? <div>Todo added!</div> : null}
19
20 <button
21 onClick={() => {
22 mutation.mutate({ id: new Date(), title: 'Do Laundry' })
23 }}
24 >
25 Create Todo
26 </button>
27 </>
28 )}
29 </div>
30 )
31}
A mutation can only be in one of the following states at any given moment:
isIdle
orstatus === 'idle'
- The mutation is currently idle or in a fresh/reset stateisPending
orstatus === 'pending'
- The mutation is currently runningisError
orstatus === 'error'
- The mutation encountered an errorisSuccess
orstatus === 'success'
- The mutation was successful and mutation data is available
Beyond those primary states, more information is available depending on the state of the mutation:
error
- If the mutation is in anerror
state, the error is available via theerror
property.data
- If the mutation is in asuccess
state, the data is available via thedata
property.
In the example above, you also saw that you can pass variables to your mutations function by calling the mutate
function with a single variable or object.
Even with just variables, mutations aren't all that special, but when used with the onSuccess
option, the Query Client's invalidateQueries
method and the Query Client's setQueryData
method, mutations become a very powerful tool.
IMPORTANT: The
mutate
function is an asynchronous function, which means you cannot use it directly in an event callback in React 16 and earlier. If you need to access the event inonSubmit
you need to wrapmutate
in another function. This is due to React event pooling.
1// This will not work in React 16 and earlier
2const CreateTodo = () => {
3 const mutation = useMutation({
4 mutationFn: (event) => {
5 event.preventDefault()
6 return fetch('/api', new FormData(event.target))
7 },
8 })
9
10 return <form onSubmit={mutation.mutate}>...</form>
11}
12
13// This will work
14const CreateTodo = () => {
15 const mutation = useMutation({
16 mutationFn: (formData) => {
17 return fetch('/api', formData)
18 },
19 })
20 const onSubmit = (event) => {
21 event.preventDefault()
22 mutation.mutate(new FormData(event.target))
23 }
24
25 return <form onSubmit={onSubmit}>...</form>
26}
Resetting Mutation State #
It's sometimes the case that you need to clear the error
or data
of a mutation request. To do this, you can use the reset
function to handle this:
1const CreateTodo = () => {
2 const [title, setTitle] = useState('')
3 const mutation = useMutation({ mutationFn: createTodo })
4
5 const onCreateTodo = (e) => {
6 e.preventDefault()
7 mutation.mutate({ title })
8 }
9
10 return (
11 <form onSubmit={onCreateTodo}>
12 {mutation.error && (
13 <h5 onClick={() => mutation.reset()}>{mutation.error}</h5>
14 )}
15 <input
16 type="text"
17 value={title}
18 onChange={(e) => setTitle(e.target.value)}
19 />
20 <br />
21 <button type="submit">Create Todo</button>
22 </form>
23 )
24}
Mutation Side Effects #
useMutation
comes with some helper options that allow quick and easy side-effects at any stage during the mutation lifecycle. These come in handy for both invalidating and refetching queries after mutations and even optimistic updates
1useMutation({
2 mutationFn: addTodo,
3 onMutate: (variables) => {
4 // A mutation is about to happen!
5
6 // Optionally return a context containing data to use when for example rolling back
7 return { id: 1 }
8 },
9 onError: (error, variables, context) => {
10 // An error happened!
11 console.log(`rolling back optimistic update with id ${context.id}`)
12 },
13 onSuccess: (data, variables, context) => {
14 // Boom baby!
15 },
16 onSettled: (data, error, variables, context) => {
17 // Error or success... doesn't matter!
18 },
19})
When returning a promise in any of the callback functions it will first be awaited before the next callback is called:
1useMutation({
2 mutationFn: addTodo,
3 onSuccess: async () => {
4 console.log("I'm first!")
5 },
6 onSettled: async () => {
7 console.log("I'm second!")
8 },
9})
You might find that you want to trigger additional callbacks beyond the ones defined on useMutation
when calling mutate
. This can be used to trigger component-specific side effects. To do that, you can provide any of the same callback options to the mutate
function after your mutation variable. Supported options include: onSuccess
, onError
and onSettled
. Please keep in mind that those additional callbacks won't run if your component unmounts before the mutation finishes.
1useMutation({
2 mutationFn: addTodo,
3 onSuccess: (data, variables, context) => {
4 // I will fire first
5 },
6 onError: (error, variables, context) => {
7 // I will fire first
8 },
9 onSettled: (data, error, variables, context) => {
10 // I will fire first
11 },
12})
13
14mutate(todo, {
15 onSuccess: (data, variables, context) => {
16 // I will fire second!
17 },
18 onError: (error, variables, context) => {
19 // I will fire second!
20 },
21 onSettled: (data, error, variables, context) => {
22 // I will fire second!
23 },
24})
Consecutive mutations #
There is a slight difference in handling onSuccess
, onError
and onSettled
callbacks when it comes to consecutive mutations. When passed to the mutate
function, they will be fired up only once and only if the component is still mounted. This is due to the fact that mutation observer is removed and resubscribed every time when the mutate
function is called. On the contrary, useMutation
handlers execute for each mutate
call.
Be aware that most likely,
mutationFn
passed touseMutation
is asynchronous. In that case, the order in which mutations are fulfilled may differ from the order ofmutate
function calls.
1useMutation({
2 mutationFn: addTodo,
3 onSuccess: (data, error, variables, context) => {
4 // Will be called 3 times
5 },
6})
7
8const todos = ['Todo 1', 'Todo 2', 'Todo 3']
9todos.forEach((todo) => {
10 mutate(todo, {
11 onSuccess: (data, error, variables, context) => {
12 // Will execute only once, for the last mutation (Todo 3),
13 // regardless which mutation resolves first
14 },
15 })
16})
Promises #
Use mutateAsync
instead of mutate
to get a promise which will resolve on success or throw on an error. This can for example be used to compose side effects.
1const mutation = useMutation({ mutationFn: addTodo })
2
3try {
4 const todo = await mutation.mutateAsync(todo)
5 console.log(todo)
6} catch (error) {
7 console.error(error)
8} finally {
9 console.log('done')
10}
Retry #
By default, TanStack Query will not retry a mutation on error, but it is possible with the retry
option:
1const mutation = useMutation({
2 mutationFn: addTodo,
3 retry: 3,
4})
If mutations fail because the device is offline, they will be retried in the same order when the device reconnects.
Persist mutations #
Mutations can be persisted to storage if needed and resumed at a later point. This can be done with the hydration functions:
1const queryClient = new QueryClient()
2
3// Define the "addTodo" mutation
4queryClient.setMutationDefaults(['addTodo'], {
5 mutationFn: addTodo,
6 onMutate: async (variables) => {
7 // Cancel current queries for the todos list
8 await queryClient.cancelQueries({ queryKey: ['todos'] })
9
10 // Create optimistic todo
11 const optimisticTodo = { id: uuid(), title: variables.title }
12
13 // Add optimistic todo to todos list
14 queryClient.setQueryData(['todos'], (old) => [...old, optimisticTodo])
15
16 // Return context with the optimistic todo
17 return { optimisticTodo }
18 },
19 onSuccess: (result, variables, context) => {
20 // Replace optimistic todo in the todos list with the result
21 queryClient.setQueryData(['todos'], (old) =>
22 old.map((todo) =>
23 todo.id === context.optimisticTodo.id ? result : todo,
24 ),
25 )
26 },
27 onError: (error, variables, context) => {
28 // Remove optimistic todo from the todos list
29 queryClient.setQueryData(['todos'], (old) =>
30 old.filter((todo) => todo.id !== context.optimisticTodo.id),
31 )
32 },
33 retry: 3,
34})
35
36// Start mutation in some component:
37const mutation = useMutation({ mutationKey: ['addTodo'] })
38mutation.mutate({ title: 'title' })
39
40// If the mutation has been paused because the device is for example offline,
41// Then the paused mutation can be dehydrated when the application quits:
42const state = dehydrate(queryClient)
43
44// The mutation can then be hydrated again when the application is started:
45hydrate(queryClient, state)
46
47// Resume the paused mutations:
48queryClient.resumePausedMutations()
Persisting Offline mutations #
If you persist offline mutations with the persistQueryClient plugin, mutations cannot be resumed when the page is reloaded unless you provide a default mutation function.
This is a technical limitation. When persisting to an external storage, only the state of mutations is persisted, as functions cannot be serialized. After hydration, the component that triggers the mutation might not be mounted, so calling resumePausedMutations
might yield an error: No mutationFn found
.
1const persister = createSyncStoragePersister({
2 storage: window.localStorage,
3})
4const queryClient = new QueryClient({
5 defaultOptions: {
6 queries: {
7 gcTime: 1000 * 60 * 60 * 24, // 24 hours
8 },
9 },
10})
11
12// we need a default mutation function so that paused mutations can resume after a page reload
13queryClient.setMutationDefaults(['todos'], {
14 mutationFn: ({ id, data }) => {
15 return api.updateTodo(id, data)
16 },
17})
18
19export default function App() {
20 return (
21 <PersistQueryClientProvider
22 client={queryClient}
23 persistOptions={{ persister }}
24 onSuccess={() => {
25 // resume mutations after initial restore from localStorage was successful
26 queryClient.resumePausedMutations()
27 }}
28 >
29 <RestOfTheApp />
30 </PersistQueryClientProvider>
31 )
32}
We also have an extensive offline example that covers both queries and mutations.
Mutation Scopes #
Per default, all mutations run in parallel - even if you invoke .mutate()
of the same mutation multiple times. Mutations can be given a scope
with an id
to avoid that. All mutations with the same scope.id
will run in serial, which means when they are triggered, they will start in isPaused: true
state if there is already a mutation for that scope in progress. They will be put into a queue and will automatically resume once their time in the queue has come.
1const mutation = useMutation({
2 mutationFn: addTodo,
3 scope: {
4 id: 'todo',
5 },
6})
Further reading #
For more information about mutations, have a look at #12: Mastering Mutations in React Query from the Community Resources.