TanStack Query (React Query): Handling HTTP Requests with EaseEntry 6 of 10

Sending Data with useMutation

useMutation handles requests that change data (POST, PUT, DELETE). Unlike useQuery, it doesn't run automatically; you trigger it with mutate().

Notes

  • useQuery is for reading; it runs on mount and caches results. A queryFn could technically POST, but it would fire on every mount and refetch. Use useMutation for writes.
  • mutationFn: the function that sends the request. It receives one argument.
  • mutate(variables): triggers the mutation. Its single argument (often an object) is passed to mutationFn, i.e. what the backend should receive.
  • Returned state: isPending, isError, error, data, reset.
  • Lifecycle callbacks:
CallbackRuns whenTypical use
onMutateRight before mutationFnOptimistic updates (Entry 8)
onSuccessRequest succeededInvalidate queries, navigate
onErrorRequest failedRollback, error UI
onSettledAlways, after success or errorCleanup, final invalidation
  • onSuccess is like the next callback of an Observable subscribe (it receives the response). onSettled is closer to finalize.

What / Why / When

What

Managed write requests.

Why

Pending/error state and post-success logic without manual state.

When

Creating, updating, or deleting data.

Code compare

React
// pages/NewEvent.tsximport { useMutation } from "@tanstack/react-query";import { useNavigate } from "react-router";import { createNewEvent, type EventInput } from "../services/events";import { queryClient } from "../util/query-client";​export default function NewEvent() {  const navigate = useNavigate();​  const { mutate, isPending, isError, error } = useMutation({    mutationFn: createNewEvent,    onSuccess: () => {      queryClient.invalidateQueries({ queryKey: ["events"] }); // Entry 7      navigate("/events");    },  });​  function handleSubmit(formData: EventInput) {    mutate(formData); // passed to createNewEvent(formData)  }​  return (    <Modal onClose={() => navigate("../")}>      <EventForm onSubmit={handleSubmit}>        {isPending && "Submitting..."}        {!isPending && (          <>            <Link to="../" className="button-text">              Cancel            </Link>            <button type="submit" className="button">              Create            </button>          </>        )}      </EventForm>      {isError && <ErrorBlock title="Failed to create event" message={error.message} />}    </Modal>  );}
Angular
// new-event.component.ts@Component({  selector: "app-new-event",  imports: [EventFormComponent, ErrorBlockComponent, RouterLink],  template: `    <app-event-form (submitted)="onSubmit($event)">      @if (isPending()) {        Submitting...      } @else {        <a routerLink="../" class="button-text">Cancel</a>        <button type="submit" class="button">Create</button>      }    </app-event-form>    @if (error()) {      <app-error-block title="Failed to create event" [message]="error()!" />    }  `,})export class NewEventComponent {  private eventsService = inject(EventsService);  private eventsCache = inject(EventsCacheService);  private router = inject(Router);​  protected isPending = signal(false);  protected error = signal<string | null>(null);​  onSubmit(formData: EventInput) {    this.isPending.set(true);    this.error.set(null);​    this.eventsService      .createEvent(formData)      .pipe(finalize(() => this.isPending.set(false))) // like onSettled      .subscribe({        next: () => {          // like onSuccess          this.eventsCache.invalidate();          this.router.navigate(["/events"]);        },        error: (err: HttpErrorResponse) => this.error.set(err.message), // like onError      });  }}

Differences and gotchas

  • mutate takes one argument. Pass an object for several values: mutate({ id, event }).
  • mutate doesn't return a Promise. Use mutateAsync if you need to await it (and handle errors yourself).
  • Angular's resource APIs are for reading only; writes stay as service calls with subscribe.

Quick reference

TaskReactAngular
Define writeuseMutation({ mutationFn })Service method (post/put/delete)
Triggermutate(variables)service.method(...).subscribe()
PendingisPendingisPending signal
SuccessonSuccessnext
ErroronErrorerror
AlwaysonSettledfinalize()