Skip to content

Commit 7ac45ed

Browse files
Newbie012TkDodo
andauthored
fix(eslint-plugin-query): ignore call targets in exhaustive-deps (#11067)
* fix(query-eslint): ignore call targets in exhaustive deps * fix(query-eslint): guard AST parent access * fix(query-eslint): accept JSX scope references --------- Co-authored-by: Dominik Dorfmeister 🔮 <office@dorfmeister.cc>
1 parent 181ea82 commit 7ac45ed

6 files changed

Lines changed: 245 additions & 217 deletions

File tree

.changeset/soft-pianos-sip.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@tanstack/eslint-plugin-query': patch
3+
---
4+
5+
Relax `exhaustive-deps` so function call targets are not required in query keys while values referenced in nested callbacks are still checked.

docs/eslint/exhaustive-deps.md

Lines changed: 9 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@ id: exhaustive-deps
33
title: Exhaustive dependencies for query keys
44
---
55

6-
Query keys should be seen like a dependency array to your query function: Every variable that is used inside the queryFn should be added to the query key.
7-
This makes sure that queries are cached independently and that queries are refetched automatically when the variables changes.
6+
Query keys should contain the serializable values that identify the data returned by your queryFn.
7+
This makes sure that queries are cached independently and that queries are refetched automatically when those values change.
8+
9+
Function call targets are not query key dependencies. For example, `fetchTodoById(todoId)` needs `todoId` in the query key, but not `fetchTodoById`. Values referenced inside nested callbacks are still dependencies, so `promise.then(() => todoId)` also needs `todoId` in the query key.
810

911
## Rule Details
1012

@@ -29,7 +31,7 @@ Examples of **correct** code for this rule:
2931
const Component = ({ todoId }) => {
3032
const todos = useTodos()
3133
useQuery({
32-
queryKey: ['todo', todos, todoId],
34+
queryKey: ['todo', todoId],
3335
queryFn: () => todos.getTodo(todoId),
3436
})
3537
}
@@ -46,24 +48,12 @@ const todoQueries = {
4648
```
4749

4850
```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()
51+
// with { allowlist: { types: ["Config"] }}
52+
class Config { ... }
53+
const Component = ({ todoId, config }: { todoId: string, config: Config }) => {
6454
useQuery({
6555
queryKey: ['todo', todoId],
66-
queryFn: () => todos.getTodo(todoId),
56+
queryFn: () => fetchTodo(todoId, config.baseUrl),
6757
})
6858
}
6959
```
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { AST_NODE_TYPES } from '@typescript-eslint/utils'
2+
import { describe, expect, it } from 'vitest'
3+
import { ExhaustiveDepsUtils } from '../rules/exhaustive-deps/exhaustive-deps.utils'
4+
import { ASTUtils } from '../utils/ast-utils'
5+
import type { TSESLint, TSESTree } from '@typescript-eslint/utils'
6+
7+
function createIdentifier(name: string): TSESTree.Identifier {
8+
return { type: AST_NODE_TYPES.Identifier, name } as TSESTree.Identifier
9+
}
10+
11+
describe('ASTUtils', () => {
12+
it('stops member traversal when a node has no parent', () => {
13+
const identifier = createIdentifier('value')
14+
15+
expect(ASTUtils.traverseUpMemberExpression(identifier)).toBe(identifier)
16+
})
17+
18+
it('handles an external reference without a parent', () => {
19+
const operation = createIdentifier('operation')
20+
const reference = {
21+
identifier: operation,
22+
isRead: () => true,
23+
resolved: null,
24+
} as TSESLint.Scope.Reference
25+
const scope = {
26+
childScopes: [],
27+
references: [reference],
28+
set: new Map(),
29+
} as unknown as TSESLint.Scope.Scope
30+
const scopeManager = {
31+
acquire: () => scope,
32+
} as unknown as TSESLint.Scope.ScopeManager
33+
const sourceCode = {
34+
getText: () => 'operation',
35+
} as unknown as Readonly<TSESLint.SourceCode>
36+
37+
expect(
38+
ASTUtils.getExternalRefs({
39+
scopeManager,
40+
sourceCode,
41+
node: operation,
42+
}),
43+
).toEqual([reference])
44+
})
45+
})
46+
47+
describe('ExhaustiveDepsUtils', () => {
48+
it('does not treat a detached identifier as a function call target', () => {
49+
expect(
50+
ExhaustiveDepsUtils.isFunctionCallTarget(createIdentifier('fetchTodos')),
51+
).toBe(false)
52+
})
53+
})

0 commit comments

Comments
 (0)