Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/exhaustive-deps-allowlist.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tanstack/eslint-plugin-query': minor
---

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.
61 changes: 56 additions & 5 deletions docs/eslint/exhaustive-deps.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,64 @@ const todoQueries = {
Examples of **correct** code for this rule:

```tsx
useQuery({
queryKey: ['todo', todoId],
queryFn: () => api.getTodo(todoId),
})
const Component = ({ todoId }) => {
const todos = useTodos()
useQuery({
queryKey: ['todo', todos, todoId],
queryFn: () => todos.getTodo(todoId),
})
}
```

```tsx
const todos = createTodos()
const todoQueries = {
detail: (id) => ({ queryKey: ['todo', id], queryFn: () => api.getTodo(id) }),
detail: (id) => ({
queryKey: ['todo', id],
queryFn: () => todos.getTodo(id),
}),
}
```

```tsx
// with { allowlist: { variables: ["todos"] }}
const Component = ({ todoId }) => {
const todos = useTodos()
useQuery({
queryKey: ['todo', todoId],
queryFn: () => todos.getTodo(todoId),
})
}
```

```tsx
// with { allowlist: { types: ["TodosClient"] }}
class TodosClient { ... }
const Component = ({ todoId }) => {
const todos: TodosClient = new TodosClient()
useQuery({
queryKey: ['todo', todoId],
queryFn: () => todos.getTodo(todoId),
})
}
```

### Options

- `allowlist.variables`: An array of variable names that should be ignored when checking dependencies
- `allowlist.types`: An array of TypeScript type names that should be ignored when checking dependencies

```json
{
"@tanstack/query/exhaustive-deps": [
"error",
{
"allowlist": {
"variables": ["api", "config"],
"types": ["ApiClient", "Config"]
}
}
]
}
```

Expand Down
21 changes: 21 additions & 0 deletions examples/react/eslint-plugin-demo/eslint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import pluginQuery from '@tanstack/eslint-plugin-query'
import tseslint from 'typescript-eslint'

export default [
...tseslint.configs.recommended,
...pluginQuery.configs['flat/recommended'],
{
files: ['src/**/*.ts', 'src/**/*.tsx'],
rules: {
'@tanstack/query/exhaustive-deps': [
'error',
{
allowlist: {
variables: ['api'],
types: ['AnalyticsClient'],
},
},
],
},
},
]
27 changes: 27 additions & 0 deletions examples/react/eslint-plugin-demo/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"name": "@tanstack/query-example-eslint-plugin-demo",
"private": true,
"type": "module",
"scripts": {
"test:eslint": "eslint ./src"
},
"dependencies": {
"@tanstack/react-query": "^5.91.0",
"react": "^19.0.0"
},
"devDependencies": {
"@tanstack/eslint-plugin-query": "^5.91.5",
"eslint": "^9.39.0",
"typescript": "5.8.3",
"typescript-eslint": "^8.48.0"
},
"nx": {
"targets": {
"test:eslint": {
"dependsOn": [
"^build"
]
}
}
}
}
55 changes: 55 additions & 0 deletions examples/react/eslint-plugin-demo/src/allowlist-demo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { queryOptions } from '@tanstack/react-query'

export function todosOptions(userId: string) {
const api = useApiClient()
return queryOptions({
// βœ… passes: 'api' is in allowlist.variables
queryKey: ['todos', userId],
queryFn: () => api.fetchTodos(userId),
})
}

export function todosByApiOptions(userId: string) {
const todoApi = useApiClient()
// ❌ fails: 'api' is in allowlist.variables, but this variable is named 'todoApi'
// eslint-disable-next-line @tanstack/query/exhaustive-deps -- The following dependencies are missing in your queryKey: todoApi
return queryOptions({
queryKey: ['todos', userId],
queryFn: () => todoApi.fetchTodos(userId),
})
}

export function todosWithTrackingOptions(
tracker: AnalyticsClient,
userId: string,
) {
return queryOptions({
// βœ… passes: AnalyticsClient is in allowlist.types
queryKey: ['todos', userId],
queryFn: async () => {
tracker.track('todos')
return fetch(`/api/todos?userId=${userId}`).then((r) => r.json())
},
})
}

export function todosWithClientOptions(client: ApiClient, userId: string) {
// ❌ fails: AnalyticsClient is in allowlist.types, but this param is typed as ApiClient
// eslint-disable-next-line @tanstack/query/exhaustive-deps -- The following dependencies are missing in your queryKey: client
return queryOptions({
queryKey: ['todos', userId],
queryFn: () => client.fetchTodos(userId),
})
}

interface ApiClient {
fetchTodos: (userId: string) => Promise<Array<{ id: string }>>
}

interface AnalyticsClient {
track: (event: string) => Promise<void>
}

function useApiClient(): ApiClient {
throw new Error('not implemented')
}
14 changes: 14 additions & 0 deletions examples/react/eslint-plugin-demo/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"compilerOptions": {
"target": "ES2020",
"lib": ["ES2020", "DOM"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "Bundler",
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"strict": true
},
"include": ["src", "eslint.config.js"]
}
Loading
Loading