Skip to content

Commit

Permalink
feat(eslint-plugin): add no-rest-destructuring rule (#6265) (#6302)
Browse files Browse the repository at this point in the history
* 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 <office@dorfmeister.cc>
  • Loading branch information
alan910127 and TkDodo committed Nov 4, 2023
1 parent 47449b7 commit d77d9d8
Show file tree
Hide file tree
Showing 8 changed files with 323 additions and 0 deletions.
4 changes: 4 additions & 0 deletions docs/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,10 @@
{
"label": "Stable Query Client",
"to": "react/eslint/stable-query-client"
},
{
"label": "No Rest Destructuring",
"to": "react/eslint/no-rest-destructuring"
}
]
},
Expand Down
1 change: 1 addition & 0 deletions docs/react/eslint/eslint-plugin-query.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ Then configure the rules you want to use under the rules section:
{
"rules": {
"@tanstack/query/exhaustive-deps": "error",
"@tanstack/query/no-rest-destructuring": "warn",
"@tanstack/query/stable-query-client": "error"
}
}
Expand Down
45 changes: 45 additions & 0 deletions docs/react/eslint/no-rest-destructuring.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
---
id: no-rest-destructuring
title: Disallow object rest destructuring on query results
---

Use object rest destructuring on query results automatically subscribes to every field of the query result, which may cause unnecessary re-renders.
This makes sure that you only subscribe to the fields that you actually need.

## Rule Details

Examples of **incorrect** code for this rule:

```tsx
/* eslint "@tanstack/query/no-rest-destructuring": "warn" */

const useTodos = () => {
const { data: todos, ...rest } = useQuery({
queryKey: ['todos'],
queryFn: () => api.getTodos(),
})
return { todos, ...rest }
}
```

Examples of **correct** code for this rule:

```tsx
const todosQuery = useQuery({
queryKey: ['todos'],
queryFn: () => api.getTodos(),
})

// normal object destructuring is fine
const { data: todos } = todosQuery
```

## When Not To Use It

If you set the `notifyOnChangeProps` options manually, you can disable this rule.
Since you are not using tracked queries, you are responsible for specifying which props should trigger a re-render.

## Attributes

- [x] ✅ Recommended
- [ ] 🔧 Fixable
1 change: 1 addition & 0 deletions packages/eslint-plugin-query/src/__tests__/configs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ describe('configs', () => {
],
"rules": {
"@tanstack/query/exhaustive-deps": "error",
"@tanstack/query/no-rest-destructuring": "warn",
"@tanstack/query/stable-query-client": "error",
},
},
Expand Down
2 changes: 2 additions & 0 deletions packages/eslint-plugin-query/src/rules.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import * as exhaustiveDeps from './rules/exhaustive-deps.rule'
import * as stableQueryClient from './rules/stable-query-client/stable-query-client.rule'
import * as noRestDestructuring from './rules/no-rest-desctructuring/no-rest-destructuring.rule'

