Skip to content

Commit 208d0ed

Browse files
authored
Merge branch 'main' into fix/query-promise-reset
2 parents 902b55d + ef62f7b commit 208d0ed

290 files changed

Lines changed: 11808 additions & 7696 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docs/community-resources.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,11 @@ others:
175175
url: 'https://github.com/rametta/rapini',
176176
description: '🥬 OpenAPI to React Query (or SWR) & Axios',
177177
},
178+
{
179+
title: 'React Query Visualizer',
180+
url: 'https://marketplace.visualstudio.com/items?itemName=fe-dudu.react-query-visualizer',
181+
description: 'VS Code extension for TanStack Query (React Query): visualize query keys, cache invalidation/refetch flows, and file impact graph',
182+
},
178183
{
179184
title: 'Tanstack Query Visualizer',
180185
url: 'https://tanstack-query-visualizer.sofi.coop/',

docs/config.json

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,10 @@
246246
"label": "Window Focus Refetching",
247247
"to": "framework/react/guides/window-focus-refetching"
248248
},
249+
{
250+
"label": "Polling",
251+
"to": "framework/react/guides/polling"
252+
},
249253
{
250254
"label": "Disabling/Pausing Queries",
251255
"to": "framework/react/guides/disabling-queries"
@@ -399,6 +403,10 @@
399403
"label": "Window Focus Refetching",
400404
"to": "framework/solid/guides/window-focus-refetching"
401405
},
406+
{
407+
"label": "Polling",
408+
"to": "framework/solid/guides/polling"
409+
},
402410
{
403411
"label": "Disabling/Pausing Queries",
404412
"to": "framework/solid/guides/disabling-queries"
@@ -536,6 +544,10 @@
536544
"label": "Window Focus Refetching",
537545
"to": "framework/vue/guides/window-focus-refetching"
538546
},
547+
{
548+
"label": "Polling",
549+
"to": "framework/vue/guides/polling"
550+
},
539551
{
540552
"label": "Disabling/Pausing Queries",
541553
"to": "framework/vue/guides/disabling-queries"
@@ -1293,6 +1305,10 @@
12931305
{
12941306
"label": "Mutation Property Order",
12951307
"to": "eslint/mutation-property-order"
1308+
},
1309+
{
1310+
"label": "Prefer Query Options",
1311+
"to": "eslint/prefer-query-options"
12961312
}
12971313
]
12981314
},

