🚀 LEVEL UP TO SENIOR:Unlock 500+ Advanced Practical Challenges & Exercises.
🎓 COURSERA PARTNER:Earn professional Google, Meta, and IBM certificates to supercharge your resume.
HTML MASTER CLASS /// LEARN TAGS /// BUILD STRUCTURE /// SEMANTIC WEB /// HTML MASTER CLASS /// LEARN TAGS ///

TanStack Query Mutations: Writing Data Back to the Server

Learn TanStack Query mutations: useMutation, onSuccess cache invalidation, onError handling, and preventing duplicate submits.

Total XP: 0|💻 react XP: 0

Skill Matrix

UNLOCK NODES BY LEARNING NEW TAGS.

System Hub

Mutations fundamentals.

Quick Quiz //

What is useMutation designed for?


🚀 LEVEL UP TO SENIOR:Unlock 500+ Advanced Practical Challenges & Exercises.
🎓 COURSERA PARTNER:Earn professional Google, Meta, and IBM certificates to supercharge your resume.

useQuery handles reads, but creating, updating, or deleting data needs a different tool: useMutation, triggered explicitly by user actions. This lesson covers the mutate function, the onSuccess/invalidateQueries pattern, centralized error handling, and preventing duplicate submissions with isPending.

1Why Writes Are Different from Reads

useQuery is designed for reads — data you want cached, kept fresh, and refetched automatically. Creating, updating, or deleting data is a fundamentally different shape of operation: it happens once, in direct response to a user action, and useMutation is the hook purpose-built for that pattern.

2The useMutation Hook

useMutation accepts a mutationFn and returns a mutate function that must be called explicitly to trigger it, unlike useQuery which runs automatically. It also returns isPending, isError, and isSuccess flags, giving writes the same predictable state shape reads already have.

3Invalidating After Success with onSuccess

The most common mutation pattern invalidates a related query right after a successful write, so the UI reflects the change. useMutation's onSuccess callback, run automatically once mutationFn resolves, is the standard place to call queryClient.invalidateQueries.

4Handling Errors with onError

onError runs when mutationFn throws or rejects, providing one centralized place to show an error message, log the failure, or trigger cleanup logic, instead of wrapping every mutation call site in its own try/catch block.

5Disabling the Trigger While Pending

The isPending flag is specifically useful for disabling a submit control (or showing a loading state on it) while a mutation is in flight, preventing an impatient user's double-click from triggering a duplicate write — a surprisingly common source of production data-integrity bugs.

6Step-by-Step Breakdown

Why Writes Are Different from Reads. useQuery is built for reading data — GET requests you want cached and kept fresh. Creating, updating, or deleting data is a fundamentally different shape of operation: it happens once, in response to a user action, and you usually want to know exactly when it starts, succeeds, or fails. That's what useMutation is for.

The useMutation Hook. useMutation takes a mutationFn and returns a mutate function you call explicitly (unlike useQuery, which runs automatically). It also returns isPending, isError, and isSuccess flags, giving you the same predictable state shape for writes that useQuery gives you for reads.

Unlike useQuery, when does the mutation function passed to useMutation actually run?

  • Only when mutate() is explicitly called, not automatically on mount
  • Automatically as soon as the component mounts, just like useQuery

Invalidating After Success with onSuccess. The most common mutation pattern: after a write succeeds, invalidate the related query so the UI reflects the new data. useMutation's onSuccess callback is the standard place to do this, run automatically right after mutationFn resolves.

Handling Errors with onError. onError runs if mutationFn throws or its promise rejects, giving you one place to show an error toast, log the failure, or trigger a rollback — instead of wrapping every call site in its own try/catch.

What's the standard place to show a toast notification when a mutation's server request fails?

  • The onError callback
  • The onSuccess callback

Disabling the Trigger While Pending. The isPending flag exists specifically to disable a submit button (or show a spinner on it) while a mutation is in flight, preventing duplicate submissions from an impatient double-click. This one boolean covers a surprisingly common source of production bugs.

