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
useQueryis for reading; it runs on mount and caches results. AqueryFncould technically POST, but it would fire on every mount and refetch. UseuseMutationfor 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 tomutationFn, i.e. what the backend should receive.- Returned state:
isPending,isError,error,data,reset. - Lifecycle callbacks:
| Callback | Runs when | Typical use |
|---|---|---|
onMutate | Right before mutationFn | Optimistic updates (Entry 8) |
onSuccess | Request succeeded | Invalidate queries, navigate |
onError | Request failed | Rollback, error UI |
onSettled | Always, after success or error | Cleanup, final invalidation |
onSuccessis like thenextcallback of an Observablesubscribe(it receives the response).onSettledis closer tofinalize.
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
// 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> );}// 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
mutatetakes one argument. Pass an object for several values:mutate({ id, event }).mutatedoesn't return a Promise. UsemutateAsyncif you need toawaitit (and handle errors yourself).- Angular's
resourceAPIs are for reading only; writes stay as service calls withsubscribe.
Quick reference
| Task | React | Angular |
|---|---|---|
| Define write | useMutation({ mutationFn }) | Service method (post/put/delete) |
| Trigger | mutate(variables) | service.method(...).subscribe() |
| Pending | isPending | isPending signal |
| Success | onSuccess | next |
| Error | onError | error |
| Always | onSettled | finalize() |