Skip to content

Commit 3443736

Browse files
Newbie012autofix-ci[bot]TkDodo
authored
feat(eslint-plugin): add allowlist option to exhaustive-deps rule (#10295)
* feat(eslint-plugin): add allowlist option to exhaustive-deps rule Introduce an `allowlist` option with `variables` and `types` arrays so stable variables and types can be excluded from dependency enforcement. Also report member expression dependencies more granularly for call expressions (e.g. `a.b.foo()` suggests `a.b` instead of only `a`). BREAKING CHANGE: exhaustive-deps now reports member expression deps more granularly, so some previously passing code may now report missing deps. Use the allowlist option to exclude stable variables/types as needed. * fix * ci: apply automated fixes * increase coverage * address false positives --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Dominik Dorfmeister 🔮 <[email protected]>
1 parent 8fe71e4 commit 3443736

File tree

11 files changed

+1538
-258
lines changed

11 files changed

+1538
-258
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@tanstack/eslint-plugin-query': minor
3+
---
4+
5+
BREAKING (eslint-plugin): The `exhaustive-deps` rule now reports member expression dependencies more granularly for call expressions (e.g. `a.b.foo()` suggests `a.b`), which may cause existing code that previously passed the rule to now report missing dependencies. To accommodate stable variables and types, the rule now accepts an `allowlist` option with `variables` and `types` arrays to exclude specific dependencies from enforcement.

docs/eslint/exhaustive-deps.md

Lines changed: 56 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,64 @@ const todoQueries = {
2626
Examples of **correct** code for this rule:
2727

2828
```tsx
29-
useQuery({
30-
queryKey: ['todo', todoId],
31-
queryFn: () => api.getTodo(todoId),
32-
})
29+
const Component = ({ todoId }) => {
30+
const todos = useTodos()
31+
useQuery({
32+
queryKey: ['todo', todos, todoId],
33+
queryFn: () => todos.getTodo(todoId),
34+
})
35+
}
36+
```
3337

38+
```tsx
39+
const todos = createTodos()
3440
const todoQueries = {
35-
detail: (id) => ({ queryKey: ['todo', id], queryFn: () => api.getTodo(id) }),
41+
detail: (id) => ({
42+
queryKey: ['todo', id],
43+
queryFn: () => todos.getTodo(id),
44+
}),
45+
}
46+
```
47+
48+
```tsx
49+
// with { allowlist: { variables: ["todos"] }}
50+
const Component = ({ todoId }) => {
51+
const todos = useTodos()
52+
useQuery({
53+
queryKey: ['todo', todoId],
54+
queryFn: () => todos.getTodo(todoId),
55+
})
56+
}
57+
```
58+
59+
```tsx
60+
// with { allowlist: { types: ["TodosClient"] }}
61+
class TodosClient { ... }
62+
const Component = ({ todoId }) => {
63+
const todos: TodosClient = new TodosClient()
64+
useQuery({
65+
queryKey: ['todo', todoId],
66+
queryFn: () => todos.getTodo(todoId),
67+
})
68+
}
69+
```
70+
71+
### Options
72+
73+
- `allowlist.variables`: An array of variable names that should be ignored when checking dependencies
74+
- `allowlist.types`: An array of TypeScript type names that should be ignored when checking dependencies
75+
76+
```json
77+
{
78+
"@tanstack/query/exhaustive-deps": [
79+
"error",
80+
{
81+
"allowlist": {
82+
"variables": ["api", "config"],
83+
"types": ["ApiClient", "Config"]
84+
}
85+
}
86+
]
3687
}
3788
```
3889

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import pluginQuery from '@tanstack/eslint-plugin-query'
2+
import tseslint from 'typescript-eslint'
3+
4+
export default [
5+
...tseslint.configs.recommended,
6+
...pluginQuery.configs['flat/recommended'],
7+
{
8+
files: ['src/**/*.ts', 'src/**/*.tsx'],
9+
rules: {
10+
'@tanstack/query/exhaustive-deps': [
11+
'error',
12+
{
13+
allowlist: {
14+
variables: ['api'],
15+
types: ['AnalyticsClient'],
16+
},
17+
},
18+
],
19+
},
20+
},
21+
]
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
{
2+
"name": "@tanstack/query-example-eslint-plugin-demo",
3+
"private": true,
4+
"type": "module",
5+
"scripts": {
6+
"test:eslint": "eslint ./src"
7+
},
8+
"dependencies": {
9+
"@tanstack/react-query": "^5.91.0",
10+
"react": "^19.0.0"
11+
},
12+
"devDependencies": {
13+
"@tanstack/eslint-plugin-query": "^5.91.5",
14+
"eslint": "^9.39.0",
15+
"typescript": "5.8.3",
16+
"typescript-eslint": "^8.48.0"
17+
},
18+
"nx": {
19+
"targets": {
20+
"test:eslint": {
21+
"dependsOn": [
22+
"^build"
23+
]
24+
}
25+
}
26+
}
27+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { queryOptions } from '@tanstack/react-query'
2+
3+
export function todosOptions(userId: string) {
4+
const api = useApiClient()
5+
return queryOptions({
6+
// ✅ passes: 'api' is in allowlist.variables
7+
queryKey: ['todos', userId],
8+
queryFn: () => api.fetchTodos(userId),
9+
})
10+
}
11+
12+
export function todosByApiOptions(userId: string) {
13+
const todoApi = useApiClient()
14+
// ❌ fails: 'api' is in allowlist.variables, but this variable is named 'todoApi'
15+
// eslint-disable-next-line @tanstack/query/exhaustive-deps -- The following dependencies are missing in your queryKey: todoApi
16+
return queryOptions({
17+
queryKey: ['todos', userId],
18+
queryFn: () => todoApi.fetchTodos(userId),
19+
})
20+
}
21+
22+
export function todosWithTrackingOptions(
23+
tracker: AnalyticsClient,
24+
userId: string,
25+
) {
26+
return queryOptions({
27+
// ✅ passes: AnalyticsClient is in allowlist.types
28+
queryKey: ['todos', userId],
29+
queryFn: async () => {
30+
tracker.track('todos')
31+
return fetch(`/api/todos?userId=${userId}`).then((r) => r.json())
32+
},
33+
})
34+
}
35+
36+
export function todosWithClientOptions(client: ApiClient, userId: string) {
37+
// ❌ fails: AnalyticsClient is in allowlist.types, but this param is typed as ApiClient
38+
// eslint-disable-next-line @tanstack/query/exhaustive-deps -- The following dependencies are missing in your queryKey: client
39+
return queryOptions({
40+
queryKey: ['todos', userId],
41+
queryFn: () => client.fetchTodos(userId),
42+
})
43+
}
44+
45+
interface ApiClient {
46+
fetchTodos: (userId: string) => Promise<Array<{ id: string }>>
47+
}
48+
49+
interface AnalyticsClient {
50+
track: (event: string) => Promise<void>
51+
}
52+
53+
function useApiClient(): ApiClient {
54+
throw new Error('not implemented')
55+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"compilerOptions": {
3+
"target": "ES2020",
4+
"lib": ["ES2020", "DOM"],
5+
"module": "ESNext",
6+
"skipLibCheck": true,
7+
"moduleResolution": "Bundler",
8+
"isolatedModules": true,
9+
"noEmit": true,
10+
"jsx": "react-jsx",
11+
"strict": true
12+
},
13+
"include": ["src", "eslint.config.js"]
14+
}

0 commit comments

Comments
 (0)