Skip to content

Commit 497804e

Browse files
committed
Merge branch 'main' into rc
# Conflicts: # docs/config.json # docs/react/eslint/eslint-plugin-query.md # docs/react/guides/dependent-queries.md # packages/eslint-plugin-query/package.json # packages/eslint-plugin-query/src/__tests__/configs.test.ts # packages/eslint-plugin-query/src/rules/index.ts # packages/eslint-plugin-query/src/utils/ast-utils.ts # packages/query-async-storage-persister/package.json # packages/query-broadcast-client-experimental/package.json # packages/query-core/package.json # packages/query-core/src/hydration.ts # packages/query-persist-client-core/package.json # packages/query-sync-storage-persister/package.json # packages/react-query-devtools/package.json # packages/react-query-persist-client/package.json # packages/react-query/package.json # packages/solid-query/package.json # packages/svelte-query/package.json # packages/vue-query/package.json # packages/vue-query/src/useBaseQuery.ts # packages/vue-query/src/useInfiniteQuery.ts # packages/vue-query/src/useIsFetching.ts # packages/vue-query/src/useIsMutating.ts # packages/vue-query/src/useMutation.ts # packages/vue-query/src/useQuery.ts
2 parents f94c28f + 85b76b8 commit 497804e

20 files changed

+586
-325
lines changed

CONTRIBUTING.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ If you have been assigned to fix an issue or develop a new feature, please follo
1818

1919
- Fork this repository.
2020
- Install dependencies by running `$ pnpm install`.
21-
- We use [pnpm](https://pnpm.io/) v7 for package management.
21+
- We use [pnpm](https://pnpm.io/) v8 for package management.
2222
- We use [nvm](https://github.com/nvm-sh/nvm) to manage node versions - please make sure to use the version mentioned in `.nvmrc`.
2323
- Run development server using `pnpm run watch`.
2424
- Implement your changes and tests to files in the `src/` directory and corresponding test files.

docs/config.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,10 @@
318318
{
319319
"label": "Exhaustive Deps",
320320
"to": "react/eslint/exhaustive-deps"
321+
},
322+
{
323+
"label": "Stable Query Client",
324+
"to": "react/eslint/stable-query-client"
321325
}
322326
]
323327
},

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ Then configure the rules you want to use under the rules section:
3333
{
3434
"rules": {
3535
"@tanstack/query/exhaustive-deps": "error",
36+
"@tanstack/query/stable-query-client": "error"
3637
}
3738
}
3839
```
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

docs/react/guides/dependent-queries.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ const { data: userIds } = useQuery({
7373

7474
// Then get the users messages
7575
const usersMessages = useQueries({
76-
queries: users
76+
queries: userIds
7777
? usersId.map((id) => {
7878
return {
7979
queryKey: ['messages', id],

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ describe('configs', () => {
1010
],
1111
"rules": {
1212
"@tanstack/query/exhaustive-deps": "error",
13+
"@tanstack/query/stable-query-client": "error",
1314
},
1415
},
1516
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import * as exhaustiveDeps from './rules/exhaustive-deps.rule'
2+
import * as stableQueryClient from './rules/stable-query-client/stable-query-client.rule'
23

34
export const rules = {
45
[exhaustiveDeps.name]: exhaustiveDeps.rule,
6+
[stableQueryClient.name]: stableQueryClient.rule,
57
}
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)