Hello! Welcome to the fourth lesson in our TanStack Query course.
In our previous lessons, we've focused on reading server state. You learned how to set up QueryClientProvider and use the powerful useQuery hook to fetch, cache, and display data, complete with loading and error states.
Now, we'll shift our focus to the other essential half of server state management: writing data. This lesson introduces the useMutation hook, which is designed for creating, updating, or deleting data on your server.
Our goal today is to learn how to submit data to a server using useMutation and correctly handle the various states of that submission process, such as loading (pending), success, and error.
1. Queries vs. Mutations: Reading vs. Writing
Before we dive into the code, let's clarify the conceptual difference between a query and a mutation.
- A query is a request to read data. It's declarative and designed to run automatically to keep your UI synchronized with the server.
- A mutation is a request to change data (create, update, delete). It's an action with side effects.

This distinction has a fundamental impact on how the hooks are designed.
Mastering Mutations in React Query - TkDodo's blog
TkDodo's blog offers an excellent explanation of this difference. Let's read the introduction to understand the imperative nature of mutations.
Please read the sections "What are mutations?", "Similarities to useQuery", and "Differences to useQuery". Focus on the idea that useQuery is declarative (it runs automatically), while useMutation is imperative (you must trigger it manually).
As the article highlights, you wouldn't want a "create post" mutation to fire automatically every time you focus the browser window. Instead, useMutation gives you a function that you call explicitly, for example, when a user clicks a "Submit" button.
2. The useMutation Hook in Action
Let's look at the basic structure of useMutation. It's very similar to useQuery but with a few key differences.
Mutations | TanStack Query React Docs
The official TanStack Query documentation provides a concise example of using useMutation to create a new todo item.
Read the introductory section and study the first code block. Pay close attention to the mutationFn, the mutation.mutate call inside the onClick handler, and how the UI changes based on isPending, isError, and isSuccess.
Let's break down the core components from that example:
mutationFn: This is the only required option. It's an asynchronous function that performs the side effect on the server (e.g., making aPOSTorPATCHrequest) and must return a promise.mutatefunction: TheuseMutationhook returns an object, one of whose properties is a function calledmutate. You call this function to trigger the mutation. The first argument you pass tomutatewill be passed as the first argument to yourmutationFn.- Status Flags: Just like
useQuery,useMutationprovides flags to help you build a responsive UI:isPending:truewhile themutationFnis executing. This is the equivalent ofisLoadinginuseQuery. You'd typically show a spinner or disable the submit button during this state.isSuccess:trueif themutationFnpromise resolves successfully. You might show a success message or redirect the user.isError:trueif themutationFnpromise is rejected.error: IfisErroris true, this property contains the error object.
Here is a simplified example of a component for adding a post:
import { useMutation } from '@tanstack/react-query';
import axios from 'axios';
// The function that will be called by the mutation
const createPost = async (newPost) => {
const { data } = await axios.post('/api/posts', newPost);
return data;
};
function AddPostForm() {
const mutation = useMutation({ mutationFn: createPost });
const handleSubmit = (event) => {
event.preventDefault();
// Call mutate with the data for the new post
mutation.mutate({ title: 'My New Post', body: '...' });
};
return (
<div>
<button onClick={handleSubmit} disabled={mutation.isPending}>
{mutation.isPending ? 'Submitting...' : 'Create Post'}
</button>
{mutation.isError && (
<div>An error occurred: {mutation.error.message}</div>
)}
{mutation.isSuccess && <div>Post created successfully!</div>}
</div>
);
}
This code directly addresses today's learning outcome by demonstrating how to use the hook and its status flags to manage the entire lifecycle of a data submission.
3. Handling Side Effects with Callbacks
What if you want to run some code only after a mutation succeeds or fails? useMutation provides helpful callback options for this.
Mutations | TanStack Query React Docs
The official docs list the available callbacks. These are extremely useful for triggering side effects.
Read the "Mutation Side Effects" section. Focus on the definitions of onSuccess, onError, and onSettled. You can skim the part about passing callbacks to the mutate function for now.
The most common callbacks are:
onSuccess: Fires when the mutation is successful. It receives the data returned from yourmutationFn. This is the perfect place to show a success notification or, as we'll see in the next lesson, to update the client's state.onError: Fires when the mutation fails. It receives the error object. You can use this for logging errors to a service.onSettled: Fires after the mutation is either successful or has errored. This is useful for code that needs to run regardless of the outcome, like hiding a loading indicator.
Here's our previous example, now with an onSuccess callback to show an alert:
function AddPostForm() {
const mutation = useMutation({
mutationFn: createPost,
onSuccess: (data) => {
// 'data' is the response from our 'createPost' function
alert(`Post created with ID: ${data.id}`);
}
});
// ... rest of the component
}
4. mutate vs. mutateAsync
You might see another function returned from useMutation called mutateAsync. It's important to understand the difference.
Mastering Mutations in React Query - TkDodo's blog
TkDodo has a great section explaining when to use mutate versus mutateAsync.
Please read the section "Mutate or MutateAsync". The key takeaway is that mutate is generally preferred because it handles error catching for you.
To summarize:
mutate: "Fire-and-forget". It doesn't return a promise. You handle side effects using theonSuccessandonErrorcallbacks. This is the recommended approach for most use cases.mutateAsync: Returns a promise that resolves with the success data or throws an error. This can be useful if you need to chain promises, but it requires you to write your owntry...catchblock to handle errors.
For most component-level logic, sticking with mutate and its callbacks leads to cleaner and safer code.
Conclusion
Congratulations! You've now learned how to perform the "write" operations in the client-server relationship using TanStack Query. You're no longer limited to just fetching data; you can now build fully interactive experiences.
Key Takeaways:
useMutationis an imperative hook used to create, update, or delete server data.- You trigger it by calling the
mutatefunction, passing the variables yourmutationFnneeds. - It provides status flags (
isPending,isSuccess,isError) that allow you to easily update your UI based on the state of the request. - Side effects, like showing notifications, are best handled in the
onSuccess,onError, andonSettledcallbacks.
Next Lesson Preview:
We've created a post, but our list of posts doesn't know about it yet! We would have to manually refresh the page to see the new item. In the next lesson, we will solve this crucial problem. You'll learn how to use the QueryClient to invalidate queries after a mutation, which tells TanStack Query to automatically refetch the relevant data and keep your UI perfectly in sync with the server.