Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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.
54 changes: 49 additions & 5 deletions docs/eslint/exhaustive-deps.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,57 @@ 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) }),
}

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

// 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