docs/eslint/eslint-plugin-query.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,19 @@ export default [
4646
]
4747
```
4848

49+
### Recommended strict setup
50+
51+
The `flat/recommended-strict` config extends `flat/recommended` with additional opinionated rules that enforce best practices more aggressively.
52+
53+
```js
54+
import pluginQuery from '@tanstack/eslint-plugin-query'
55+
56+
export default [
57+
...pluginQuery.configs['flat/recommended-strict'],
58+
// Any other config...
59+
]
60+
```
61+
4962
### Custom setup
5063

5164
Alternatively, you can load the plugin and configure only the rules you want to use:
@@ -78,6 +91,16 @@ To enable all of the recommended rules for our plugin, add `plugin:@tanstack/que
7891
}
7992
```
8093

94+
### Recommended strict setup
95+
96+
The `recommendedStrict` config extends `recommended` with additional opinionated rules:
97+
98+
```json
99+
{
100+
"extends": ["plugin:@tanstack/query/recommendedStrict"]
101+
}
102+
```
103+
81104
### Custom setup
82105

83106
Alternatively, add `@tanstack/query` to the plugins section, and configure the rules you want to use:
@@ -100,3 +123,4 @@ Alternatively, add `@tanstack/query` to the plugins section, and configure the r
100123
- [@tanstack/query/infinite-query-property-order](./infinite-query-property-order.md)
101124
- [@tanstack/query/no-void-query-fn](./no-void-query-fn.md)
102125
- [@tanstack/query/mutation-property-order](./mutation-property-order.md)
126+
- [@tanstack/query/prefer-query-options](./prefer-query-options.md)
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
---
2+
id: prefer-query-options
3+
title: Prefer the use of queryOptions
4+
---
5+
6+
Separating `queryKey` and `queryFn` can cause unexpected runtime issues when the same query key is accidentally used with more than one `queryFn`. Wrapping them in `queryOptions` (or `infiniteQueryOptions`) co-locates the key and function, making queries safer and easier to reuse.
7+
8+
## Rule Details
9+
10+
Examples of **incorrect** code for this rule:
11+
12+
```tsx
13+
/* eslint "@tanstack/query/prefer-query-options": "error" */
14+
15+
function Component({ id }) {
16+
const query = useQuery({
17+
queryKey: ['get', id],
18+
queryFn: () => Api.get(`/foo/${id}`),
19+
})
20+
// ...
21+
}
22+
```
23+
24+
```tsx
25+
/* eslint "@tanstack/query/prefer-query-options": "error" */
26+
27+
function useFooQuery(id) {
28+
return useQuery({
29+
queryKey: ['get', id],
30+
queryFn: () => Api.get(`/foo/${id}`),
31+
})
32+
}
33+
```
34+
35+
Examples of **correct** code for this rule:
36+
37+
```tsx
38+
/* eslint "@tanstack/query/prefer-query-options": "error" */
39+
40+
function getFooOptions(id) {
41+
return queryOptions({
42+
queryKey: ['get', id],
43+
queryFn: () => Api.get(`/foo/${id}`),
44+
})
45+
}
46+
47+
function Component({ id }) {
48+
const query = useQuery(getFooOptions(id))
49+
// ...
50+
}
51+
```
52+
53+
```tsx
54+
/* eslint "@tanstack/query/prefer-query-options": "error" */
55+
56+
function getFooOptions(id) {
57+
return queryOptions({
58+
queryKey: ['get', id],
59+
queryFn: () => Api.get(`/foo/${id}`),
60+
})
61+
}
62+
63+
function useFooQuery(id) {
64+
return useQuery({ ...getFooOptions(id), select: (data) => data.foo })
65+
}
66+
```
67+
68+
The rule also enforces reusing `queryKey` from a `queryOptions` result instead of typing it manually in `QueryClient` methods or filters.
69+
70+
Examples of **incorrect** `queryKey` references for this rule:
71+
72+
```tsx
73+
/* eslint "@tanstack/query/prefer-query-options": "error" */
74+
75+
function todoOptions(id) {
76+
return queryOptions({
77+
queryKey: ['todo', id],
78+
queryFn: () => api.getTodo(id),
79+
})
80+
}
81+
82+
function Component({ id }) {
83+
const queryClient = useQueryClient()
84+
return queryClient.getQueryData(['todo', id])
85+
}
86+
```
87+
88+
```tsx
89+
/* eslint "@tanstack/query/prefer-query-options": "error" */
90+
91+
function todoOptions(id) {
92+
return queryOptions({
93+
queryKey: ['todo', id],
94+
queryFn: () => api.getTodo(id),
95+
})
96+
}
97+
98+
function Component({ id }) {
99+
const queryClient = useQueryClient()
100+
return queryClient.invalidateQueries({ queryKey: ['todo', id] })
101+
}
102+
```
103+
104+
Examples of **correct** `queryKey` references for this rule:
105+
106+
```tsx
107+
/* eslint "@tanstack/query/prefer-query-options": "error" */
108+
109+
function todoOptions(id) {
110+
return queryOptions({
111+
queryKey: ['todo', id],
112+
queryFn: () => api.getTodo(id),
113+
})
114+
}
115+
116+
function Component({ id }) {
117+
const queryClient = useQueryClient()
118+
return queryClient.getQueryData(todoOptions(id).queryKey)
119+
}
120+
```
121+
122+
```tsx
123+
/* eslint "@tanstack/query/prefer-query-options": "error" */
124+
125+
function todoOptions(id) {
126+
return queryOptions({
127+
queryKey: ['todo', id],
128+
queryFn: () => api.getTodo(id),
129+
})
130+
}
131+
132+
function Component({ id }) {
133+
const queryClient = useQueryClient()
134+
return queryClient.invalidateQueries({ queryKey: todoOptions(id).queryKey })
135+
}
136+
```
137+
138+
## When Not To Use It
139+
140+
If you do not want to enforce the use of `queryOptions` in your codebase, you will not need this rule.
141+
142+
## Attributes
143+
144+
- [x] ✅ Recommended (strict)
145+
- [ ] 🔧 Fixable

docs/framework/react/guides/important-defaults.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,16 @@ Out of the box, TanStack Query is configured with **aggressive but sane** defaul
1414
- set `staleTime` to `Infinity` to never trigger a refetch until the Query is [invalidated manually](./query-invalidation.md).
1515
- set `staleTime` to `'static'` to **never** trigger a refetch, even if the Query is [invalidated manually](./query-invalidation.md).
1616

17+
> `'static'` and `Infinity` both prevent staleness-based refetches, but `'static'` is stricter: `queryClient.invalidateQueries()` can invalidate a query with `staleTime: Infinity`, but has no effect on `staleTime: 'static'`. `refetchOnMount`, `refetchOnWindowFocus`, and `refetchOnReconnect` set to `"always"` are also blocked by `'static'`. Use `'static'` for data that cannot change while the app is running: feature flags fetched at boot, user permissions loaded at login, static reference tables. Use `Infinity` when you still want manual invalidation to work.
18+
1719
- Stale queries are refetched automatically in the background when:
1820
- New instances of the query mount
1921
- The window is refocused
2022
- The network is reconnected
2123

2224
> Setting `staleTime` is the recommended way to avoid excessive refetches, but you can also customize the points in time for refetches by setting options like `refetchOnMount`, `refetchOnWindowFocus` and `refetchOnReconnect`.
2325
24-
- Queries can optionally be configured with a `refetchInterval` to trigger refetches periodically, which is independent of the `staleTime` setting.
26+
- Queries can optionally be configured with a `refetchInterval` to trigger refetches periodically, which is independent of the `staleTime` setting. See [Polling](./polling.md) for details.
2527

2628
- Query results that have no more active instances of `useQuery`, `useInfiniteQuery` or query observers are labeled as "inactive" and remain in the cache in case they are used again at a later time.
2729
- By default, "inactive" queries are garbage collected after **5 minutes**.
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
---
2+
id: polling
3+
title: Polling
4+
---
5+
6+
`refetchInterval` makes a query refetch on a timer. Set it to a number in milliseconds and the query runs every N ms while there's at least one active observer:
7+
8+
[//]: # 'Example1'
9+
10+
```tsx
11+
useQuery({
12+
queryKey: ['prices'],
13+
queryFn: fetchPrices,
14+
refetchInterval: 5_000, // every 5 seconds
15+
})
16+
```
17+
18+
[//]: # 'Example1'
19+
20+
Polling is independent of `staleTime`. A query can be fresh and still poll on schedule; see [Important Defaults](./important-defaults.md) for how `staleTime` interacts with other refetch behaviors. `refetchInterval` fires on its own clock regardless of freshness.
21+
22+
## Adapting the interval to query state
23+
24+
Pass a function instead of a number to compute the interval from the current query. The function receives the `Query` object and should return a number in ms or `false` to stop polling:
25+
26+
[//]: # 'Example2'
27+
28+
```tsx
29+
useQuery({
30+
queryKey: ['job', jobId],
31+
queryFn: () => fetchJobStatus(jobId),
32+
refetchInterval: (query) => {
33+
// Stop polling once the job finishes
34+
if (query.state.data?.status === 'complete') return false
35+
return 2_000
36+
},
37+
})
38+
```
39+
40+
[//]: # 'Example2'
41+
42+
Returning `false` clears the interval timer. If the query result changes so the function would return a positive number again, polling resumes automatically.
43+
44+
## Background polling
45+
46+
By default, polling pauses when the browser tab loses focus. For dashboards or any interface where data needs to stay current even while the user is in another tab, disable that behavior:
47+
48+
[//]: # 'Example3'
49+
50+
```tsx
51+
useQuery({
52+
queryKey: ['portfolio'],
53+
queryFn: fetchPortfolio,
54+
refetchInterval: 30_000,
55+
refetchIntervalInBackground: true,
56+
})
57+
```
58+
59+
[//]: # 'Example3'
60+
61+
## Pausing polling
62+
63+
Pass a function to `refetchInterval` and close over component state to control when polling runs:
64+
65+
[//]: # 'Example4'
66+
67+
```tsx
68+
useQuery({
69+
queryKey: ['prices', tokenAddress],
70+
queryFn: () => fetchPrice(tokenAddress),
71+
refetchInterval: () => {
72+
if (!tokenAddress || isPaused) return false
73+
return 15_000
74+
},
75+
})
76+
```
77+
78+
[//]: # 'Example4'
79+
80+
## Polling with offline support
81+
82+
TanStack Query detects connectivity by listening to the browser's `online` and `offline` events. In environments where those events don't fire reliably (Electron, some embedded WebViews), set `networkMode: 'always'` to skip the connectivity check:
83+
84+
[//]: # 'Example5'
85+
86+
```tsx
87+
useQuery({
88+
queryKey: ['chainStatus'],
89+
queryFn: fetchChainStatus,
90+
refetchInterval: 10_000,
91+
networkMode: 'always',
92+
})
93+
```
94+
95+
[//]: # 'Example5'
96+
97+
For more on network modes, see [Network Mode](./network-mode.md).
98+
99+
## Note on deduplication
100+
101+
Each `QueryObserver` (each component using `useQuery` with `refetchInterval`) runs its own timer. Two components subscribed to the same key with `refetchInterval: 5000` each fire their timer every 5 seconds. What gets deduplicated is concurrent in-flight fetches: if two timers fire at the same time, only one network request goes out. The timers are observer-level; the deduplication is query-level.
102+
103+
[//]: # 'ReactNative'
104+
105+
## Non-browser environments
106+
107+
For non-browser runtimes like React Native, the standard `online`/`offline` and focus events aren't available. The [React Native guide](../react-native.md) covers how to connect `focusManager` and `onlineManager` to native app state APIs.
108+
109+
[//]: # 'ReactNative'

0 commit comments

Comments
 (0)