Skip to content

fix(query-core): correctly refetch resetQueries matches#10837

Open
raashish1601 wants to merge 3 commits into
TanStack:mainfrom
raashish1601:fix/query-core-resetqueries-state-filter-refetch
Open

fix(query-core): correctly refetch resetQueries matches#10837
raashish1601 wants to merge 3 commits into
TanStack:mainfrom
raashish1601:fix/query-core-resetqueries-state-filter-refetch

Conversation

@raashish1601
Copy link
Copy Markdown
Contributor

@raashish1601 raashish1601 commented May 30, 2026

Fix
esetQueries so it refetches queries selected by predicates that depend on pre-reset state (eg status === 'error').

The implementation now snapshots the initially-matched query set before calling
eset(), then refetches only those query hashes so status mutations during reset do not mutate filtering behavior.

Included regression test:

  • should refetch queries matched by a state-dependent predicate even though reset() mutates state

Summary by CodeRabbit

  • Bug Fixes

    • Fixed Angular Query to properly handle pending tasks when query signals are accessed before render, preventing Angular's whenStable() from resolving prematurely.
    • Improved resetQueries() to correctly refetch only the queries that were initially matched by the provided filter.
  • Tests

    • Added test coverage for query signal access behavior and pending task handling in Angular Query.
    • Added test coverage for resetQueries() predicate-based filtering.

Review Change Stack

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 30, 2026

📝 Walkthrough

Walkthrough

This PR adds pending task tracking to Angular Query to stabilize Angular's whenStable() until async query functions resolve, and fixes a Query Core bug where resetQueries predicates evaluated mutated state instead of the initial match.

Changes

Angular pending task tracking for whenStable stability

Layer / File(s) Summary
Pending task tracking in createBaseQuery
packages/angular-query-experimental/src/create-base-query.ts
Wraps default queryFn to track promise completion as pending tasks via pendingTaskRefsFromQueryFn. Refactors subscriber update into updateState helper managing task refs and error handling within ngZone.run. Cleanup completes all pending tasks before clearing.
Tests for whenStable stability
packages/angular-query-experimental/src/__tests__/pending-tasks.test.ts, packages/angular-query-experimental/src/__tests__/inject-query.test.ts
Validates whenStable() remains pending when query data is pre-accessed before render, and correctly resolves after destroying a component with unresolved queries. NeverResolvesComponent simulates never-resolving query scenario.
Changeset documentation
.changeset/angular-query-preaccess-whenstable.md
Documents patch release for @tanstack/angular-query-experimental recording whenStable stability when query signals are accessed before render.

Query Core resetQueries predicate filtering

Layer / File(s) Summary
resetQueries snapshot-and-refetch logic
packages/query-core/src/queryClient.ts
Collects matched query hashes before resetting, then refetches only queries in that precomputed set via predicate, decoupling refetch selection from mutations that reset() introduces.
Test for state-dependent predicate
packages/query-core/src/__tests__/queryClient.test.tsx
Creates observers reaching success and error states, verifies resetQueries with state-dependent predicates refetches only originally matching queries despite state mutation during reset.

Sequence Diagram

sequenceDiagram
  participant Component
  participant createBaseQuery
  participant QueryFn
  participant PendingTask
  participant whenStable
  Component->>createBaseQuery: inject query, access data signal before render
  createBaseQuery->>QueryFn: wrapped call, register pending task
  QueryFn->>PendingTask: promise created
  Component->>whenStable: called immediately
  whenStable->>PendingTask: check pending (task incomplete)
  whenStable->>whenStable: remains pending
  Component->>Component: advance timers
  QueryFn->>PendingTask: promise resolves
  PendingTask->>createBaseQuery: complete pending task ref
  createBaseQuery->>whenStable: stability check
  whenStable->>Component: resolves
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 When queries dance and signals call,
We track each task before they fall,
With pending marks and snapshot care,
The stable state floats through the air!
Angular's zen is ours to keep,
No premature awake from sleep! 🌙✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description explains the bug, the implementation approach, and includes a regression test, but lacks the standard template sections (Changes, Checklist, Release Impact). Fill in the template sections: 🎯 Changes with detailed explanation, ✅ Checklist with required boxes checked, and 🚀 Release Impact indicating changeset generation.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main fix: correcting resetQueries to properly refetch matched queries.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
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

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: 2

