Skip to content

Commit a701340

Browse files
prateek3255TkDodo
andauthored
refactor: Remove deprecated promise cancel (#2996)
* πŸ”₯ Remove the cancel method on promise for cancelling promise * βœ… Fix query client tests * βœ… Update query and useQuery tests * βœ… Update use infinite query tests * πŸ“ Update migartion guide * πŸ› Fix linking in documentation * πŸ“ Fix grammatical errors in docs Co-authored-by: Dominik Dorfmeister <[email protected]> * :refactor: Use abortSignal for query cancellation in InfiniteQueryBehavior * 🚨 Fix lint errors * ♻️ Move define signal property to a separate function Co-authored-by: Dominik Dorfmeister <[email protected]>
1 parent 5b464da commit a701340

File tree

8 files changed

+56
-100
lines changed

8 files changed

+56
-100
lines changed

β€Ždocs/src/pages/guides/migrating-to-react-query-4.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,10 @@ Since these plugins are no longer experimental, their import paths have also bee
184184
+ import { createAsyncStoragePersister } from 'react-query/createAsyncStoragePersister'
185185
```
186186

187+
### The `cancel` method on promises is no longer supported
188+
189+
The [old `cancel` method](../guides/query-cancellation#old-cancel-function) that allowed you to define a `cancel` function on promises, which was then used by the library to support query cancellation, has been removed. We recommend to use the [newer API](../guides/query-cancellation) (introduced with v3.30.0) for query cancellation that uses the [`AbortController` API](https://developer.mozilla.org/en-US/docs/Web/API/AbortController) internally and provides you with an [`AbortSignal` instance](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal) for your query function to support query cancellation.
190+
187191
## New Features πŸš€
188192

189193
### Mutation Cache Garbage Collection

β€Žsrc/core/infiniteQueryBehavior.ts

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { QueryBehavior } from './query'
2-
import { isCancelable } from './retryer'
2+
33
import type {
44
InfiniteData,
55
QueryFunctionContext,
@@ -73,11 +73,6 @@ export function infiniteQueryBehavior<
7373
buildNewPages(pages, param, page, previous)
7474
)
7575

76-
if (isCancelable(queryFnResult)) {
77-
const promiseAsAny = promise as any
78-
promiseAsAny.cancel = queryFnResult.cancel
79-
}
80-
8176
return promise
8277
}
8378

@@ -148,15 +143,10 @@ export function infiniteQueryBehavior<
148143
pageParams: newPageParams,
149144
}))
150145

151-
const finalPromiseAsAny = finalPromise as any
152-
153-
finalPromiseAsAny.cancel = () => {
146+
context.signal?.addEventListener('abort', () => {
154147
cancelled = true
155148
abortController?.abort()
156-
if (isCancelable(promise)) {
157-
promise.cancel()
158-
}
159-
}
149+
})
160150

161151
return finalPromise
162152
}

β€Žsrc/core/query.ts

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ export interface FetchContext<
6363
> {
6464
fetchFn: () => unknown | Promise<unknown>
6565
fetchOptions?: FetchOptions
66+
signal?: AbortSignal
6667
options: QueryOptions<TQueryFnData, TError, TData, any>
6768
queryKey: TQueryKey
6869
state: QueryState<TData, TError>
@@ -316,7 +317,7 @@ export class Query<
316317
// If the transport layer does not support cancellation
317318
// we'll let the query continue so the result can be cached
318319
if (this.retryer) {
319-
if (this.retryer.isTransportCancelable || this.abortSignalConsumed) {
320+
if (this.abortSignalConsumed) {
320321
this.retryer.cancel({ revert: true })
321322
} else {
322323
this.retryer.cancelRetry()
@@ -381,16 +382,23 @@ export class Query<
381382
meta: this.meta,
382383
}
383384

384-
Object.defineProperty(queryFnContext, 'signal', {
385-
enumerable: true,
386-
get: () => {
387-
if (abortController) {
388-
this.abortSignalConsumed = true
389-
return abortController.signal
390-
}
391-
return undefined
392-
},
393-
})
385+
// Adds an enumerable signal property to the object that
386+
// which sets abortSignalConsumed to true when the signal
387+
// is read.
388+
const addSignalProperty = (object: unknown) => {
389+
Object.defineProperty(object, 'signal', {
390+
enumerable: true,
391+
get: () => {
392+
if (abortController) {
393+
this.abortSignalConsumed = true
394+
return abortController.signal
395+
}
396+
return undefined
397+
},
398+
})
399+
}
400+
401+
addSignalProperty(queryFnContext)
394402

395403
// Create fetch function
396404
const fetchFn = () => {
@@ -411,6 +419,8 @@ export class Query<
411419
meta: this.meta,
412420
}
413421

422+
addSignalProperty(context)
423+
414424
if (this.options.behavior?.onFetch) {
415425
this.options.behavior?.onFetch(context)
416426
}

β€Žsrc/core/retryer.ts

Lines changed: 0 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -34,15 +34,6 @@ type RetryDelayFunction<TError = unknown> = (
3434
function defaultRetryDelay(failureCount: number) {
3535
return Math.min(1000 * 2 ** failureCount, 30000)
3636
}
37-
38-
interface Cancelable {
39-
cancel(): void
40-
}
41-
42-
export function isCancelable(value: any): value is Cancelable {
43-
return typeof value?.cancel === 'function'
44-
}
45-
4637
export class CancelledError {
4738
revert?: boolean
4839
silent?: boolean
@@ -65,7 +56,6 @@ export class Retryer<TData = unknown, TError = unknown> {
6556
failureCount: number
6657
isPaused: boolean
6758
isResolved: boolean
68-
isTransportCancelable: boolean
6959
promise: Promise<TData>
7060

7161
private abort?: () => void
@@ -86,7 +76,6 @@ export class Retryer<TData = unknown, TError = unknown> {
8676
this.failureCount = 0
8777
this.isPaused = false
8878
this.isResolved = false
89-
this.isTransportCancelable = false
9079
this.promise = new Promise<TData>((outerResolve, outerReject) => {
9180
promiseResolve = outerResolve
9281
promiseReject = outerReject
@@ -144,19 +133,9 @@ export class Retryer<TData = unknown, TError = unknown> {
144133
reject(new CancelledError(cancelOptions))
145134

146135
this.abort?.()
147-
148-
// Cancel transport if supported
149-
if (isCancelable(promiseOrValue)) {
150-
try {
151-
promiseOrValue.cancel()
152-
} catch {}
153-
}
154136
}
155137
}
156138

157-
// Check if the transport layer support cancellation
158-
this.isTransportCancelable = isCancelable(promiseOrValue)
159-
160139
Promise.resolve(promiseOrValue)
161140
.then(resolve)
162141
.catch(error => {

β€Žsrc/core/tests/query.test.tsx

Lines changed: 0 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -335,43 +335,6 @@ describe('query', () => {
335335
expect(isCancelledError(error)).toBe(true)
336336
})
337337

338-
test('should call cancel() fn if it was provided and not continue when last observer unsubscribed', async () => {
339-
const key = queryKey()
340-
341-
const cancel = jest.fn()
342-
343-
queryClient.prefetchQuery(key, async () => {
344-
const promise = new Promise((resolve, reject) => {
345-
sleep(100).then(() => resolve('data'))
346-
cancel.mockImplementation(() => {
347-
reject(new Error('Cancelled'))
348-
})
349-
}) as any
350-
promise.cancel = cancel
351-
return promise
352-
})
353-
354-
await sleep(10)
355-
356-
// Subscribe and unsubscribe to simulate cancellation because the last observer unsubscribed
357-
const observer = new QueryObserver(queryClient, {
358-
queryKey: key,
359-
enabled: false,
360-
})
361-
const unsubscribe = observer.subscribe()
362-
unsubscribe()
363-
364-
await sleep(100)
365-
366-
const query = queryCache.find(key)!
367-
368-
expect(cancel).toHaveBeenCalled()
369-
expect(query.state).toMatchObject({
370-
data: undefined,
371-
status: 'idle',
372-
})
373-
})
374-
375338
test('should not continue if explicitly cancelled', async () => {
376339
const key = queryKey()
377340

β€Žsrc/core/tests/queryClient.test.tsx

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -983,7 +983,7 @@ describe('queryClient', () => {
983983

984984
test('should cancel ongoing fetches if cancelRefetch option is set (default value)', async () => {
985985
const key = queryKey()
986-
const cancelFn = jest.fn()
986+
const abortFn = jest.fn()
987987
let fetchCount = 0
988988
const observer = new QueryObserver(queryClient, {
989989
queryKey: key,
@@ -992,25 +992,29 @@ describe('queryClient', () => {
992992
})
993993
observer.subscribe()
994994

995-
queryClient.fetchQuery(key, () => {
995+
queryClient.fetchQuery(key, ({ signal }) => {
996996
const promise = new Promise(resolve => {
997997
fetchCount++
998998
setTimeout(() => resolve(5), 10)
999+
if (signal) {
1000+
signal.addEventListener('abort', abortFn)
1001+
}
9991002
})
1000-
// @ts-expect-error
1001-
promise.cancel = cancelFn
1003+
10021004
return promise
10031005
})
10041006

10051007
await queryClient.refetchQueries()
10061008
observer.destroy()
1007-
expect(cancelFn).toHaveBeenCalledTimes(1)
1009+
if (typeof AbortSignal === 'function') {
1010+
expect(abortFn).toHaveBeenCalledTimes(1)
1011+
}
10081012
expect(fetchCount).toBe(2)
10091013
})
10101014

10111015
test('should not cancel ongoing fetches if cancelRefetch option is set to false', async () => {
10121016
const key = queryKey()
1013-
const cancelFn = jest.fn()
1017+
const abortFn = jest.fn()
10141018
let fetchCount = 0
10151019
const observer = new QueryObserver(queryClient, {
10161020
queryKey: key,
@@ -1019,19 +1023,23 @@ describe('queryClient', () => {
10191023
})
10201024
observer.subscribe()
10211025

1022-
queryClient.fetchQuery(key, () => {
1026+
queryClient.fetchQuery(key, ({ signal }) => {
10231027
const promise = new Promise(resolve => {
10241028
fetchCount++
10251029
setTimeout(() => resolve(5), 10)
1030+
if (signal) {
1031+
signal.addEventListener('abort', abortFn)
1032+
}
10261033
})
1027-
// @ts-expect-error
1028-
promise.cancel = cancelFn
1034+
10291035
return promise
10301036
})
10311037

10321038
await queryClient.refetchQueries(undefined, { cancelRefetch: false })
10331039
observer.destroy()
1034-
expect(cancelFn).toHaveBeenCalledTimes(0)
1040+
if (typeof AbortSignal === 'function') {
1041+
expect(abortFn).toHaveBeenCalledTimes(0)
1042+
}
10351043
expect(fetchCount).toBe(1)
10361044
})
10371045
})

β€Žsrc/reactjs/tests/useInfiniteQuery.test.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1794,14 +1794,13 @@ describe('useInfiniteQuery', () => {
17941794
const key = queryKey()
17951795
let cancelFn: jest.Mock = jest.fn()
17961796

1797-
const queryFn = () => {
1797+
const queryFn = ({ signal }: { signal?: AbortSignal }) => {
17981798
const promise = new Promise<string>((resolve, reject) => {
17991799
cancelFn = jest.fn(() => reject('Cancelled'))
1800+
signal?.addEventListener('abort', cancelFn)
18001801
sleep(10).then(() => resolve('OK'))
18011802
})
18021803

1803-
;(promise as any).cancel = cancelFn
1804-
18051804
return promise
18061805
}
18071806

@@ -1823,6 +1822,8 @@ describe('useInfiniteQuery', () => {
18231822

18241823
await waitFor(() => rendered.getByText('off'))
18251824

1826-
expect(cancelFn).toHaveBeenCalled()
1825+
if (typeof AbortSignal === 'function') {
1826+
expect(cancelFn).toHaveBeenCalled()
1827+
}
18271828
})
18281829
})

β€Žsrc/reactjs/tests/useQuery.test.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3895,14 +3895,13 @@ describe('useQuery', () => {
38953895
const key = queryKey()
38963896
let cancelFn: jest.Mock = jest.fn()
38973897

3898-
const queryFn = () => {
3898+
const queryFn = ({ signal }: { signal?: AbortSignal }) => {
38993899
const promise = new Promise<string>((resolve, reject) => {
39003900
cancelFn = jest.fn(() => reject('Cancelled'))
3901+
signal?.addEventListener('abort', cancelFn)
39013902
sleep(10).then(() => resolve('OK'))
39023903
})
39033904

3904-
;(promise as any).cancel = cancelFn
3905-
39063905
return promise
39073906
}
39083907

@@ -3924,7 +3923,9 @@ describe('useQuery', () => {
39243923

39253924
await waitFor(() => rendered.getByText('off'))
39263925

3927-
expect(cancelFn).toHaveBeenCalled()
3926+
if (typeof AbortSignal === 'function') {
3927+
expect(cancelFn).toHaveBeenCalled()
3928+
}
39283929
})
39293930

39303931
it('should cancel the query if the signal was consumed and there are no more subscriptions', async () => {

0 commit comments

Comments
Β (0)