Skip to content

Conversation

@m2na7
Copy link
Contributor

@m2na7 m2na7 commented Sep 10, 2025

Fixes #9638

useQueries was causing infinite re-renders when dynamically removing the last query from the queries array. This issue was introduced in v5.85.9 after PR #9592.

Problem

The queriesObserver.ts change detection logic had a flaw when handling array length changes:

  • When removing the last query: hasIndexChange = false but hasResultChange = true
  • This bypassed the early return condition, leading to infinite update cycles

Solution

Improved the change detection logic in queriesObserver.ts and added test in useQueries.test.tsx

// AS-IS
const hasIndexChange = newObservers.some(
  (observer, index) => observer !== prevObservers[index],
)

const hasResultChange =
  prevObservers.length === newObservers.length && !hasIndexChange
    ? newResult.some((result, index) => {
        const prev = this.#result[index]
        return !prev || !shallowEqualObjects(result, prev)
      })
    : true  // Issue: array length changes always return true

if (!hasIndexChange && !hasResultChange) return  // Missing condition!
// TO-BE
const hasLengthChange = prevObservers.length !== newObservers.length
const hasIndexChange = newObservers.some(
  (observer, index) => observer !== prevObservers[index],
)
const hasStructuralChange = hasLengthChange || hasIndexChange

// Only check result changes when there’s no structural change
const hasResultChange = hasStructuralChange
  ? true
  : newResult.some((result, index) => {
      const prev = this.#result[index]
      return !prev || !shallowEqualObjects(result, prev)
    })

if (!hasStructuralChange && !hasResultChange) return

Summary by CodeRabbit

  • Bug Fixes

    • Prevents infinite re-renders when removing the last query in useQueries.
    • Improves stability when adding, removing, or reordering queries, ensuring results update correctly and only when needed.
    • Ensures new queries are properly subscribed and removed ones are cleaned up to avoid stale updates.
  • Tests

    • Added coverage to verify no re-render loops when removing the last or first query and that the query count updates as expected.
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Sep 10, 2025

Walkthrough

Introduces structural-change detection in queriesObserver to handle length/index differences, adjusts result-change and early-return logic accordingly, and updates subscription/destroy behavior for added/removed observers. Adds a React test to verify no infinite re-renders when removing the last query from useQueries.

Changes

Cohort / File(s) Summary of edits
Observer structural/change detection
packages/query-core/src/queriesObserver.ts
Added hasLengthChange/hasIndexChange and derived hasStructuralChange; recalculated hasResultChange; modified early-exit; on structural change, replaced observer list, destroyed removed observers, subscribed new ones, and set options before updates.
React test for regressions
packages/react-query/src/__tests__/useQueries.test.tsx
Added test ensuring removing the last query from a useQueries array does not cause infinite re-renders; validates render count and query count updates.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor C as React Component (useQueries)
  participant QO as QueriesObserver
  participant Obs as QueryObserver(s)

  C->>QO: setQueries(newQueries)
  QO->>QO: Compute hasLengthChange, hasIndexChange
  QO->>QO: hasStructuralChange = OR(len, idx)
  alt Structural change
    QO->>Obs: Destroy removed observers
    QO->>Obs: Subscribe newly added observers
    QO->>Obs: Set options on new/current observers
    QO->>QO: Update internal observers list
  else No structural change
    QO->>QO: Compare results (shallow) to detect changes
  end
  QO-->>C: Notify only if structural or result change
  note over QO,C: Prevents re-notify loops when removing last item
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested reviewers

  • TkDodo

Pre-merge checks (4 passed, 1 warning)

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The pull request title clearly and succinctly identifies the key fix of infinite re-renders in useQueries and accurately notes the affected packages, making it a concise summary of the primary change.
Linked Issues Check ✅ Passed The pull request directly addresses the bug described in issue #9638 by refining the change detection logic to handle structural and result changes correctly and includes a new test case for removing the last query, thereby fulfilling the linked issue’s primary objectives.
Out of Scope Changes Check ✅ Passed All code modifications are confined to the change detection logic in query-core and the corresponding test in react-query, aligning with the linked issue’s scope and introducing no unrelated or extraneous changes.

Poem

I twitch my nose at lists that shrink,
The last one hops—no endless blink.
I tidy burrows, add and prune,
Subscriptions hum a calmer tune.
Now queries rest, no frantic spree—
One hop, one render, bug set free. 🐇✨

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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.

@nx-cloud
Copy link

nx-cloud bot commented Sep 10, 2025

View your CI Pipeline Execution ↗ for commit 8dda7d3

