Skip to content

Commit d77d9d8

Browse files
alan910127TkDodo
andauthored
feat(eslint-plugin): add no-rest-destructuring rule (#6265) (#6302)
* feat(eslint-plugin): add no-rest-destructuring rule (#6265) * chore: format test case code * test: add null array element testcase * test: add non-tanstack-query hooks testcase --------- Co-authored-by: Dominik Dorfmeister <[email protected]>
1 parent 47449b7 commit d77d9d8

File tree

8 files changed

+323
-0
lines changed

8 files changed

+323
-0
lines changed

docs/config.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,10 @@
310310
{
311311
"label": "Stable Query Client",
312312
"to": "react/eslint/stable-query-client"
313+
},
314+
{
315+
"label": "No Rest Destructuring",
316+
"to": "react/eslint/no-rest-destructuring"
313317
}
314318
]
315319
},

docs/react/eslint/eslint-plugin-query.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ Then configure the rules you want to use under the rules section:
4444
{
4545
"rules": {
4646
"@tanstack/query/exhaustive-deps": "error",
47+
"@tanstack/query/no-rest-destructuring": "warn",
4748
"@tanstack/query/stable-query-client": "error"
4849
}
4950
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
---
2+
id: no-rest-destructuring
3+
title: Disallow object rest destructuring on query results
4+
---
5+
6+
Use object rest destructuring on query results automatically subscribes to every field of the query result, which may cause unnecessary re-renders.
7+
This makes sure that you only subscribe to the fields that you actually need.
8+
9+
## Rule Details
10+
11+
Examples of **incorrect** code for this rule:
12+
13+
```tsx
14+
/* eslint "@tanstack/query/no-rest-destructuring": "warn" */
15+
16+
const useTodos = () => {
17+
const { data: todos, ...rest } = useQuery({
18+
queryKey: ['todos'],
19+
queryFn: () => api.getTodos(),
20+
})
21+
return { todos, ...rest }
22+
}
23+
```
24+
25+
Examples of **correct** code for this rule:
26+
27+
```tsx
28+
const todosQuery = useQuery({
29+
queryKey: ['todos'],
30+
queryFn: () => api.getTodos(),
31+
})
32+
33+
// normal object destructuring is fine
34+
const { data: todos } = todosQuery
35+
```
36+
37+
## When Not To Use It
38+
39+
If you set the `notifyOnChangeProps` options manually, you can disable this rule.
40+
Since you are not using tracked queries, you are responsible for specifying which props should trigger a re-render.
41+
42+
## Attributes
43+
44+
- [x] ✅ Recommended
45+
- [ ] 🔧 Fixable

packages/eslint-plugin-query/src/__tests__/configs.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ describe('configs', () => {
1111
],
1212
"rules": {
1313
"@tanstack/query/exhaustive-deps": "error",
14+
"@tanstack/query/no-rest-destructuring": "warn",
1415
"@tanstack/query/stable-query-client": "error",
1516
},
1617
},
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import * as exhaustiveDeps from './rules/exhaustive-deps.rule'
22
import * as stableQueryClient from './rules/stable-query-client/stable-query-client.rule'
3+
import * as noRestDestructuring from './rules/no-rest-desctructuring/no-rest-destructuring.rule'
34

45
export const rules = {
56
[exhaustiveDeps.name]: exhaustiveDeps.rule,
67
[stableQueryClient.name]: stableQueryClient.rule,
8+
[noRestDestructuring.name]: noRestDestructuring.rule,
79
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { AST_NODE_TYPES } from '@typescript-eslint/utils'
2+
import { createRule } from '../../utils/create-rule'
3+
import { ASTUtils } from '../../utils/ast-utils'
4+
import { NoRestDestructuringUtils } from './no-rest-destructuring.utils'
5+
6+
export const name = 'no-rest-destructuring'
7+
8+
const queryHooks = ['useQuery', 'useQueries', 'useInfiniteQuery']
9+
10+
export const rule = createRule({
11+
name,
12+
meta: {
13+
type: 'problem',
14+
docs: {
15+
description: 'Disallows rest destructuring in queries',
16+
recommended: 'warn',
17+
},
18+
messages: {
19+
objectRestDestructure: `Object rest destructuring on a query will observe all changes to the query, leading to excessive re-renders.`,
20+
},
21+
schema: [],
22+
},
23+
defaultOptions: [],
24+
25+
create(context, _, helpers) {
26+
return {
27+
CallExpression(node) {
28+
if (
29+
!ASTUtils.isIdentifierWithOneOfNames(node.callee, queryHooks) ||
30+
!helpers.isTanstackQueryImport(node.callee) ||
31+
node.parent?.type !== AST_NODE_TYPES.VariableDeclarator
32+
) {
33+
return
34+
}
35+
36+
const returnValue = node.parent.id
37+
if (node.callee.name !== 'useQueries') {
38+
if (NoRestDestructuringUtils.isObjectRestDestructuring(returnValue)) {
39+
context.report({
40+
node: node.parent,
41+
messageId: 'objectRestDestructure',
42+
})
43+
}
44+
return
45+
}
46+
47+
if (returnValue.type !== AST_NODE_TYPES.ArrayPattern) {
48+
return
49+
}
50+
returnValue.elements.forEach((queryResult) => {
51+
if (queryResult === null) {
52+
return
53+
}
54+
if (NoRestDestructuringUtils.isObjectRestDestructuring(queryResult)) {
55+
context.report({
56+
node: queryResult,
57+
messageId: 'objectRestDestructure',
58+
})
59+
}
60+
})
61+
},
62+
}
63+
},
64+
})
Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
import { ESLintUtils } from '@typescript-eslint/utils'
2+
import { normalizeIndent } from '../../utils/test-utils'
3+
import { rule } from './no-rest-destructuring.rule'
4+
5+
const ruleTester = new ESLintUtils.RuleTester({
6+
parser: '@typescript-eslint/parser',
7+
settings: {},
8+
})
9+
10+
ruleTester.run('no-rest-desctructuring', rule, {
11+
valid: [
12+
{
13+
name: 'useQuery is not captured',
14+
code: normalizeIndent`
15+
import { useQuery } from '@tanstack/react-query'
16+
17+
function Component() {
18+
useQuery()
19+
return
20+
}
21+
`,
22+
},
23+
{
24+
name: 'useQuery is not destructured',
25+
code: normalizeIndent`
26+
import { useQuery } from '@tanstack/react-query'
27+
28+
function Component() {
29+
const query = useQuery()
30+
return
31+
}
32+
`,
33+
},
34+
{
35+
name: 'useQuery is destructured without rest',
36+
code: normalizeIndent`
37+
import { useQuery } from '@tanstack/react-query'
38+
39+
function Component() {
40+
const { data, isLoading, isError } = useQuery()
41+
return
42+
}
43+
`,
44+
},
45+
{
46+
name: 'useInfiniteQuery is not captured',
47+
code: normalizeIndent`
48+
import { useInfiniteQuery } from '@tanstack/react-query'
49+
50+
function Component() {
51+
useInfiniteQuery()
52+
return
53+
}
54+
`,
55+
},
56+
{
57+
name: 'useInfiniteQuery is not destructured',
58+
code: normalizeIndent`
59+
import { useInfiniteQuery } from '@tanstack/react-query'
60+
61+
function Component() {
62+
const query = useInfiniteQuery()
63+
return
64+
}
65+
`,
66+
},
67+
{
68+
name: 'useInfiniteQuery is destructured without rest',
69+
code: normalizeIndent`
70+
import { useInfiniteQuery } from '@tanstack/react-query'
71+
72+
function Component() {
73+
const { data, isLoading, isError } = useInfiniteQuery()
74+
return
75+
}
76+
`,
77+
},
78+
{
79+
name: 'useQueries is not captured',
80+
code: normalizeIndent`
81+
import { useQueries } from '@tanstack/react-query'
82+
83+
function Component() {
84+
useQueries([])
85+
return
86+
}
87+
`,
88+
},
89+
{
90+
name: 'useQueries is not destructured',
91+
code: normalizeIndent`
92+
import { useQueries } from '@tanstack/react-query'
93+
94+
function Component() {
95+
const queries = useQueries([])
96+
return
97+
}
98+
`,
99+
},
100+
{
101+
name: 'useQueries array has no rest destructured element',
102+
code: normalizeIndent`
103+
import { useQueries } from '@tanstack/react-query'
104+
105+
function Component() {
106+
const [query1, { data, isLoading },, ...others] = useQueries([
107+
{ queryKey: ['key1'], queryFn: () => {} },
108+
{ queryKey: ['key2'], queryFn: () => {} },
109+
{ queryKey: ['key3'], queryFn: () => {} },
110+
{ queryKey: ['key4'], queryFn: () => {} },
111+
{ queryKey: ['key5'], queryFn: () => {} },
112+
])
113+
return
114+
}
115+
`,
116+
},
117+
{
118+
name: 'useQuery is destructured with rest but not from tanstack query',
119+
code: normalizeIndent`
120+
import { useQuery } from 'other-package'
121+
122+
function Component() {
123+
const { data, ...rest } = useQuery()
124+
return
125+
}
126+
`,
127+
},
128+
{
129+
name: 'useInfiniteQuery is destructured with rest but not from tanstack query',
130+
code: normalizeIndent`
131+
import { useInfiniteQuery } from 'other-package'
132+
133+
function Component() {
134+
const { data, ...rest } = useInfiniteQuery()
135+
return
136+
}
137+
`,
138+
},
139+
{
140+
name: 'useQueries array has rest destructured element but not from tanstack query',
141+
code: normalizeIndent`
142+
import { useQueries } from 'other-package'
143+
144+
function Component() {
145+
const [query1, { data, ...rest }] = useQueries([
146+
{ queryKey: ['key1'], queryFn: () => {} },
147+
{ queryKey: ['key2'], queryFn: () => {} },
148+
])
149+
return
150+
}
151+
`,
152+
},
153+
],
154+
invalid: [
155+
{
156+
name: 'useQuery is destructured with rest',
157+
code: normalizeIndent`
158+
import { useQuery } from '@tanstack/react-query'
159+
160+
function Component() {
161+
const { data, ...rest } = useQuery()
162+
return
163+
}
164+
`,
165+
errors: [{ messageId: 'objectRestDestructure' }],
166+
},
167+
{
168+
name: 'useInfiniteQuery is destructured with rest',
169+
code: normalizeIndent`
170+
import { useInfiniteQuery } from '@tanstack/react-query'
171+
172+
function Component() {
173+
const { data, ...rest } = useInfiniteQuery()
174+
return
175+
}
176+
`,
177+
errors: [{ messageId: 'objectRestDestructure' }],
178+
},
179+
{
180+
name: 'useQueries array has rest destructured element',
181+
code: normalizeIndent`
182+
import { useQueries } from '@tanstack/react-query'
183+
184+
function Component() {
185+
const [query1, { data, ...rest }] = useQueries([
186+
{ queryKey: ['key1'], queryFn: () => {} },
187+
{ queryKey: ['key2'], queryFn: () => {} },
188+
])
189+
return
190+
}
191+
`,
192+
errors: [{ messageId: 'objectRestDestructure' }],
193+
},
194+
],
195+
})
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { AST_NODE_TYPES } from '@typescript-eslint/utils'
2+
import type { TSESTree } from '@typescript-eslint/utils'
3+
4+
export const NoRestDestructuringUtils = {
5+
isObjectRestDestructuring(node: TSESTree.Node): boolean {
6+
if (node.type !== AST_NODE_TYPES.ObjectPattern) {
7+
return false
8+
}
9+
return node.properties.some((p) => p.type === AST_NODE_TYPES.RestElement)
10+
},
11+
}

0 commit comments

Comments
 (0)