Skip to content

Conversation

@arnoud-dv
Copy link
Collaborator

@arnoud-dv arnoud-dv commented Nov 23, 2025

Improves reactivity and timing

To benefit from the more mature signal ecosystem and PendingTasks integration in recent Angular versions this drops support for Angular v16-v18. These Angular versions are also not under LTS support anymore, so we keep support for all Angular versions currently under active or LTS support: v19 to v21

🎯 Changes

  • Bumped peer dependency from Angular 16+ to Angular 19+
  • Use queueMicrotask instead of notifyManager.batchCalls to prevent render glitches fixes Angular query: Optimistic updates are not synchronous #9735
  • Improve Angular PendingTasks integration
  • Query timing now correctly depends on framework component effect scheduling
  • use linkedSignal

Breaking Changes

  • Requires Angular v19+
  • Component effect timing may slightly change initialization order in edge cases

Aliasing query properties on a component will result in an error:

readonly #hasPreviousPage = this.query.hasPreviousPage

This should be resolved once the signalProxy is replaced.

✅ 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

Release Notes

  • Breaking Changes

    • Angular v19+ is now required (previously v16+)
  • New Features

    • Component effect scheduling support for Angular v19
  • Improvements

    • Simplified query API with direct access to public query state properties
    • Enhanced query lifecycle management for better component integration

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

@changeset-bot
Copy link

changeset-bot bot commented Nov 23, 2025

🦋 Changeset detected

Latest commit: 75cb8ca

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

This PR includes changesets to release 2 packages
Name Type
@tanstack/angular-query-experimental Minor
@tanstack/angular-query-persist-client Major

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

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 23, 2025

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Walkthrough

This changeset modernizes the Angular Query experimental package for Angular 19+, replacing the PendingTasks compatibility shim with Angular's native API, refactoring test suites to use standalone components with OnPush change detection, restructuring provider functions to return EnvironmentProviders, and updating core query/mutation lifecycle management to use Angular's component effect scheduling.

Changes

Cohort / File(s) Summary
Changeset & Documentation
.changeset/deep-crews-open.md, packages/angular-query-experimental/README.md
Added changeset entry for minor release marking Angular 19+ requirement and notes about component effect scheduling. Updated README to reflect Angular 19+ minimum version requirement.
Package Configuration
packages/angular-query-experimental/package.json
Updated peer dependency constraints for @angular/common and @angular/core from >=16.0.0 to >=19.0.0.
Example Components
examples/angular/infinite-query-with-max-pages/src/app/components/example.component.ts
Replaced private wrapper methods with direct public query methods (hasNextPage, isFetchingNextPage, hasPreviousPage, isFetchingPreviousPage). Removed private method declarations.
Example Templates
examples/angular/optimistic-updates/src/app/components/optimistic-updates.component.ts
Changed ngFor tracking from "task" item to "$index" for template loop optimization.
Core Query Logic
packages/angular-query-experimental/src/create-base-query.ts
Major refactor of observer lifecycle management: introduced dedicated observer tracking, result signal composition via linkedSignal, explicit pending tasks lifecycle via PendingTasks and DestroyRef, batch updates through notifyManager and NgZone, and error handling for fatal errors.
Core Mutation Logic
packages/angular-query-experimental/src/inject-mutation.ts
Replaced custom PENDING_TASKS token with Angular's native PendingTasks API. Renamed pending task tracking variable from pendingTaskRef to taskCleanupRef to store cleanup function from PendingTasks.add().
Compatibility Removal
packages/angular-query-experimental/src/pending-tasks-compat.ts
Removed entire compatibility shim file including PENDING_TASKS injection token, PendingTasksCompat type, and fallback factory (no longer needed with Angular 19+).
Provider Architecture
packages/angular-query-experimental/src/providers.ts
Updated provideTanStackQuery and provideAngularQuery to return EnvironmentProviders instead of Provider arrays via makeEnvironmentProviders. Expanded QueryFeature ɵproviders type to accept Provider | EnvironmentProviders.
Devtools Integration
packages/angular-query-experimental/src/devtools/with-devtools.ts
Replaced ENVIRONMENT_INITIALIZER factory pattern with provideEnvironmentInitializer wrapping. Introduced explicit destroyDevtools helper and lazy initialization of TanstackQueryDevtools via async import. Maintains error handling for missing devtools installation.
Test Suite: Infinite Query
packages/angular-query-experimental/src/__tests__/inject-infinite-query.test.ts
Replaced TestBed.runInInjectionContext with standalone component approach. Created temporary standalone components with OnPush change detection to host injectInfiniteQuery calls. Updated test flow to create fixture and extract query from componentInstance.
Test Suite: Mutation State
packages/angular-query-experimental/src/__tests__/inject-mutation-state.test.ts
Added ChangeDetectionStrategy import and set OnPush change detection on FakeComponent decorator.
Test Suite: Mutation
packages/angular-query-experimental/src/__tests__/inject-mutation.test.ts
Added ChangeDetectionStrategy import and configured OnPush change detection in two @Component decorators for test components.
Test Suite: Queries
packages/angular-query-experimental/src/__tests__/inject-queries.test.ts
Added ChangeDetectionStrategy import and applied OnPush change detection to Page component decorator.
Test Suite: Query (Extensive)
packages/angular-query-experimental/src/__tests__/inject-query.test.ts
Comprehensive refactor from direct injection context to component-based testing. Created standalone test components with OnPush change detection for each test scenario. Updated test setup to use TestBed.createComponent and fixture.detectChanges(). Preserved all type inferences and assertions on signals (data(), error(), status()). Expanded coverage with advanced scenarios: dependent queries, dynamic keys via signals, throwOnError handling, staleTime, refetch, enabled toggling, and HttpClient integration.
Test Suite: Pending Tasks
packages/angular-query-experimental/src/__tests__/pending-tasks.test.ts
Refactored multiple test cases to wrap query injections in standalone components with OnPush change detection. Replaced TestBed.runInInjectionContext blocks with fixture-based component instantiation. Updated assertions to reference fixture.componentInstance. Adjusted lifecycle interactions with explicit fixture.detectChanges() calls.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

  • packages/angular-query-experimental/src/create-base-query.ts: Major refactor of observer lifecycle with new signal composition patterns, pending tasks integration, and error handling—requires careful verification that all observer state transitions work correctly and that synchronous updates are now properly handled.
  • packages/angular-query-experimental/src/__tests__/inject-query.test.ts: Extensive test refactoring with numerous test cases migrated to component-based approach; requires verification that all type inferences, assertions, and behavioral scenarios are correctly preserved.
  • packages/angular-query-experimental/src/providers.ts: API signature changes (Provider arrays → EnvironmentProviders); review impacts on all consumers and verify composition logic with makeEnvironmentProviders.
  • packages/angular-query-experimental/src/devtools/with-devtools.ts: Control-flow restructuring from factory to provideEnvironmentInitializer pattern; verify initialization and teardown behavior.
  • Multiple test files: Consistent OnPush change detection additions across test suite require cross-verification that the pattern is correctly applied and doesn't introduce change detection issues.

