Skip to content

stable query client#37

Open
AlbertSmit wants to merge 2 commits into
mainfrom
feature/stable-query-client
Open

stable query client#37
AlbertSmit wants to merge 2 commits into
mainfrom
feature/stable-query-client

Conversation

@AlbertSmit

@AlbertSmit AlbertSmit commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

What

Ports the stable-query-client rule from @tanstack/eslint-plugin-query into our own plugin — without adding the dependency.

Prevents creating a new QueryClient() inside a React component body. The QueryClient must be stable across renders, otherwise the entire query cache is lost on every re-render.

Why not just install the TanStack plugin?

The TanStack plugin has 6 rules. After analysis, only stable-query-client applies to our (JavaScript) codebases:

Rule Applies? Why
stable-query-client Pure AST check, JS-compatible
exhaustive-deps Needs TypeScript type information
no-unstable-deps Needs TypeScript type information
no-rest-destructuring Niche, our codebases don't do this
infinite-query-property-order TypeScript inference ordering
mutation-property-order TypeScript inference ordering

One useful rule doesn't justify an entire plugin dependency.

Valid patterns

// Module scope — perfectly fine
const queryClient = new QueryClient()

// Inside useState — stable (lazy initializer runs once)
function App() {
  const [queryClient] = useState(() => new QueryClient())
}

Invalid patterns

// Re-created every render — cache lost
function App() {
  const queryClient = new QueryClient()  // ← error
}

Why useState and not useMemo?

React's own docs (useMemo caveats):

useMemo is a performance optimization, not a semantic guarantee. React may throw away the cached value [...] a state variable or a ref may be more appropriate.

React can discard memoized values during Suspense, concurrent features, or future APIs. useState's lazy initializer is guaranteed to run exactly once.

Changes

File What
rules/tanstack-query/stable-client/index.js Rule implementation
rules/tanstack-query/stable-client/test.js Tests (valid + invalid cases)
rules/tanstack-query/stable-client/readme.md Documentation
index.js Registered the rule
machinery/ast.js Added reusable helpers: isInsideComponent, isArrowConciseBody, isUseStateCall

Reusable helpers moved to machinery/ast.js

These helpers are generic enough to be useful in future rules:

  • isInsideComponent(node) — walks up AST to check if a node is inside a PascalCase function (React component)
  • isArrowConciseBody(arrowFn, bodyNode) — checks if a node is the concise body of an arrow function
  • isUseStateCall(node) — detects useState(...) or React.useState(...) call expressions

Summary by CodeRabbit

  • New Features

    • Added opt-in configs.jsdoc preset for JSDoc linting with four rules (require JSDoc on exported functions, validate type syntax, check parameter names, validate defined types).
    • Added new ESLint rule for TanStack Query detecting unstable QueryClient instantiation inside React component bodies.
  • Documentation

    • Added JSDoc baseline configuration documentation.
    • Added JSDoc baseline rules overview.
    • Added documentation and examples for the new TanStack Query rule.

Add configs.jsdoc preset wiring up eslint-plugin-jsdoc with:
- jsdoc/require-jsdoc (publicOnly: true — exported functions only)
- jsdoc/valid-types
- jsdoc/check-param-names
- jsdoc/no-undefined-types

Opt-in per project, not part of the default Kaliber config.
@AlbertSmit AlbertSmit self-assigned this Jun 19, 2026
@coderabbitai

coderabbitai Bot commented Jun 19, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

Adds a new tanstack-query/stable-client ESLint rule that flags new QueryClient() inside React component render paths unless wrapped in useState/React.useState. Introduces shared AST helpers in machinery/ast.js to support component detection. Separately adds a configs.jsdoc opt-in preset backed by a new eslint-plugin-jsdoc dependency, with corresponding documentation updates.

Changes

tanstack-query/stable-client Rule

