Skip to content

Commit dc9142c

Browse files
authored
Merge branch 'main' into docs/contributing
2 parents 7991d47 + 85b76b8 commit dc9142c

File tree

10 files changed

+403
-15
lines changed

10 files changed

+403
-15
lines changed

docs/config.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,10 @@
306306
{
307307
"label": "Prefer object syntax",
308308
"to": "react/eslint/prefer-query-object-syntax"
309+
},
310+
{
311+
"label": "Stable Query Client",
312+
"to": "react/eslint/stable-query-client"
309313
}
310314
]
311315
},

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@ Then configure the rules you want to use under the rules section:
3333
{
3434
"rules": {
3535
"@tanstack/query/exhaustive-deps": "error",
36-
"@tanstack/query/prefer-query-object-syntax": "error"
36+
"@tanstack/query/prefer-query-object-syntax": "error",
37+
"@tanstack/query/stable-query-client": "error"
3738
}
3839
}
3940
```
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
---
2+
id: stable-query-client
3+
title: Stable Query Client
4+
---
5+
6+
The QueryClient contains the QueryCache, so you'd only want to create one instance of the QueryClient for the lifecycle of your application - _not_ a new instance on every render.
7+
8+
> Exception: It's allowed to create a new QueryClient inside an async Server Component, because the async function is only called once on the server.
9+
10+
## Rule Details
11+
12+
Examples of **incorrect** code for this rule:
13+
14+
```tsx
15+
/* eslint "@tanstack/query/stable-query-client": "error" */
16+
17+
function App() {
18+
const queryClient = new QueryClient()
19+
return (
20+
<QueryClientProvider client={queryClient}>
21+
<Home />
22+
</QueryClientProvider>
23+
)
24+
}
25+
```
26+
27+
28+
Examples of **correct** code for this rule:
29+
30+
```tsx
31+
function App() {
32+
const [queryClient] = useState(() => new QueryClient())
33+
return (
34+
<QueryClientProvider client={queryClient}>
35+
<Home />
36+
</QueryClientProvider>
37+
)
38+
}
39+
```
40+
41+
```tsx
42+
const queryClient = new QueryClient()
43+
function App() {
44+
return (
45+
<QueryClientProvider client={queryClient}>
46+
<Home />
47+
</QueryClientProvider>
48+
)
49+
}
50+
```
51+
52+
```
53+
async function App() {
54+
const queryClient = new QueryClient()
55+
await queryClient.prefetchQuery(options)
56+
}
57+
```
58+
59+
## Attributes
60+
61+
- [x] ✅ Recommended
62+
- [x] 🔧 Fixable

packages/eslint-plugin-query/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@tanstack/eslint-plugin-query",
3-
"version": "4.34.1",
3+
"version": "4.36.0",
44
"description": "ESLint plugin for TanStack Query",
55
"author": "Eliya Cohen",
66
"license": "MIT",

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ describe('configs', () => {
1111
"rules": {
1212
"@tanstack/query/exhaustive-deps": "error",
1313
"@tanstack/query/prefer-query-object-syntax": "error",
14+
"@tanstack/query/stable-query-client": "error",
1415
},
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 './exhaustive-deps/exhaustive-deps.rule'
22
import * as preferObjectSyntax from './prefer-query-object-syntax/prefer-query-object-syntax'
3+
import * as stableQueryClient from './stable-query-client/stable-query-client.rule'
34

45
export const rules = {
56
[exhaustiveDeps.name]: exhaustiveDeps.rule,
67
[preferObjectSyntax.name]: preferObjectSyntax.rule,
8+
[stableQueryClient.name]: stableQueryClient.rule,
79
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { AST_NODE_TYPES } from '@typescript-eslint/utils'
2+
import { ASTUtils } from '../../utils/ast-utils'
3+
import { createRule } from '../../utils/create-rule'
4+
import type { TSESLint } from '@typescript-eslint/utils'
5+
6+
export const name = 'stable-query-client'
7+
8+
export const rule = createRule({
9+
name,
10+
meta: {
11+
type: 'problem',
12+
docs: {
13+
description: 'Makes sure that QueryClient is stable',
14+
recommended: 'error',
15+
},
16+
messages: {
17+
unstable: [
18+
'QueryClient is not stable. It should be either extracted from the component or wrapped in React.useState.',
19+
'See https://tkdodo.eu/blog/react-query-fa-qs#2-the-queryclient-is-not-stable',
20+
].join('\n'),
21+
fixTo: 'Fix to {{result}}',
22+
},
23+
hasSuggestions: true,
24+
fixable: 'code',
25+
schema: [],
26+
},
27+
defaultOptions: [],
28+
29+
create(context, _, helpers) {
30+
return {
31+
NewExpression(node) {
32+
if (
33+
node.callee.type !== AST_NODE_TYPES.Identifier ||
34+
node.callee.name !== 'QueryClient' ||
35+
node.parent?.type !== AST_NODE_TYPES.VariableDeclarator ||
36+
!helpers.isSpecificTanstackQueryImport(
37+
node.callee,
38+
'@tanstack/react-query',
39+
)
40+
) {
41+
return
42+
}
43+
44+
const fnAncestor = ASTUtils.getFunctionAncestor(context)
45+
const isReactServerComponent = fnAncestor?.async === true
46+
47+
if (
48+
!ASTUtils.isValidReactComponentOrHookName(fnAncestor?.id) ||
49+
isReactServerComponent
50+
) {
51+
return
52+
}
53+
54+
context.report({
55+
node: node.parent,
56+
messageId: 'unstable',
57+
fix: (() => {
58+
const { parent } = node
59+
60+
if (parent.id.type !== AST_NODE_TYPES.Identifier) {
61+
return
62+
}
63+
64+
const nodeText = context.getSourceCode().getText(node)
65+
const variableName = parent.id.name
66+
67+
return (fixer: TSESLint.RuleFixer) => {
68+
return fixer.replaceTextRange(
69+
[parent.range[0], parent.range[1]],
70+
`[${variableName}] = React.useState(() => ${nodeText})`,
71+
)
72+
}
73+
})(),
74+
})
75+
},
76+
}
77+
},
78+
})
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 './stable-query-client.rule'
4+
5+
const ruleTester = new ESLintUtils.RuleTester({
6+
parser: '@typescript-eslint/parser',
7+
settings: {},
8+
})
9+
10+
ruleTester.run('stable-query-client', rule, {
11+
valid: [
12+
{
13+
name: 'QueryClient is stable when wrapped in React.useState',
14+
code: normalizeIndent`
15+
import { QueryClient } from "@tanstack/react-query";
16+
17+
function Component() {
18+
const [queryClient] = React.useState(() => new QueryClient());
19+
return;
20+
}
21+
`,
22+
},
23+
{
24+
name: 'QueryClient is stable when wrapped in useState',
25+
code: normalizeIndent`
26+
import { QueryClient } from "@tanstack/react-query";
27+
28+
function Component() {
29+
const [queryClient] = useState(() => new QueryClient());
30+
return;
31+
}
32+
`,
33+
},
34+
{
35+
name: 'QueryClient is stable when wrapped in React.useMemo',
36+
code: normalizeIndent`
37+
import { QueryClient } from "@tanstack/react-query";
38+
39+
function Component() {
40+
const [queryClient] = React.useMemo(() => new QueryClient(), []);
41+
return;
42+
}
43+
`,
44+
},
45+
{
46+
name: 'QueryClient is stable when wrapped in useAnything',
47+
code: normalizeIndent`
48+
import { QueryClient } from "@tanstack/react-query";
49+
50+
function Component() {
51+
const [queryClient] = useAnything(() => new QueryClient());
52+
return;
53+
}
54+
`,
55+
},
56+
{
57+
name: 'QueryClient is imported from a non-tanstack package',
58+
code: normalizeIndent`
59+
import { QueryClient } from "other-library";
60+
61+
function Component() {
62+
const queryClient = new QueryClient();
63+
return;
64+
}
65+
`,
66+
},
67+
{
68+
name: 'QueryClient is not imported from @tanstack/react-query',
69+
code: normalizeIndent`
70+
import { QueryClient } from "@tanstack/solid-query";
71+
72+
function Component() {
73+
const queryClient = new QueryClient();
74+
return;
75+
}
76+
`,
77+
},
78+
{
79+
name: 'QueryClient is invoked outside of a function',
80+
code: normalizeIndent`
81+
import { QueryClient } from "@tanstack/solid-query";
82+
83+
const queryClient = new QueryClient();
84+
85+
function Component() {
86+
return;
87+
}
88+
`,
89+
},
90+
{
91+
name: 'QueryClient is invoked in a non-component function',
92+
code: normalizeIndent`
93+
import { QueryClient } from "@tanstack/solid-query";
94+
95+
function someFn() {
96+
const queryClient = new QueryClient();
97+
return;
98+
}
99+
`,
100+
},
101+
{
102+
name: 'QueryClient is invoked in an async (react server) component',
103+
code: normalizeIndent`
104+
import { QueryClient } from "@tanstack/solid-query";
105+
106+
async function AsyncComponent() {
107+
const queryClient = new QueryClient();
108+
return;
109+
}
110+
`,
111+
},
112+
],
113+
invalid: [
114+
{
115+
name: 'QueryClient is not stable when it is not wrapped in React.useState in component',
116+
code: normalizeIndent`
117+
import { QueryClient } from "@tanstack/react-query";
118+
119+
function Component() {
120+
const queryClient = new QueryClient();
121+
return;
122+
}
123+
`,
124+
output: normalizeIndent`
125+
import { QueryClient } from "@tanstack/react-query";
126+
127+
function Component() {
128+
const [queryClient] = React.useState(() => new QueryClient());
129+
return;
130+
}
131+
`,
132+
errors: [{ messageId: 'unstable' }],
133+
},
134+
{
135+
name: 'QueryClient is not stable when it is not wrapped in React.useState in custom hook',
136+
code: normalizeIndent`
137+
import { QueryClient } from "@tanstack/react-query";
138+
139+
function useHook() {
140+
const queryClient = new QueryClient();
141+
return;
142+
}
143+
`,
144+
output: normalizeIndent`
145+
import { QueryClient } from "@tanstack/react-query";
146+
147+
function useHook() {
148+
const [queryClient] = React.useState(() => new QueryClient());
149+
return;
150+
}
151+
`,
152+
errors: [{ messageId: 'unstable' }],
153+
},
154+
{
155+
name: 'preserve QueryClient options',
156+
code: normalizeIndent`
157+
import { QueryClient } from "@tanstack/react-query";
158+
159+
function Component() {
160+
const queryClient = new QueryClient({ defaultOptions: { /* */ } });
161+
return;
162+
}
163+
`,
164+
output: normalizeIndent`
165+
import { QueryClient } from "@tanstack/react-query";
166+
167+
function Component() {
168+
const [queryClient] = React.useState(() => new QueryClient({ defaultOptions: { /* */ } }));
169+
return;
170+
}
171+
`,
172+
errors: [{ messageId: 'unstable' }],
173+
},
174+
{
175+
name: 'preserve QueryClient variable declarator name',
176+
code: normalizeIndent`
177+
import { QueryClient } from "@tanstack/react-query";
178+
179+
function Component() {
180+
const customName = new QueryClient();
181+
return;
182+
}
183+
`,
184+
output: normalizeIndent`
185+
import { QueryClient } from "@tanstack/react-query";
186+
187+
function Component() {
188+
const [customName] = React.useState(() => new QueryClient());
189+
return;
190+
}
191+
`,
192+
errors: [{ messageId: 'unstable' }],
193+
},
194+
],
195+
})

0 commit comments

Comments
 (0)