Mastery Achieved. You now understand mutations: useMutation for explicit, on-demand writes, the standard onSuccess + invalidateQueries pattern, centralized error handling with onError, and using isPending to prevent duplicate submissions. Next, you'll learn Optimistic Updates for making writes feel instant.

Level Up 🚀

Advanced cheat sheets, SEO tricks, and interview prep for this topic.

Browser Support

ChromeSupported

Fully supported.

FirefoxSupported

Fully supported.

SafariSupported

Fully supported.

EdgeSupported

Fully supported.

Accessibility (A11y)

1A Disabled, Pending Submit Button Needs a Clear Accessible Label

When disabling a button via isPending, update its visible text or aria-label (e.g. 'Saving...' instead of 'Save') so screen reader users understand why the control is temporarily unavailable, rather than assuming it's broken.

SEO Implications

  • 1

    Mutations Are a Purely Client-Side Interaction Concern

    useMutation governs client-triggered writes after hydration and has no bearing on server-rendered HTML content or crawlability.

Best Practices

Always Pair a Mutation with Cache Invalidation or an Update

A successful write that doesn't invalidate or manually update the related query leaves the UI showing stale data until an unrelated refetch trigger happens to fire — always handle this explicitly in onSuccess.

Use isPending to Prevent Duplicate Submissions

Disable the triggering control (button, form) while isPending is true for any mutation that shouldn't be triggered twice in a row, like a payment submission or a record creation.

Frequent Bugs

THE BUG

Clicking a save button twice quickly creates two duplicate records on the server.

THE FIX

The button wasn't disabled while the mutation was pending. Bind the button's disabled attribute to the mutation's isPending flag to prevent a second click from firing before the first request completes.

THE BUG

A mutation succeeds according to the server, but the UI keeps showing old data.

THE FIX

The mutation's onSuccess callback isn't invalidating (or manually updating) the related query cache. Add queryClient.invalidateQueries with the appropriate queryKey inside onSuccess.

Real-World Examples

Adding a Todo Item with Cache Invalidation

A todo app needs the todo list to update immediately after a new item is successfully added via a POST request. Wiring useMutation's onSuccess to invalidate the ['todos'] query ensures the list automatically refetches and displays the new item without any manual state manipulation.

function useAddTodo() {
  const queryClient = useQueryClient();
  return useMutation({
    mutationFn: (text) => fetch('/api/todos', { method: 'POST', body: JSON.stringify({ text }) }),
    onSuccess: () => queryClient.invalidateQueries({ queryKey: ['todos'] }),
    onError: (error) => toast.error(`Failed to add todo: ${error.message}`),
  });
}

Interview Prep

?Frequently Asked Questions

Pascual Vila

Pascual Vila

Frontend Instructor // Code Syllabus

Common Pitfalls & Errors

The Error //

Not disabling the submit control while a mutation is pending, allowing duplicate submissions

<button onClick={() => mutate(data)} disabled={isPending}> {isPending ? 'Saving...' : 'Save'} </button>

The Solution //

Bind the triggering button's disabled attribute to the mutation's isPending flag, and ideally change its label (e.g. to 'Saving...') to make the pending state clear to the user.

The Error //

A successful mutation doesn't update the UI because the related query was never invalidated

useMutation({ mutationFn: updateTodo, onSuccess: () => queryClient.invalidateQueries({ queryKey: ['todos'] }), });

The Solution //

Add an onSuccess callback to the mutation that calls queryClient.invalidateQueries with the queryKey of the data that changed.

Lesson Glossary

[01]useMutation

The hook for triggering create/update/delete operations, unlike useQuery's automatic reads.

Code Preview
useMutation({ mutationFn })

[02]mutate

The function returned by useMutation, called explicitly to trigger the mutation.

Code Preview
mutate(newTodo)

[03]onSuccess

A useMutation callback run automatically after the mutation resolves successfully.

Code Preview
onSuccess: () => invalidateQueries(...)

[04]onError

A useMutation callback run automatically if the mutation throws or rejects.

Code Preview
onError: (error) => toast.error(...)

[05]isPending

A boolean from useMutation indicating whether the mutation is currently in flight.

Code Preview
disabled={isPending}

Continue Learning