Layer / File(s) Summary
Shared AST helper utilities
machinery/ast.js
Exports and implements getFunctionNodeName, isFunctionNode, isPascalCase, isInsideComponent, isArrowConciseBody, and isUseStateCall to enable component-scope detection.
stable-client rule implementation
rules/tanstack-query/stable-client/index.js
Defines the rule meta with the unstable messageId and a NewExpression visitor; reports when isQueryClientConstructor and isInsideComponent both pass but isWrappedInUseState does not, covering concise/block arrow bodies and React.useState.
Rule tests and documentation
rules/tanstack-query/stable-client/test.js, rules/tanstack-query/stable-client/readme.md
Adds valid fixtures (module-scope and useState-wrapped patterns) and invalid fixtures (render-path and inline-prop instantiation), plus the rule readme with rationale and usage examples.

configs.jsdoc Preset

Layer / File(s) Summary
Dependency, config wiring, and docs
package.json, index.js, README.md, rules-overview.md
Adds eslint-plugin-jsdoc ^50.6.9, imports and registers it in plugin.configs.jsdoc with require-jsdoc, valid-types, check-param-names, and no-undefined-types at warn level, and documents the preset in README and rules-overview.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 30.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'stable query client' directly corresponds to the main change: a new ESLint rule for ensuring QueryClient stability across React renders.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/stable-query-client

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
rules/tanstack-query/stable-client/test.js (1)

28-38: ⚡ Quick win

Add a valid fixture for lazy initializer with an intermediate variable.

Please add coverage for useState(() => { const qc = new QueryClient(); return qc }) so this supported stable pattern stays protected from false positives.

