Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/modern-llamas-warn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@tanstack/eslint-plugin-query": patch
---

Fix stable-query-client detection for arrow functions, function expressions, and clients created directly in JSX. Keep state initializers and nested helper functions valid.
2 changes: 2 additions & 0 deletions docs/eslint/stable-query-client.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ The QueryClient contains the QueryCache, so you'd only want to create one instan

## Rule Details

This rule checks function declarations, function expressions, and arrow functions used as React components or custom hooks. It also reports clients created directly in JSX. Automatic fixes are available when the client is assigned to a variable.

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

```tsx
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,19 @@ const ruleTester = new RuleTester()

ruleTester.run('stable-query-client', rule, {
valid: [
...[
'const Component = () => { const [client] = React.useState(() => new QueryClient()); return client; }',
'const useClient = () => React.useState(() => new QueryClient());',
'const Component = function() { const [client] = React.useState(() => new QueryClient()); return client; }',
'function Component() { const [client] = React.useState(() => { const client = new QueryClient(); return client; }); return client; }',
'const Component = () => { const [client] = React.useState(() => { const client = new QueryClient(); return client; }); return client; }',
'function Component() { function createClient() { const client = new QueryClient(); return client; } return createClient; }',
'const createClient = () => { const client = new QueryClient(); return client; }',
'const ServerComponent = async () => { const client = new QueryClient(); return client; }',
'const client = new QueryClient();',
].map((code) => ({
code: `import { QueryClient } from "@tanstack/react-query";\n${code}`,
})),
{
name: 'QueryClient is stable when wrapped in React.useState',
code: normalizeIndent`
Expand Down Expand Up @@ -119,6 +132,27 @@ ruleTester.run('stable-query-client', rule, {
},
],
invalid: [
...['Component', 'useClient'].flatMap((name) =>
['() =>', 'function()'].map((fn) => ({
code: `import { QueryClient } from "@tanstack/react-query";
const ${name} = ${fn} { const client = new QueryClient(); return client; }`,
output: `import { QueryClient } from "@tanstack/react-query";
const ${name} = ${fn} { const [client] = React.useState(() => new QueryClient()); return client; }`,
errors: [{ messageId: 'unstable' as const }],
})),
),
...[
'function Component() { return <QueryClientProvider client={new QueryClient()} />; }',
'const Component = () => { return <QueryClientProvider client={new QueryClient()} />; }',
'const Component = () => <QueryClientProvider client={new QueryClient()} />;',
'const useClient = () => new QueryClient();',
].map((code) => ({
filename: 'test.tsx',
languageOptions: { parserOptions: { ecmaFeatures: { jsx: true } } },
code: `import { QueryClient, QueryClientProvider } from "@tanstack/react-query";\n${code}`,
output: null,
errors: [{ messageId: 'unstable' as const }],
})),
{
name: 'QueryClient is not stable when it is not wrapped in React.useState in component',
code: normalizeIndent`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ export const rule = createRule({
if (
node.callee.type !== AST_NODE_TYPES.Identifier ||
node.callee.name !== 'QueryClient' ||
node.parent.type !== AST_NODE_TYPES.VariableDeclarator ||
!helpers.isSpecificTanstackQueryImport(
node.callee,
'@tanstack/react-query',
Expand All @@ -45,26 +44,43 @@ export const rule = createRule({
return
}

const fnAncestor = ASTUtils.getFunctionAncestor(
context.sourceCode,
node,
)
const fnAncestor = context.sourceCode
.getAncestors(node)
.reverse()
.find((ancestor) =>
ASTUtils.isNodeOfOneOf(ancestor, [
AST_NODE_TYPES.FunctionDeclaration,
AST_NODE_TYPES.FunctionExpression,
AST_NODE_TYPES.ArrowFunctionExpression,
]),
)
const fnName =
fnAncestor?.parent.type === AST_NODE_TYPES.VariableDeclarator &&
fnAncestor.parent.id.type === AST_NODE_TYPES.Identifier
? fnAncestor.parent.id
: fnAncestor?.id
const isReactServerComponent = fnAncestor?.async === true

if (
!ASTUtils.isValidReactComponentOrHookName(fnAncestor?.id) ||
!ASTUtils.isValidReactComponentOrHookName(fnName) ||
isReactServerComponent
) {
return
}

context.report({
node: node.parent,
node:
node.parent.type === AST_NODE_TYPES.VariableDeclarator
? node.parent
: node,
messageId: 'unstable',
fix: (() => {
const { parent } = node

if (parent.id.type !== AST_NODE_TYPES.Identifier) {
if (
parent.type !== AST_NODE_TYPES.VariableDeclarator ||
parent.id.type !== AST_NODE_TYPES.Identifier
) {
return
}

Expand Down
Loading