use-abort 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,248 @@
1
+ # use-abort
2
+
3
+ A lightweight, production-ready React hook for safely handling async API calls with automatic request cancellation using `AbortController`.
4
+
5
+ ## Features
6
+
7
+ ✅ **Automatic Cancellation** - Aborts previous requests when a new one starts
8
+ ✅ **Cleanup on Unmount** - Automatically cancels pending requests when component unmounts
9
+ ✅ **Stale Response Prevention** - Ensures only the latest request updates state
10
+ ✅ **Error Handling** - Gracefully handles errors while ignoring abort errors
11
+ ✅ **TypeScript First** - Full type safety with TypeScript generics
12
+ ✅ **Zero Dependencies** - Only requires React (peer dependency)
13
+ ✅ **Framework Agnostic** - Works with any async function (fetch, axios, etc.)
14
+
15
+ ## Installation
16
+
17
+ ```bash
18
+ npm install use-abort
19
+ ```
20
+
21
+ ```bash
22
+ yarn add use-abort
23
+ ```
24
+
25
+ ```bash
26
+ pnpm add use-abort
27
+ ```
28
+
29
+ ## Usage
30
+
31
+ ### Basic Example
32
+
33
+ ```tsx
34
+ import { useAbort } from "use-abort";
35
+
36
+ // Define your async function (must accept AbortSignal as first parameter)
37
+ const fetchUser = async (signal: AbortSignal, userId: string) => {
38
+ const response = await fetch(`/api/users/${userId}`, { signal });
39
+ if (!response.ok) throw new Error("Failed to fetch user");
40
+ return response.json();
41
+ };
42
+
43
+ function UserProfile({ userId }: { userId: string }) {
44
+ const { run, cancel, data, error, loading } = useAbort(fetchUser);
45
+
46
+ useEffect(() => {
47
+ run(userId);
48
+ }, [userId, run]);
49
+
50
+ if (loading) return <div>Loading...</div>;
51
+ if (error) return <div>Error: {error.message}</div>;
52
+ if (data) return <div>User: {data.name}</div>;
53
+ return null;
54
+ }
55
+ ```
56
+
57
+ ### Search with Auto-Cancel Example
58
+
59
+ ```tsx
60
+ import { useAbort } from "use-abort";
61
+ import { useState, useEffect } from "react";
62
+
63
+ const searchAPI = async (signal: AbortSignal, query: string) => {
64
+ const response = await fetch(`/api/search?q=${query}`, { signal });
65
+ return response.json();
66
+ };
67
+
68
+ function SearchBox() {
69
+ const [query, setQuery] = useState("");
70
+ const { run, cancel, data, error, loading } = useAbort(searchAPI);
71
+
72
+ // Debounced search with automatic cancellation
73
+ useEffect(() => {
74
+ if (query.trim()) {
75
+ const timer = setTimeout(() => run(query), 300);
76
+ return () => clearTimeout(timer);
77
+ }
78
+ }, [query, run]);
79
+
80
+ return (
81
+ <div>
82
+ <input
83
+ value={query}
84
+ onChange={(e) => setQuery(e.target.value)}
85
+ placeholder="Search..."
86
+ />
87
+ <button onClick={cancel} disabled={!loading}>
88
+ Cancel
89
+ </button>
90
+ {loading && <div>Searching...</div>}
91
+ {error && <div>Error: {error.message}</div>}
92
+ {data && <div>Results: {data.results.length}</div>}
93
+ </div>
94
+ );
95
+ }
96
+ ```
97
+
98
+ ### With Axios
99
+
100
+ ```tsx
101
+ import axios from "axios";
102
+ import { useAbort } from "use-abort";
103
+
104
+ const fetchData = async (signal: AbortSignal, endpoint: string) => {
105
+ const { data } = await axios.get(endpoint, { signal });
106
+ return data;
107
+ };
108
+
109
+ function DataFetcher() {
110
+ const { run, data, error, loading } = useAbort(fetchData);
111
+
112
+ useEffect(() => {
113
+ run("/api/data");
114
+ }, [run]);
115
+
116
+ // Component rendering...
117
+ }
118
+ ```
119
+
120
+ ### With Custom Headers
121
+
122
+ ```tsx
123
+ const fetchWithAuth = async (
124
+ signal: AbortSignal,
125
+ url: string,
126
+ token: string,
127
+ ) => {
128
+ const response = await fetch(url, {
129
+ signal,
130
+ headers: {
131
+ Authorization: `Bearer ${token}`,
132
+ "Content-Type": "application/json",
133
+ },
134
+ });
135
+ return response.json();
136
+ };
137
+
138
+ function ProtectedData({ token }: { token: string }) {
139
+ const { run, data, error, loading } = useAbort(fetchWithAuth);
140
+
141
+ useEffect(() => {
142
+ run("/api/protected", token);
143
+ }, [token, run]);
144
+
145
+ // Component rendering...
146
+ }
147
+ ```
148
+
149
+ ## API
150
+
151
+ ### `useAbort<TArgs, TData>(asyncFunction)`
152
+
153
+ #### Parameters
154
+
155
+ - **`asyncFunction`**: `(signal: AbortSignal, ...args: TArgs) => Promise<TData>`
156
+ - An async function that accepts an `AbortSignal` as its first parameter
157
+ - Can accept any number of additional arguments
158
+ - Must return a Promise
159
+
160
+ #### Returns
161
+
162
+ An object with the following properties:
163
+
164
+ - **`run`**: `(...args: TArgs) => Promise<void>`
165
+ - Executes the async function with the provided arguments
166
+ - Automatically cancels any previous pending request
167
+ - Passes an `AbortSignal` as the first argument
168
+
169
+ - **`cancel`**: `() => void`
170
+ - Manually cancels the currently running request
171
+ - Safe to call even if no request is running
172
+
173
+ - **`data`**: `TData | null`
174
+ - The data returned from the most recent successful request
175
+ - `null` if no request has completed successfully yet
176
+
177
+ - **`error`**: `Error | null`
178
+ - The error from the most recent failed request
179
+ - `null` if no error has occurred or request is in progress
180
+ - Abort errors are automatically filtered out
181
+
182
+ - **`loading`**: `boolean`
183
+ - `true` when a request is in progress
184
+ - `false` otherwise
185
+
186
+ ## TypeScript Support
187
+
188
+ The hook is fully typed and provides excellent type inference:
189
+
190
+ ```tsx
191
+ // Type inference works automatically
192
+ const fetchUser = async (signal: AbortSignal, id: number) => {
193
+ const response = await fetch(`/api/users/${id}`, { signal });
194
+ return response.json() as Promise<User>;
195
+ };
196
+
197
+ // TypeScript knows that:
198
+ // - run() expects a number argument
199
+ // - data is User | null
200
+ const { run, data } = useAbort(fetchUser);
201
+
202
+ run(123); // ✅ Correct
203
+ run("123"); // ❌ Type error
204
+ ```
205
+
206
+ ## How It Works
207
+
208
+ 1. **Automatic Abort**: When `run()` is called, any previous request is automatically aborted before starting the new one
209
+ 2. **Unmount Cleanup**: The hook automatically aborts pending requests when the component unmounts
210
+ 3. **Stale Response Prevention**: Uses request IDs to ensure only the latest request can update state
211
+ 4. **Error Filtering**: Automatically filters out `AbortError` to avoid showing errors for intentionally cancelled requests
212
+
213
+ ## Best Practices
214
+
215
+ 1. **Always pass AbortSignal to your API calls**:
216
+
217
+ ```tsx
218
+ fetch(url, { signal }); // ✅ Good
219
+ fetch(url); // ❌ Won't be cancellable
220
+ ```
221
+
222
+ 2. **Memoize the async function if needed**:
223
+
224
+ ```tsx
225
+ const fetchData = useCallback(
226
+ async (signal: AbortSignal, id: string) => {
227
+ // ...
228
+ },
229
+ [dependency],
230
+ );
231
+ const { run } = useAbort(fetchData);
232
+ ```
233
+
234
+ 3. **Handle errors appropriately**:
235
+ ```tsx
236
+ if (error) {
237
+ // Show user-friendly error message
238
+ return <ErrorMessage error={error} />;
239
+ }
240
+ ```
241
+
242
+ ## License
243
+
244
+ MIT
245
+
246
+ ## Contributing
247
+
248
+ Contributions are welcome! Please feel free to submit a Pull Request.
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Type for async functions that accept an AbortSignal
3
+ */
4
+ export type AbortableAsyncFunction<TArgs extends any[], TData> = (signal: AbortSignal, ...args: TArgs) => Promise<TData>;
5
+ /**
6
+ * Return type of the useAbort hook
7
+ */
8
+ export interface UseAbortReturn<TArgs extends any[], TData> {
9
+ /** Execute the async function with given arguments */
10
+ run: (...args: TArgs) => Promise<void>;
11
+ /** Cancel the currently running request */
12
+ cancel: () => void;
13
+ /** Data returned from the async function */
14
+ data: TData | null;
15
+ /** Error that occurred during execution (excluding abort errors) */
16
+ error: Error | null;
17
+ /** Whether a request is currently in progress */
18
+ loading: boolean;
19
+ }
20
+ /**
21
+ * A React hook for safely handling async API calls with AbortController.
22
+ *
23
+ * Automatically:
24
+ * - Aborts previous requests when a new one starts
25
+ * - Aborts requests on component unmount
26
+ * - Prevents stale responses from updating state
27
+ * - Handles errors gracefully (ignoring abort errors)
28
+ *
29
+ * @param asyncFunction - An async function that accepts an AbortSignal as its first parameter
30
+ * @returns Object containing run, cancel, data, error, and loading
31
+ *
32
+ * @example
33
+ * ```tsx
34
+ * const fetchUser = async (signal: AbortSignal, userId: string) => {
35
+ * const response = await fetch(`/api/users/${userId}`, { signal });
36
+ * return response.json();
37
+ * };
38
+ *
39
+ * function UserProfile({ userId }: { userId: string }) {
40
+ * const { run, cancel, data, error, loading } = useAbort(fetchUser);
41
+ *
42
+ * useEffect(() => {
43
+ * run(userId);
44
+ * }, [userId]);
45
+ *
46
+ * if (loading) return <div>Loading...</div>;
47
+ * if (error) return <div>Error: {error.message}</div>;
48
+ * if (data) return <div>{data.name}</div>;
49
+ * return null;
50
+ * }
51
+ * ```
52
+ */
53
+ export declare function useAbort<TArgs extends any[], TData>(asyncFunction: AbortableAsyncFunction<TArgs, TData>): UseAbortReturn<TArgs, TData>;
54
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA;;GAEG;AACH,MAAM,MAAM,sBAAsB,CAAC,KAAK,SAAS,GAAG,EAAE,EAAE,KAAK,IAAI,CAC/D,MAAM,EAAE,WAAW,EACnB,GAAG,IAAI,EAAE,KAAK,KACX,OAAO,CAAC,KAAK,CAAC,CAAC;AAEpB;;GAEG;AACH,MAAM,WAAW,cAAc,CAAC,KAAK,SAAS,GAAG,EAAE,EAAE,KAAK;IACxD,sDAAsD;IACtD,GAAG,EAAE,CAAC,GAAG,IAAI,EAAE,KAAK,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACvC,2CAA2C;IAC3C,MAAM,EAAE,MAAM,IAAI,CAAC;IACnB,4CAA4C;IAC5C,IAAI,EAAE,KAAK,GAAG,IAAI,CAAC;IACnB,oEAAoE;IACpE,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IACpB,iDAAiD;IACjD,OAAO,EAAE,OAAO,CAAC;CAClB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,wBAAgB,QAAQ,CAAC,KAAK,SAAS,GAAG,EAAE,EAAE,KAAK,EACjD,aAAa,EAAE,sBAAsB,CAAC,KAAK,EAAE,KAAK,CAAC,GAClD,cAAc,CAAC,KAAK,EAAE,KAAK,CAAC,CAmF9B"}
@@ -0,0 +1,113 @@
1
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
3
+ return new (P || (P = Promise))(function (resolve, reject) {
4
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
5
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
6
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
7
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
8
+ });
9
+ };
10
+ import { useCallback, useEffect, useRef, useState } from "react";
11
+ /**
12
+ * A React hook for safely handling async API calls with AbortController.
13
+ *
14
+ * Automatically:
15
+ * - Aborts previous requests when a new one starts
16
+ * - Aborts requests on component unmount
17
+ * - Prevents stale responses from updating state
18
+ * - Handles errors gracefully (ignoring abort errors)
19
+ *
20
+ * @param asyncFunction - An async function that accepts an AbortSignal as its first parameter
21
+ * @returns Object containing run, cancel, data, error, and loading
22
+ *
23
+ * @example
24
+ * ```tsx
25
+ * const fetchUser = async (signal: AbortSignal, userId: string) => {
26
+ * const response = await fetch(`/api/users/${userId}`, { signal });
27
+ * return response.json();
28
+ * };
29
+ *
30
+ * function UserProfile({ userId }: { userId: string }) {
31
+ * const { run, cancel, data, error, loading } = useAbort(fetchUser);
32
+ *
33
+ * useEffect(() => {
34
+ * run(userId);
35
+ * }, [userId]);
36
+ *
37
+ * if (loading) return <div>Loading...</div>;
38
+ * if (error) return <div>Error: {error.message}</div>;
39
+ * if (data) return <div>{data.name}</div>;
40
+ * return null;
41
+ * }
42
+ * ```
43
+ */
44
+ export function useAbort(asyncFunction) {
45
+ const [data, setData] = useState(null);
46
+ const [error, setError] = useState(null);
47
+ const [loading, setLoading] = useState(false);
48
+ // Keep track of the current AbortController
49
+ const abortControllerRef = useRef(null);
50
+ // Keep track of the latest request ID to prevent stale updates
51
+ const requestIdRef = useRef(0);
52
+ /**
53
+ * Cancel the currently running request
54
+ */
55
+ const cancel = useCallback(() => {
56
+ if (abortControllerRef.current) {
57
+ abortControllerRef.current.abort();
58
+ abortControllerRef.current = null;
59
+ setLoading(false);
60
+ }
61
+ }, []);
62
+ /**
63
+ * Execute the async function with automatic abort handling
64
+ */
65
+ const run = useCallback((...args) => __awaiter(this, void 0, void 0, function* () {
66
+ // Cancel any previous request
67
+ cancel();
68
+ // Create a new AbortController for this request
69
+ const controller = new AbortController();
70
+ abortControllerRef.current = controller;
71
+ // Increment and capture the current request ID
72
+ requestIdRef.current += 1;
73
+ const currentRequestId = requestIdRef.current;
74
+ // Reset error and set loading state
75
+ setError(null);
76
+ setLoading(true);
77
+ try {
78
+ // Execute the async function with the abort signal
79
+ const result = yield asyncFunction(controller.signal, ...args);
80
+ // Only update state if this is still the latest request
81
+ if (currentRequestId === requestIdRef.current) {
82
+ setData(result);
83
+ setLoading(false);
84
+ }
85
+ }
86
+ catch (err) {
87
+ // Only update state if this is still the latest request
88
+ if (currentRequestId === requestIdRef.current) {
89
+ // Ignore abort errors
90
+ if (err instanceof Error && err.name === "AbortError") {
91
+ setLoading(false);
92
+ return;
93
+ }
94
+ // Handle other errors
95
+ setError(err instanceof Error ? err : new Error(String(err)));
96
+ setLoading(false);
97
+ }
98
+ }
99
+ }), [asyncFunction, cancel]);
100
+ // Clean up on unmount
101
+ useEffect(() => {
102
+ return () => {
103
+ cancel();
104
+ };
105
+ }, [cancel]);
106
+ return {
107
+ run,
108
+ cancel,
109
+ data,
110
+ error,
111
+ loading,
112
+ };
113
+ }
package/dist/index.js ADDED
@@ -0,0 +1,116 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.useAbort = useAbort;
13
+ const react_1 = require("react");
14
+ /**
15
+ * A React hook for safely handling async API calls with AbortController.
16
+ *
17
+ * Automatically:
18
+ * - Aborts previous requests when a new one starts
19
+ * - Aborts requests on component unmount
20
+ * - Prevents stale responses from updating state
21
+ * - Handles errors gracefully (ignoring abort errors)
22
+ *
23
+ * @param asyncFunction - An async function that accepts an AbortSignal as its first parameter
24
+ * @returns Object containing run, cancel, data, error, and loading
25
+ *
26
+ * @example
27
+ * ```tsx
28
+ * const fetchUser = async (signal: AbortSignal, userId: string) => {
29
+ * const response = await fetch(`/api/users/${userId}`, { signal });
30
+ * return response.json();
31
+ * };
32
+ *
33
+ * function UserProfile({ userId }: { userId: string }) {
34
+ * const { run, cancel, data, error, loading } = useAbort(fetchUser);
35
+ *
36
+ * useEffect(() => {
37
+ * run(userId);
38
+ * }, [userId]);
39
+ *
40
+ * if (loading) return <div>Loading...</div>;
41
+ * if (error) return <div>Error: {error.message}</div>;
42
+ * if (data) return <div>{data.name}</div>;
43
+ * return null;
44
+ * }
45
+ * ```
46
+ */
47
+ function useAbort(asyncFunction) {
48
+ const [data, setData] = (0, react_1.useState)(null);
49
+ const [error, setError] = (0, react_1.useState)(null);
50
+ const [loading, setLoading] = (0, react_1.useState)(false);
51
+ // Keep track of the current AbortController
52
+ const abortControllerRef = (0, react_1.useRef)(null);
53
+ // Keep track of the latest request ID to prevent stale updates
54
+ const requestIdRef = (0, react_1.useRef)(0);
55
+ /**
56
+ * Cancel the currently running request
57
+ */
58
+ const cancel = (0, react_1.useCallback)(() => {
59
+ if (abortControllerRef.current) {
60
+ abortControllerRef.current.abort();
61
+ abortControllerRef.current = null;
62
+ setLoading(false);
63
+ }
64
+ }, []);
65
+ /**
66
+ * Execute the async function with automatic abort handling
67
+ */
68
+ const run = (0, react_1.useCallback)((...args) => __awaiter(this, void 0, void 0, function* () {
69
+ // Cancel any previous request
70
+ cancel();
71
+ // Create a new AbortController for this request
72
+ const controller = new AbortController();
73
+ abortControllerRef.current = controller;
74
+ // Increment and capture the current request ID
75
+ requestIdRef.current += 1;
76
+ const currentRequestId = requestIdRef.current;
77
+ // Reset error and set loading state
78
+ setError(null);
79
+ setLoading(true);
80
+ try {
81
+ // Execute the async function with the abort signal
82
+ const result = yield asyncFunction(controller.signal, ...args);
83
+ // Only update state if this is still the latest request
84
+ if (currentRequestId === requestIdRef.current) {
85
+ setData(result);
86
+ setLoading(false);
87
+ }
88
+ }
89
+ catch (err) {
90
+ // Only update state if this is still the latest request
91
+ if (currentRequestId === requestIdRef.current) {
92
+ // Ignore abort errors
93
+ if (err instanceof Error && err.name === "AbortError") {
94
+ setLoading(false);
95
+ return;
96
+ }
97
+ // Handle other errors
98
+ setError(err instanceof Error ? err : new Error(String(err)));
99
+ setLoading(false);
100
+ }
101
+ }
102
+ }), [asyncFunction, cancel]);
103
+ // Clean up on unmount
104
+ (0, react_1.useEffect)(() => {
105
+ return () => {
106
+ cancel();
107
+ };
108
+ }, [cancel]);
109
+ return {
110
+ run,
111
+ cancel,
112
+ data,
113
+ error,
114
+ loading,
115
+ };
116
+ }
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "use-abort",
3
+ "version": "1.0.0",
4
+ "description": "A React hook for safely handling async API calls with AbortController",
5
+ "main": "dist/index.js",
6
+ "module": "dist/index.esm.js",
7
+ "types": "dist/index.d.ts",
8
+ "files": [
9
+ "dist"
10
+ ],
11
+ "scripts": {
12
+ "build": "npm run build:cjs && npm run build:esm && npm run build:types",
13
+ "build:cjs": "tsc",
14
+ "build:esm": "tsc --module esnext --outDir dist/esm && mv dist/esm/index.js dist/index.esm.js && rm -rf dist/esm",
15
+ "build:types": "tsc --emitDeclarationOnly --declaration --outDir dist",
16
+ "prepublishOnly": "npm run build"
17
+ },
18
+ "keywords": [
19
+ "react",
20
+ "hooks",
21
+ "abort",
22
+ "abortcontroller",
23
+ "async",
24
+ "fetch",
25
+ "cancel",
26
+ "typescript"
27
+ ],
28
+ "author": "Suraj Sharma",
29
+ "license": "MIT",
30
+ "peerDependencies": {
31
+ "react": ">=16.8.0"
32
+ },
33
+ "devDependencies": {
34
+ "@types/react": "^18.2.0",
35
+ "react": "^18.2.0",
36
+ "typescript": "^5.0.0"
37
+ },
38
+ "repository": {
39
+ "type": "git",
40
+ "url": "https://github.com/yourusername/use-abort.git"
41
+ }
42
+ }