Skip to content

fix(eslint-plugin-query): track custom query hook wrappers in deps#10829

Open
raashish1601 wants to merge 1 commit into
TanStack:mainfrom
raashish1601:fix/no-unstable-deps-nested-hooks
Open

fix(eslint-plugin-query): track custom query hook wrappers in deps#10829
raashish1601 wants to merge 1 commit into
TanStack:mainfrom
raashish1601:fix/no-unstable-deps-nested-hooks

Conversation

@raashish1601
Copy link
Copy Markdown
Contributor

@raashish1601 raashish1601 commented May 30, 2026

Description

Handle custom TanStack Query React hook wrappers (including function declarations and nested custom hooks) in the eslint-plugin no-unstable-deps rule.

Changes

  • Track use* custom hooks by resolving returned query hook from:
    • const arrow/function assignments
    • function declarations
    • nested custom wrappers
  • Defer dependency reporting until Program exit so wrapper tracking is complete.
  • Add tests covering:
    • custom useMutation wrapper in hook callback dependency
    • custom useQuery wrapper in hook callback dependency
    • custom useQuery with parameters passed through another hook and consumed in dependency array

Summary by CodeRabbit

  • Bug Fixes

    • Enhanced the no-unstable-deps ESLint rule to properly detect unstable dependencies when custom hooks wrap TanStack Query hooks. The rule now tracks wrapped query and mutation results across custom hook layers.
  • Tests

    • Added test cases for custom wrapper hook patterns to ensure comprehensive rule coverage

Review Change Stack

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 30, 2026

📝 Walkthrough

Walkthrough

The PR extends the no-unstable-deps ESLint rule to detect and report unstable query hook results when wrapped by custom hooks. It refactors error reporting to defer until all custom hook definitions are resolved, preventing false negatives for custom hook return values used as dependencies.

Changes

Custom Hook Wrapper Tracking

Layer / File(s) Summary
State initialization and hook resolution helpers
packages/eslint-plugin-query/src/rules/no-unstable-deps/no-unstable-deps.rule.ts
New state maps track custom hook name-to-resolved-hook mappings, a queue for deferred variable assignments, and dependency identifiers paired with their React hook. Helper functions infer returned hooks from function bodies, unwrap expressions, filter use* identifiers, and recursively resolve nested custom hooks with cycle prevention.
AST visitor expansion for custom hook tracking
packages/eslint-plugin-query/src/rules/no-unstable-deps/no-unstable-deps.rule.ts
Visitors record custom hooks declared via FunctionDeclaration or VariableDeclarator. Variables initialized by custom hook calls are queued for deferred resolution. Direct TanStack query hook tracking and useQueries/useSuspenseQueries combine special-case behavior are preserved.
Deferred reporting mechanism
packages/eslint-plugin-query/src/rules/no-unstable-deps/no-unstable-deps.rule.ts
Error reporting shifts from immediate inline inspection to deferred: CallExpression collects dependency identifiers and their React hook names, then Program:exit resolves each dependency to its underlying query hook before reporting.
Test cases for custom-wrapped hooks
packages/eslint-plugin-query/src/__tests__/no-unstable-deps.test.ts
Three invalid cases verify the rule flags custom wrapper hook results (useMyMutation, useMyQuery, useTodoQuery) used as dependencies, including scenarios with parameterized query keys.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • TanStack/query#10730: Initial implementation of custom hook wrapper tracking in no-unstable-deps, which this PR extends with additional test coverage and deferred reporting logic.

Suggested labels

package: eslint-plugin-query

Suggested reviewers

  • TkDodo

Poem

🐰 A wrapper wraps a wrapper's hook, so deep we peer,
Yet ESLint now resolves the chain without a fear,
From useMyQuery to the source, dependencies flow clear,
Deferred till exit, wisely checked—stability prevails here! ✨

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description provides details about the changes made and includes a testing section, but it does not follow the required template structure with the 🎯 Changes, ✅ Checklist, and 🚀 Release Impact sections. Reformat the description to match the repository template, including explicit sections for Changes, Checklist (with Contributing guide and test confirmation), and Release Impact (with changeset generation confirmation).
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly describes the main change: tracking custom query hook wrappers in the no-unstable-deps ESLint rule, which aligns with the changeset's primary objectives.
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 unit tests (beta)
  • Create PR with unit tests
⚔️ Resolve merge conflicts
  • Resolve merge conflict in branch fix/no-unstable-deps-nested-hooks

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
Copy Markdown
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

🧹 Nitpick comments (1)
packages/eslint-plugin-query/src/rules/no-unstable-deps/no-unstable-deps.rule.ts (1)

195-206: 💤 Low value

Missing use prefix check for function declarations.

The VariableDeclarator visitor explicitly checks node.id.name.startsWith('use') (line 211), but this FunctionDeclaration visitor doesn't. This inconsistency could lead to tracking non-hook functions like function getQuery() { return useQuery(...) } unnecessarily.

