WORK IN PROGRESS
real-cancellable-promise
A simple cancellable promise implementation for JavaScript and TypeScript.
Unlike p-cancelable and
make-cancellable-promise
which only prevent your promise's callbacks from executing, real-cancellable-promise
cancels the underlying asynchronous operation (usually an HTTP call). That's
why it's called Real Cancellable Promise.
- ⚡ Compatible with fetch, axios, and
jQuery.ajax
- ⚛ Built with React in mind — no more "setState after unmount" errors!
- 🐦 Lightweight — zero dependencies and less than 1 kB
minified and gzipped
- 🏭 Used in production by Interface
Technologies
- 💻 Optimized for TypeScript
- 🔎 Compatible with
react-query
query cancellation out of the box
The Basics
yarn add real-cancellable-promise
import { CancellablePromise } from 'real-cancellable-promise'
const cancellablePromise = new CancellablePromise(normalPromise, cancel)
cancellablePromise.cancel()
await cancellablePromise
Usage with HTTP Libraries
How do I convert a normal Promise
to a CancellablePromise
?
TODO
TODO
TODO
CancellablePromise
supports all the methods that the normal Promise
object
supports, with the exception of Promise.any
(ES2021). See the API Reference for
details.
Examples
React: Prevent setState after unmount
React will give you a warning if you attempt to update a component's state after
it has unmounted. This will happen if your component makes an API call but gets
unmounted before the API call completes.
You can fix this by canceling the API call in the cleanup function of an effect.
function listBlogPosts(): CancellablePromise<Post[]> {
}
export function Blog() {
const [posts, setPosts] = useState<Post[]>([])
useEffect(() => {
const cancellablePromise = listBlogPosts().then(setPosts).catch(console.error)
return cancellablePromise.cancel
}, [])
return <div>{posts.map(p => /* ... */)}</div>
}
CodeSandbox
React: Cancel the in-progress API call when query parameters change
Sometimes API calls have parameters, like a search string entered by the user.
function searchUsers(searchTerm: string): CancellablePromise<User[]> {
}
export function UserList() {
const [searchTerm, setSearchTerm] = useState('')
const [users, setUsers] = useState<User[]>([])
useEffect(() => {
const cancellablePromise = searchUsers(searchTerm).then(setUsers).catch(console.error)
return cancellablePromise.cancel
}, [searchTerm])
return (
<div>
<SearchInput searchTerm={searchTerm} onChange={setSearchTerm} />
{users.map(p => /* ... */)}
</div>
)
}
CodeSandbox
Combine multiple API calls into a single async flow
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).
async function bigQuery(userId: number): CancellablePromise<QueryResult> {
return buildCancellablePromise(async (capture) => {
const userPromise = api.user.get(userId)
const rolePromise = api.user.listRoles(userId)
const [user, roles] = await capture(
CancellablePromise.all([userPromise, rolePromise])
)
const customer = await capture(api.customer.get(user.customerId))
return { user, roles, customer }
})
}
Usage with react-query
If your query key changes and there's an API call in progress, react-query
will cancel the CancellablePromise
automatically.
CodeSandbox
Handling Cancellation
Usually, you'll want to ignore Cancellation
objects that get thrown:
try {
await capture(cancellablePromise)
} catch (e) {
if (e instanceof Cancellation) {
return
}
}
Handling promises that can't truly be canceled
Sometimes you need to call an asynchronous function that doesn't support
cancellation. In this case, you can use pseudoCancellable
to prevent the
promise from resolving after cancel
has been called.
const cancellablePromise = pseudoCancellable(normalPromise)
cancellablePromise.cancel()
await cancellablePromise
CancellablePromise.delay
await CancellablePromise.delay(1000)
React: useCancellablePromiseCleanup
Here's a React hook that facilitates cancellation of CancellablePromise
s that
occur outside of useEffect
. Any pending API calls will be canceled when the
component unmounts. (Just be sure this is what you want to happen.)
export function useCancellablePromiseCleanup(): CaptureCancellablePromise {
const cancellablePromisesRef = useRef<CancellablePromise<unknown>[]>([])
useEffect(
() => (): void => {
for (const promise of cancellablePromisesRef.current) {
promise.cancel()
}
},
[]
)
const capture: CaptureCancellablePromise = useCallback((promise) => {
cancellablePromisesRef.current.push(promise)
return promise
}, [])
return capture
}
Then in your React components...
function updateUser(id: number, name: string): CancellablePromise<void> {
}
export function UserDetail(props: UserDetailProps) {
const capture = useCancellablePromiseCleanup()
async function saveChanges(): Promise<void> {
try {
await capture(updateUser(id, name))
} catch {
}
}
return <div>...</div>
}
CodeSandbox
Supported Platforms
Browser: anything that's not Internet Explorer
React Native / Expo: should work in any recent release
Node.js: current release and active LTS releases
License
MIT
Contributing
See CONTRIBUTING.md
.