Command Status Duration Result
nx affected --targets=test:sherif,test:knip,tes... ✅ Succeeded 3m 21s View ↗
nx run-many --target=build --exclude=examples/*... ✅ Succeeded 1m 21s View ↗

☁️ Nx Cloud last updated this comment at 2025-09-10 12:45:14 UTC

@pkg-pr-new
Copy link

pkg-pr-new bot commented Sep 10, 2025

More templates

@tanstack/angular-query-devtools-experimental

npm i https://pkg.pr.new/@tanstack/angular-query-devtools-experimental@9639

@tanstack/angular-query-experimental

npm i https://pkg.pr.new/@tanstack/angular-query-experimental@9639

@tanstack/eslint-plugin-query

npm i https://pkg.pr.new/@tanstack/eslint-plugin-query@9639

@tanstack/query-async-storage-persister

npm i https://pkg.pr.new/@tanstack/query-async-storage-persister@9639

@tanstack/query-broadcast-client-experimental

npm i https://pkg.pr.new/@tanstack/query-broadcast-client-experimental@9639

@tanstack/query-core

npm i https://pkg.pr.new/@tanstack/query-core@9639

@tanstack/query-devtools

npm i https://pkg.pr.new/@tanstack/query-devtools@9639

@tanstack/query-persist-client-core

npm i https://pkg.pr.new/@tanstack/query-persist-client-core@9639

@tanstack/query-sync-storage-persister

npm i https://pkg.pr.new/@tanstack/query-sync-storage-persister@9639

@tanstack/react-query

npm i https://pkg.pr.new/@tanstack/react-query@9639

@tanstack/react-query-devtools

npm i https://pkg.pr.new/@tanstack/react-query-devtools@9639

@tanstack/react-query-next-experimental

npm i https://pkg.pr.new/@tanstack/react-query-next-experimental@9639

@tanstack/react-query-persist-client

npm i https://pkg.pr.new/@tanstack/react-query-persist-client@9639

@tanstack/solid-query

npm i https://pkg.pr.new/@tanstack/solid-query@9639

@tanstack/solid-query-devtools

npm i https://pkg.pr.new/@tanstack/solid-query-devtools@9639

@tanstack/solid-query-persist-client

npm i https://pkg.pr.new/@tanstack/solid-query-persist-client@9639

@tanstack/svelte-query

npm i https://pkg.pr.new/@tanstack/svelte-query@9639

@tanstack/svelte-query-devtools

npm i https://pkg.pr.new/@tanstack/svelte-query-devtools@9639

@tanstack/svelte-query-persist-client

npm i https://pkg.pr.new/@tanstack/svelte-query-persist-client@9639

@tanstack/vue-query

npm i https://pkg.pr.new/@tanstack/vue-query@9639

@tanstack/vue-query-devtools

npm i https://pkg.pr.new/@tanstack/vue-query-devtools@9639

commit: 8dda7d3

@codecov
Copy link

codecov bot commented Sep 10, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 59.64%. Comparing base (ccedf33) to head (8dda7d3).
⚠️ Report is 1 commits behind head on main.

Additional details and impacted files

Impacted file tree graph

@@             Coverage Diff             @@
##             main    #9639       +/-   ##
===========================================
+ Coverage   45.50%   59.64%   +14.13%     
===========================================
  Files         209      138       -71     
  Lines        8377     5617     -2760     
  Branches     1897     1514      -383     
===========================================
- Hits         3812     3350      -462     
+ Misses       4118     1963     -2155     
+ Partials      447      304      -143     
Components Coverage Δ
@tanstack/angular-query-devtools-experimental ∅ <ø> (∅)
@tanstack/angular-query-experimental 87.00% <ø> (ø)
@tanstack/eslint-plugin-query ∅ <ø> (∅)
@tanstack/query-async-storage-persister 43.85% <ø> (ø)
@tanstack/query-broadcast-client-experimental 24.39% <ø> (ø)
@tanstack/query-codemods ∅ <ø> (∅)
@tanstack/query-core 97.48% <100.00%> (+<0.01%) ⬆️
@tanstack/query-devtools 3.48% <ø> (ø)
@tanstack/query-persist-client-core 79.60% <ø> (ø)
@tanstack/query-sync-storage-persister 84.61% <ø> (ø)
@tanstack/query-test-utils ∅ <ø> (∅)
@tanstack/react-query 96.00% <ø> (ø)
@tanstack/react-query-devtools 10.00% <ø> (ø)
@tanstack/react-query-next-experimental ∅ <ø> (∅)
@tanstack/react-query-persist-client 100.00% <ø> (ø)
@tanstack/solid-query 78.13% <ø> (ø)
@tanstack/solid-query-devtools ∅ <ø> (∅)
@tanstack/solid-query-persist-client 100.00% <ø> (ø)
@tanstack/svelte-query 87.58% <ø> (ø)
@tanstack/svelte-query-devtools ∅ <ø> (∅)
@tanstack/svelte-query-persist-client 100.00% <ø> (ø)
@tanstack/vue-query 71.10% <ø> (ø)
@tanstack/vue-query-devtools ∅ <ø> (∅)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.
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: 0

🧹 Nitpick comments (4)
packages/query-core/src/queriesObserver.ts (2)

121-126: Skip index scan when length already differs.

A tiny perf nit: short-circuit hasIndexChange if lengths differ to avoid an O(n) scan you already know will be irrelevant.

Apply:

-      const hasLengthChange = prevObservers.length !== newObservers.length
-      const hasIndexChange = newObservers.some(
-        (observer, index) => observer !== prevObservers[index],
-      )
+      const hasLengthChange = prevObservers.length !== newObservers.length
+      const hasIndexChange =
+        !hasLengthChange &&
+        newObservers.some(
+          (observer, index) => observer !== prevObservers[index],
+        )

144-153: Avoid building two Sets for differences.

Minor micro-opt: compute added/removed with precomputed sets once instead of calling difference twice (each builds a Set).

Apply:

-      if (hasStructuralChange) {
-        difference(prevObservers, newObservers).forEach((observer) => {
-          observer.destroy()
-        })
-        difference(newObservers, prevObservers).forEach((observer) => {
-          observer.subscribe((result) => {
-            this.#onUpdate(observer, result)
-          })
-        })
-      }
+      if (hasStructuralChange) {
+        const prevSet = new Set(prevObservers)
+        const nextSet = new Set(newObservers)
+        const removed = prevObservers.filter((o) => !nextSet.has(o))
+        const added = newObservers.filter((o) => !prevSet.has(o))
+
+        removed.forEach((observer) => {
+          observer.destroy()
+        })
+        added.forEach((observer) => {
+          observer.subscribe((result) => {
+            this.#onUpdate(observer, result)
+          })
+        })
+      }
packages/react-query/src/__tests__/useQueries.test.tsx (2)

1749-1758: Prefer unique keys via queryKey() to avoid cross-test collisions.

Using generated keys mirrors existing tests and minimizes shared-client interference.

Apply:

-    function Page() {
-      const [queries, setQueries] = React.useState([
+    function Page() {
+      const k1 = queryKey()
+      const k2 = queryKey()
+      const [queries, setQueries] = React.useState([
         {
-          queryKey: ['query1'],
+          queryKey: k1,
           queryFn: () => 'data1',
         },
         {
-          queryKey: ['query2'],
+          queryKey: k2,
           queryFn: () => 'data2',
         },
       ])
@@
           <button
             onClick={() => {
               setQueries([
                 {
-                  queryKey: ['query1'],
+                  queryKey: k1,
                   queryFn: () => 'data1',
                 },
               ])
             }}
           >
             remove last
           </button>
@@
           <button
             onClick={() => {
               setQueries([
                 {
-                  queryKey: ['query2'],
+                  queryKey: k2,
                   queryFn: () => 'data2',
                 },
               ])
             }}
           >
             remove first
           </button>

Also applies to: 1769-1775, 1781-1787


1746-1766: Track render count with a ref; avoid manual resets mid-test.

Keeps counting logic inside the component and removes the need to mutate outer scope.

Apply:

-    let renderCount = 0
+    let renderCount = 0
@@
-      renderCount++
+      renderCount++
@@
-    await vi.advanceTimersByTimeAsync(0)
-    renderCount = 0
+    await vi.advanceTimersByTimeAsync(0)
@@
-    renderCount = 0
+    // no manual reset needed

Alternative (cleaner inside component):

-    let renderCount = 0
+    const rendersRef = React.useRef(0)
@@
-      renderCount++
+      rendersRef.current++
@@
-          <div data-testid="render-count">renders: {renderCount}</div>
+          <div data-testid="render-count">renders: {rendersRef.current}</div>

Then assert by reading the DOM value instead of mutating a closed-over variable.

Also applies to: 1797-1807

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ccedf33 and 8dda7d3.

📒 Files selected for processing (2)
  • packages/query-core/src/queriesObserver.ts (1 hunks)
  • packages/react-query/src/__tests__/useQueries.test.tsx (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
packages/react-query/src/__tests__/useQueries.test.tsx (3)
packages/query-core/src/queriesObserver.ts (3)
  • queries (235-261)
  • setQueries (87-157)
  • result (195-210)
packages/react-query/src/useQueries.ts (1)
  • useQueries (208-332)
packages/react-query/src/__tests__/utils.tsx (1)
  • renderWithClient (9-23)
packages/query-core/src/queriesObserver.ts (1)
packages/query-core/src/utils.ts (1)
  • shallowEqualObjects (306-321)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Test
🔇 Additional comments (2)
packages/query-core/src/queriesObserver.ts (1)

121-139: Solid structural-change detection fix; aligns with the bug’s root cause.

Introducing hasLengthChange and folding it into hasStructuralChange, plus guarding result comparisons behind structural checks, correctly handles removals at the tail and reorders. The early-return now reflects actual list structure/result stability, preventing the infinite update loop.

packages/react-query/src/__tests__/useQueries.test.tsx (1)

1745-1813: Good regression test; reproduces and guards the original failure mode.

Covers both removing the last and the first element; assertions are lenient enough to avoid flakes.

@TkDodo TkDodo merged commit 19445a0 into TanStack:main Sep 10, 2025
8 checks passed
Hellol77 pushed a commit to Hellol77/query that referenced this pull request Oct 14, 2025
* test(useQueries): add test for preventing infinite re-renders

* fix(query-core): resolve infinite re-renders in queriesObserver

* style(useQueries): remove unnecessary comments

* ci: apply automated fixes

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment