Mutate data and invalidate queries in TanStack Query with useMutation and the client's invalidateQueries method. A mutation sends the change to the server, then invalidates the affected query keys so they refetch. This guide shows the full add, update, and refetch flow.
A mutation changes server data
Queries read data, but changes go through mutations. useMutation wraps a function that creates, updates, or deletes data, and it exposes pending, error, and success states for that change.
Mutations are not cached the way queries are, because the point is the side effect on the server. Queries are declarative and run on their own; mutations are imperative and run when you call them.
const mutation = useMutation({
mutationFn: (newTodo) =>
fetch("/api/todos", {
method: "POST",
body: JSON.stringify(newTodo),
}).then((res) => res.json()),
});The mutationFn receives the variable you pass to mutate later. In the example it posts JSON, but any promise-returning function works, including a call through a plain fetch or Axios.
The mutation does not run until you call mutate, and unlike queries it does not retry by default. If it fails, the error lands in mutation.error rather than being retried automatically.
Invalidate the affected queries
After a successful mutation, the cached queries that depend on the changed data are out of date. invalidateQueries marks them stale and refetches the ones currently on screen.
const queryClient = useQueryClient();
queryClient.invalidateQueries({ queryKey: ["todos"] });Invalidation is safer than updating the cache by hand, because you do not have to reproduce the server's response logic. Pass a query key and every matching query refetches. An invalidated query refetches in the background when a component is using it, and inactive queries refetch only when they mount again.
A prefix match means ["todos"] also invalidates ["todos", { status: "draft" }], which is why key structure from useQuery explained matters.
Put the flow together
Most mutations follow the same shape: run the change, then invalidate everything that depends on it. Combine the two pieces in onSuccess, so a successful add refreshes the list without a manual reload. This round trip is the standard replacement for a manual list refresh after a write.
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: addTodo,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["todos"] });
},
});When the mutation succeeds, invalidateQueries marks the todos queries stale and refetches them. The list component that uses the same key updates on its own, with no prop drilling or manual state sync.
The list keeps its current data while the refetch happens, then swaps in the fresh result. This relies on the shared client from how to set up TanStack Query in React.
Handle the mutation states
The mutation reports its own loading and error states, separate from the query states. Drive the submit button and the error message from those values. The mutation does not know about your form, so read the input yourself and pass the value along.
<form onSubmit={handleSubmit}>
<label htmlFor="title">Title</label>
<input id="title" value={title} onChange={(e) => setTitle(e.target.value)} />
<button type="submit" disabled={mutation.isPending}>
{mutation.isPending ? "Adding..." : "Add todo"}
</button>
</form>The button disables while the mutation runs, which prevents duplicate submissions. mutation.error holds the failure, and mutation.reset clears the success and error states before the next attempt.
Show mutation.error in a paragraph with role alert so the failure is announced, and let reset clear it for the next attempt. When you need the UI to change instantly instead of waiting for the refetch, see how to build optimistic updates in React.
Call mutate from the handler
The mutation does nothing until you call mutate. Do that from a submit handler with the data the user entered.
const [title, setTitle] = useState("");
function handleSubmit(e) {
e.preventDefault();
mutation.mutate({ title });
}preventDefault stops the page reload, and mutate sends the title as the mutation variable. The mutationFn you defined earlier receives this object, so the variable shape has to match what that function expects.
You can pass a second onSuccess argument to mutate for one-off side effects that only this call needs. For a promise you can await, use mutation.mutateAsync instead. Keep the handler small so the form logic stays in one place.
Rune AI
Key Insights
- Use useMutation for create, update, and delete calls.
- Call mutation.mutate with the data to send.
- Invalidate matching query keys in onSuccess.
- Let invalidated queries refetch in the background.
- Disable the submit button with mutation.isPending.
Frequently Asked Questions
What is the difference between a query and a mutation?
Why invalidate instead of updating the cache directly?
How do I show a loading state while mutating?
Conclusion
Wrap server changes in useMutation, then invalidate the affected query keys in onSuccess. The list refetches on its own, and the mutation states drive the button and any visible errors.
More in this topic
How to Build a Dropdown Menu in React
Build a React dropdown menu with the ARIA menu button pattern. Handle open and close, keyboard arrows, and clicks outside the menu.
How to Animate React Components with Motion
Animate React components with the Motion library. Set up motion, add enter, hover, and exit animations, and respect reduced motion.
Headless UI Components Explained: Logic Without Locked Styling
Understand headless UI components and how libraries like Radix give you unstyled, accessible behavior that you style yourself.