♻️ Add prefix check for consistency
 FunctionDeclaration(node: TSESTree.FunctionDeclaration) {
   if (!node.id || node.id.type !== AST_NODE_TYPES.Identifier) {
     return
   }
+  if (!node.id.name.startsWith('use')) {
+    return
+  }

   const returnedHookName = getFunctionReturnedHookName(node)
🤖 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
`@packages/eslint-plugin-query/src/rules/no-unstable-deps/no-unstable-deps.rule.ts`
around lines 195 - 206, The FunctionDeclaration visitor currently records any
function that returns a hook via getFunctionReturnedHookName into
trackedCustomHooks without verifying the function name prefix; add the same
name-prefix guard used in the VariableDeclarator visitor by checking
node.id.name.startsWith('use') inside the FunctionDeclaration handler and only
assign trackedCustomHooks[node.id.name] = returnedHookName when that check
passes to avoid tracking non-hook functions (refer to FunctionDeclaration,
VariableDeclarator, getFunctionReturnedHookName, and trackedCustomHooks).
🤖 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
`@packages/eslint-plugin-query/src/rules/no-unstable-deps/no-unstable-deps.rule.ts`:
- Around line 252-254: The current check only pushes to
pendingCustomHookAssignments when resolveTrackedHook (resolvedQueryHook) is
truthy, which misses forward-referenced custom hooks; change the logic in the
visitor handling use* calls so that any non-TanStack call (i.e., custom hook
candidates determined by calleeName / isTanStackHook) is always added to
pendingCustomHookAssignments (push { id: node.id, calleeName }) for deferred
resolution, instead of gating the push on resolvedQueryHook; keep
resolveTrackedHook/trackedCustomHooks usage for later resolution but ensure
pendingCustomHookAssignments is populated unconditionally for custom hooks to
handle forward references.

---

Nitpick comments:
In
`@packages/eslint-plugin-query/src/rules/no-unstable-deps/no-unstable-deps.rule.ts`:
- Around line 195-206: The FunctionDeclaration visitor currently records any
function that returns a hook via getFunctionReturnedHookName into
trackedCustomHooks without verifying the function name prefix; add the same
name-prefix guard used in the VariableDeclarator visitor by checking
node.id.name.startsWith('use') inside the FunctionDeclaration handler and only
assign trackedCustomHooks[node.id.name] = returnedHookName when that check
passes to avoid tracking non-hook functions (refer to FunctionDeclaration,
VariableDeclarator, getFunctionReturnedHookName, and trackedCustomHooks).
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 24b9c6b9-8fba-4fb0-b909-689e9897c31d

📥 Commits

Reviewing files that changed from the base of the PR and between 7fa2781 and 527e641.

📒 Files selected for processing (2)
  • packages/eslint-plugin-query/src/__tests__/no-unstable-deps.test.ts
  • packages/eslint-plugin-query/src/rules/no-unstable-deps/no-unstable-deps.rule.ts

Comment on lines +252 to 254
if (resolvedQueryHook) {
pendingCustomHookAssignments.push({ id: node.id, calleeName })
}
Copy link
Copy Markdown
Contributor

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

Forward-reference custom hooks won't be tracked.

The condition if (resolvedQueryHook) only pushes to pendingCustomHookAssignments when the hook is already resolvable at visit time. Since ESLint visits nodes in tree order, a custom hook defined after its usage in the file won't be in trackedCustomHooks yet, causing resolveTrackedHook to return undefined and skipping the push. This defeats the purpose of the deferred mechanism.

Example that would be missed:

function Component() {
  const query = useMyQuery(); // visited first
  useEffect(() => {}, [query]); // false negative
}
function useMyQuery() { return useQuery({...}); } // visited second
🐛 Proposed fix: Always defer non-TanStack `use*` calls
-        if (resolvedQueryHook) {
-          pendingCustomHookAssignments.push({ id: node.id, calleeName })
-        }
+        // Always defer - custom hook may be defined later in file
+        pendingCustomHookAssignments.push({ id: node.id, calleeName })
🤖 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
`@packages/eslint-plugin-query/src/rules/no-unstable-deps/no-unstable-deps.rule.ts`
around lines 252 - 254, The current check only pushes to
pendingCustomHookAssignments when resolveTrackedHook (resolvedQueryHook) is
truthy, which misses forward-referenced custom hooks; change the logic in the
visitor handling use* calls so that any non-TanStack call (i.e., custom hook
candidates determined by calleeName / isTanStackHook) is always added to
pendingCustomHookAssignments (push { id: node.id, calleeName }) for deferred
resolution, instead of gating the push on resolvedQueryHook; keep
resolveTrackedHook/trackedCustomHooks usage for later resolution but ensure
pendingCustomHookAssignments is populated unconditionally for custom hooks to
handle forward references.

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