🤖 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/angular-query-experimental/src/__tests__/pending-tasks.test.ts`:
- Around line 329-349: The test may be a false positive because it never asserts
that app.whenStable() (stableResult) is pending before destroy; update the test
around NeverResolvesComponent to first create stableResult = app.whenStable()
after fixture.detectChanges(), then assert it has not yet resolved (e.g., race
it against a short timeout and expect the timeout to win) before calling
fixture.destroy(), and only then advance timers and assert stableResult
resolves; reference the symbols TestBed.inject(ApplicationRef),
fixture.detectChanges(), stableResult, fixture.destroy(), and
vi.advanceTimersByTimeAsync to find and modify the code paths.

In `@packages/angular-query-experimental/src/create-base-query.ts`:
- Around line 67-75: The wrapper for defaultedOptions.queryFn currently calls
markPendingQueryFnTask() and only invokes complete() on the promise settle,
leaving the pending task open if the AbortSignal aborts before the promise
settles; update the wrapper around originalQueryFn to also listen to
context.signal (or context.signal.addEventListener('abort', ...)) and call
complete() when the signal aborts, and ensure the abort listener is removed when
the promise settles (or when complete() runs) so markPendingQueryFnTask() is
always completed on either promise settle or signal abort.
🪄 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: e45f78c7-ae8b-4ad6-ae5c-4632e2a7476e

📥 Commits

Reviewing files that changed from the base of the PR and between 7fa2781 and 9a323f1.

📒 Files selected for processing (6)
  • .changeset/angular-query-preaccess-whenstable.md
  • packages/angular-query-experimental/src/__tests__/inject-query.test.ts
  • packages/angular-query-experimental/src/__tests__/pending-tasks.test.ts
  • packages/angular-query-experimental/src/create-base-query.ts
  • packages/query-core/src/__tests__/queryClient.test.tsx
  • packages/query-core/src/queryClient.ts

Comment on lines +329 to +349
it('should cleanup query promise pending task when component with unresolved query is destroyed', async () => {
const app = TestBed.inject(ApplicationRef)
const fixture = TestBed.createComponent(NeverResolvesComponent)

fixture.detectChanges()

const stableResult = app.whenStable().then(() => true)

fixture.destroy()

const timeoutResult = new Promise<boolean>((resolve) => {
setTimeout(() => {
resolve(false)
}, 10)
})

await vi.advanceTimersByTimeAsync(10)

const isStableResolved = await Promise.race([stableResult, timeoutResult])
expect(isStableResolved).toBe(true)
})
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 | 🟡 Minor | ⚡ Quick win

Assert instability before destroy so this can't pass as a false positive.

As written, this still passes if whenStable() resolves immediately after detectChanges() and destroy cleanup never does anything. Check that the promise is still pending before fixture.destroy(), then assert it resolves after destroy.

Suggested test tightening
     const app = TestBed.inject(ApplicationRef)
     const fixture = TestBed.createComponent(NeverResolvesComponent)
 
     fixture.detectChanges()
 
-    const stableResult = app.whenStable().then(() => true)
+    let stableResolved = false
+    const stableResult = app.whenStable().then(() => {
+      stableResolved = true
+      return true
+    })
+
+    await Promise.resolve()
+    expect(stableResolved).toBe(false)
 
     fixture.destroy()
 
     const timeoutResult = new Promise<boolean>((resolve) => {
       setTimeout(() => {
📝 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
it('should cleanup query promise pending task when component with unresolved query is destroyed', async () => {
const app = TestBed.inject(ApplicationRef)
const fixture = TestBed.createComponent(NeverResolvesComponent)
fixture.detectChanges()
const stableResult = app.whenStable().then(() => true)
fixture.destroy()
const timeoutResult = new Promise<boolean>((resolve) => {
setTimeout(() => {
resolve(false)
}, 10)
})
await vi.advanceTimersByTimeAsync(10)
const isStableResolved = await Promise.race([stableResult, timeoutResult])
expect(isStableResolved).toBe(true)
})
it('should cleanup query promise pending task when component with unresolved query is destroyed', async () => {
const app = TestBed.inject(ApplicationRef)
const fixture = TestBed.createComponent(NeverResolvesComponent)
fixture.detectChanges()
let stableResolved = false
const stableResult = app.whenStable().then(() => {
stableResolved = true
return true
})
await Promise.resolve()
expect(stableResolved).toBe(false)
fixture.destroy()
const timeoutResult = new Promise<boolean>((resolve) => {
setTimeout(() => {
resolve(false)
}, 10)
})
await vi.advanceTimersByTimeAsync(10)
const isStableResolved = await Promise.race([stableResult, timeoutResult])
expect(isStableResolved).toBe(true)
})
🤖 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/angular-query-experimental/src/__tests__/pending-tasks.test.ts`
around lines 329 - 349, The test may be a false positive because it never
asserts that app.whenStable() (stableResult) is pending before destroy; update
the test around NeverResolvesComponent to first create stableResult =
app.whenStable() after fixture.detectChanges(), then assert it has not yet
resolved (e.g., race it against a short timeout and expect the timeout to win)
before calling fixture.destroy(), and only then advance timers and assert
stableResult resolves; reference the symbols TestBed.inject(ApplicationRef),
fixture.detectChanges(), stableResult, fixture.destroy(), and
vi.advanceTimersByTimeAsync to find and modify the code paths.