Suggested test addition
   valid: [
@@
     // useState with block body
     {
       code: `
         function App() {
           const [queryClient] = useState(() => {
             return new QueryClient()
           })
           return null
         }
       `,
     },
+    // useState with local binding in lazy initializer
+    {
+      code: `
+        function App() {
+          const [queryClient] = useState(() => {
+            const qc = new QueryClient()
+            return qc
+          })
+          return null
+        }
+      `,
+    },
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rules/tanstack-query/stable-client/test.js` around lines 28 - 38, Add a new
test fixture object to the test array that covers the useState lazy initializer
pattern where an intermediate variable is declared and returned, specifically
testing useState(() => { const qc = new QueryClient(); return qc }). This test
case should be added after the existing useState with block body test case to
ensure this supported stable pattern with intermediate variable assignment is
protected from false positives in the linting rule.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@README.md`:
- Around line 55-60: The code snippet in the README is incomplete and will fail
when copy-pasted because it references kaliberConfig on line 58 without
declaring it. Add the missing require statement for kaliberConfig at the
beginning of the snippet, before the kaliberPlugin require statement, so that
when users copy-paste this example code, it will work standalone without
requiring them to reference other parts of the documentation.

In `@rules/tanstack-query/stable-client/index.js`:
- Around line 45-58: The isWrappedInUseState function only validates QueryClient
instantiations that are direct returns or arrow function concise bodies, missing
valid patterns where QueryClient is assigned to a variable and then returned.
Extend the logic to handle cases where the node is nested inside a
VariableDeclarator within a function body that is wrapped in useState by adding
a branch that traverses the parent chain to find the containing function body,
verifies it's actually a function, and then checks if that function is wrapped
in a useState call.

---

Nitpick comments:
In `@rules/tanstack-query/stable-client/test.js`:
- Around line 28-38: Add a new test fixture object to the test array that covers
the useState lazy initializer pattern where an intermediate variable is declared
and returned, specifically testing useState(() => { const qc = new
QueryClient(); return qc }). This test case should be added after the existing
useState with block body test case to ensure this supported stable pattern with
intermediate variable assignment is protected from false positives in the
linting rule.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a632d194-22cc-4e52-acec-1b890af90d52

📥 Commits

Reviewing files that changed from the base of the PR and between fc42ad9 and a5f1b9f.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (8)
  • README.md
  • index.js
  • machinery/ast.js
  • package.json
  • rules-overview.md
  • rules/tanstack-query/stable-client/index.js
  • rules/tanstack-query/stable-client/readme.md
  • rules/tanstack-query/stable-client/test.js

Comment thread README.md
Comment on lines +55 to +60
const kaliberPlugin = require('@kaliber/eslint-plugin')

module.exports = [
...kaliberConfig,
kaliberPlugin.configs.jsdoc,
]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Make the JSDoc usage snippet self-contained.

Line 58 uses kaliberConfig, but this snippet only declares kaliberPlugin; copy-pasting it as-is will fail.

Proposed doc fix
+const kaliberConfig = require('`@kaliber/eslint-plugin/eslint.config`')
 const kaliberPlugin = require('`@kaliber/eslint-plugin`')

 module.exports = [
   ...kaliberConfig,
   kaliberPlugin.configs.jsdoc,
 ]
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const kaliberPlugin = require('@kaliber/eslint-plugin')
module.exports = [
...kaliberConfig,
kaliberPlugin.configs.jsdoc,
]
const kaliberConfig = require('`@kaliber/eslint-plugin/eslint.config`')
const kaliberPlugin = require('`@kaliber/eslint-plugin`')
module.exports = [
...kaliberConfig,
kaliberPlugin.configs.jsdoc,
]
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@README.md` around lines 55 - 60, The code snippet in the README is incomplete
and will fail when copy-pasted because it references kaliberConfig on line 58
without declaring it. Add the missing require statement for kaliberConfig at the
beginning of the snippet, before the kaliberPlugin require statement, so that
when users copy-paste this example code, it will work standalone without
requiring them to reference other parts of the documentation.

Comment on lines +45 to +58
function isWrappedInUseState(node) {
const parent = node.parent
if (!parent) return false

// Concise arrow body: useState(() => new QueryClient())
if (isArrowConciseBody(parent, node) && isUseStateCall(parent.parent)) return true

// Block body: useState(() => { return new QueryClient() })
if (parent.type === 'ReturnStatement') {
const blockParent = parent.parent
if (blockParent && blockParent.parent && isFunctionNode(blockParent.parent)) {
if (isUseStateCall(blockParent.parent.parent)) return true
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

isWrappedInUseState misses valid lazy-initializer forms and produces false positives.

The current logic only accepts direct-return shapes. A valid stable pattern like useState(() => { const qc = new QueryClient(); return qc }) is incorrectly reported because new QueryClient() is under VariableDeclarator, not the two hardcoded branches.

Proposed fix
 function isWrappedInUseState(node) {
-  const parent = node.parent
-  if (!parent) return false
-
-  // Concise arrow body: useState(() => new QueryClient())
-  if (isArrowConciseBody(parent, node) && isUseStateCall(parent.parent)) return true
-
-  // Block body: useState(() => { return new QueryClient() })
-  if (parent.type === 'ReturnStatement') {
-    const blockParent = parent.parent
-    if (blockParent && blockParent.parent && isFunctionNode(blockParent.parent)) {
-      if (isUseStateCall(blockParent.parent.parent)) return true
-    }
-  }
-
-  return false
+  let current = node.parent
+  while (current) {
+    if (isFunctionNode(current)) {
+      return isUseStateCall(current.parent)
+    }
+    current = current.parent
+  }
+  return false
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rules/tanstack-query/stable-client/index.js` around lines 45 - 58, The
isWrappedInUseState function only validates QueryClient instantiations that are
direct returns or arrow function concise bodies, missing valid patterns where
QueryClient is assigned to a variable and then returned. Extend the logic to
handle cases where the node is nested inside a VariableDeclarator within a
function body that is wrapped in useState by adding a branch that traverses the
parent chain to find the containing function body, verifies it's actually a
function, and then checks if that function is wrapped in a useState call.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant