Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/deep-crews-open.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tanstack/angular-query-experimental': minor
---

require Angular v19+ and use Angular component effect scheduling
4 changes: 1 addition & 3 deletions docs/framework/angular/guides/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@ TanStack Query's `inject*` functions integrate with [`PendingTasks`](https://ang

This means tests and SSR can wait until mutations and queries resolve. In unit tests you can use `ApplicationRef.whenStable()` or `fixture.whenStable()` to await query completion. This works for both Zone.js and Zoneless setups.

> This integration requires Angular 19 or later. Earlier versions of Angular do not support `PendingTasks`.

## TestBed setup

Create a fresh `QueryClient` for every spec and provide it with `provideTanStackQuery` or `provideQueryClient`. This keeps caches isolated and lets you change default options per test:
Expand All @@ -31,7 +29,7 @@ TestBed.configureTestingModule({

> If your applications actual TanStack Query config is used in unit tests, make sure `withDevtools` is not accidentally included in test providers. This can cause slow tests. It is best to keep test and production configs separate.

If you share helpers, remember to call `queryClient.clear()` (or build a new instance) in `afterEach` so data from one test never bleeds into another.
If you share helpers, remember to call `queryClient.clear()` (or build a new instance) in `afterEach` so data from one test never bleeds into another. Prefer creating a fresh `QueryClient` per test: clearing only removes cached data, not custom defaults or listeners, so a reused client can leak configuration changes between specs and make failures harder to reason about. A new client keeps setup explicit and avoids any “invisible globals” influencing results.

## First query test

Expand Down
2 changes: 1 addition & 1 deletion docs/framework/angular/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ title: Installation

### NPM

_Angular Query is compatible with Angular v16 and higher_
_Angular Query is compatible with Angular v19 and higher_

```bash
npm i @tanstack/angular-query-experimental
Expand Down
2 changes: 1 addition & 1 deletion docs/framework/angular/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ We are in the process of getting to a stable API for TanStack Query on Angular.

## Supported Angular Versions

TanStack Query is compatible with Angular v16 and higher.
TanStack Query is compatible with Angular v19 and higher.

TanStack Query (FKA React Query) is often described as the missing data-fetching library for web applications, but in more technical terms, it makes **fetching, caching, synchronizing and updating server state** in your web applications a breeze.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,30 +30,25 @@ export class ExampleComponent {
}))

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.isFetchingPreviousPage(),
)
readonly previousButtonText = computed(() =>
this.#isFetchingPreviousPage()
this.query.isFetchingPreviousPage()
? 'Loading more...'
: this.#hasPreviousPage()
: this.query.hasPreviousPage()
? 'Load Older'
: 'Nothing more to load',
)

readonly #hasPreviousPage = this.query.hasPreviousPage
readonly #hasNextPage = this.query.hasNextPage
readonly #isFetchingPreviousPage = this.query.isFetchingPreviousPage
readonly #isFetchingNextPage = this.query.isFetchingNextPage
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ import { TasksService } from '../services/tasks.service'
<input type="text" [(ngModel)]="newItem" placeholder="Enter text" />
<button (click)="addItem()">Create</button>
<ul>
@for (task of tasks.data(); track task) {
@for (task of tasks.data(); track $index) {
<li>{{ task }}</li>
}
</ul>
Expand Down
2 changes: 1 addition & 1 deletion packages/angular-query-experimental/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ Visit https://tanstack.com/query/latest/docs/framework/angular/overview

# Quick Start

> The Angular adapter for TanStack Query requires Angular 16 or higher.
> The Angular adapter for TanStack Query requires Angular 19 or higher.
1. Install `angular-query`

Expand Down
4 changes: 2 additions & 2 deletions packages/angular-query-experimental/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -105,8 +105,8 @@
"@tanstack/query-devtools": "workspace:*"
},
"peerDependencies": {
"@angular/common": ">=16.0.0",
"@angular/core": ">=16.0.0"
"@angular/common": ">=19.0.0",
"@angular/core": ">=19.0.0"
},
"publishConfig": {
"directory": "dist",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@
import {
ElementRef,
provideZonelessChangeDetection,
signal,
} from '@angular/core'
import { ElementRef, signal } from '@angular/core'
import { TestBed } from '@angular/core/testing'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { QueryClient } from '@tanstack/query-core'
import { provideTanStackQuery } from '../providers'
import { injectDevtoolsPanel } from '../devtools-panel'
import { setupTanStackQueryTestBed } from './test-utils'

const mockDevtoolsPanelInstance = {
mount: vi.fn(),
Expand Down Expand Up @@ -40,12 +36,8 @@ describe('injectDevtoolsPanel', () => {
beforeEach(() => {
queryClient = new QueryClient()
mockElementRef = new ElementRef(document.createElement('div'))
TestBed.configureTestingModule({
providers: [
provideZonelessChangeDetection(),
provideTanStackQuery(queryClient),
{ provide: ElementRef, useValue: signal(mockElementRef) },
],
setupTanStackQueryTestBed(queryClient, {
providers: [{ provide: ElementRef, useValue: signal(mockElementRef) }],
})
})

Expand Down
Original file line number Diff line number Diff line change
@@ -1,38 +1,42 @@
import { TestBed } from '@angular/core/testing'
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'
import { Injector, provideZonelessChangeDetection } from '@angular/core'
import { ChangeDetectionStrategy, Component, Injector } from '@angular/core'
import { sleep } from '@tanstack/query-test-utils'
import { QueryClient, injectInfiniteQuery, provideTanStackQuery } from '..'
import { expectSignals } from './test-utils'
import { QueryClient, injectInfiniteQuery } from '..'
import { expectSignals, setupTanStackQueryTestBed } from './test-utils'

describe('injectInfiniteQuery', () => {
let queryClient: QueryClient

beforeEach(() => {
queryClient = new QueryClient()
vi.useFakeTimers()
TestBed.configureTestingModule({
providers: [
provideZonelessChangeDetection(),
provideTanStackQuery(queryClient),
],
})
setupTanStackQueryTestBed(queryClient)
})

afterEach(() => {
vi.useRealTimers()
})

test('should properly execute infinite query', async () => {
const query = TestBed.runInInjectionContext(() => {
return injectInfiniteQuery(() => ({
@Component({
selector: 'app-test',
template: '',
changeDetection: ChangeDetectionStrategy.OnPush,
})
class TestComponent {
query = injectInfiniteQuery(() => ({
queryKey: ['infiniteQuery'],
queryFn: ({ pageParam }) =>
sleep(10).then(() => 'data on page ' + pageParam),
initialPageParam: 0,
getNextPageParam: () => 12,
}))
})
}

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

expectSignals(query, {
data: undefined,
Expand Down Expand Up @@ -76,6 +80,9 @@ describe('injectInfiniteQuery', () => {
})

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

// Call injectInfiniteQuery directly outside any component
const query = injectInfiniteQuery(
() => ({
queryKey: ['manualInjector'],
Expand All @@ -85,10 +92,12 @@ describe('injectInfiniteQuery', () => {
getNextPageParam: () => 12,
}),
{
injector: TestBed.inject(Injector),
injector: injector,
},
)

TestBed.tick()

expect(query.status()).toBe('pending')
})
})
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@
import { TestBed } from '@angular/core/testing'
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'
import { Injector, provideZonelessChangeDetection } from '@angular/core'
import { Injector } from '@angular/core'
import { sleep } from '@tanstack/query-test-utils'
import {
QueryClient,
injectIsFetching,
injectQuery,
provideTanStackQuery,
} from '..'
import { QueryClient, injectIsFetching, injectQuery } from '..'
import { setupTanStackQueryTestBed } from './test-utils'

describe('injectIsFetching', () => {
let queryClient: QueryClient
Expand All @@ -16,12 +12,7 @@ describe('injectIsFetching', () => {
vi.useFakeTimers()
queryClient = new QueryClient()

TestBed.configureTestingModule({
providers: [
provideZonelessChangeDetection(),
provideTanStackQuery(queryClient),
],
})
setupTanStackQueryTestBed(queryClient)
})

afterEach(() => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'
import { TestBed } from '@angular/core/testing'
import { Injector, provideZonelessChangeDetection } from '@angular/core'
import { Injector } from '@angular/core'
import { sleep } from '@tanstack/query-test-utils'
import {
QueryClient,
injectIsMutating,
injectMutation,
provideTanStackQuery,
} from '..'
import { QueryClient, injectIsMutating, injectMutation } from '..'
import { flushQueryUpdates, setupTanStackQueryTestBed } from './test-utils'

describe('injectIsMutating', () => {
let queryClient: QueryClient
Expand All @@ -16,12 +12,7 @@ describe('injectIsMutating', () => {
vi.useFakeTimers()
queryClient = new QueryClient()

TestBed.configureTestingModule({
providers: [
provideZonelessChangeDetection(),
provideTanStackQuery(queryClient),
],
})
setupTanStackQueryTestBed(queryClient)
})

afterEach(() => {
Expand All @@ -44,7 +35,7 @@ describe('injectIsMutating', () => {
})

expect(isMutating()).toBe(0)
await vi.advanceTimersByTimeAsync(0)
await flushQueryUpdates()
expect(isMutating()).toBe(1)
await vi.advanceTimersByTimeAsync(11)
expect(isMutating()).toBe(0)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
ChangeDetectionStrategy,
Component,
Injector,
input,
Expand All @@ -7,15 +8,14 @@ import {
} from '@angular/core'
import { TestBed } from '@angular/core/testing'
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'
import { By } from '@angular/platform-browser'
import { sleep } from '@tanstack/query-test-utils'
import {
QueryClient,
injectMutation,
injectMutationState,
provideTanStackQuery,
} from '..'
import { setFixtureSignalInputs } from './test-utils'
import { registerSignalInput } from './test-utils'

describe('injectMutationState', () => {
let queryClient: QueryClient
Expand Down Expand Up @@ -145,6 +145,7 @@ describe('injectMutationState', () => {
<span>{{ mutation.status }}</span>
}
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
class FakeComponent {
name = input.required<string>()
Expand All @@ -157,23 +158,35 @@ describe('injectMutationState', () => {
}))
}

const fixture = TestBed.createComponent(FakeComponent)
const { debugElement } = fixture
setFixtureSignalInputs(fixture, { name: fakeName })
registerSignalInput(FakeComponent, 'name')

@Component({
template: `<app-fake [name]="name()" />`,
imports: [FakeComponent],
})
class HostComponent {
protected readonly name = signal(fakeName)
}

const fixture = TestBed.createComponent(HostComponent)
fixture.detectChanges()
await vi.advanceTimersByTimeAsync(0)

let spans = debugElement
.queryAll(By.css('span'))
.map((span) => span.nativeNode.textContent)
const readSpans = () =>
Array.from(
fixture.nativeElement.querySelectorAll(
'span',
) as NodeListOf<HTMLSpanElement>,
).map((span) => span.textContent)

let spans = readSpans()

expect(spans).toEqual(['pending', 'pending'])

await vi.advanceTimersByTimeAsync(11)
fixture.detectChanges()

spans = debugElement
.queryAll(By.css('span'))
.map((span) => span.nativeNode.textContent)
spans = readSpans()

expect(spans).toEqual(['success', 'error'])
})
Expand Down
Loading
Loading