Possibly related PRs

Poem

🐰 A rabbit's tale of Angular grace,
Where pending tasks find their place,
No more compat shims to hide—
Version nineteen, our trusted guide!
Synchronous updates, smooth and fast,
This refactor was worth the task!

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 71.43% 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
Title check ✅ Passed The title 'Refactor/require angular v19' is directly related to the main objective of the PR, which requires Angular v19+ by bumping peer dependencies and refactoring internal code.
Description check ✅ Passed The PR description follows the template structure with a 'Changes' section detailing modifications, breaking changes, and notes about limitations, plus a completed Release Impact checkbox indicating a changeset was generated.
Linked Issues check ✅ Passed The PR addresses the synchronous optimistic updates issue (#9735) by replacing notifyManager.batchCalls with queueMicrotask and improving Angular PendingTasks integration to ensure query timing depends on component effect scheduling.
Out of Scope Changes check ✅ Passed All changes are scoped to the Angular v19+ migration: peer dependency updates, refactoring for PendingTasks integration, use of linkedSignal, and test updates to reflect Angular component-based patterns.

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 Nov 23, 2025

View your CI Pipeline Execution ↗ for commit 75cb8ca

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

☁️ Nx Cloud last updated this comment at 2025-11-23 17:02:21 UTC

@arnoud-dv
Copy link
Collaborator Author

@coderabbitai review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 23, 2025

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@pkg-pr-new
Copy link

pkg-pr-new bot commented Nov 23, 2025

More templates

@tanstack/angular-query-experimental

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

@tanstack/eslint-plugin-query

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

@tanstack/query-async-storage-persister

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

@tanstack/query-broadcast-client-experimental

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

@tanstack/query-core

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

@tanstack/query-devtools

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

@tanstack/query-persist-client-core

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

@tanstack/query-sync-storage-persister

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

@tanstack/react-query

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

@tanstack/react-query-devtools

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

@tanstack/react-query-next-experimental

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

@tanstack/react-query-persist-client

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

@tanstack/solid-query

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

@tanstack/solid-query-devtools

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

@tanstack/solid-query-persist-client

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

@tanstack/svelte-query

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

@tanstack/svelte-query-devtools

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

@tanstack/svelte-query-persist-client

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

@tanstack/vue-query

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

@tanstack/vue-query-devtools

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

commit: 75cb8ca

@codecov
Copy link

codecov bot commented Nov 23, 2025

Codecov Report

❌ Patch coverage is 94.89051% with 7 lines in your changes missing coverage. Please review.
✅ Project coverage is 94.48%. Comparing base (66a194e) to head (75cb8ca).
⚠️ Report is 4 commits behind head on main.

Additional details and impacted files

Impacted file tree graph

@@             Coverage Diff             @@
##             main    #9893       +/-   ##
===========================================
+ Coverage   45.78%   94.48%   +48.69%     
===========================================
  Files         200       20      -180     
  Lines        8413      435     -7978     
  Branches     1922       99     -1823     
===========================================
- Hits         3852      411     -3441     
+ Misses       4113       23     -4090     
+ Partials      448        1      -447     
Components Coverage Δ
@tanstack/angular-query-experimental 94.23% <94.89%> (+0.37%) ⬆️
@tanstack/eslint-plugin-query ∅ <ø> (∅)
@tanstack/query-async-storage-persister ∅ <ø> (∅)
@tanstack/query-broadcast-client-experimental ∅ <ø> (∅)
@tanstack/query-codemods ∅ <ø> (∅)
@tanstack/query-core ∅ <ø> (∅)
@tanstack/query-devtools ∅ <ø> (∅)
@tanstack/query-persist-client-core ∅ <ø> (∅)
@tanstack/query-sync-storage-persister ∅ <ø> (∅)
@tanstack/query-test-utils ∅ <ø> (∅)
@tanstack/react-query ∅ <ø> (∅)
@tanstack/react-query-devtools ∅ <ø> (∅)
@tanstack/react-query-next-experimental ∅ <ø> (∅)
@tanstack/react-query-persist-client ∅ <ø> (∅)
@tanstack/solid-query ∅ <ø> (∅)
@tanstack/solid-query-devtools ∅ <ø> (∅)
@tanstack/solid-query-persist-client ∅ <ø> (∅)
@tanstack/svelte-query ∅ <ø> (∅)
@tanstack/svelte-query-devtools ∅ <ø> (∅)
@tanstack/svelte-query-persist-client ∅ <ø> (∅)
@tanstack/vue-query ∅ <ø> (∅)
@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.

@arnoud-dv arnoud-dv changed the title Refactor/require angular v19 refactor(angular-query): require Angular v19+ and rely on component effect schedule timing Nov 23, 2025
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: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/angular-query-experimental/src/__tests__/inject-query.test.ts (1)

712-983: Add direct-call test variant to properly exercise "outside injection context" behavior

The current test at line 712 instantiates a component inside TestBed, which provides an injection context. This means injectQuery is called within an existing injection context, not outside it—unlike the pattern used consistently across injectMutation, injectIsFetching, injectIsMutating, and injectMutationState tests.

To match the established pattern and properly verify that passing { injector } allows usage outside an injection context, add a direct-call variant alongside the component-based test:

const injector = TestBed.inject(Injector)

expect(() =>
  injectQuery(
    () => ({
      queryKey: ['manualInjector'],
      queryFn: () => sleep(0).then(() => 'Some data'),
    }),
    { injector },
  ),
).not.toThrow()

The new PendingTasks-oriented tests (whenStable(), synchronous queryFn, staleTime, enabled transitions, and invalidation) are correctly implemented per Angular 20 testing guidance.

🧹 Nitpick comments (6)
examples/angular/optimistic-updates/src/app/components/optimistic-updates.component.ts (1)

39-41: Tracking by $index is acceptable for this demo

Using track $index is fine here since the list is effectively append-only and tasks are simple values. Be aware this will remount items on reordering; if you ever introduce reordering or stable ids, consider tracking by a unique task identifier instead.

packages/angular-query-experimental/src/providers.ts (1)

1-9: Consider flattening feature providers before calling makeEnvironmentProviders

provideTanStackQuery now returns EnvironmentProviders, which is good. However, the current call:

return makeEnvironmentProviders([
  provideQueryClient(queryClient),
  features.map((feature) => feature.ɵproviders),
])

builds a nested structure (Provider | Array<Provider | EnvironmentProviders>). While Angular DI usually flattens provider arrays at runtime, this shape is a bit awkward for TypeScript and diverges from common patterns.

To align more closely with Angular’s own helpers and keep types clean, consider flattening:

export function provideTanStackQuery(
  queryClient: QueryClient | InjectionToken<QueryClient>,
  ...features: Array<QueryFeatures>,
): EnvironmentProviders {
  const featureProviders = features.flatMap((feature) => feature.ɵproviders)

  return makeEnvironmentProviders([
    provideQueryClient(queryClient),
    ...featureProviders,
  ])
}

This also fits well with the widened ɵproviders: Array<Provider | EnvironmentProviders> and keeps QueryFeature composition straightforward.

Please verify this against the current makeEnvironmentProviders signature and Angular’s own helper implementations (e.g. provideRouter, provideHttpClient) to ensure consistent usage.

Also applies to: 110-118, 129-132, 142-145, 153-156

packages/angular-query-experimental/src/devtools/with-devtools.ts (1)

11-12: Devtools env-initializer and lifecycle management look solid; refine missing-client error path

The move to provideEnvironmentInitializer with a signal-driven effect is well-structured:

  • Browser-only guard and DEVTOOLS_PROVIDED flag prevent duplicate initialization.
  • shouldLoadToolsSignal + effect correctly handle “load, update, destroy” behavior as options change.
  • Dynamic import is safely guarded by injectorIsDestroyed, and devtools teardown is wired into DestroyRef.

One small nit: getResolvedQueryClient() throws 'No QueryClient found', but that error is funneled into the same .catch block that logs:

'Install @tanstack/query-devtools or reinstall without --omit=optional.'

This message is misleading when the real issue is a missing QueryClient rather than a missing devtools package.

Consider branching in the catch to distinguish these cases, e.g.:

.catch((error: unknown) => {
  if (error instanceof Error && error.message === 'No QueryClient found') {
    console.error(
      'Angular Query Devtools: No QueryClient was found. ' +
        'Provide one via provideTanStackQuery or via devtools options.',
      error,
    )
    return
  }

  console.error(
    'Install @tanstack/query-devtools or reinstall without --omit=optional.',
    error,
  )
})

This will make configuration issues much easier to diagnose.

Please confirm this behavior against your intended public docs for withDevtools so the error messaging matches how you expect users to wire up their QueryClient.

Also applies to: 13-14, 61-71, 72-112, 113-169

packages/angular-query-experimental/src/create-base-query.ts (1)

2-10: Observer lifecycle, PendingTasks, and microtask batching are wired correctly; tiny naming nit

This refactor in createBaseQuery looks well thought out:

  • PendingTasks integration via startPendingTask/stopPendingTask ties task lifetime to fetchStatus and ensures cleanup on both subscription teardown and DestroyRef.onDestroy.
  • queueMicrotask around notifyManager.batch plus ngZone.run should align query notifications with Angular’s component effect scheduling and avoid the optimistic-update flicker you were seeing.
  • linkedSignal keyed by defaultedOptionsSignal, together with trackObserverResult/autoTrackResultProperties, maintains notifyOnChangeProps semantics while still allowing optimistic results.
  • OBSERVER_NOT_READY_ERROR is a good defensive guard against reading query state during construction; it will surface misuses clearly.

Only minor nit: the error message

'injectQuery: QueryObserver not initialized yet. Avoid reading the query result during construction'

is slightly misleading when createBaseQuery backs both injectQuery and injectInfiniteQuery. If you ever touch this again, consider a more generic wording (e.g. “Angular Query injection: …”) to avoid confusion in stack traces.

Overall, the lifecycle and batching changes look consistent with the PR objectives.

Given the subtle timing behavior this is meant to fix, please re-run the original optimistic-update repro (issue #9735) and any SSR/restoration scenarios you have to confirm the microtask-based batching behaves as intended across Angular 19–21.

Also applies to: 48-75, 91-147, 148-185, 186-215, 216-217

packages/angular-query-experimental/src/__tests__/inject-query.test.ts (2)

345-520: Reactive options tests look good; consider explicit detectChanges() after enabling

The signal-driven tests (options contained signal change, enabled signal, dependent queries, and dynamic queryKey for refetch) are nicely migrated to component-scoped queries and make good use of external signals + class fields to verify that options are recomputed under Angular signals.

One small robustness tweak: in test('should only run query once enabled signal is set to true'), the query’s enabled option depends on this.enabled() but after enabled.set(true) there is no explicit fixture.detectChanges(). While this likely works today because injectQuery tracks signals directly, adding a change detection call would make the test less coupled to internal implementation details:

@@
-    enabled.set(true)
-
-    await vi.advanceTimersByTimeAsync(11)
+    enabled.set(true)
+    fixture.detectChanges()
+
+    await vi.advanceTimersByTimeAsync(11)

This keeps behavior the same while making the dependency on Angular’s re-evaluation semantics explicit. Based on learnings.


522-645: throwOnError tests are thorough; watch global process handler usage

The updated throwOnError tests now:

  • Verify callback-based throwOnError using an OnPush component.
  • For the “throws” variants, hook both NgZone.onError and process.on('uncaughtException') and assert they receive the expected error, cleaning up the process listener in both the handler and finally block.

This gives strong coverage of how errors escape into Angular and Node. The only caveat is that relying on process.on('uncaughtException') is globally scoped; if other tests ever emit uncaught exceptions while these promises are pending, it could cause cross-test interference. If that becomes an issue, you might want to gate these tests behind a dedicated runner or add some defensive checks in the handler to ensure the error matches the expected shape/message before resolving.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ae47807 and d71c4e3.

📒 Files selected for processing (16)
  • .changeset/deep-crews-open.md (1 hunks)
  • examples/angular/infinite-query-with-max-pages/src/app/components/example.component.ts (1 hunks)
  • examples/angular/optimistic-updates/src/app/components/optimistic-updates.component.ts (1 hunks)
  • packages/angular-query-experimental/README.md (1 hunks)
  • packages/angular-query-experimental/package.json (1 hunks)
  • packages/angular-query-experimental/src/__tests__/inject-infinite-query.test.ts (3 hunks)
  • packages/angular-query-experimental/src/__tests__/inject-mutation-state.test.ts (2 hunks)
  • packages/angular-query-experimental/src/__tests__/inject-mutation.test.ts (3 hunks)
  • packages/angular-query-experimental/src/__tests__/inject-queries.test.ts (2 hunks)
  • packages/angular-query-experimental/src/__tests__/inject-query.test.ts (20 hunks)
  • packages/angular-query-experimental/src/__tests__/pending-tasks.test.ts (8 hunks)
  • packages/angular-query-experimental/src/create-base-query.ts (4 hunks)
  • packages/angular-query-experimental/src/devtools/with-devtools.ts (2 hunks)
  • packages/angular-query-experimental/src/inject-mutation.ts (4 hunks)
  • packages/angular-query-experimental/src/pending-tasks-compat.ts (0 hunks)
  • packages/angular-query-experimental/src/providers.ts (5 hunks)
💤 Files with no reviewable changes (1)
  • packages/angular-query-experimental/src/pending-tasks-compat.ts
🧰 Additional context used
🧠 Learnings (5)
📓 Common learnings
Learnt from: sukvvon
Repo: TanStack/query PR: 9892
File: packages/solid-query-persist-client/src/__tests__/PersistQueryClientProvider.test.tsx:331-335
Timestamp: 2025-11-22T09:06:05.219Z
Learning: In TanStack/query test files, when a queryFn contains side effects (e.g., setting flags for test verification), prefer async/await syntax for clarity; when there are no side effects, prefer the .then() pattern for conciseness.
Learnt from: arnoud-dv
Repo: TanStack/query PR: 9669
File: docs/framework/angular/guides/testing.md:49-56
Timestamp: 2025-09-21T00:31:02.518Z
Learning: TestBed.tick() is a valid API introduced in Angular 20 for triggering effects in unit tests, similar to ApplicationRef.tick() but for testing contexts.
📚 Learning: 2025-09-02T17:57:33.184Z
Learnt from: TkDodo
Repo: TanStack/query PR: 9612
File: packages/query-async-storage-persister/src/asyncThrottle.ts:0-0
Timestamp: 2025-09-02T17:57:33.184Z
Learning: When importing from tanstack/query-core in other TanStack Query packages like query-async-storage-persister, a workspace dependency "tanstack/query-core": "workspace:*" needs to be added to the package.json.

Applied to files:

  • packages/angular-query-experimental/package.json
  • packages/angular-query-experimental/src/inject-mutation.ts
  • packages/angular-query-experimental/src/create-base-query.ts
  • packages/angular-query-experimental/src/providers.ts
📚 Learning: 2025-11-22T09:06:05.219Z
Learnt from: sukvvon
Repo: TanStack/query PR: 9892
File: packages/solid-query-persist-client/src/__tests__/PersistQueryClientProvider.test.tsx:331-335
Timestamp: 2025-11-22T09:06:05.219Z
Learning: In TanStack/query test files, when a queryFn contains side effects (e.g., setting flags for test verification), prefer async/await syntax for clarity; when there are no side effects, prefer the .then() pattern for conciseness.

Applied to files:

  • .changeset/deep-crews-open.md
  • packages/angular-query-experimental/src/inject-mutation.ts
  • packages/angular-query-experimental/src/__tests__/inject-infinite-query.test.ts
  • 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
📚 Learning: 2025-09-21T00:31:02.518Z
Learnt from: arnoud-dv
Repo: TanStack/query PR: 9669
File: docs/framework/angular/guides/testing.md:49-56
Timestamp: 2025-09-21T00:31:02.518Z
Learning: TestBed.tick() is a valid API introduced in Angular 20 for triggering effects in unit tests, similar to ApplicationRef.tick() but for testing contexts.

Applied to files:

  • .changeset/deep-crews-open.md
  • packages/angular-query-experimental/src/__tests__/inject-queries.test.ts
  • packages/angular-query-experimental/src/__tests__/inject-infinite-query.test.ts
  • packages/angular-query-experimental/src/__tests__/inject-query.test.ts
  • packages/angular-query-experimental/src/__tests__/pending-tasks.test.ts
📚 Learning: 2025-08-19T03:18:18.303Z
Learnt from: oscartbeaumont
Repo: TanStack/query PR: 9564
File: packages/solid-query-devtools/src/production.tsx:2-3
Timestamp: 2025-08-19T03:18:18.303Z
Learning: In the solid-query-devtools package, the codebase uses a pattern of type-only default imports combined with typeof for component type annotations (e.g., `import type SolidQueryDevtoolsComp from './devtools'` followed by `typeof SolidQueryDevtoolsComp`). This pattern is consistently used across index.tsx and production.tsx files, and the maintainers prefer consistency over changing this approach.

Applied to files:

  • packages/angular-query-experimental/src/devtools/with-devtools.ts
⏰ 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 (15)
packages/angular-query-experimental/README.md (1)

32-32: Clear and well-placed version requirement notice.

The Quick Start update effectively communicates the Angular 19+ requirement before installation steps, preventing compatibility issues upfront.

.changeset/deep-crews-open.md (1)

1-5: Well-structured changeset entry.

The format and content are correct: minor version bump with a clear summary linking the version requirement to the underlying architectural change (component effect scheduling).

packages/angular-query-experimental/package.json (1)

108-109: Peer dependency constraints are correct and well-scoped.

The constraints (>=19.0.0) properly enforce the Angular 19+ requirement while allowing compatible future versions. DevDependencies at ^20.0.0 provide a reasonable development environment aligned with the peer constraint.

packages/angular-query-experimental/src/inject-mutation.ts (1)

4-11: PendingTasks integration and cleanup look correct

The switch to Angular’s PendingTasks plus taskCleanupRef works as expected: you add once when state.isPending becomes true, clear on resolution, and also clean up on effect teardown. This keeps the pending-task count accurate and avoids leaks even if the effect re-runs or unsubscribes while pending.

Please double-check against the Angular 19+/20 PendingTasks.add() contract (return type and availability) for all supported targets, just to be sure there are no surprising platform differences.

Also applies to: 61-63, 128-144, 161-164

packages/angular-query-experimental/src/__tests__/inject-mutation-state.test.ts (1)

2-8: OnPush change detection on FakeComponent looks correct

Using ChangeDetectionStrategy.OnPush on FakeComponent while driving it via setFixtureSignalInputs and explicit fixture.detectChanges() keeps the test closer to real-world usage without changing its assertions. No issues spotted.

Also applies to: 142-181

packages/angular-query-experimental/src/__tests__/inject-mutation.test.ts (1)

3-9: OnPush for mutation input tests is consistent and safe

The added ChangeDetectionStrategy.OnPush on the fake components used in the required-signal-input tests fits the new testing pattern (standalone + OnPush + explicit detectChanges()) and doesn't change the mutation semantics. Looks good.

Also applies to: 305-341

packages/angular-query-experimental/src/__tests__/inject-queries.test.ts (1)

4-8: OnPush on Page component keeps the test aligned with production usage

Switching the test Page component to ChangeDetectionStrategy.OnPush while still rendering it via @testing-library/angular maintains the intended behavior of the injectQueries effect-driven snapshot while aligning with the library's recommended pattern.

Also applies to: 32-42

packages/angular-query-experimental/src/__tests__/inject-infinite-query.test.ts (1)

31-78: Component-scoped infinite query test matches the new Angular integration

Refactoring the infinite-query test to a standalone, OnPush TestComponent whose query field uses injectInfiniteQuery and is accessed via a fixture keeps the assertions identical while better exercising the real integration (component effects, PendingTasks, fake timers). This pattern looks solid.

packages/angular-query-experimental/src/__tests__/pending-tasks.test.ts (5)

55-89: Synchronous queryFn + whenStable() test correctly exercises PendingTasks

Wrapping the synchronous queryFn in a standalone, OnPush component and asserting status: 'pending' before app.whenStable() then 'success' afterward ensures the PendingTasks integration still treats synchronous resolutions as async from Angular’s perspective. The flow and timer usage look sound.


195-231: Rapid refetch test nicely validates PendingTasks with side-effectful queryFn

The new TestComponent with callCount and an async queryFn (incrementing callCount then awaiting sleep) is a good fit for validating that multiple rapid refetch() calls do not leak PendingTasks. Using await vi.advanceTimersByTimeAsync(20) plus app.whenStable() should flush all attempts deterministically. This aligns with the async/await style for side-effectful queryFns. Based on learnings.


233-279: Offline-start test robustly covers paused queries and PendingTasks blocking

The new "start-offline" test that:

  • sets onlineManager offline,
  • initializes an injectQuery with networkMode: 'online',
  • verifies status: 'pending' + fetchStatus: 'paused', and
  • checks that app.whenStable() does not resolve until the app comes back online

is a good regression test for PendingTasks behavior when queries never enter the fetching state. The microtask and timer sequencing looks appropriate.


348-403: Component destruction tests correctly combine OnPush and PendingTasks cleanup

Adding ChangeDetectionStrategy.OnPush to TestComponent and calling fixture.detectChanges() before starting the query/mutation ensures the operations are actually in-flight when the component is destroyed. The subsequent app.whenStable() expectations validate that PendingTasks entries are cleaned up even if queries/mutations are torn down mid-flight. This is a solid coverage improvement.


406-459: Concurrent queries component test aligns well with new effect scheduling

The new standalone, OnPush TestComponent hosting three concurrent queries (two async, one synchronous) and driving them through a fixture gives good coverage of concurrent PendingTasks behavior in a realistic component context. The expectations before and after app.whenStable() look correct and should catch regressions in scheduling or task cleanup.

packages/angular-query-experimental/src/__tests__/inject-query.test.ts (2)

52-260: Type inference test via standalone OnPush component looks correct

Moving the “should return the correct types” assertions into a standalone, OnPush TestComponent that hosts a variety of injectQuery usages keeps the expectTypeOf checks intact while aligning with the rest of the test suite’s component-based setup. Although a few queryFns (e.g. the fetch('return Promise<any') case) will run at runtime, they don’t influence the compile-time type assertions and mirror the previous behavior.


262-343: Basic pending/success/error signal tests are well-adapted to the component pattern

The refactored tests for:

  • initial pending state,
  • successful resolution, and
  • rejection with appropriate error state

now use standalone, OnPush components with query as a field plus fixture-driven detectChanges(). The timer usage and assertions on status, data, and the various is* helpers correctly exercise the adapter’s behavior under the new effect scheduling.

Comment on lines 32 to 53
readonly nextButtonDisabled = computed(
() => !this.#hasNextPage() || this.#isFetchingNextPage(),
() => !this.query.hasNextPage() || this.query.isFetchingNextPage(),
)

readonly nextButtonText = computed(() =>
this.#isFetchingNextPage()
this.query.isFetchingNextPage()
? 'Loading more...'
: this.#hasNextPage()
: this.query.hasNextPage()
? 'Load newer'
: 'Nothing more to load',
)

readonly previousButtonDisabled = computed(
() => !this.#hasPreviousPage() || this.#isFetchingNextPage(),
() => !this.query.hasPreviousPage() || this.query.isFetchingNextPage(),
)
readonly previousButtonText = computed(() =>
this.#isFetchingPreviousPage()
this.query.isFetchingPreviousPage()
? 'Loading more...'
: this.#hasPreviousPage()
: this.query.hasPreviousPage()
? 'Load Older'
: 'Nothing more to load',
)
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Fix previousButtonDisabled to use isFetchingPreviousPage()

previousButtonDisabled currently checks this.query.isFetchingNextPage(), while the text and behavior for the previous button use isFetchingPreviousPage(). This likely means the "Previous" button can remain enabled while a previous-page fetch is in-flight, or be disabled for an unrelated next-page fetch.

Consider aligning the disabled logic with the previous-page fetch state:

-  readonly previousButtonDisabled = computed(
-    () => !this.query.hasPreviousPage() || this.query.isFetchingNextPage(),
-  )
+  readonly previousButtonDisabled = computed(
+    () => !this.query.hasPreviousPage() || this.query.isFetchingPreviousPage(),
+  )
📝 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
readonly nextButtonDisabled = computed(
() => !this.#hasNextPage() || this.#isFetchingNextPage(),
() => !this.query.hasNextPage() || this.query.isFetchingNextPage(),
)
readonly nextButtonText = computed(() =>
this.#isFetchingNextPage()
this.query.isFetchingNextPage()
? 'Loading more...'
: this.#hasNextPage()
: this.query.hasNextPage()
? 'Load newer'
: 'Nothing more to load',
)
readonly previousButtonDisabled = computed(
() => !this.#hasPreviousPage() || this.#isFetchingNextPage(),
() => !this.query.hasPreviousPage() || this.query.isFetchingNextPage(),
)
readonly previousButtonText = computed(() =>
this.#isFetchingPreviousPage()
this.query.isFetchingPreviousPage()
? 'Loading more...'
: this.#hasPreviousPage()
: this.query.hasPreviousPage()
? 'Load Older'
: 'Nothing more to load',
)
readonly nextButtonDisabled = computed(
() => !this.query.hasNextPage() || this.query.isFetchingNextPage(),
)
readonly nextButtonText = computed(() =>
this.query.isFetchingNextPage()
? 'Loading more...'
: this.query.hasNextPage()
? 'Load newer'
: 'Nothing more to load',
)
readonly previousButtonDisabled = computed(
() => !this.query.hasPreviousPage() || this.query.isFetchingPreviousPage(),
)
readonly previousButtonText = computed(() =>
this.query.isFetchingPreviousPage()
? 'Loading more...'
: this.query.hasPreviousPage()
? 'Load Older'
: 'Nothing more to load',
)
🤖 Prompt for AI Agents
In
examples/angular/infinite-query-with-max-pages/src/app/components/example.component.ts
around lines 32 to 53, the previousButtonDisabled computed currently checks
this.query.isFetchingNextPage() instead of using
this.query.isFetchingPreviousPage(), causing the Previous button's disabled
state to be tied to next-page fetches; update the computed to call
isFetchingPreviousPage() so the disabled state aligns with previous-page fetch
activity and remains consistent with previousButtonText and the intended
behavior.

Comment on lines 93 to 102
test('can be used outside injection context when passing an injector', () => {
const query = injectInfiniteQuery(
() => ({
queryKey: ['manualInjector'],
queryFn: ({ pageParam }) =>
sleep(0).then(() => 'data on page ' + pageParam),
initialPageParam: 0,
getNextPageParam: () => 12,
}),
{
injector: TestBed.inject(Injector),
},
)
const injector = TestBed.inject(Injector)

@Component({
selector: 'app-test',
template: '',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
})
class TestComponent {
query = injectInfiniteQuery(
() => ({
queryKey: ['manualInjector'],
queryFn: ({ pageParam }) =>
sleep(0).then(() => 'data on page ' + pageParam),
initialPageParam: 0,
getNextPageParam: () => 12,
}),
{
injector: injector,
},
)
}

const fixture = TestBed.createComponent(TestComponent)
fixture.detectChanges()
const query = fixture.componentInstance.query

expect(query.status()).toBe('pending')
})
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

Injection-context test no longer calls injectInfiniteQuery outside an injection context

In test('can be used outside injection context when passing an injector'), injectInfiniteQuery is now invoked inside a component constructor/field initializer, which already has an Angular injection context. The description suggests we still want to verify the "no injection context but manual injector" path.

To keep that behavior covered, consider adding (or restoring) a direct call pattern alongside the component-based test, for example:

-    @Component({
-      selector: 'app-test',
-      template: '',
-      standalone: true,
-      changeDetection: ChangeDetectionStrategy.OnPush,
-    })
-    class TestComponent {
-      query = injectInfiniteQuery(
-        () => ({
-          queryKey: ['manualInjector'],
-          queryFn: ({ pageParam }) =>
-            sleep(0).then(() => 'data on page ' + pageParam),
-          initialPageParam: 0,
-          getNextPageParam: () => 12,
-        }),
-        {
-          injector: injector,
-        },
-      )
-    }
+    // Ensure calling injectInfiniteQuery outside an Angular injection
+    // context works when an explicit injector is provided.
+    expect(() =>
+      injectInfiniteQuery(
+        () => ({
+          queryKey: ['manualInjector'],
+          queryFn: ({ pageParam }) =>
+            sleep(0).then(() => 'data on page ' + pageParam),
+          initialPageParam: 0,
+          getNextPageParam: () => 12,
+        }),
+        { injector },
+      ),
+    ).not.toThrow()

I'll verify this review comment by examining the test file and understanding the injection context behavior.

</script> ```

🏁 Script executed:

#!/bin/bash
# Examine the test file to see the full context and surrounding tests
cat packages/angular-query-experimental/src/__tests__/inject-infinite-query.test.ts

Length of output: 3294


Add a direct test call outside injection context to match test description

The test "can be used outside injection context when passing an injector" calls injectInfiniteQuery() inside a component's field initializer, which runs within Angular's automatic injection context. This contradicts the test's stated purpose.

The first injection context test correctly demonstrates calling outside a context (and verifies it throws without an injector). The second test should actually verify that providing an explicit injector allows the call to succeed outside a context.

Add a direct call pattern to verify the manual injector works without relying on Angular's context:

test('can be used outside injection context when passing an injector', () => {
  const injector = TestBed.inject(Injector)

+  // Ensure calling injectInfiniteQuery outside an Angular injection
+  // context works when an explicit injector is provided.
+  expect(() =>
+    injectInfiniteQuery(
+      () => ({
+        queryKey: ['manualInjector'],
+        queryFn: ({ pageParam }) =>
+          sleep(0).then(() => 'data on page ' + pageParam),
+        initialPageParam: 0,
+        getNextPageParam: () => 12,
+      }),
+      { injector },
+    ),
+  ).not.toThrow()
+
   @Component({
🤖 Prompt for AI Agents
In
packages/angular-query-experimental/src/__tests__/inject-infinite-query.test.ts
around lines 93 to 122, the test claims to verify calling injectInfiniteQuery
outside an injection context when passing an injector but currently calls it
inside a component field initializer (an automatic injection context). Change
the test to perform a direct call to injectInfiniteQuery outside any component:
obtain the injector via TestBed.inject(Injector), then call
injectInfiniteQuery(...) directly with the injector option (not inside a
component) and assert the returned query behaves as expected (e.g., status() ===
'pending'); remove or replace the component-based call so the test truly
exercises manual injector usage outside Angular's automatic context.

@arnoud-dv arnoud-dv force-pushed the refactor/require-angular-v19 branch from d71c4e3 to 39b8ae7 Compare November 23, 2025 16:17
@github-actions github-actions bot added the documentation Improvements or additions to documentation label Nov 23, 2025
@arnoud-dv arnoud-dv force-pushed the refactor/require-angular-v19 branch from 39b8ae7 to f1d23ff Compare November 23, 2025 16:31
@arnoud-dv arnoud-dv force-pushed the refactor/require-angular-v19 branch from f1d23ff to 75cb8ca Compare November 23, 2025 16:59
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: angular-query-experimental

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Angular query: Optimistic updates are not synchronous

1 participant