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 +21 -0
- package/README.md +248 -0
- package/dist/index.d.ts +54 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.esm.js +113 -0
- package/dist/index.js +116 -0
- package/package.json +42 -0
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.
|
package/dist/index.d.ts
ADDED
|
@@ -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
|
+
}
|