Skip to content

Conversation

@jackleslie
Copy link

@jackleslie jackleslie commented Jan 20, 2026

🎯 Changes

Resolves: #10060

New rule for ensuring that QueryClient isn't created in the root module scope (for Next.js applications)

✅ Checklist

  • I have followed the steps in the Contributing guide.
  • I have tested this code locally with pnpm run test:pr.

🚀 Release Impact

  • This change affects published code, and I have generated a changeset.
  • This change is docs/CI/dev-only (no release).

Summary by CodeRabbit

  • New Features

    • Added an ESLint rule that flags module-level QueryClient creation in Next.js projects to encourage component-level instantiation (useState/useRef).
  • Documentation

    • Added comprehensive docs with valid/invalid examples and guidance for Next.js usage.
  • Tests

    • Added unit tests covering valid and invalid scenarios across Next.js file types and usage patterns.
  • Chores

    • Added a changeset entry for a minor release.

✏️ Tip: You can customize this high-level summary in your review settings.

@changeset-bot
Copy link

changeset-bot bot commented Jan 20, 2026

🦋 Changeset detected

Latest commit: ae6d75e

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@tanstack/eslint-plugin-query Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions github-actions bot added documentation Improvements or additions to documentation package: eslint-plugin-query labels Jan 20, 2026
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 20, 2026

📝 Walkthrough

Walkthrough

This PR adds a new ESLint rule no-module-query-client to detect and forbid module-level QueryClient instantiation in Next.js app and pages files. It includes the rule implementation, tests, documentation, and exports the rule from the plugin registry.

Changes

Cohort / File(s) Summary
Rule implementation & export
packages/eslint-plugin-query/src/rules/no-module-query-client/no-module-query-client.rule.ts, packages/eslint-plugin-query/src/rules.ts
New ESLint rule that detects new QueryClient() from @tanstack/react-query at module scope in Next.js-specific files (e.g., _app, _document, app/layout, pages). Adds rule export to the plugin registry.
Tests
packages/eslint-plugin-query/src/__tests__/no-module-query-client.test.ts
New RuleTester suite with valid cases (useState/useRef/inside hooks/non-Next files/alternate packages) and invalid cases (module-level QueryClient in Next.js app/pages), asserting noModuleQueryClient diagnostics.
Documentation
docs/eslint/no-module-query-client.md
New doc describing the rule, applicability to Next.js SSR contexts, examples of incorrect and correct patterns, and rule metadata.
Changeset
.changeset/afraid-walls-glow.md
New changeset declaring a minor release for the plugin including this rule.

Sequence Diagram(s)

(omitted — changes are an ESLint rule and tests; no multi-component runtime flow to visualize)

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Suggested reviewers

  • TkDodo

Poem

🐰 A rule in the meadow takes a stand,
No module clients across Next.js land.
Inside components they now belong,
useState or useRef keeps them strong,
Hooray — tidy queries all the day long! 🥕

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat(eslint-plugin-query): add new rule for SSR QueryClient setup' clearly and concisely describes the main change: adding a new ESLint rule for handling QueryClient in SSR contexts, which aligns with the changeset content.
Description check ✅ Passed The pull request description includes all required template sections: a clear explanation of changes with a linked issue, a completed checklist confirming contribution guide adherence and local testing, and appropriate release impact documentation.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

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


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.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@docs/eslint/no-module-query-client.md`:
- Around line 35-51: The "incorrect" example in docs shows new QueryClient()
created inside the MyApp component (queryClient), but the rule
`@tanstack/query/no-module-query-client` only detects module-level creations, so
update the documentation to reflect the rule's actual behavior: either remove or
move the per-render example so it shows QueryClient created at module scope
(e.g., const queryClient = new QueryClient() at top-level) or explicitly state
that the rule only flags module-level (no function ancestor) new QueryClient()
usages; update the text around MyApp/QueryClient/queryClient accordingly to
avoid claiming the shown per-render case is flagged.
🧹 Nitpick comments (3)
docs/eslint/no-module-query-client.md (1)

98-114: The useRef example still creates a new QueryClient on every render.

While the ref preserves the first instance, the expression new QueryClient() is evaluated on every render—the result is just discarded. This wastes resources and could cause issues if QueryClient constructor has side effects.

Consider updating this example to use lazy initialization:

Suggested lazy initialization pattern
// app/providers.tsx
'use client'

import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { useRef } from 'react'

function getQueryClient() {
  return new QueryClient()
}

export function Providers({ children }: { children: React.ReactNode }) {
  const queryClientRef = useRef<QueryClient | null>(null)
  if (queryClientRef.current === null) {
    queryClientRef.current = getQueryClient()
  }

  return (
    <QueryClientProvider client={queryClientRef.current}>
      {children}
    </QueryClientProvider>
  )
}
packages/eslint-plugin-query/src/rules/no-module-query-client/no-module-query-client.rule.ts (1)

56-63: Consider renaming component to functionAncestor for clarity.

The variable name component suggests it's specifically a React component, but ASTUtils.getFunctionAncestor returns any enclosing function. This could be a regular function, arrow function, or custom hook—not necessarily a React component.

Suggested rename
-        const component = ASTUtils.getFunctionAncestor(context.sourceCode, node)
+        const functionAncestor = ASTUtils.getFunctionAncestor(context.sourceCode, node)

-        if (component === undefined) {
+        if (functionAncestor === undefined) {
           context.report({
             node,
             messageId: 'noModuleQueryClient',
           })
         }
packages/eslint-plugin-query/src/__tests__/no-module-query-client.test.ts (1)

75-85: Good coverage for non-Next.js paths.

The test correctly verifies that src/utils/query.ts is not flagged as a Next.js file.

Consider adding a test case for potential false positives like myapp/components/Provider.tsx to ensure the regex doesn't incorrectly match paths where app is part of a larger directory name.

Suggested additional test case
{
  name: 'QueryClient at module level in directory containing "app" as substring',
  filename: 'myapp/components/Provider.tsx',
  code: normalizeIndent`
    import { QueryClient } from "@tanstack/react-query";

    const queryClient = new QueryClient();

    export { queryClient };
  `,
},

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

Labels

documentation Improvements or additions to documentation package: eslint-plugin-query

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant