TanStack Query (React Query): Handling HTTP Requests with EaseEntry 1 of 10
What TanStack Query Is and Setup
TanStack Query (formerly React Query) is a library for fetching, caching, and synchronizing server data. It replaces hand-written useEffect + useState fetching logic.
Notes
- Without it, every component manages its own loading, error, and data state (see HTTP Requests and Custom Hooks chapters).
- TanStack Query adds what
useFetchlacks: caching, background refetching, request deduplication, cancellation, retries, and cache invalidation after mutations. - It is not an HTTP client. You still write the
fetchcalls; TanStack Query manages when they run and what happens to their results. - Setup:
- Install
@tanstack/react-query. - Create one
QueryClient(the cache) in its own module so it can be imported outside components. - Wrap the app in
<QueryClientProvider client={queryClient}>.
- Install
What / Why / When
- What
A server-state manager.
- Why
Removes fetching boilerplate and makes data feel instant through caching.
- When
Apps that read and write backend data in many places.
Code compare
npm install @tanstack/react-querynpm install -D @tanstack/react-query-devtools # optional cache inspector// util/query-client.tsimport { QueryClient } from "@tanstack/react-query";export const queryClient = new QueryClient();// App.tsximport { QueryClientProvider } from "@tanstack/react-query";import { ReactQueryDevtools } from "@tanstack/react-query-devtools";import { RouterProvider } from "react-router/dom";import { queryClient } from "./util/query-client";export default function App() { return ( <QueryClientProvider client={queryClient}> <RouterProvider router={router} /> <ReactQueryDevtools /> </QueryClientProvider> );}// services/events.ts — plain fetch functions used by queries and mutationsconst API = import.meta.env.VITE_API_URL;export type EventItem = { id: string; title: string; date: string; image: string; description: string;};export type EventInput = Omit<EventItem, "id">;export class HttpError extends Error { constructor( public code: number, message: string, public info?: unknown, ) { super(message); }}async function request<T>(url: string, init?: RequestInit): Promise<T> { const response = await fetch(url, init); if (!response.ok) { throw new HttpError( response.status, "Request failed.", await response.json().catch(() => null), ); } return response.json();}export async function fetchEvents({ signal, search, max,}: { signal?: AbortSignal; search?: string; max?: number;}) { const params = new URLSearchParams(); if (search) params.set("search", search); if (max) params.set("max", String(max)); const { events } = await request<{ events: EventItem[] }>(`${API}/events?${params}`, { signal }); return events;}export async function fetchEvent({ id, signal }: { id: string; signal?: AbortSignal }) { const { event } = await request<{ event: EventItem }>(`${API}/events/${id}`, { signal }); return event;}export function createNewEvent(event: EventInput) { return request<{ event: EventItem }>(`${API}/events`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ event }), });}export function updateEvent({ id, event }: { id: string; event: EventInput }) { return request<{ event: EventItem }>(`${API}/events/${id}`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ event }), });}export function deleteEvent({ id }: { id: string }) { return request<void>(`${API}/events/${id}`, { method: "DELETE" });}Angular's built-in tools cover the same ground without a library:
HttpClientservices hold the requests (unchanged from the HTTP chapter).rxResource()wraps a service Observable in signals:value(),isLoading(),error(),reload(). It re-runs when itsparamssignal changes and cancels the previous request.httpResource()is a shorter version that takes a URL directly.- A root service with signals acts as the shared cache when data must survive across components.
// services/events.service.ts@Injectable({ providedIn: "root" })export class EventsService { private http = inject(HttpClient); private api = environment.apiUrl; getEvents(params: { search?: string; max?: number } = {}) { let httpParams = new HttpParams(); if (params.search) httpParams = httpParams.set("search", params.search); if (params.max) httpParams = httpParams.set("max", params.max); return this.http .get<{ events: EventItem[] }>(`${this.api}/events`, { params: httpParams }) .pipe(map((r) => r.events)); } getEvent(id: string) { return this.http .get<{ event: EventItem }>(`${this.api}/events/${id}`) .pipe(map((r) => r.event)); } createEvent(event: EventInput) { return this.http.post<{ event: EventItem }>(`${this.api}/events`, { event }); } updateEvent(id: string, event: EventInput) { return this.http.put<{ event: EventItem }>(`${this.api}/events/${id}`, { event }); } deleteEvent(id: string) { return this.http.delete<void>(`${this.api}/events/${id}`); }}Quick reference
| Concern | React | Angular (built-in) |
|---|---|---|
| HTTP calls | fetch functions in services/ | HttpClient service |
| Request state | useQuery | rxResource() / httpResource() |
| Shared cache | QueryClient | Root service with signals |
| Setup | <QueryClientProvider> | provideHttpClient() |