export const rules = {
[exhaustiveDeps.name]: exhaustiveDeps.rule,
[stableQueryClient.name]: stableQueryClient.rule,
[noRestDestructuring.name]: noRestDestructuring.rule,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { AST_NODE_TYPES } from '@typescript-eslint/utils'
import { createRule } from '../../utils/create-rule'
import { ASTUtils } from '../../utils/ast-utils'
import { NoRestDestructuringUtils } from './no-rest-destructuring.utils'

export const name = 'no-rest-destructuring'

const queryHooks = ['useQuery', 'useQueries', 'useInfiniteQuery']

export const rule = createRule({
name,
meta: {
type: 'problem',
docs: {
description: 'Disallows rest destructuring in queries',
recommended: 'warn',
},
messages: {
objectRestDestructure: `Object rest destructuring on a query will observe all changes to the query, leading to excessive re-renders.`,
},
schema: [],
},
defaultOptions: [],

create(context, _, helpers) {
return {
CallExpression(node) {
if (
!ASTUtils.isIdentifierWithOneOfNames(node.callee, queryHooks) ||
!helpers.isTanstackQueryImport(node.callee) ||
node.parent?.type !== AST_NODE_TYPES.VariableDeclarator
) {
return
}

const returnValue = node.parent.id
if (node.callee.name !== 'useQueries') {
if (NoRestDestructuringUtils.isObjectRestDestructuring(returnValue)) {
context.report({
node: node.parent,
messageId: 'objectRestDestructure',
})
}
return
}

if (returnValue.type !== AST_NODE_TYPES.ArrayPattern) {
return
}
returnValue.elements.forEach((queryResult) => {
if (queryResult === null) {
return
}
if (NoRestDestructuringUtils.isObjectRestDestructuring(queryResult)) {
context.report({
node: queryResult,
messageId: 'objectRestDestructure',
})
}
})
},
}
},
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
import { ESLintUtils } from '@typescript-eslint/utils'
import { normalizeIndent } from '../../utils/test-utils'
import { rule } from './no-rest-destructuring.rule'

const ruleTester = new ESLintUtils.RuleTester({
parser: '@typescript-eslint/parser',
settings: {},
})

ruleTester.run('no-rest-desctructuring', rule, {
valid: [
{
name: 'useQuery is not captured',
code: normalizeIndent`
import { useQuery } from '@tanstack/react-query'
function Component() {
useQuery()
return
}
`,
},
{
name: 'useQuery is not destructured',
code: normalizeIndent`
import { useQuery } from '@tanstack/react-query'
function Component() {
const query = useQuery()
return
}
`,
},
{
name: 'useQuery is destructured without rest',
code: normalizeIndent`
import { useQuery } from '@tanstack/react-query'
function Component() {
const { data, isLoading, isError } = useQuery()
return
}
`,
},
{
name: 'useInfiniteQuery is not captured',
code: normalizeIndent`
import { useInfiniteQuery } from '@tanstack/react-query'
function Component() {
useInfiniteQuery()
return
}
`,
},
{
name: 'useInfiniteQuery is not destructured',
code: normalizeIndent`
import { useInfiniteQuery } from '@tanstack/react-query'
function Component() {
const query = useInfiniteQuery()
return
}
`,
},
{
name: 'useInfiniteQuery is destructured without rest',
code: normalizeIndent`
import { useInfiniteQuery } from '@tanstack/react-query'
function Component() {
const { data, isLoading, isError } = useInfiniteQuery()
return
}
`,
},
{
name: 'useQueries is not captured',
code: normalizeIndent`
import { useQueries } from '@tanstack/react-query'
function Component() {
useQueries([])
return
}
`,
},
{
name: 'useQueries is not destructured',
code: normalizeIndent`
import { useQueries } from '@tanstack/react-query'
function Component() {
const queries = useQueries([])
return
}
`,
},
{
name: 'useQueries array has no rest destructured element',
code: normalizeIndent`
import { useQueries } from '@tanstack/react-query'
function Component() {
const [query1, { data, isLoading },, ...others] = useQueries([
{ queryKey: ['key1'], queryFn: () => {} },
{ queryKey: ['key2'], queryFn: () => {} },
{ queryKey: ['key3'], queryFn: () => {} },
{ queryKey: ['key4'], queryFn: () => {} },
{ queryKey: ['key5'], queryFn: () => {} },
])
return
}
`,
},
{
name: 'useQuery is destructured with rest but not from tanstack query',
code: normalizeIndent`
import { useQuery } from 'other-package'
function Component() {
const { data, ...rest } = useQuery()
return
}
`,
},
{
name: 'useInfiniteQuery is destructured with rest but not from tanstack query',
code: normalizeIndent`
import { useInfiniteQuery } from 'other-package'
function Component() {
const { data, ...rest } = useInfiniteQuery()
return
}
`,
},
{
name: 'useQueries array has rest destructured element but not from tanstack query',
code: normalizeIndent`
import { useQueries } from 'other-package'
function Component() {
const [query1, { data, ...rest }] = useQueries([
{ queryKey: ['key1'], queryFn: () => {} },
{ queryKey: ['key2'], queryFn: () => {} },
])
return
}
`,
},
],
invalid: [
{
name: 'useQuery is destructured with rest',
code: normalizeIndent`
import { useQuery } from '@tanstack/react-query'
function Component() {
const { data, ...rest } = useQuery()
return
}
`,
errors: [{ messageId: 'objectRestDestructure' }],
},
{
name: 'useInfiniteQuery is destructured with rest',
code: normalizeIndent`
import { useInfiniteQuery } from '@tanstack/react-query'
function Component() {
const { data, ...rest } = useInfiniteQuery()
return
}
`,
errors: [{ messageId: 'objectRestDestructure' }],
},
{
name: 'useQueries array has rest destructured element',
code: normalizeIndent`
import { useQueries } from '@tanstack/react-query'
function Component() {
const [query1, { data, ...rest }] = useQueries([
{ queryKey: ['key1'], queryFn: () => {} },
{ queryKey: ['key2'], queryFn: () => {} },
])
return
}
`,
errors: [{ messageId: 'objectRestDestructure' }],
},
],
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { AST_NODE_TYPES } from '@typescript-eslint/utils'
import type { TSESTree } from '@typescript-eslint/utils'

export const NoRestDestructuringUtils = {
isObjectRestDestructuring(node: TSESTree.Node): boolean {
if (node.type !== AST_NODE_TYPES.ObjectPattern) {
return false
}
return node.properties.some((p) => p.type === AST_NODE_TYPES.RestElement)
},
}

0 comments on commit d77d9d8

Please sign in to comment.