If you ever had to work with a GraphQL API featuring a few slow queries, you’ll probably understand the need to be able to cancel apollo-client
queries.
I’m using apollo-client
3.2.5, the latest at the time of writing. Since apollo-client
uses fetch
, my first attempt to cancel a query was using abortController
:
interface CancellablePromise<T> extends Promise<T> { cancel: () => void }
export function cancellableQuery(options: QueryOptions<any, any>): CancellablePromise<ApolloQueryResult<any>> {
const abortController = new AbortController()
let promise = apolloClient.query({
...options,
context: {
fetchOptions: {signal: abortController.signal}
}
}) as CancellablePromise<ApolloQueryResult<any>>
promise.cancel = () => abortController.abort()
return promise
}
It partially works: the request is cancelled, and the promise doesn’t complete. However there’s a big issue: subsequent calls of the same query with the same variables don’t work anymore, no attempt is being made. You can follow this pull request since it might one day be resolved: handle external fetch abort.
Not discouraged, I tried another method using watchQuery
and the unsubscribe
method:
interface CancellablePromise<T> extends Promise<T> { cancel: () => void }
export function cancellableQuery(options: QueryOptions<any, any>): CancellablePromise<ApolloQueryResult<any>> {
const observable = apolloClient.watchQuery(options)
let subscription: ZenObservable.Subscription
let promise = new Promise((resolve, reject) => {
subscription = observable.subscribe(
(res) => resolve(res),
() => reject()
)
}) as CancellablePromise<ApolloQueryResult<any>>
promise.cancel = () => subscription.unsubscribe()
return promise
}
The logic behind it is that the call to unsubscribe
should cancel the request when the browser supports it. However, that just doesn’t happen for me using Google Chrome (version 86.0.4240.111), the promise doesn’t complete however the request isn’t cancelled and subsequent calls of the same query with the same variables don’t work. It seems like I’m not the only one to have noticed, you can track the progress on this issue in the hope that it get fixed at some point: Unsubscribing from a query does not abort network requests.
You can find a few more opened issues related to cancelling apollo-client requests:
- Unmounting Query component no longer cancels network request in v2.6.0 & v3 beta
- updateQuery merging different data when changing query while fetchMore is not yet finished
- Cancel fetchMore network requests
After searching more about it, I found out about queryDeduplication, it’s apparently the reason why subsequent calls to cancelled queries don’t work anymore: the request is still considered to be “in-flight” and thus no further attempt is being made. Thus our code can become:
interface CancellablePromise<T> extends Promise<T> { cancel: () => void }
export function cancellableQuery(options: QueryOptions<any, any>): CancellablePromise<ApolloQueryResult<any>> {
const abortController = new AbortController()
let promise = apolloClient.query({
...options,
context: {
fetchOptions: {signal: abortController.signal},
queryDeduplication: false
}
}) as CancellablePromise<ApolloQueryResult<any>>
promise.cancel = () => abortController.abort()
return promise
}
This time it finally works: the request is cancelled, the promise doesn’t complete, the next call of the same query with the same variables can complete successfully and subsequent calls will be retrieved from cache.
It’s also possible to combine watchQuery
, abortController
and queryDeduplication
disabled:
export function cancellableQuery(options: QueryOptions<any, any>): CancellablePromise<ApolloQueryResult<any>> {
const abortController = new AbortController()
const observable = apolloClient.watchQuery({
...options,
context: {
fetchOptions: {
signal: abortController.signal
},
queryDeduplication: false
}
})
let subscription: ZenObservable.Subscription
let promise = new Promise((resolve, reject) => {
subscription = observable.subscribe(
(res) => resolve(res),
() => reject()
)
}) as CancellablePromise<ApolloQueryResult<any>>
promise.cancel = () => {
abortController.abort()
subscription.unsubscribe()
}
return promise
}
The result is the same as the previous version, with no further benefit that I could see. Finally, I also tried to change the fetchPolicy
to network-only
but as far as I could see it only prevents the completed queries from being retrieved from cache, which isn’t something which I need.