Gathering detailed insights and metrics for real-cancellable-promise
Gathering detailed insights and metrics for real-cancellable-promise
Gathering detailed insights and metrics for real-cancellable-promise
Gathering detailed insights and metrics for real-cancellable-promise
Cancellable promise library for JavaScript and TypeScript.
npm install real-cancellable-promise
Module System
Min. Node Version
Typescript Support
Node Version
NPM Version
34 Stars
100 Commits
2 Forks
3 Watching
4 Branches
3 Contributors
Updated on 02 Oct 2024
TypeScript (94.68%)
JavaScript (5.16%)
Shell (0.15%)
Cumulative downloads
Total Downloads
Last day
29.9%
6,897
Compared to previous day
Last week
83.3%
40,056
Compared to previous week
Last month
11.9%
114,200
Compared to previous month
Last year
383.8%
1,300,790
Compared to previous year
24
A simple cancellable promise implementation for JavaScript and TypeScript.
Read the announcement post for a full explanation. In particular, see the "Prior art" section for a comparison to existing cancellable promise libraries.
1yarn add real-cancellable-promise
1import { CancellablePromise } from 'real-cancellable-promise'; 2 3const cancellablePromise = new CancellablePromise(normalPromise, cancel); 4 5cancellablePromise.cancel(); 6 7await cancellablePromise; // throws a Cancellation object that subclasses Error
The CancellablePromise
constructor takes in a promise
and a cancel
function.
Your cancel
function MUST cause promise
to reject with a Cancellation
object.
This will NOT work, your callbacks with still run:
1new CancellablePromise(normalPromise, () => {});
How do I convert a normal Promise
to a CancellablePromise
?
1export function cancellableFetch( 2 input: RequestInfo, 3 init: RequestInit = {} 4): CancellablePromise<Response> { 5 const controller = new AbortController(); 6 7 const promise = fetch(input, { 8 ...init, 9 signal: controller.signal, 10 }).catch((e) => { 11 if (e.name === 'AbortError') { 12 throw new Cancellation(); 13 } 14 15 // rethrow the original error 16 throw e; 17 }); 18 19 return new CancellablePromise<Response>(promise, () => controller.abort()); 20} 21 22// Use just like normal fetch: 23const cancellablePromise = cancellableFetch(url, { 24 /* pass options here */ 25});
fetch
with response handling1export function cancellableFetch<T>( 2 input: RequestInfo, 3 init: RequestInit = {} 4): CancellablePromise<T> { 5 const controller = new AbortController(); 6 7 const promise = fetch(input, { 8 ...init, 9 signal: controller.signal, 10 }) 11 .then((response) => { 12 // Handle the response object however you want 13 if (!response.ok) { 14 throw new Error(`Fetch failed with status code ${response.status}.`); 15 } 16 17 if (response.headers.get('content-type')?.includes('application/json')) { 18 return response.json(); 19 } else { 20 return response.text(); 21 } 22 }) 23 .catch((e) => { 24 if (e.name === 'AbortError') { 25 throw new Cancellation(); 26 } 27 28 // rethrow the original error 29 throw e; 30 }); 31 32 return new CancellablePromise<T>(promise, () => controller.abort()); 33}
1export function cancellableAxios<T>( 2 config: AxiosRequestConfig 3): CancellablePromise<T> { 4 const source = axios.CancelToken.source(); 5 config = { ...config, cancelToken: source.token }; 6 7 const promise = axios(config) 8 .then((response) => response.data) 9 .catch((e) => { 10 if (e instanceof axios.Cancel) { 11 throw new Cancellation(); 12 } 13 14 // rethrow the original error 15 throw e; 16 }); 17 18 return new CancellablePromise<T>(promise, () => source.cancel()); 19} 20 21// Use just like normal axios: 22const cancellablePromise = cancellableAxios({ url });
1export function cancellableJQueryAjax<T>( 2 settings: JQuery.AjaxSettings 3): CancellablePromise<T> { 4 const xhr = $.ajax(settings); 5 6 const promise = xhr.catch((e) => { 7 if (e.statusText === 'abort') throw new Cancellation(); 8 9 // rethrow the original error 10 throw e; 11 }); 12 13 return new CancellablePromise<T>(promise, () => xhr.abort()); 14} 15 16// Use just like normal $.ajax: 17const cancellablePromise = cancellableJQueryAjax({ url, dataType: 'json' });
CancellablePromise
supports all the methods that the normal Promise
object
supports, except Promise.any
(ES2021). See the API
Reference for details.
If your React component makes an API call, you probably don't care about the result of that API call after the component has unmounted. You can cancel the API in the cleanup function of an effect like this:
1function listBlogPosts(): CancellablePromise<Post[]> { 2 // call the API 3} 4 5export function Blog() { 6 const [posts, setPosts] = useState<Post[]>([]); 7 8 useEffect(() => { 9 const cancellablePromise = listBlogPosts() 10 .then(setPosts) 11 .catch(console.error); 12 13 // The promise will get canceled when the component unmounts 14 return cancellablePromise.cancel; 15 }, []); 16 17 return ( 18 <div> 19 {posts.map((p) => { 20 /* ... */ 21 })} 22 </div> 23 ); 24}
Before React 18, this was necessary to prevent the infamous "setState after unmount" warning. This warning was removed from React in React 18 because setting state after the component unmounts is usually not indicative of a real problem.
CodeSandbox: prevent setState after unmount
Sometimes API calls have parameters, like a search string entered by the user. If the query parameters change, you should cancel any in-progress API calls.
1function searchUsers(searchTerm: string): CancellablePromise<User[]> { 2 // call the API 3} 4 5export function UserList() { 6 const [searchTerm, setSearchTerm] = useState(''); 7 const [users, setUsers] = useState<User[]>([]); 8 9 // In a real app you should debounce the searchTerm 10 useEffect(() => { 11 const cancellablePromise = searchUsers(searchTerm) 12 .then(setUsers) 13 .catch(console.error); 14 15 // The old API call gets canceled whenever searchTerm changes. This prevents 16 // setUsers from being called with incorrect results if the API calls complete 17 // out of order. 18 return cancellablePromise.cancel; 19 }, [searchTerm]); 20 21 return ( 22 <div> 23 <SearchInput searchTerm={searchTerm} onChange={setSearchTerm} /> 24 {users.map((u) => { 25 /* ... */ 26 })} 27 </div> 28 ); 29}
CodeSandbox: cancel the in-progress API call when query parameters change
The utility function buildCancellablePromise
lets you capture
every
cancellable operation in a multi-step process. In this example, if bigQuery
is
canceled, each of the 3 API calls will be canceled (though some might have
already completed).
1function bigQuery(userId: number): CancellablePromise<QueryResult> { 2 return buildCancellablePromise(async (capture) => { 3 const userPromise = api.user.get(userId); 4 const rolePromise = api.user.listRoles(userId); 5 6 const [user, roles] = await capture( 7 CancellablePromise.all([userPromise, rolePromise]) 8 ); 9 10 // User must be loaded before this query can run 11 const customer = await capture(api.customer.get(user.customerId)); 12 13 return { user, roles, customer }; 14 }); 15}
react-query
If your query key changes and there's an API call in progress, react-query
will cancel the CancellablePromise
automatically.
CodeSandbox: react-query integration
Cancellation
Usually, you'll want to ignore Cancellation
objects that get thrown:
1try { 2 await capture(cancellablePromise); 3} catch (e) { 4 if (e instanceof Cancellation) { 5 // do nothing — the component probably just unmounted. 6 // or you could do something here it's up to you 😆 7 return; 8 } 9 10 // log the error or display it to the user 11}
Sometimes you need to call an asynchronous function that doesn't support
cancellation. In this case, you can use pseudoCancellable
:
1const cancellablePromise = pseudoCancellable(normalPromise); 2 3// Later... 4cancellablePromise.cancel(); 5 6await cancellablePromise; // throws Cancellation object if promise did not already resolve
CancellablePromise.delay
1await CancellablePromise.delay(1000); // wait 1 second
Browser: anything that's not Internet Explorer.
React Native / Expo: should work in any recent release. AbortController
has been available since 0.60.
Node.js: 14+. AbortController
is only available in Node 15+. Both require()
(CommonJS) and import
(ES modules) are supported without use of a transpiler or bundler.
MIT
See CONTRIBUTING.md
.
No vulnerabilities found.
No security vulnerabilities found.