Skip to content

Commit ddab6d4

Browse files
authored
Merge branch 'master' into fix-storage-full
2 parents a10573e + 8013d4a commit ddab6d4

File tree

15 files changed

+495
-86
lines changed

15 files changed

+495
-86
lines changed

docs/src/pages/guides/initial-query-data.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ By default, `initialData` is treated as totally fresh, as if it were just fetche
5555
- So what if your `initialData` isn't totally fresh? That leaves us with the last configuration that is actually the most accurate and uses an option called `initialDataUpdatedAt`. This option allows you to pass a numeric JS timestamp in milliseconds of when the initialData itself was last updated, e.g. what `Date.now()` provides. Take note that if you have a unix timestamp, you'll need to convert it to a JS timestamp by multiplying it by `1000`.
5656
```js
5757
function Todos() {
58-
// Show initialTodos immeidately, but won't refetch until another interaction event is encountered after 1000 ms
58+
// Show initialTodos immediately, but won't refetch until another interaction event is encountered after 1000 ms
5959
const result = useQuery('todos', () => fetch('/todos'), {
6060
initialData: initialTodos,
6161
staleTime: 60 * 1000 // 1 minute

docs/src/pages/guides/query-cancellation.md

Lines changed: 108 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,55 @@ id: query-cancellation
33
title: Query Cancellation
44
---
55

6-
By default, queries that unmount or become unused before their promises are resolved are simply ignored instead of canceled. Why is this?
6+
[_Previous method requiring a `cancel` function_](#old-cancel-function)
77

8-
- For most applications, ignoring out-of-date queries is sufficient.
9-
- Cancellation APIs may not be available for every query function.
10-
- If cancellation APIs are available, they typically vary in implementation between utilities/libraries (eg. Fetch vs Axios vs XMLHttpRequest).
8+
React Query provides each query function with an [`AbortSignal` instance](https://developer.mozilla.org/docs/Web/API/AbortSignal) **if it's available in your runtime environment**. When a query becomes out-of-date or inactive, this `signal` will become aborted. This means that all queries are cancellable and you can respond to the cancellation inside your query function if desired. The best part about this is that it allow you to continue to use normal async/await syntax while getting all the benefits of automatic cancellation. Additionally, this solution works better with TypeScript than the old solution.
119

12-
But don't worry! If your queries are high-bandwidth or potentially very expensive to download, React Query exposes a generic way to **cancel** query requests using a cancellation token or other related API. To integrate with this feature, attach a `cancel` function to the promise returned by your query that implements your request cancellation. When a query becomes out-of-date or inactive, this `promise.cancel` function will be called (if available):
10+
The `AbortController` API is available in [most runtime environments](https://developer.mozilla.org/docs/Web/API/AbortController#browser_compatibility), but if the runtime environment does not support it then the query function will receive `undefined` in its place. You may choose to polyfill the `AbortController` API if you wish, there are [several available](https://www.npmjs.com/search?q=abortcontroller%20polyfill).
11+
12+
## Using `fetch`
13+
14+
```js
15+
const query = useQuery('todos', async ({ signal }) => {
16+
const todosResponse = await fetch('/todos', {
17+
// Pass the signal to one fetch
18+
signal,
19+
})
20+
const todos = await todosResponse.json()
21+
22+
const todoDetails = todos.map(async ({ details } => {
23+
const response = await fetch(details, {
24+
// Or pass it to several
25+
signal,
26+
})
27+
return response.json()
28+
})
29+
30+
return Promise.all(todoDetails)
31+
})
32+
```
1333
1434
## Using `axios`
1535
36+
### Using `axios` [v0.22.0+](https://github.com/axios/axios/releases/tag/v0.22.0)
37+
1638
```js
1739
import axios from 'axios'
1840

19-
const query = useQuery('todos', () => {
41+
const query = useQuery('todos', ({ signal }) =>
42+
axios.get('/todos', {
43+
// Pass the signal to `axios`
44+
signal,
45+
})
46+
)
47+
```
48+
49+
### Using an `axios` version less than v0.22.0
50+
51+
```js
52+
import axios from 'axios'
53+
54+
const query = useQuery('todos', ({ signal }) => {
2055
// Create a new CancelToken source for this request
2156
const CancelToken = axios.CancelToken
2257
const source = CancelToken.source()
@@ -26,50 +61,98 @@ const query = useQuery('todos', () => {
2661
cancelToken: source.token,
2762
})
2863

29-
// Cancel the request if React Query calls the `promise.cancel` method
30-
promise.cancel = () => {
64+
// Cancel the request if React Query signals to abort
65+
signal?.addEventListener('abort', () => {
3166
source.cancel('Query was cancelled by React Query')
32-
}
67+
})
3368

3469
return promise
3570
})
3671
```
3772
38-
## Using `fetch`
73+
## Using `XMLHttpRequest`
74+
75+
```js
76+
const query = useQuery('todos', ({ signal }) => {
77+
return new Promise((resolve, reject) => {
78+
var oReq = new XMLHttpRequest()
79+
oReq.addEventListener('load', () => {
80+
resolve(JSON.parse(oReq.responseText))
81+
})
82+
signal?.addEventListener('abort', () => {
83+
oReq.abort()
84+
reject()
85+
})
86+
oReq.open('GET', '/todos')
87+
oReq.send()
88+
})
89+
})
90+
```
91+
92+
## Manual Cancellation
93+
94+
You might want to cancel a query manually. For example, if the request takes a long time to finish, you can allow the user to click a cancel button to stop the request. To do this, you just need to call `queryClient.cancelQueries(key)`. If `promise.cancel` is available or you have consumed the `signal` passed to the query function then React Query will cancel the request.
95+
96+
```js
97+
const [queryKey] = useState('todos')
98+
99+
const query = useQuery(queryKey, await ({ signal }) => {
100+
const resp = fetch('/todos', { signal })
101+
return resp.json()
102+
})
103+
104+
const queryClient = useQueryClient()
105+
106+
return (
107+
<button onClick={(e) => {
108+
e.preventDefault()
109+
queryClient.cancelQueries(queryKey)
110+
}}>Cancel</button>
111+
)
112+
```
113+
114+
## Old `cancel` function
115+
116+
Don't worry! The previous cancellation functionality will continue to work. But we do recommend that you move away from [the withdrawn cancelable promise proposal](https://github.com/tc39/proposal-cancelable-promises) to the [new `AbortSignal` interface](#_top) which has been [stardardized](https://dom.spec.whatwg.org/#interface-abortcontroller) as a general purpose construct for aborting ongoing activities in [most browsers](https://caniuse.com/abortcontroller) and in [Node](https://nodejs.org/api/globals.html#globals_class_abortsignal). The old cancel function might be removed in a future major version.
117+
118+
To integrate with this feature, attach a `cancel` function to the promise returned by your query that implements your request cancellation. When a query becomes out-of-date or inactive, this `promise.cancel` function will be called (if available).
119+
120+
## Using `axios` with `cancel` function
39121
40122
```js
123+
import axios from 'axios'
124+
41125
const query = useQuery('todos', () => {
42-
// Create a new AbortController instance for this request
43-
const controller = new AbortController()
44-
// Get the abortController's signal
45-
const signal = controller.signal
126+
// Create a new CancelToken source for this request
127+
const CancelToken = axios.CancelToken
128+
const source = CancelToken.source()
46129

47-
const promise = fetch('/todos', {
48-
method: 'get',
49-
// Pass the signal to your request
50-
signal,
130+
const promise = axios.get('/todos', {
131+
// Pass the source token to your request
132+
cancelToken: source.token,
51133
})
52134

53135
// Cancel the request if React Query calls the `promise.cancel` method
54-
promise.cancel = () => controller.abort()
136+
promise.cancel = () => {
137+
source.cancel('Query was cancelled by React Query')
138+
}
55139

56140
return promise
57141
})
58142
```
59143
60-
## Manual Cancellation
61-
62-
You might want to cancel a query manually. For example, if the request takes a long time to finish, you can allow the user to click a cancel button to stop the request. To do this, you just need to call `queryClient.cancelQueries(key)`. If `promise.cancel` is available, React Query will cancel the request.
144+
## Using `fetch` with `cancel` function
63145
64146
```js
65-
const [queryKey] = useState('todos')
66-
67-
const query = useQuery(queryKey, () => {
147+
const query = useQuery('todos', () => {
148+
// Create a new AbortController instance for this request
68149
const controller = new AbortController()
150+
// Get the abortController's signal
69151
const signal = controller.signal
70152

71153
const promise = fetch('/todos', {
72154
method: 'get',
155+
// Pass the signal to your request
73156
signal,
74157
})
75158

@@ -78,13 +161,4 @@ const query = useQuery(queryKey, () => {
78161

79162
return promise
80163
})
81-
82-
const queryClient = useQueryClient();
83-
84-
return (
85-
<button onClick={(e) => {
86-
e.preventDefault();
87-
queryClient.cancelQueries(queryKey);
88-
}}>Cancel</button>
89-
)
90164
```

docs/src/pages/plugins/createAsyncStoragePersistor.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,10 @@ interface CreateAsyncStoragePersistorOptions {
6060
/** To avoid localstorage spamming,
6161
* pass a time in ms to throttle saving the cache to disk */
6262
throttleTime?: number
63+
/** How to serialize the data to storage */
64+
serialize?: (client: PersistedClient) => string
65+
/** How to deserialize the data from storage */
66+
deserialize?: (cachedString: string) => PersistedClient
6367
}
6468

6569
interface AsyncStorage {
@@ -75,5 +79,7 @@ The default options are:
7579
{
7680
key = `REACT_QUERY_OFFLINE_CACHE`,
7781
throttleTime = 1000,
82+
serialize = JSON.stringify,
83+
deserialize = JSON.parse,
7884
}
7985
```

docs/src/pages/plugins/createWebStoragePersistor.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,10 @@ interface CreateWebStoragePersistorOptions {
5757
/** To avoid spamming,
5858
* pass a time in ms to throttle saving the cache to disk */
5959
throttleTime?: number
60+
/** How to serialize the data to storage */
61+
serialize?: (client: PersistedClient) => string
62+
/** How to deserialize the data from storage */
63+
deserialize?: (cachedString: string) => PersistedClient
6064
}
6165
```
6266

@@ -66,5 +70,7 @@ The default options are:
6670
{
6771
key = `REACT_QUERY_OFFLINE_CACHE`,
6872
throttleTime = 1000,
73+
serialize = JSON.stringify,
74+
deserialize = JSON.parse,
6975
}
7076
```

src/core/infiniteQueryBehavior.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import type {
66
QueryOptions,
77
RefetchQueryFilters,
88
} from './types'
9+
import { getAbortController } from './utils'
910

1011
export function infiniteQueryBehavior<
1112
TQueryFnData,
@@ -23,6 +24,8 @@ export function infiniteQueryBehavior<
2324
const isFetchingPreviousPage = fetchMore?.direction === 'backward'
2425
const oldPages = context.state.data?.pages || []
2526
const oldPageParams = context.state.data?.pageParams || []
27+
const abortController = getAbortController()
28+
const abortSignal = abortController?.signal
2629
let newPageParams = oldPageParams
2730
let cancelled = false
2831

@@ -59,6 +62,7 @@ export function infiniteQueryBehavior<
5962

6063
const queryFnContext: QueryFunctionContext = {
6164
queryKey: context.queryKey,
65+
signal: abortSignal,
6266
pageParam: param,
6367
meta: context.meta,
6468
}
@@ -148,6 +152,7 @@ export function infiniteQueryBehavior<
148152

149153
finalPromiseAsAny.cancel = () => {
150154
cancelled = true
155+
abortController?.abort()
151156
if (isCancelable(promise)) {
152157
promise.cancel()
153158
}

src/core/query.ts

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {
2+
getAbortController,
23
Updater,
34
functionalUpdate,
45
isValidTimeout,
@@ -15,12 +16,14 @@ import type {
1516
QueryFunctionContext,
1617
EnsuredQueryKey,
1718
QueryMeta,
19+
CancelOptions,
20+
SetDataOptions,
1821
} from './types'
1922
import type { QueryCache } from './queryCache'
2023
import type { QueryObserver } from './queryObserver'
2124
import { notifyManager } from './notifyManager'
2225
import { getLogger } from './logger'
23-
import { Retryer, CancelOptions, isCancelledError } from './retryer'
26+
import { Retryer, isCancelledError } from './retryer'
2427

2528
// TYPES
2629

@@ -84,10 +87,6 @@ export interface FetchOptions {
8487
meta?: any
8588
}
8689

87-
export interface SetDataOptions {
88-
updatedAt?: number
89-
}
90-
9190
interface FailedAction {
9291
type: 'failed'
9392
}
@@ -163,8 +162,10 @@ export class Query<
163162
private retryer?: Retryer<TData, TError>
164163
private observers: QueryObserver<any, any, any, any, any>[]
165164
private defaultOptions?: QueryOptions<TQueryFnData, TError, TData, TQueryKey>
165+
private abortSignalConsumed: boolean
166166

167167
constructor(config: QueryConfig<TQueryFnData, TError, TData, TQueryKey>) {
168+
this.abortSignalConsumed = false
168169
this.defaultOptions = config.defaultOptions
169170
this.setOptions(config.options)
170171
this.observers = []
@@ -333,7 +334,7 @@ export class Query<
333334
// If the transport layer does not support cancellation
334335
// we'll let the query continue so the result can be cached
335336
if (this.retryer) {
336-
if (this.retryer.isTransportCancelable) {
337+
if (this.retryer.isTransportCancelable || this.abortSignalConsumed) {
337338
this.retryer.cancel({ revert: true })
338339
} else {
339340
this.retryer.cancelRetry()
@@ -390,6 +391,7 @@ export class Query<
390391
}
391392

392393
const queryKey = ensureQueryKeyArray(this.queryKey)
394+
const abortController = getAbortController()
393395

394396
// Create query function context
395397
const queryFnContext: QueryFunctionContext<TQueryKey> = {
@@ -398,11 +400,25 @@ export class Query<
398400
meta: this.meta,
399401
}
400402

403+
Object.defineProperty(queryFnContext, 'signal', {
404+
enumerable: true,
405+
get: () => {
406+
if (abortController) {
407+
this.abortSignalConsumed = true
408+
return abortController.signal
409+
}
410+
return undefined
411+
},
412+
})
413+
401414
// Create fetch function
402-
const fetchFn = () =>
403-
this.options.queryFn
404-
? this.options.queryFn(queryFnContext)
405-
: Promise.reject('Missing queryFn')
415+
const fetchFn = () => {
416+
if (!this.options.queryFn) {
417+
return Promise.reject('Missing queryFn')
418+
}
419+
this.abortSignalConsumed = false
420+
return this.options.queryFn(queryFnContext)
421+
}
406422

407423
// Trigger behavior hook
408424
const context: FetchContext<TQueryFnData, TError, TData, TQueryKey> = {
@@ -432,6 +448,7 @@ export class Query<
432448
// Try to fetch the data
433449
this.retryer = new Retryer({
434450
fn: context.fetchFn as () => TData,
451+
abort: abortController?.abort?.bind(abortController),
435452
onSuccess: data => {
436453
this.setData(data as TData)
437454

src/core/queryClient.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,15 +27,16 @@ import type {
2727
RefetchQueryFilters,
2828
ResetOptions,
2929
ResetQueryFilters,
30+
SetDataOptions,
3031
} from './types'
31-
import type { QueryState, SetDataOptions } from './query'
32+
import type { QueryState } from './query'
3233
import { QueryCache } from './queryCache'
3334
import { MutationCache } from './mutationCache'
3435
import { focusManager } from './focusManager'
3536
import { onlineManager } from './onlineManager'
3637
import { notifyManager } from './notifyManager'
37-
import { CancelOptions } from './retryer'
3838
import { infiniteQueryBehavior } from './infiniteQueryBehavior'
39+
import { CancelOptions } from './types'
3940

4041
// TYPES
4142

0 commit comments

Comments
 (0)