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 useFetch lacks: caching, background refetching, request deduplication, cancellation, retries, and cache invalidation after mutations.
  • It is not an HTTP client. You still write the fetch calls; TanStack Query manages when they run and what happens to their results.
  • Setup:
    1. Install @tanstack/react-query.
    2. Create one QueryClient (the cache) in its own module so it can be imported outside components.
    3. Wrap the app in <QueryClientProvider client={queryClient}>.

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

React
npm install @tanstack/react-querynpm install -D @tanstack/react-query-devtools   # optional cache inspector
React
// util/query-client.tsimport { QueryClient } from "@tanstack/react-query";​export const queryClient = new QueryClient();
React
// 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>  );}
React
// 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:

  • HttpClient services hold the requests (unchanged from the HTTP chapter).
  • rxResource() wraps a service Observable in signals: value(), isLoading(), error(), reload(). It re-runs when its params signal 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.
Angular
// 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

ConcernReactAngular (built-in)
HTTP callsfetch functions in services/HttpClient service
Request stateuseQueryrxResource() / httpResource()
Shared cacheQueryClientRoot service with signals
Setup<QueryClientProvider>provideHttpClient()