react-fetch-utils
v1.2.1
Published
Lightweight React hooks and utilities for fetch requests with cancellation support
Maintainers
Readme
react-fetch-utils
Lightweight React hooks and utilities for fetch requests with cancellation support.
Install
npm install react-fetch-utilsRequirements
- React 16.8+
- Node 18+ (for package development/build tooling)
Import
import {
FetchPromise,
createFetchClient,
useQueries,
useRequest,
statusEnum,
type CancellablePromise,
type FetchClientDefaults,
type FetchPromiseError,
type FetchPromiseParams,
type FetchRequestConfig,
type Status,
type UseRequestOptions,
type UseRequestResult,
} from "react-fetch-utils";Exports
FetchPromisecreateFetchClientuseQueriesuseRequeststatusEnumCancellablePromise(type)FetchClientDefaults(type)FetchPromiseError(type)FetchPromiseParams(type)FetchRequestConfig(type)Status(type)UseRequestOptions(type)UseRequestResult(type)
Quick Start
import { useEffect } from "react";
import { FetchPromise, useRequest } from "react-fetch-utils";
type Todo = { id: number; title: string; completed: boolean };
function getTodo() {
return FetchPromise<Todo>({
url: "https://jsonplaceholder.typicode.com/todos/1",
method: "GET",
});
}
export function TodoCard() {
const { status, response } = useRequest(getTodo);
useEffect(() => {
if (status !== 2 || !response) return;
console.log("Loaded:", response.title);
}, [status, response]);
if (status === 0) return <p>Idle</p>;
if (status === 1) return <p>Loading...</p>;
return <p>{response?.title}</p>;
}API
FetchPromise
Creates a cancellable promise around fetch.
Signature:
function FetchPromise<T = unknown>(
params: FetchPromiseParams,
): CancellablePromise<T>;FetchPromiseParams:
type FetchPromiseParams = {
url: string;
method: string;
body?: unknown;
headers?: HeadersInit | Record<string, string>;
timeoutMs?: number;
baseUrl?: string;
includeContentType?: boolean;
parseAs?: "json" | "raw" | "text" | "response";
onRequest?: (
request: FetchRequestConfig,
) => FetchRequestConfig | void | Promise<FetchRequestConfig | void>;
validateStatus?: (status: number, response: Response) => boolean;
getAuthToken?: () => string | null | undefined | Promise<string | null | undefined>;
allowBodyForGetHead?: boolean;
};Behavior:
- New parsing modes via
parseAs:json(default)raw(Blob)text(string)response(Response)
- Supports request timeout with
timeoutMsusingAbortController. - Supports base URL joining with
baseUrl. - Supports request customization with
headers,onRequest,validateStatus, andgetAuthToken. Acceptis set automatically only when caller did not provide it.Content-Type: application/jsonis added only when:- there is a body,
- method is
POST/PUT/PATCH, includeContentType !== false,- caller did not already provide
Content-Type.
- Request body behavior:
GET/HEADbodies are not sent by default.- use
allowBodyForGetHead: trueto allow body onGET/HEAD. - non-JSON bodies (
FormData,Blob,string,URLSearchParams, etc.) are passed through unchanged. - plain objects/values are JSON-stringified when body is allowed.
- Error model:
401rejects with{ reason: "Unauthorized", details: Response }- timeout abort rejects with
{ reason: "Timeout", details: ... } - other failures reject with
{ reason: "Unknown", details: ... }
- Returned promise remains cancellable with
.cancel()and is safe to call repeatedly.
Example (JSON):
import { FetchPromise } from "react-fetch-utils";
type User = { id: number; name: string };
export function getUser() {
return FetchPromise<User>({
url: "/user",
method: "POST",
baseUrl: "https://api.example.com/v1",
body: { includeDetails: true },
timeoutMs: 8000,
getAuthToken: () => localStorage.getItem("token"),
});
}Example (text parse mode + custom status validation):
import { FetchPromise } from "react-fetch-utils";
export function getHealth() {
return FetchPromise<string>({
url: "/health",
method: "GET",
parseAs: "text",
validateStatus: status => status >= 200 && status < 500,
});
}Example (full Response mode):
import { FetchPromise } from "react-fetch-utils";
export function getRawResponse() {
return FetchPromise<Response>({
url: "/diagnostics",
method: "GET",
parseAs: "response",
});
}Example (raw Blob + cancellation):
import { FetchPromise } from "react-fetch-utils";
const download = FetchPromise<Blob>({
url: "/api/report",
method: "GET",
parseAs: "raw",
});
// Cancel if no longer needed
download.cancel();createFetchClient
Creates a reusable request function with merged defaults so app code does not need a custom API wrapper utility.
Signature:
function createFetchClient(
defaults?: FetchClientDefaults,
): <T = unknown>(params: FetchPromiseParams) => CancellablePromise<T>;Example (replace a custom APIRequest utility):
import { createFetchClient } from "react-fetch-utils";
export const apiRequest = createFetchClient({
baseUrl: "https://api.example.com/v1",
timeoutMs: 10000,
headers: {
"X-App-Client": "web",
},
getAuthToken: async () => localStorage.getItem("access_token"),
validateStatus: status => status >= 200 && status < 300,
onRequest: request => {
request.headers.set("X-Trace-Id", crypto.randomUUID());
},
});
export const getProfile = () =>
apiRequest<{ id: string; name: string }>({
url: "/profile",
method: "GET",
parseAs: "json",
});
export const downloadReport = () =>
apiRequest<Blob>({
url: "/reports/monthly",
method: "GET",
parseAs: "raw",
});Example (FormData upload without forced JSON content-type):
const formData = new FormData();
formData.append("file", file);
apiRequest({
url: "/upload",
method: "POST",
body: formData,
});useQueries
Tracks active cancellable promises and provides list management helpers.
Signature:
function useQueries(): {
list: CancellablePromise[];
cancelAll: () => void;
add: (query: CancellablePromise) => void;
remove: (query: CancellablePromise) => void;
};Example:
import { useEffect } from "react";
import { FetchPromise, useQueries } from "react-fetch-utils";
export function SearchBox({ term }: { term: string }) {
const queries = useQueries();
useEffect(() => {
// Cancel older in-flight requests before starting a new one
queries.cancelAll();
const query = FetchPromise({
url: `/api/search?q=${encodeURIComponent(term)}`,
method: "GET",
});
queries.add(query);
query
.then(result => {
console.log(result);
})
.catch(error => {
// Abort rejections are surfaced in error.details
if (error?.details?.name === "AbortError") return;
console.error(error);
})
.finally(() => {
queries.remove(query);
});
}, [term]);
return <div>Active queries: {queries.list.length}</div>;
}useRequest
Runs a cancellable request factory with error handling, cancellation, and optional dependency-driven reruns.
Signature:
function useRequest<T = unknown>(
fetchPromise: (() => CancellablePromise<T>) | null | undefined,
disableCacheOrOptions?: boolean | UseRequestOptions,
): UseRequestResult<T>;
type UseRequestOptions = {
disableCache?: boolean;
cacheKey?: string;
enabled?: boolean;
deps?: ReadonlyArray<unknown>;
staleTimeMs?: number;
dedupe?: boolean;
};
type UseRequestResult<T> = {
status: Status;
response: T | null;
error: unknown;
refetch: () => void;
cancel: () => void;
reset: () => void;
};Behavior:
- Status transitions:
0(idle) ->1(fetching) ->2(done) or3(error) - Handles promise rejections and exposes them through
error. - Uses
cacheKeyoption when provided. Fallback key is derived from factory name (or function source hash for anonymous factories). - Reuses fresh cache across hook instances unless
disableCache: true. staleTimeMscontrols cache freshness (default:Infinity).dedupereuses in-flight requests that share the samecacheKey(default:true).- Supports automatic reruns by passing
depsand can be disabled withenabled: false. - Exposes
refetch(),cancel(), andreset()helpers.
Example (cached by default):
import { useEffect } from "react";
import { FetchPromise, useRequest, statusEnum } from "react-fetch-utils";
type Profile = { name: string; role: string };
function loadProfile() {
return FetchPromise<Profile>({
url: "/api/profile",
method: "GET",
});
}
export function ProfilePanel() {
const { status, response, error, refetch } = useRequest(loadProfile, {
cacheKey: "profile",
});
useEffect(() => {
if (status !== 2 || !response) return;
console.log("Ready:", response.name);
}, [status, response]);
if (status === 3) {
return (
<div>
<p>Failed to load profile.</p>
<button onClick={refetch}>Retry</button>
<pre>{String(error)}</pre>
</div>
);
}
return <p>State: {statusEnum[status]}</p>;
}Example (disable cache):
const { status, response } = useRequest(loadProfile, true);statusEnum
Maps Status values to labels.
const statusEnum = {
0: "idle",
1: "fetching",
2: "done",
3: "error",
} as const;Example:
import { statusEnum, type Status } from "react-fetch-utils";
function labelFor(status: Status) {
return statusEnum[status];
}CancellablePromise (type)
type CancellablePromise<T = unknown> = Promise<T> & {
cancel: () => void;
};Example:
import { type CancellablePromise, FetchPromise } from "react-fetch-utils";
const req: CancellablePromise<{ ok: boolean }> = FetchPromise({
url: "/api/health",
method: "GET",
});
req.cancel();FetchPromiseParams (type)
Example:
import { type FetchPromiseParams, FetchPromise } from "react-fetch-utils";
const request: FetchPromiseParams = {
url: "/api/items",
method: "POST",
body: { page: 1 },
headers: { Authorization: "Bearer token" },
timeoutMs: 5000,
parseAs: "json",
};
FetchPromise(request);FetchPromiseError (type)
type FetchPromiseError = {
reason: "Unauthorized" | "Timeout" | "Unknown";
details: unknown;
status?: number;
response?: Response;
originalError?: unknown;
};FetchRequestConfig (type)
type FetchRequestConfig = {
url: string;
init: RequestInit;
headers: Headers;
};FetchClientDefaults (type)
type FetchClientDefaults = Omit<FetchPromiseParams, "url" | "method"> & {
method?: string;
};Status (type)
type Status = 0 | 1 | 2 | 3;Example:
import { type Status, statusEnum } from "react-fetch-utils";
const status: Status = 1;
console.log(statusEnum[status]); // "fetching"Notes
- Use
cacheKeywithuseRequestwhen a request has dynamic arguments to avoid collisions. - Set
staleTimeMs: 0to force revalidation on each mount while still allowing in-flight dedupe.
License
MIT
CONTRIBUTING
See CONTRIBUTING
