Data Fetching Patterns: HttpClient vs. fetch / axios / TanStack Query
For .NET engineers who know:
HttpClient,IHttpClientFactory,DelegatingHandler,System.Text.Json, typed HTTP clients, and Polly for resilience You’ll learn: How the JS/TS data fetching stack layers from rawfetchup through TanStack Query — and why TanStack Query’s caching model changes how you think about server state in the UI Time: 15-20 minutes
The .NET Way (What You Already Know)
In .NET, HttpClient is the standard HTTP client, registered through IHttpClientFactory to manage connection pooling and handle socket exhaustion. You configure it in Program.cs, inject it by interface, and it arrives typed and ready:
// Program.cs — register a typed HttpClient
builder.Services.AddHttpClient<IOrderApiClient, OrderApiClient>(client =>
{
client.BaseAddress = new Uri("https://api.example.com");
client.DefaultRequestHeaders.Add("Accept", "application/json");
})
.AddHttpMessageHandler<AuthTokenHandler>() // DelegatingHandler for auth
.AddTransientHttpErrorPolicy(policy => // Polly: retry on 5xx and network errors
policy.WaitAndRetryAsync(3, _ => TimeSpan.FromSeconds(1)));
// The typed client
public class OrderApiClient : IOrderApiClient
{
private readonly HttpClient _client;
public OrderApiClient(HttpClient client)
{
_client = client;
}
public async Task<Order?> GetOrderAsync(int id)
{
var response = await _client.GetAsync($"/api/orders/{id}");
response.EnsureSuccessStatusCode(); // throws on 4xx, 5xx
return await response.Content.ReadFromJsonAsync<Order>();
}
public async Task<IReadOnlyList<Order>> GetOrdersAsync(int userId)
{
return await _client.GetFromJsonAsync<List<Order>>($"/api/orders?userId={userId}")
?? [];
}
}
// DelegatingHandler for auth tokens — middleware pattern for HttpClient
public class AuthTokenHandler : DelegatingHandler
{
private readonly ITokenProvider _tokenProvider;
public AuthTokenHandler(ITokenProvider tokenProvider)
{
_tokenProvider = tokenProvider;
}
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken)
{
var token = await _tokenProvider.GetTokenAsync();
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
return await base.SendAsync(request, cancellationToken);
}
}
Key behaviors your code relies on:
EnsureSuccessStatusCode()throws on non-2xx responses — you always know when a request failedReadFromJsonAsync<T>()deserializes withSystem.Text.Json— typed deserializationDelegatingHandlerintercepts every request in the pipeline — one place for auth, logging, retry- Polly handles transient failures — retry policies configured once, applied everywhere
The JavaScript/TypeScript Way
The Foundation: The Fetch API
fetch is the browser’s built-in HTTP client, available in all modern browsers and in Node.js 18+. It is the lowest-level tool in the stack — the equivalent of creating an HttpClient instance directly without IHttpClientFactory, without typed deserialization, and without automatic error throwing.
// Basic GET request
const response = await fetch("https://api.example.com/orders/42");
const order: Order = await response.json();
// That looks simple. Here is what it does NOT do that HttpClient does:
The critical difference from HttpClient: fetch does not throw on HTTP errors. A 404, a 500, a 403 — all of these resolve successfully. The response.ok property and response.status tell you what happened, but you have to check them yourself.
// WRONG — this will not throw on 404 or 500
const response = await fetch("/api/orders/999");
const order = await response.json(); // may parse an error body, not an order
// CORRECT — check response.ok before deserializing
async function fetchOrder(id: number): Promise<Order> {
const response = await fetch(`/api/orders/${id}`);
if (!response.ok) {
// response.status: 404, 500, 403, etc.
// response.statusText: "Not Found", "Internal Server Error", etc.
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return response.json() as Promise<Order>;
// No automatic type validation — if the API returns wrong shape, TypeScript won't catch it at runtime
}
// POST with JSON body
async function createOrder(data: CreateOrderRequest): Promise<Order> {
const response = await fetch("/api/orders", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify(data),
});
if (!response.ok) {
const errorBody = await response.json().catch(() => ({}));
throw new ApiError(response.status, errorBody.message ?? "Request failed");
}
return response.json();
}
fetch is low-level and verbose for production use. You can use it directly in server-side code (Next.js server components, API routes) where you control the environment, but in client-side code you almost always layer on top of it.
Axios: A Better HttpClient Wrapper
Axios is the closest analog to HttpClient in JavaScript — it wraps fetch (or XMLHttpRequest in older environments) and adds the behaviors you expect from .NET:
- Throws on non-2xx responses automatically
- Serializes request body to JSON automatically
- Deserializes response body from JSON automatically
- Interceptors equivalent to
DelegatingHandler - Configurable base URL and default headers
npm install axios
// lib/api-client.ts — configured Axios instance
import axios, { AxiosError } from "axios";
export const apiClient = axios.create({
baseURL: process.env.NEXT_PUBLIC_API_URL ?? "https://api.example.com",
headers: {
"Content-Type": "application/json",
},
timeout: 10_000, // 10 seconds — like HttpClient.Timeout
});
// Request interceptor — equivalent to DelegatingHandler for outgoing requests
apiClient.interceptors.request.use((config) => {
const token = authStore.getToken();
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
// Response interceptor — equivalent to DelegatingHandler for incoming responses
apiClient.interceptors.response.use(
(response) => response, // pass through on success
async (error: AxiosError) => {
if (error.response?.status === 401) {
// Token expired — try to refresh
const refreshed = await authStore.refreshToken();
if (refreshed && error.config) {
// Retry the original request with the new token
error.config.headers.Authorization = `Bearer ${authStore.getToken()}`;
return apiClient.request(error.config);
}
authStore.logout();
}
return Promise.reject(error);
}
);
// Typed API functions using the configured client
export const orderApi = {
async getById(id: number): Promise<Order> {
const { data } = await apiClient.get<Order>(`/orders/${id}`);
return data;
// axios throws on non-2xx — no manual response.ok check needed
// data is typed as Order — but still no runtime type validation
},
async list(userId: number): Promise<Order[]> {
const { data } = await apiClient.get<Order[]>("/orders", {
params: { userId }, // appends as ?userId=42
});
return data;
},
async create(payload: CreateOrderRequest): Promise<Order> {
const { data } = await apiClient.post<Order>("/orders", payload);
return data;
},
async update(id: number, payload: UpdateOrderRequest): Promise<Order> {
const { data } = await apiClient.put<Order>(`/orders/${id}`, payload);
return data;
},
async delete(id: number): Promise<void> {
await apiClient.delete(`/orders/${id}`);
},
};
Axios is sufficient for server-to-server HTTP calls and for simple client applications. Where it falls short is managing server state in the UI — caching responses, knowing when data is stale, showing loading states, deduplicating parallel requests for the same data, and handling background refetching. That is where TanStack Query fits.
TanStack Query: Server State Management
TanStack Query (formerly React Query, with a Vue adapter called @tanstack/vue-query) is not an HTTP client. It sits above your HTTP client — you bring your own fetching function (Axios, fetch, or anything else) and TanStack Query manages what to do with the results:
- Caches responses and returns the cached value immediately on subsequent requests
- Deduplicates simultaneous requests for the same data (if ten components mount at once and all want
/orders/42, only one HTTP request goes out) - Refetches in the background when the cached data is stale (configurable)
- Manages loading, error, and success states
- Handles optimistic updates (show the change immediately, roll back on failure)
- Synchronizes with browser focus and network reconnection events
npm install @tanstack/react-query
# For Vue:
npm install @tanstack/vue-query
Setup
// app/providers.tsx — React setup (must wrap your application)
"use client";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
import { useState } from "react";
export function Providers({ children }: { children: React.ReactNode }) {
const [queryClient] = useState(
() =>
new QueryClient({
defaultOptions: {
queries: {
staleTime: 60 * 1000, // data stays fresh for 1 minute
gcTime: 5 * 60 * 1000, // unused cache entries cleared after 5 minutes
retry: 2, // retry failed queries twice (like Polly)
refetchOnWindowFocus: true, // refetch when user returns to the tab
},
},
})
);
return (
<QueryClientProvider client={queryClient}>
{children}
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
);
}
Queries: Reading Data
A query fetches and caches data. The queryKey is the cache key — an array that uniquely identifies this data. Think of it as the string key in IMemoryCache.GetOrCreateAsync(key, ...):
// hooks/useOrder.ts
import { useQuery } from "@tanstack/react-query";
import { orderApi } from "../lib/api-client";
export function useOrder(id: number) {
return useQuery({
queryKey: ["orders", id], // cache key — ["orders", 42] is distinct from ["orders", 99]
queryFn: () => orderApi.getById(id),
enabled: id > 0, // only fetch if we have a valid id
staleTime: 30 * 1000, // override default: this data stays fresh for 30s
});
}
export function useOrders(userId: number) {
return useQuery({
queryKey: ["orders", { userId }], // objects work — compared by deep equality
queryFn: () => orderApi.list(userId),
});
}
// components/OrderDetail.tsx — consuming the query
export function OrderDetail({ id }: { id: number }) {
const { data: order, isLoading, isError, error, isFetching } = useOrder(id);
// isLoading: true on first load (no cached data)
// isFetching: true whenever a request is in-flight (including background refetches)
// isError: true if the query failed
// data: the Order, or undefined if not yet loaded
if (isLoading) return <div>Loading order...</div>;
if (isError) return <div>Error: {error.message}</div>;
if (!order) return null;
return (
<div>
<h1>Order #{order.id}</h1>
{isFetching && <span>Refreshing...</span>}
<p>{order.customerName}</p>
</div>
);
}
When two components both call useOrder(42) — say, a detail panel and a breadcrumb — TanStack Query deduplicates the request and shares the cached result. In .NET, you would manually implement this with IMemoryCache. Here it is automatic.
Stale-While-Revalidate
The caching model is stale-while-revalidate: data is served from cache immediately (fast), and if the cached data is older than staleTime, a background request is fired to refresh it. The user never waits for a spinner on subsequent visits to the same page.
// The lifecycle of a query:
//
// 1. First call: no cache → isLoading: true → HTTP request → data cached → isLoading: false
// 2. Same component re-mounts within staleTime: cache hit → data returned immediately, no request
// 3. Same component re-mounts after staleTime: cache hit → data returned immediately
// AND a background request is fired (isFetching: true, isLoading: false)
// 4. User focuses the tab after being away: background refetch triggered
Mutations: Writing Data
A mutation is any operation that changes server state — POST, PUT, PATCH, DELETE. Mutations pair with cache invalidation: after a successful mutation, you tell TanStack Query to throw away cached data so it refetches fresh state:
// hooks/useCreateOrder.ts
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { orderApi } from "../lib/api-client";
import type { CreateOrderRequest } from "../schemas/order.schema";
export function useCreateOrder() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: CreateOrderRequest) => orderApi.create(data),
onSuccess: (newOrder) => {
// Invalidate the orders list so it refetches with the new order included
queryClient.invalidateQueries({ queryKey: ["orders"] });
// This is like calling Response.Redirect() to refresh data after a POST
// Optionally, seed the cache for the new order's detail page
queryClient.setQueryData(["orders", newOrder.id], newOrder);
},
onError: (error) => {
console.error("Order creation failed:", error);
},
});
}
// components/CreateOrderForm.tsx
export function CreateOrderForm() {
const createOrder = useCreateOrder();
const onSubmit = async (data: CreateOrderFormValues) => {
await createOrder.mutateAsync(data);
// mutateAsync throws on error — wraps in try/catch
// mutate(data) is fire-and-forget — does not throw, use onError instead
router.push(`/orders/${createOrder.data?.id}`);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
{/* ... form fields ... */}
<button type="submit" disabled={createOrder.isPending}>
{createOrder.isPending ? "Creating..." : "Create Order"}
</button>
{createOrder.isError && (
<div role="alert">{createOrder.error.message}</div>
)}
</form>
);
}
Optimistic Updates
Optimistic updates show the change in the UI immediately before the server confirms it — then roll back if the server returns an error. This is the pattern that makes UIs feel responsive at any network speed.
export function useDeleteOrder() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (id: number) => orderApi.delete(id),
onMutate: async (id) => {
// Cancel any in-flight refetches (prevents race conditions)
await queryClient.cancelQueries({ queryKey: ["orders"] });
// Snapshot the current cache value
const previousOrders = queryClient.getQueryData<Order[]>(["orders"]);
// Optimistically update the cache — remove the order immediately
queryClient.setQueryData<Order[]>(["orders"], (old) =>
old?.filter((o) => o.id !== id) ?? []
);
// Return snapshot for rollback in onError
return { previousOrders };
},
onError: (_error, _id, context) => {
// Roll back to the snapshot if the mutation fails
if (context?.previousOrders) {
queryClient.setQueryData(["orders"], context.previousOrders);
}
},
onSettled: () => {
// Always refetch after mutation settles (success or failure)
queryClient.invalidateQueries({ queryKey: ["orders"] });
},
});
}
Dependent Queries
Sometimes one query depends on the result of another — like fetching a user’s settings only after fetching the user:
function useUserSettings(userId: number | undefined) {
const userQuery = useQuery({
queryKey: ["users", userId],
queryFn: () => userApi.getById(userId!),
enabled: userId !== undefined,
});
const settingsQuery = useQuery({
queryKey: ["settings", userQuery.data?.settingsId],
queryFn: () => settingsApi.getById(userQuery.data!.settingsId),
enabled: userQuery.data?.settingsId !== undefined, // only runs after user is loaded
});
return { user: userQuery.data, settings: settingsQuery.data };
}
In .NET you would await these sequentially in a service method. TanStack Query handles the dependency declaratively — you describe what depends on what, and it manages the sequencing and caching.
Vue Integration
// Nuxt / Vue 3 — setup in app.vue or a plugin
import { VueQueryPlugin, QueryClient } from "@tanstack/vue-query";
const queryClient = new QueryClient({
defaultOptions: { queries: { staleTime: 60_000 } },
});
app.use(VueQueryPlugin, { queryClient });
<!-- components/OrderDetail.vue -->
<script setup lang="ts">
import { useQuery } from "@tanstack/vue-query";
import { orderApi } from "@/lib/api-client";
const props = defineProps<{ id: number }>();
const { data: order, isLoading, isError, error } = useQuery({
queryKey: computed(() => ["orders", props.id]),
queryFn: () => orderApi.getById(props.id),
});
</script>
<template>
<div v-if="isLoading">Loading...</div>
<div v-else-if="isError">Error: {{ error?.message }}</div>
<div v-else-if="order">
<h1>Order #{{ order.id }}</h1>
<p>{{ order.customerName }}</p>
</div>
</template>
Key Differences
| Concept | .NET HttpClient | fetch API | Axios | TanStack Query |
|---|---|---|---|---|
| Error on non-2xx | EnsureSuccessStatusCode() manual | Manual if (!response.ok) | Automatic | Via your fetcher |
| JSON deserialization | ReadFromJsonAsync<T>() | await response.json() (untyped) | { data } (TypeScript typed) | Via your fetcher |
| Request interceptors | DelegatingHandler | Manual wrapper | interceptors.request.use() | Not applicable |
| Response interceptors | DelegatingHandler | Manual wrapper | interceptors.response.use() | onError callbacks |
| Retry logic | Polly | Manual | Axios retry plugin | Built-in retry option |
| Caching | IMemoryCache (separate) | None | None | Built-in, automatic |
| Loading state | Manual bool loading | Manual | Manual | isLoading, isFetching |
| Deduplication | None | None | None | Automatic |
| Background refresh | None | None | None | staleTime + refetch |
| Optimistic updates | Manual | Manual | Manual | onMutate + rollback |
| Cache invalidation | Manual | None | None | invalidateQueries() |
| DI registration | IHttpClientFactory | Module import | Module import | QueryClientProvider |
Gotchas for .NET Engineers
Gotcha 1: fetch does not throw on HTTP errors — ever
This is the single biggest footgun for engineers coming from HttpClient. In .NET, non-2xx responses throw HttpRequestException unless you catch it. With fetch, you check response.ok manually or you silently receive the error response body and try to parse it as your expected type.
// This compiles. It runs. It is wrong.
const response = await fetch("/api/orders/99999");
const order: Order = await response.json();
// If the response was 404, you just parsed { "type": "NotFound", "title": "Not Found", ... }
// as an Order. TypeScript won't warn you — the cast is just a lie at runtime.
console.log(order.customerName); // undefined — no runtime error, silent data corruption
The fix: always check response.ok, or use Axios, or write a wrapper:
async function apiFetch<T>(url: string, options?: RequestInit): Promise<T> {
const response = await fetch(url, options);
if (!response.ok) {
const body = await response.json().catch(() => ({}));
throw new ApiError(response.status, body.detail ?? response.statusText);
}
return response.json() as Promise<T>;
}
Gotcha 2: TypeScript generics on fetch/axios do not validate at runtime
axios.get<Order>(url) and (await fetch(url)).json() as Order are compile-time assertions only. If your API changes its response shape, TypeScript will not protect you at runtime. You get undefined on fields that no longer exist, or worse, old values on renamed fields.
// This type annotation is a promise to TypeScript, not a runtime guarantee
const { data } = await apiClient.get<Order>("/orders/42");
// If the API now returns { order_id instead of id }, data.id is undefined at runtime
// TypeScript sees it as Order and is satisfied — it trusted your generic
// Solution: parse with Zod for runtime safety
const rawData = await apiClient.get("/orders/42");
const order = orderSchema.parse(rawData.data); // throws ZodError if shape is wrong
For internal APIs where you control both sides, the TypeScript generic is usually sufficient — mismatches will be caught quickly. For third-party APIs or where the backend team is independent, add Zod parsing on the response.
Gotcha 3: Query keys must be stable — objects and arrays have reference equality gotchas
TanStack Query compares query keys using deep equality, so ["orders", { userId: 1 }] and ["orders", { userId: 1 }] are the same key even though they are different object references. However, keys must not include unstable references like function definitions or class instances:
// WRONG — a new object is created on every render, but TanStack Query handles it correctly
// (because it deep-compares) — this is fine for objects and primitives
// WRONG — including a function in the key is not meaningful and not comparable
queryKey: ["orders", filterFn]; // filterFn is a reference — do not do this
// CORRECT — use the function's outputs in the key, not the function itself
queryKey: ["orders", { status: "active", page: 1 }];
// WRONG — including a Date object without serializing it
queryKey: ["orders", new Date()]; // creates a new Date every render — causes refetch storm
// CORRECT — serialize the date
queryKey: ["orders", dateFilter.toISOString()];
A good convention: every key segment should be a string, number, boolean, or a plain object/array of those. No class instances, no functions.
Gotcha 4: invalidateQueries matches by prefix — invalidating too broadly is easy
queryClient.invalidateQueries({ queryKey: ["orders"] }) invalidates every query whose key starts with ["orders"]. This includes ["orders", 42], ["orders", { userId: 1 }], and ["orders", "recent"]. If you meant to only invalidate a specific order’s cache, be precise:
// Invalidates ALL orders queries — may trigger more refetches than expected
queryClient.invalidateQueries({ queryKey: ["orders"] });
// Invalidates only the specific order — use when you know exactly what changed
queryClient.invalidateQueries({ queryKey: ["orders", id], exact: true });
// Invalidates all user-specific orders lists
queryClient.invalidateQueries({ queryKey: ["orders", { userId }] });
After a successful create, invalidating all ["orders"] is usually correct — the list needs to show the new item. After a successful update to a specific order, exact: true on that order’s key is more efficient.
Gotcha 5: Mutations do not automatically invalidate the cache
Coming from .NET, you might expect that after a POST/PUT/DELETE, any component displaying related data would automatically refresh. TanStack Query does not do this automatically — you must invalidate the relevant queries in onSuccess. If you forget, the UI shows stale data indefinitely.
// WRONG — cache is not updated after mutation
useMutation({
mutationFn: (data: UpdateOrderRequest) => orderApi.update(id, data),
// No onSuccess — the UI still shows the old data after this runs
});
// CORRECT — invalidate related queries after mutation succeeds
useMutation({
mutationFn: (data: UpdateOrderRequest) => orderApi.update(id, data),
onSuccess: (updatedOrder) => {
queryClient.invalidateQueries({ queryKey: ["orders"] });
// Or, more surgical — set the cache directly to avoid a refetch
queryClient.setQueryData(["orders", id], updatedOrder);
},
});
Hands-On Exercise
Build a complete data layer for an order management feature using Axios + TanStack Query.
Setup:
npm install @tanstack/react-query @tanstack/react-query-devtools axios
Task 1 — Configure the Axios client in lib/api-client.ts:
- Base URL from
process.env.NEXT_PUBLIC_API_URL - Request interceptor that reads an auth token from
localStorage(key:"auth-token") and addsAuthorization: Bearer <token>header - Response interceptor that logs all 4xx and 5xx responses to
console.errorwith the status code and URL - 10-second timeout
Task 2 — Write the query hooks in hooks/use-orders.ts:
// Implement these three hooks:
export function useOrders(userId: number) { ... }
// queryKey: ["orders", { userId }]
// staleTime: 2 minutes (order lists don't change that often)
export function useOrder(id: number) { ... }
// queryKey: ["orders", id]
// enabled: only when id > 0
export function useCreateOrder() { ... }
// On success: invalidate ["orders"] queries AND set the new order in cache
// Hint: use mutateAsync in your form, mutate if you don't need to await
Task 3 — Build the OrderList component that:
- Shows a skeleton loader when
isLoadingis true (three placeholder<div>elements with a pulse animation) - Shows an error message with a “Try again” button that calls
refetch()whenisErroris true - Shows the list of orders when
datais available - Displays a subtle “Refreshing…” indicator when
isFetchingis true butisLoadingis false (background refetch in progress)
Task 4 — Add optimistic deletion. When the user clicks Delete:
- Remove the order from the cached list immediately
- Fire the delete API call
- If the call fails, restore the removed order and show an error message
- If the call succeeds, refetch the list to confirm
Task 5 — Verify deduplication. Render <OrderDetail id={1} /> twice on the same page. Open the Network panel in browser DevTools. Navigate to the page and confirm that only one HTTP request is made, not two.
Quick Reference
| Task | fetch | Axios | TanStack Query |
|---|---|---|---|
| GET request | fetch(url).then(r => r.json()) | apiClient.get<T>(url) | useQuery({ queryKey, queryFn }) |
| POST request | fetch(url, { method: "POST", body }) | apiClient.post<T>(url, data) | useMutation({ mutationFn }) |
| Check for HTTP errors | if (!response.ok) throw ... | Automatic | Via your queryFn |
| Add auth header | Manual per-request | interceptors.request.use() | In your queryFn / Axios interceptor |
| Retry on failure | Manual | axios-retry package | retry: 3 in QueryClient defaults |
| Cache response | Manual | None | Automatic (staleTime) |
| Loading state | Manual | Manual | isLoading, isFetching |
| Error state | Manual try/catch | isAxiosError(err) | isError, error |
| Invalidate cache | N/A | N/A | queryClient.invalidateQueries() |
| Set cache directly | N/A | N/A | queryClient.setQueryData() |
| Optimistic update | Manual | Manual | onMutate + return context |
| Rollback on failure | Manual | Manual | onError(err, vars, context) |
| Deduplicate requests | Manual | Manual | Automatic |
| Background refresh | Manual | Manual | refetchOnWindowFocus, staleTime |
| Prefetch | Manual | Manual | queryClient.prefetchQuery() |
| Cancel in-flight request | AbortController | CancelToken / AbortSignal | queryClient.cancelQueries() |
.NET to JS/TS concept map
| .NET concept | JS/TS equivalent |
|---|---|
HttpClient | axios instance or fetch |
IHttpClientFactory | Module-level singleton (Axios instance) |
DelegatingHandler | Axios request/response interceptor |
EnsureSuccessStatusCode() | if (!response.ok) throw / Axios default |
ReadFromJsonAsync<T>() | response.json() (no runtime validation) |
GetFromJsonAsync<T>() | apiClient.get<T>(url).then(r => r.data) |
| Polly retry | TanStack Query retry option |
| Polly circuit breaker | No direct equivalent (use custom interceptor) |
IMemoryCache | TanStack Query cache |
IDistributedCache | No direct equivalent in browser context |
CancellationToken | AbortController / AbortSignal |
Further Reading
- TanStack Query documentation — canonical reference; especially the “Important Defaults” page which explains staleTime and gcTime
- TanStack Query: Practical React Query — Dominik Dorfmeister’s blog series; the most detailed practical guide to TanStack Query patterns
- Axios documentation — configuration, interceptors, and error handling
- MDN: Fetch API — low-level reference for
fetchoptions, streaming, and AbortController - Article 2.3 — TypeScript Type System — explains why TypeScript generics on HTTP responses don’t provide runtime safety