Resilient API Consumption in Unreliable Enterprise Networks (TypeScript/React)
Build robust React clients with explicit timeouts, retries, circuit breakers, cancellation, idempotency, and optimistic UI, plus Axios–RTK Query integration guidance.
Join the DZone community and get the full member experience.
Join For FreeEnterprise networks are often noisy. VPNs, WAFs, proxies, mobile hotspots, and transient gateway hiccups can cause timeouts, packet loss, throttling, and abrupt connection resets. Designing resilient clients minimizes checkout/MACD friction, prevents duplicate actions, and keeps the UI responsive even when backends or the network are unstable.
We have a strong toolkit for making API calls, but how do we make them safe for users and painless for developers? Which stack should we choose? How do we cut duplication and keep code maintainable at enterprise scale? These questions matter when you have hundreds of endpoints: some triggered by CTAs, some on page load, others quietly prefetching data in the background, and a few that need streaming. There’s no one-size-fits-all — each job has a best-fit approach.
In this article, we’ll compare three common strategies — Fetch, Axios, and RTK Query — then help you choose the right toolset for your context. We’ll also share practical examples that save time and solve everyday problems developers face.
Goals
- Resilience: Tolerate flaky networks (WAFs, proxies, mobile, VPN).
- Correctness: Avoid duplicate writes, handle concurrency, and enforce contracts.
- Simplicity: Centralize configuration, reduce boilerplate.
- Observability: Trace requests, measure latency/errors/retries.
- Security: Protect tokens, validate inputs/outputs.
What to Cover for Every API Call
Contract and Types
- Generate types from OpenAPI/Swagger or validate with schemas at runtime.
- Tools: openapi-typescript, openapi-generator, zod, or valibot.
Authentication and Headers
- Attach tokens/cookies securely; rotate/refresh as needed.
- Include correlation headers (e.g., X-Request-Id).
Timeouts
- Explicit per-endpoint timeouts; keep below gateway/WAF limits.
- Keep defaults tight (e.g., 8s); use shorter timeouts for read endpoints and slightly longer for write paths that touch several services.
TypeScript
// api.ts import axios from 'axios'; export const api = axios.create({ baseURL: '/api', timeout: 8000, // sensible default; override per request where needed withCredentials: true, headers: { 'Content-Type': 'application/json' } }); // Attach a requestId and simple timing metadata api.interceptors.request.use((config) => { (config as any).metadata = { start: Date.now() }; config.headers = { ...config.headers, 'X-Request-Id': crypto.randomUUID() }; return config; }); api.interceptors.response.use( (res) => { const meta = (res.config as any).metadata; if (meta) res.headers['x-client-latency-ms'] = String(Date.now() - meta.start); return res; }, (error) => Promise.reject(error) ); - Configure per-request timeouts and provide a friendly timeout error message.
TypeScript
// timeouts.ts import { api } from './api'; export async function fetchCatalog(signal?: AbortSignal) { // Read path: lower timeout return api.get('/catalog', { timeout: 5000, signal, timeoutErrorMessage: 'Catalog timed out' }); } export async function submitOrder(payload: unknown, signal?: AbortSignal) { // Write path: a bit higher, but still below gateway/WAF timeouts return api.post('/orders', payload, { timeout: 12000, signal, timeoutErrorMessage: 'Order submission timed out' }); }
Retries and Backoff
- Only retry idempotent GET/PUT/DELETE or POST with an idempotency key.
- Back off with jitter to avoid thundering herds; cap max attempts.
TypeScript
// retry.ts import axios, { AxiosError } from 'axios'; import { api } from './api'; const MAX_RETRIES = 3; const BASE_DELAY_MS = 300; //could you debounce lib instead function sleep(ms: number) { return new Promise((r) => setTimeout(r, ms)); } function isIdempotent(method?: string) { return ['get', 'head', 'options', 'put', 'delete'].includes((method || '').toLowerCase()); } function hasIdempotencyKey(headers?: any) { const key = headers?.['Idempotency-Key'] || headers?.['idempotency-key']; return Boolean(key); } function shouldRetry(error: AxiosError) { if (!error.config) return false; const status = error.response?.status; const transient = !status || [408, 425, 429, 500, 502, 503, 504].includes(status) || (error.code === 'ECONNABORTED'); const safeMethod = isIdempotent(error.config.method || ''); const postIsSafe = (error.config.method || '').toLowerCase() === 'post' && hasIdempotencyKey(error.config.headers); return transient && (safeMethod || postIsSafe); } api.interceptors.response.use(undefined, async (error: AxiosError) => { const config: any = error.config || {}; if (!shouldRetry(error)) return Promise.reject(error); config._retryCount = (config._retryCount ?? 0) + 1; if (config._retryCount > MAX_RETRIES) return Promise.reject(error); const jitter = Math.random() * 100; const delay = BASE_DELAY_MS * 2 ** (config._retryCount - 1) + jitter; // 300, 600, 1200 (+ jitter) await sleep(delay); return api.request(config); });
Circuit Breaker
- Prevents hammering a failing downstream service. Trips open after N consecutive failures, cool down, then half-open to probe recovery.
TypeScript
// circuitBreaker.ts import type { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'; import axios from 'axios'; type State = 'CLOSED' | 'OPEN' | 'HALF_OPEN'; export class AxiosCircuitBreaker { private state: State = 'CLOSED'; private failures = 0; private nextAttemptAt = 0; constructor( private readonly client: AxiosInstance, private readonly opts = { failureThreshold: 5, cooldownMs: 15000 } ) {} async request<T = any>(config: AxiosRequestConfig): Promise<AxiosResponse<T>> { const now = Date.now(); if (this.state === 'OPEN') { if (now < this.nextAttemptAt) { const err = new axios.AxiosError('Circuit open', 'ECIRCUITOPEN', config); return Promise.reject(err); } this.state = 'HALF_OPEN'; } try { const res = await this.client.request<T>(config); this.onSuccess(); return res; } catch (err) { this.onFailure(); throw err; } } private onSuccess() { this.failures = 0; this.state = 'CLOSED'; } private onFailure() { if (this.state === 'HALF_OPEN') { this.trip(); return; } this.failures += 1; if (this.failures >= this.opts.failureThreshold) this.trip(); } private trip() { this.state = 'OPEN'; this.nextAttemptAt = Date.now() + this.opts.cooldownMs; } } // usage import { api } from './api'; export const breaker = new AxiosCircuitBreaker(api, { failureThreshold: 4, cooldownMs: 10000 }); // Example call // await breaker.request({ method: 'get', url: '/inventory' });
Cancellation
- Use AbortController (Axios supports the standard AbortSignal).
- With Redux Toolkit createAsyncThunk, the abort signal is provided for you.
// products.slice.ts
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
import { api } from '../api';
export const fetchProducts = createAsyncThunk('products/fetch', async (_, { signal }) => {
const res = await api.get('/products', { signal, timeout: 6000 });
return res.data as { id: string; name: string }[];
});
const productsSlice = createSlice({
name: 'products',
initialState: { items: [] as { id: string; name: string }[], status: 'idle' as 'idle'|'loading'|'succeeded'|'failed' },
reducers: {},
extraReducers: (b) => {
b.addCase(fetchProducts.pending, (s) => { s.status = 'loading'; })
.addCase(fetchProducts.fulfilled, (s, a) => { s.items = a.payload; s.status = 'succeeded'; })
.addCase(fetchProducts.rejected, (s) => { s.status = 'failed'; });
}
});
export default productsSlice.reducer;
// ProductsList.tsx
import { useEffect } from 'react';
import { useAppDispatch, useAppSelector } from './store';
import { fetchProducts } from './products.slice';
export function ProductsList() {
const dispatch = useAppDispatch();
const items = useAppSelector((s) => s.products.items);
useEffect(() => {
const promise = dispatch(fetchProducts());
return () => {
// Abort when the component unmounts or route changes
promise.abort();
};
}, [dispatch]);
return <ul>{items.map((p) => <li key={p.id}>{p.name}</li>)}</ul>;
}
Idempotency and Concurrency
- Choose PUT/PATCH for idempotent updates when possible. For POSTs that create or change server state, include an idempotency key header to ensure the server applies the operation at most once, even under retries.
- Use optimistic UI to immediately reflect intended changes, then reconcile or rollback on failure.
- For conflicting updates, consider ETags + If-Match to avoid lost updates.
Optimistic update with idempotency key:
// checkout.slice.ts
import { createSlice, createAsyncThunk, nanoid } from '@reduxjs/toolkit';
import { api } from '../api';
type CartItem = { id: string; qty: number };
type State = { items: CartItem[]; pendingOps: Record<string, { prev?: CartItem }> };
const initialState: State = { items: [], pendingOps: {} };
export const updateCartItem = createAsyncThunk(
'checkout/updateCartItem',
async ({ id, qty, idempotencyKey }: { id: string; qty: number; idempotencyKey: string }, { signal, rejectWithValue }) => {
try {
const res = await api.put(`/cart/items/${id}`, { qty }, {
signal,
timeout: 10000,
headers: { 'Idempotency-Key': idempotencyKey }
});
return res.data as CartItem;
} catch (e: any) {
return rejectWithValue({ id, qty, message: e?.message ?? 'update failed' });
}
}
);
const slice = createSlice({
name: 'checkout',
initialState,
reducers: {
startOptimisticUpdate(s, a: { payload: { id: string; qty: number; opId: string } }) {
const { id, qty, opId } = a.payload;
const idx = s.items.findIndex((it) => it.id === id);
const prev = idx >= 0 ? { ...s.items[idx] } : undefined;
if (idx >= 0) s.items[idx].qty = qty;
s.pendingOps[opId] = { prev };
},
rollback(s, a: { payload: { opId: string } }) {
const { prev } = s.pendingOps[a.payload.opId] || {};
if (prev) {
const idx = s.items.findIndex((it) => it.id === prev.id);
if (idx >= 0) s.items[idx] = prev;
}
delete s.pendingOps[a.payload.opId];
}
},
extraReducers: (b) => {
b.addCase(updateCartItem.fulfilled, (s, a) => {
// Confirm final state from server
const updated = a.payload;
const idx = s.items.findIndex((it) => it.id === updated.id);
if (idx >= 0) s.items[idx] = updated;
// Clean up any matching pending op if you track by id
})
.addCase(updateCartItem.rejected, (s, a) => {
// Rollback is performed from UI where opId is known
});
}
});
export const { startOptimisticUpdate, rollback } = slice.actions;
export default slice.reducer;
// CartItem.tsx
import { useAppDispatch } from './store';
import { startOptimisticUpdate, updateCartItem, rollback } from './checkout.slice';
export function CartItem({ id, qty }: { id: string; qty: number }) {
const dispatch = useAppDispatch();
const onChangeQty = (nextQty: number) => {
const opId = crypto.randomUUID();
const idempotencyKey = opId; // reuse opId for Idempotency-Key
// 1) optimistic update
dispatch(startOptimisticUpdate({ id, qty: nextQty, opId }));
// 2) server update with retry/cancellation baked via api config
const thunk = dispatch(updateCartItem({ id, qty: nextQty, idempotencyKey }));
// 3) rollback on failure
thunk.unwrap().catch(() => dispatch(rollback({ opId })));
};
return (
<div>
<span>Qty: {qty}</span>
<button onClick={() => onChangeQty(qty + 1)}>+</button>
<button onClick={() => onChangeQty(qty - 1)}>-</button>
</div>
);
}
Notes for checkout/MACD:
- Use the same idempotency key when retrying POST/PUT requests to prevent duplicate order lines or duplicated MACD changes.
- For MACD, consider representing each change as a deterministic resource (PUT /services/{id}/config), so retries remain safe.
- If the server supports ETag, add If-Match headers to ensure you aren’t overwriting concurrent updates.
Error Handling
- Normalize errors to a single shape for UI; map known status codes to friendly messages.
- One error shape – consistent fields your UI can rely on (message, status, retryable, requestId, details).
- Deterministic mapping – known status codes and transport errors become clear, human-readable messages.
- Separation of concerns – keep low-level errors in the transport; normalize at the boundary (thunks/RTK Query baseQuery), so retries/circuit breakers still work.
TypeScript example: normalizer and usage (Axios and RTK Query):
// error.ts
import type { AxiosError } from 'axios';
export type AppError = {
code: string;
status?: number;
message: string;
requestId?: string;
retryable: boolean;
details?: unknown;
};
const FRIENDLY: Record<number, { code: string; message: string; retryable: boolean }> = {
400: { code: 'BAD_REQUEST', message: 'Request is invalid. Please check inputs.', retryable: false },
401: { code: 'UNAUTHENTICATED', message: 'You’re signed out. Please sign in and try again.', retryable: false },
403: { code: 'FORBIDDEN', message: 'You don’t have permission to do this.', retryable: false },
404: { code: 'NOT_FOUND', message: 'The resource was not found.', retryable: false },
409: { code: 'CONFLICT', message: 'Your changes conflict with another update.', retryable: true },
412: { code: 'PRECONDITION', message: 'Version mismatch. Refresh and try again.', retryable: true },
422: { code: 'VALIDATION', message: 'Some fields need attention.', retryable: false },
425: { code: 'TOO_EARLY', message: 'Service not ready. Please try again shortly.', retryable: true },
429: { code: 'RATE_LIMITED', message: 'Too many requests. Please wait and retry.', retryable: true },
500: { code: 'SERVER_ERROR', message: 'We hit a snag. Please try again.', retryable: true },
502: { code: 'BAD_GATEWAY', message: 'Upstream gateway error. Try again.', retryable: true },
503: { code: 'UNAVAILABLE', message: 'Service is temporarily unavailable.', retryable: true },
504: { code: 'GATEWAY_TIMEOUT', message: 'Service timed out. Please try again.', retryable: true }
};
export function normalizeAxiosError(e: unknown): AppError {
const ax = e as AxiosError<any>;
const status = ax?.response?.status;
const requestId =
(ax?.response?.headers?.['x-request-id'] as string) ||
(ax?.response?.headers?.['x-correlation-id'] as string);
// Transport-level signals
if ((ax as any)?.code === 'ECONNABORTED') {
return { code: 'TIMEOUT', message: 'Request timed out. Please try again.', retryable: true, status, requestId, details: ax?.response?.data };
}
if ((ax as any)?.code === 'ERR_NETWORK' || !status) {
return { code: 'NETWORK', message: 'Network issue detected. Check connection and retry.', retryable: true, status, requestId, details: ax?.message };
}
if ((ax as any)?.code === 'ECIRCUITOPEN') {
return { code: 'CIRCUIT_OPEN', message: 'Service is recovering. Please try again shortly.', retryable: true, status, requestId };
}
// HTTP mapping
const preset = status ? (FRIENDLY[status] ?? defaultFor(status)) : defaultFor(undefined);
const serverMessage = ax?.response?.data?.message || ax?.response?.data?.error || ax?.message;
return {
code: preset.code,
status,
message: serverMessage || preset.message,
retryable: preset.retryable,
requestId,
details: ax?.response?.data
};
}
function defaultFor(status?: number) {
if (!status) return { code: 'UNKNOWN', message: 'Unexpected error. Please try again.', retryable: true };
if (status >= 500) return { code: 'SERVER_ERROR', message: 'We hit a snag. Please try again.', retryable: true };
return { code: 'CLIENT_ERROR', message: 'Something went wrong with the request.', retryable: false };
}
Use with Axios thunks (keep retries/circuit breakers in transport; normalize at boundary):
// orders.thunk.ts
import { createAsyncThunk } from '@reduxjs/toolkit';
import { api } from './api';
import { normalizeAxiosError, type AppError } from './error';
export const fetchOrder = createAsyncThunk<any, string, { rejectValue: AppError }>(
'orders/fetch',
async (id, { signal, rejectWithValue }) => {
try {
const res = await api.get(`/orders/${id}`, { signal, timeout: 8000 });
return res.data;
} catch (err) {
return rejectWithValue(normalizeAxiosError(err));
}
}
);
Use with RTK Query baseQuery (all errors become AppError):
// rtkAxiosBaseQuery.ts
import type { BaseQueryFn } from '@reduxjs/toolkit/query';
import type { AxiosRequestConfig } from 'axios';
import { api } from './api';
import { normalizeAxiosError, type AppError } from './error';
export const axiosBaseQuery = (): BaseQueryFn<AxiosRequestConfig, unknown, AppError> =>
async (config) => {
try {
const res = await api.request(config);
return { data: res.data };
} catch (e) {
return { error: normalizeAxiosError(e) };
}
};
Simple UI usage example:
// ErrorBanner.tsx
import type { AppError } from './error';
export function ErrorBanner({ error }: { error: AppError }) {
return (
<div role="alert">
<strong>{error.message}</strong>
{error.requestId && <small> · Ref: {error.requestId}</small>}
{error.retryable && <button onClick={() => window.location.reload()}>Try again</button>}
</div>
);
}
Observability
- Log requestId, latency, retry count, and breaker state; integrate with your APM.
Performance and Caching
- Deduplicate in-flight requests
- Cache read data (with TTL)
- Invalidate with tags (RTK Query)
Deduplicate in-flight requests (Axios). Avoid duplicate GETs fired concurrently from multiple components by reusing the same Promise.
// inflight.ts
import { api } from './api';
const inflight = new Map<string, Promise<any>>();
function stable(params: Record<string, any> = {}) {
return JSON.stringify(Object.keys(params).sort().reduce((a, k) => (a[k] = params[k], a), {} as any));
}
export function getWithDedupe<T>(url: string, params: Record<string, any> = {}, timeout = 6000) {
const key = `GET ${url}?${stable(params)}`;
const existing = inflight.get(key);
if (existing) return existing as Promise<{ data: T }>;
const p = api.get<T>(url, { params, timeout })
.finally(() => inflight.delete(key));
inflight.set(key, p as Promise<any>);
return p;
}
// usage
// const { data } = await getWithDedupe<Product[]>('/products', { q: 'router' });
Simple read cache with TTL (Axios). Cache successful GET responses for a time window to reduce network load.
// ttlCache.ts
import { api } from './api';
type Entry<T> = { expires: number; data: T };
const cache = new Map<string, Entry<any>>();
function now() { return Date.now(); }
function key(url: string, params?: Record<string, any>) {
return `GET ${url}:${JSON.stringify(params ?? {})}`;
}
export async function getCached<T>(url: string, params?: Record<string, any>, ttlMs = 30_000) {
const k = key(url, params);
const hit = cache.get(k);
if (hit && hit.expires > now()) {
return { data: hit.data as T, fromCache: true as const };
}
const res = await api.get<T>(url, { params, timeout: 6000 });
cache.set(k, { data: res.data, expires: now() + ttlMs });
return { data: res.data, fromCache: false as const };
}
// usage
// const res = await getCached<Product[]>('/products', { q: '5g' }, 60000);
RTK Query: caching, de-duplication, and tag-based invalidation. RTK Query caches responses, dedupes in-flight requests, and lets you invalidate specific data via tags.
// services/products.api.ts
import { createApi } from '@reduxjs/toolkit/query/react';
import { axiosBaseQuery } from './rtkAxiosBaseQuery'; // wraps your Axios instance
type Product = { id: string; name: string; price: number };
export const productsApi = createApi({
reducerPath: 'productsApi',
baseQuery: axiosBaseQuery(),
tagTypes: ['Product'],
keepUnusedDataFor: 60, // seconds
endpoints: (build) => ({
getProducts: build.query<Product[], void>({
query: () => ({ url: '/products', method: 'GET', timeout: 5000 }),
providesTags: (result) =>
result
? [
...result.map((p) => ({ type: 'Product' as const, id: p.id })),
{ type: 'Product' as const, id: 'LIST' }
]
: [{ type: 'Product', id: 'LIST' }]
}),
updateProduct: build.mutation<Product, Partial<Product> & Pick<Product, 'id'>>({
query: ({ id, ...patch }) => ({
url: `/products/${id}`,
method: 'PATCH',
data: patch,
headers: { 'Idempotency-Key': crypto.randomUUID() }
}),
// Invalidate the specific product and the list
invalidatesTags: (result, error, { id }) => [{ type: 'Product', id }, { type: 'Product', id: 'LIST' }]
}),
createProduct: build.mutation<Product, Omit<Product, 'id'>>({
query: (body) => ({ url: '/products', method: 'POST', data: body }),
invalidatesTags: [{ type: 'Product', id: 'LIST' }]
})
})
});
export const { useGetProductsQuery, useUpdateProductMutation, useCreateProductMutation } = productsApi;
// usage in a component
// const { data, isFetching } = useGetProductsQuery();
// const [updateProduct] = useUpdateProductMutation();
Security and Compliance
- Don’t log PII.
- Redact sensitive fields.
- Follow CSP/CORS.
- Protect tokens in memory only.
Quick Checklist for Enterprise Networks
- Timeouts are defined per request, always below gateway/WAF limits.
- Retries limited, exponential backoff, and jitter.
- Circuit breaker enabled per service domain.
- All requests are cancellable; navigation aborts in-flight calls.
- Optimistic UI with rollback and server-side idempotency keys.
- Observability: track retry counts, breaker state, and timeout rates.
Simple end-to-end example in one place:
// endToEnd.ts
import { breaker } from './circuitBreaker';
export async function safeGet<T>(url: string, signal?: AbortSignal, timeout = 6000) {
// GET with timeout, retries (via interceptor), and circuit breaker
return breaker.request<T>({ method: 'GET', url, signal, timeout }).then((r) => r.data);
}
export async function safeIdempotentPost<T>(url: string, body: unknown, signal?: AbortSignal, timeout = 10000) {
const idempotencyKey = crypto.randomUUID();
return breaker
.request<T>({ method: 'POST', url, data: body, signal, timeout, headers: { 'Idempotency-Key': idempotencyKey } })
.then((r) => r.data);
}
Decision Guide (Quick Comparison)
Variants: Fetch vs. Axios vs. RTK Query (where each fits)
Fetch (Native)
- Pros: built-in, standards-based, streaming support, Request/AbortController.
- Cons: no interceptors, manual timeouts (via AbortController), manual JSON/error parsing, no built-in retries.
- Use when: you want minimal dependencies or need streaming; prepared to build wrappers.
- Doc: https://developer.mozilla.org/docs/Web/API/Fetch_API
Axios (Library)
- Pros: interceptors, concise API, per-request timeout, JSON by default, good error objects, and upload/download progress.
- Cons: no built-in caching/dedupe, you manage retries and invalidation yourself.
- Use when: you need centralized control (headers, auth, tracing) and enterprise behaviors (retries, circuit breaker).
- Doc: https://axios-http.com/docs/intro
RTK Query (Redux Toolkit Query)
- Pros: cache/dedupe, polling, refetch on focus/reconnect, optimistic updates, auto-cancel, generated hooks, integration with Redux DevTools.
- Cons: learning curve for tags/invalidation; you’ll still choose fetch or axios as the underlying transport.
- Use when: you want to standardize data fetching with first-class caching and request lifecycle management.
- Doc: https://redux-toolkit.js.org/rtk-query/overview
Decision Guide
Choose Axios if you need:
- You need fine-grained control of interceptors, custom circuit breakers, corporate proxy/WAF nuances, and non-standard auth headers.
- You already have significant middleware, logging, and observability built around Axios.
- You prefer explicit ownership of caching, dedupe, and invalidation logic.
Choose RTK Query (with Axios under the hood) if you need:
- You want built-in caching, de-duplication, polling, refetch on focus/reconnect, and request lifecycle management.
- You prefer generated hooks and minimal boilerplate for data fetching.
- You want first-class optimistic updates and cancellation via onQueryStarted and patchQueryData.
- You can standardize on a baseQuery (fetch or axios) and tags for cache invalidation.
Choose plain Fetch if you:
- Want zero dependency and are comfortable building wrappers for timeouts, retries, and error handling.
Good compromise: Keep Axios as the transport and use RTK Query’s axios-compatible baseQuery so you retain interceptors, timeouts, retries, and circuit breaker logic while gaining RTK Query’s caching and lifecycle.
Using RTK Query with Axios:
// rtkAxiosBaseQuery.ts
import type { BaseQueryFn } from '@reduxjs/toolkit/query';
import type { AxiosRequestConfig, AxiosError } from 'axios';
import { api } from './api'; // your axios instance with interceptors
export const axiosBaseQuery =
(): BaseQueryFn<AxiosRequestConfig, unknown, unknown> =>
async (config) => {
try {
const result = await api.request(config);
return { data: result.data };
} catch (axiosError) {
const err = axiosError as AxiosError;
return {
error: { status: err.response?.status, data: err.response?.data || err.message }
};
}
};
| Feature | Fetch (native) | Axios (library) | RTK Query (Redux Toolkit Query) |
|---|---|---|---|
| Built into the platform | ✓ | — | — |
| Streaming response (browser) | ✓ | — | — |
| Interceptors | — | ✓ | ◐ (via baseQuery/transport) |
| Per-request timeout | — | ✓ | ◐ (via transport) |
| Abort/cancel requests | ✓ (AbortController) | ✓ (AbortSignal) | ✓ (auto-cancel) |
| Built-in retries | — | — | ◐ (retry wrapper) |
| Caching and de-duplication | — | — | ✓ |
| Refetch on focus/reconnect | — | — | ✓ |
| Polling | — | — | ✓ |
| Optimistic updates | — | — | ✓ |
| Generated React hooks | — | — | ✓ |
| Redux DevTools integration | — | — | ✓ |
| Upload/download progress (browser) | — | ✓ | — |
| Rich error objects | — | ✓ | ◐ (standardized shape) |
| Automatic JSON parsing | — (res.json) | ✓ | ✓ |
| Cache invalidation by tags | — | — | ✓ |
| Request de-duplication | — | — | ✓ |
| Works with custom auth headers | ✓ (manual) | ✓ | ✓ (via baseQuery) |
Key documents:
- Axios: https://axios-http.com/docs/intro
- Axios timeouts: https://axios-http.com/docs/req_config
- Axios cancellation (AbortController): https://axios-http.com/docs/cancellation
- Redux Toolkit (RTK): https://redux-toolkit.js.org/
- Redux Toolkit createAsyncThunk: https://redux-toolkit.js.org/api/createAsyncThunk
- RTK Query: https://redux-toolkit.js.org/rtk-query/overview
- MDN AbortController: https://developer.mozilla.org/docs/Web/API/AbortController
Opinions expressed by DZone contributors are their own.
Comments