How to Mutate Data and Invalidate Queries with TanStack Query

Use useMutation to change server data and invalidateQueries to refresh affected queries in TanStack Query. See the full add, update, and refetch flow.

7 min read

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.

App.jsxApp.jsx
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.

App.jsxApp.jsx
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.

App.jsxApp.jsx
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.

App.jsxApp.jsx
<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.

App.jsxApp.jsx
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

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.
RunePowered by Rune AI

Frequently Asked Questions

What is the difference between a query and a mutation?

A query reads data and is cached by key. A mutation changes data on the server and is not cached, so you trigger it with mutate instead of it running automatically.

Why invalidate instead of updating the cache directly?

Invalidation marks the affected queries stale and refetches them, so the server stays the source of truth. Direct cache updates are faster but require you to reproduce server logic.

How do I show a loading state while mutating?

Use mutation.isPending to disable the submit button and show progress. mutation.isError and mutation.error report failures, and mutation.reset clears the state before the next attempt.

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.