Comment on lines +67 to +75
defaultedOptions.queryFn = (context) => {
const result = originalQueryFn(context)

if (result && typeof result.then === 'function') {
const complete = markPendingQueryFnTask()
void result.then(
complete,
complete,
)
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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Verify query function contexts expose AbortSignal:"
rg -n -C2 'signal:\s*AbortSignal' packages/query-core/src

echo
echo "Inspect the wrapped queryFn and PendingTask cleanup paths:"
sed -n '64,96p' packages/angular-query-experimental/src/create-base-query.ts
sed -n '183,193p' packages/angular-query-experimental/src/create-base-query.ts

Repository: TanStack/query

Length of output: 3458


Complete queryFn PendingTask on AbortSignal abort (not only promise settle)

packages/angular-query-experimental/src/create-base-query.ts wraps defaultedOptions.queryFn so thenables call markPendingQueryFnTask() but only complete() inside result.then(complete, complete). Since @tanstack/query-core provides queryFn context with an AbortSignal, a cancelled/superseded request whose promise never settles will keep the pending task open until Angular cleanup/destroy, which can keep whenStable() waiting. Hook complete() to context.signal abort too.

Suggested fix
       defaultedOptions.queryFn = (context) =&gt; {
         const result = originalQueryFn(context)
 
         if (result &amp;&amp; typeof result.then === 'function') {
           const complete = markPendingQueryFnTask()
+          const onAbort = () =&gt; {
+            complete()
+          }
+
+          context.signal.addEventListener('abort', onAbort, { once: true })
           void result.then(
-            complete,
-            complete,
+            () =&gt; {
+              context.signal.removeEventListener('abort', onAbort)
+              complete()
+            },
+            () =&gt; {
+              context.signal.removeEventListener('abort', onAbort)
+              complete()
+            },
           )
         }
 
         return result
       }
🤖 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/angular-query-experimental/src/create-base-query.ts` around lines 67
- 75, The wrapper for defaultedOptions.queryFn currently calls
markPendingQueryFnTask() and only invokes complete() on the promise settle,
leaving the pending task open if the AbortSignal aborts before the promise
settles; update the wrapper around originalQueryFn to also listen to
context.signal (or context.signal.addEventListener('abort', ...)) and call
complete() when the signal aborts, and ensure the abort listener is removed when
the promise settles (or when complete() runs) so markPendingQueryFnTask() is
always completed on either promise settle or signal abort.

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