TanStack Query (React Query): Handling HTTP Requests with EaseEntry 7 of 10
Invalidating Queries
queryClient.invalidateQueries() marks cached queries as stale and refetches the ones currently on screen, so the UI shows the latest data after a mutation.
Notes
- After a POST/PUT/DELETE, any query showing that data is outdated. Invalidate it in
onSuccess(oronSettled). - Prefix matching:
invalidateQueries({ queryKey: ['events'] })matches every key starting with'events':['events'],['events', { max: 3 }],['events', { search: 'x' }],['events', '123']. exact: true: only the key that matches exactly (['events']), not the dynamic variants.- Target one item:
invalidateQueries({ queryKey: ['events', id] }). refetchType:
| Value | Behavior |
|---|---|
'active' (default) | Mark stale; refetch queries used by mounted components |
'all' | Mark stale; refetch every matching query, mounted or not |
'none' | Mark stale only; refetch later, when next used |
- Use
refetchType: 'none'when an immediate refetch would fail or waste a request. Course example: after deleting an event, refetching that event's detail query (still mounted while navigating away) would return 404. queryClientis imported from its module (Entry 1), so it works in components, mutations, and router loaders. Inside components you can also useuseQueryClient().
What / Why / When
- What
Cache invalidation after writes.
- Why
Keeps all screens consistent with the backend without manual refetch calls.
- When
In every mutation that changes data some query displays.
Code compare
// All event lists and detailsqueryClient.invalidateQueries({ queryKey: ["events"] });// Only the plain ['events'] queryqueryClient.invalidateQueries({ queryKey: ["events"], exact: true });// One event's detailqueryClient.invalidateQueries({ queryKey: ["events", id] });// pages/EventDetails.tsx — delete without refetching the deleted eventconst { mutate: deleteMutate, isPending: isDeleting } = useMutation({ mutationFn: deleteEvent, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["events"], refetchType: "none" }); navigate("/events"); },});function handleDelete() { deleteMutate({ id: id! });}A version signal in a root service plays the role of invalidation: resources that read it reload whenever it changes.
// services/events.service.ts (addition)private _version = signal(0);readonly version = this._version.asReadonly();invalidate() { this._version.update((v) => v + 1);}// Any component that should refresh after mutationsprotected events = rxResource({ params: () => ({ max: 3, version: this.eventsService.version() }), stream: ({ params }) => this.eventsService.getEvents({ max: params.max }),});// After a mutationthis.eventsService.createEvent(formData).subscribe(() => { this.eventsService.invalidate(); // every resource reading version() reloads this.router.navigate(["/events"]);});For a single component, call resource.reload() directly.
Differences and gotchas
- TanStack Query invalidation is key-based and global. The Angular version signal is coarse (everything reading it reloads); use one signal per data type for finer control.
- The notes compare invalidation to
switchMap. It's closer to emitting a "refresh" trigger that queries listen to, which is exactly what the version signal does. - Angular has no
refetchType: 'none'. Navigating away first destroys the component and its resource, so the deleted item is never refetched.
Quick reference
| Task | React | Angular |
|---|---|---|
| Refresh all related | invalidateQueries({ queryKey: ['events'] }) | eventsService.invalidate() |
| Exact key only | exact: true | Separate version signal |
| One item | queryKey: ['events', id] | resource.reload() |
| Mark stale only | refetchType: 'none' | — (component destroyed on navigate) |
| Access client | Imported queryClient / useQueryClient() | inject(EventsService) |