diff --git a/docs/config.json b/docs/config.json
index ef151ed38f..8d14e3c7da 100644
--- a/docs/config.json
+++ b/docs/config.json
@@ -1046,6 +1046,10 @@
"label": "Basic",
"to": "framework/angular/examples/basic"
},
+ {
+ "label": "Auto Refetching / Polling / Realtime",
+ "to": "framework/angular/examples/auto-refetching"
+ },
{
"label": "Pagination",
"to": "framework/angular/examples/pagination"
diff --git a/examples/angular/auto-refetching/.devcontainer/devcontainer.json b/examples/angular/auto-refetching/.devcontainer/devcontainer.json
new file mode 100644
index 0000000000..365adf8f4c
--- /dev/null
+++ b/examples/angular/auto-refetching/.devcontainer/devcontainer.json
@@ -0,0 +1,4 @@
+{
+ "name": "Node.js",
+ "image": "mcr.microsoft.com/devcontainers/javascript-node:22"
+}
diff --git a/examples/angular/auto-refetching/.eslintrc.cjs b/examples/angular/auto-refetching/.eslintrc.cjs
new file mode 100644
index 0000000000..cca134ce16
--- /dev/null
+++ b/examples/angular/auto-refetching/.eslintrc.cjs
@@ -0,0 +1,6 @@
+// @ts-check
+
+/** @type {import('eslint').Linter.Config} */
+const config = {}
+
+module.exports = config
diff --git a/examples/angular/auto-refetching/README.md b/examples/angular/auto-refetching/README.md
new file mode 100644
index 0000000000..571955a305
--- /dev/null
+++ b/examples/angular/auto-refetching/README.md
@@ -0,0 +1,6 @@
+# TanStack Query Angular auto-refetching example
+
+To run this example:
+
+- `npm install` or `yarn` or `pnpm i` or `bun i`
+- `npm run start` or `yarn start` or `pnpm start` or `bun start`
diff --git a/examples/angular/auto-refetching/angular.json b/examples/angular/auto-refetching/angular.json
new file mode 100644
index 0000000000..2fd625bebe
--- /dev/null
+++ b/examples/angular/auto-refetching/angular.json
@@ -0,0 +1,104 @@
+{
+ "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
+ "version": 1,
+ "cli": {
+ "packageManager": "pnpm",
+ "analytics": false,
+ "cache": {
+ "enabled": false
+ }
+ },
+ "newProjectRoot": "projects",
+ "projects": {
+ "auto-refetching": {
+ "projectType": "application",
+ "schematics": {
+ "@schematics/angular:component": {
+ "inlineTemplate": true,
+ "inlineStyle": true,
+ "skipTests": true
+ },
+ "@schematics/angular:class": {
+ "skipTests": true
+ },
+ "@schematics/angular:directive": {
+ "skipTests": true
+ },
+ "@schematics/angular:guard": {
+ "skipTests": true
+ },
+ "@schematics/angular:interceptor": {
+ "skipTests": true
+ },
+ "@schematics/angular:pipe": {
+ "skipTests": true
+ },
+ "@schematics/angular:resolver": {
+ "skipTests": true
+ },
+ "@schematics/angular:service": {
+ "skipTests": true
+ }
+ },
+ "root": "",
+ "sourceRoot": "src",
+ "prefix": "app",
+ "architect": {
+ "build": {
+ "builder": "@angular/build:application",
+ "options": {
+ "outputPath": "dist/auto-refetching",
+ "index": "src/index.html",
+ "browser": "src/main.ts",
+ "polyfills": ["zone.js"],
+ "tsConfig": "tsconfig.app.json",
+ "assets": ["src/favicon.ico", "src/assets"],
+ "styles": [],
+ "scripts": []
+ },
+ "configurations": {
+ "production": {
+ "budgets": [
+ {
+ "type": "initial",
+ "maximumWarning": "500kb",
+ "maximumError": "1mb"
+ },
+ {
+ "type": "anyComponentStyle",
+ "maximumWarning": "2kb",
+ "maximumError": "4kb"
+ }
+ ],
+ "outputHashing": "all"
+ },
+ "development": {
+ "optimization": false,
+ "extractLicenses": false,
+ "sourceMap": true
+ }
+ },
+ "defaultConfiguration": "production"
+ },
+ "serve": {
+ "builder": "@angular/build:dev-server",
+ "configurations": {
+ "production": {
+ "buildTarget": "auto-refetching:build:production"
+ },
+ "development": {
+ "buildTarget": "auto-refetching:build:development"
+ }
+ },
+ "defaultConfiguration": "development"
+ },
+ "extract-i18n": {
+ "builder": "@angular/build:extract-i18n",
+ "options": {
+ "buildTarget": "auto-refetching:build"
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/examples/angular/auto-refetching/package.json b/examples/angular/auto-refetching/package.json
new file mode 100644
index 0000000000..f4867de9a8
--- /dev/null
+++ b/examples/angular/auto-refetching/package.json
@@ -0,0 +1,28 @@
+{
+ "name": "@tanstack/query-example-angular-auto-refetching",
+ "type": "module",
+ "scripts": {
+ "ng": "ng",
+ "start": "ng serve",
+ "build": "ng build",
+ "watch": "ng build --watch --configuration development"
+ },
+ "private": true,
+ "dependencies": {
+ "@angular/common": "^19.1.0-next.0",
+ "@angular/compiler": "^19.1.0-next.0",
+ "@angular/core": "^19.1.0-next.0",
+ "@angular/platform-browser": "^19.1.0-next.0",
+ "@angular/platform-browser-dynamic": "^19.1.0-next.0",
+ "@tanstack/angular-query-experimental": "^5.62.2",
+ "rxjs": "^7.8.1",
+ "tslib": "^2.6.3",
+ "zone.js": "^0.15.0"
+ },
+ "devDependencies": {
+ "@angular/build": "^19.0.2",
+ "@angular/cli": "^19.0.2",
+ "@angular/compiler-cli": "^19.1.0-next.0",
+ "typescript": "5.7.2"
+ }
+}
diff --git a/examples/angular/auto-refetching/src/app/app.component.ts b/examples/angular/auto-refetching/src/app/app.component.ts
new file mode 100644
index 0000000000..180391550b
--- /dev/null
+++ b/examples/angular/auto-refetching/src/app/app.component.ts
@@ -0,0 +1,11 @@
+import { ChangeDetectionStrategy, Component } from '@angular/core'
+import { AutoRefetchingExampleComponent } from './components/auto-refetching.component'
+
+@Component({
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ selector: 'app-root',
+ standalone: true,
+ template: ` `,
+ imports: [AutoRefetchingExampleComponent],
+})
+export class AppComponent {}
diff --git a/examples/angular/auto-refetching/src/app/app.config.ts b/examples/angular/auto-refetching/src/app/app.config.ts
new file mode 100644
index 0000000000..65a84a0c25
--- /dev/null
+++ b/examples/angular/auto-refetching/src/app/app.config.ts
@@ -0,0 +1,28 @@
+import {
+ provideHttpClient,
+ withFetch,
+ withInterceptors,
+} from '@angular/common/http'
+import {
+ QueryClient,
+ provideTanStackQuery,
+ withDevtools,
+} from '@tanstack/angular-query-experimental'
+import { mockInterceptor } from './interceptor/mock-api.interceptor'
+import type { ApplicationConfig } from '@angular/core'
+
+export const appConfig: ApplicationConfig = {
+ providers: [
+ provideHttpClient(withFetch(), withInterceptors([mockInterceptor])),
+ provideTanStackQuery(
+ new QueryClient({
+ defaultOptions: {
+ queries: {
+ gcTime: 1000 * 60 * 60 * 24, // 24 hours
+ },
+ },
+ }),
+ withDevtools(),
+ ),
+ ],
+}
diff --git a/examples/angular/auto-refetching/src/app/components/auto-refetching.component.html b/examples/angular/auto-refetching/src/app/components/auto-refetching.component.html
new file mode 100644
index 0000000000..f0359aae9b
--- /dev/null
+++ b/examples/angular/auto-refetching/src/app/components/auto-refetching.component.html
@@ -0,0 +1,35 @@
+
+
Auto Refetch with stale-time set to {{ intervalMs() }}ms
+
+ This example is best experienced on your own machine, where you can open
+ multiple tabs to the same localhost server and see your changes propagate
+ between the two.
+
+
+ Query Interval speed (ms):
+
+
+
+
Todo List
+
+
+
+ @for (item of tasks.data(); track item) {
+ {{ item }}
+ }
+
+
+ Clear All
+
+
diff --git a/examples/angular/auto-refetching/src/app/components/auto-refetching.component.ts b/examples/angular/auto-refetching/src/app/components/auto-refetching.component.ts
new file mode 100644
index 0000000000..01e86f2c9d
--- /dev/null
+++ b/examples/angular/auto-refetching/src/app/components/auto-refetching.component.ts
@@ -0,0 +1,46 @@
+import {
+ ChangeDetectionStrategy,
+ Component,
+ inject,
+ signal,
+} from '@angular/core'
+import {
+ injectMutation,
+ injectQuery,
+} from '@tanstack/angular-query-experimental'
+import { NgStyle } from '@angular/common'
+import { TasksService } from '../services/tasks.service'
+
+@Component({
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ selector: 'auto-refetching-example',
+ standalone: true,
+ templateUrl: './auto-refetching.component.html',
+ imports: [NgStyle],
+})
+export class AutoRefetchingExampleComponent {
+ #tasksService = inject(TasksService)
+
+ intervalMs = signal(1000)
+
+ tasks = injectQuery(() => this.#tasksService.allTasks(this.intervalMs()))
+
+ addMutation = injectMutation(() => this.#tasksService.addTask())
+ clearMutation = injectMutation(() => this.#tasksService.clearAllTasks())
+
+ clearTasks() {
+ this.clearMutation.mutate()
+ }
+
+ inputChange($event: Event) {
+ const target = $event.target as HTMLInputElement
+ this.intervalMs.set(Number(target.value))
+ }
+
+ addItem($event: Event) {
+ const target = $event.target as HTMLInputElement
+ const value = target.value
+ this.addMutation.mutate(value)
+ target.value = ''
+ }
+}
diff --git a/examples/angular/auto-refetching/src/app/interceptor/mock-api.interceptor.ts b/examples/angular/auto-refetching/src/app/interceptor/mock-api.interceptor.ts
new file mode 100644
index 0000000000..273e0d5a67
--- /dev/null
+++ b/examples/angular/auto-refetching/src/app/interceptor/mock-api.interceptor.ts
@@ -0,0 +1,46 @@
+/**
+ * MockApiInterceptor is used to simulate API responses for `/api/tasks` endpoints.
+ * It handles the following operations:
+ * - GET: Fetches all tasks from localStorage.
+ * - POST: Adds a new task to localStorage.
+ * - DELETE: Clears all tasks from localStorage.
+ * Simulated responses include a delay to mimic network latency.
+ */
+import { HttpResponse } from '@angular/common/http'
+import { delay, of } from 'rxjs'
+import type {
+ HttpEvent,
+ HttpHandlerFn,
+ HttpInterceptorFn,
+ HttpRequest,
+} from '@angular/common/http'
+import type { Observable } from 'rxjs'
+
+export const mockInterceptor: HttpInterceptorFn = (
+ req: HttpRequest,
+ next: HttpHandlerFn,
+): Observable> => {
+ const respondWith = (status: number, body: any) =>
+ of(new HttpResponse({ status, body })).pipe(delay(100))
+ if (req.url === '/api/tasks') {
+ switch (req.method) {
+ case 'GET':
+ return respondWith(
+ 200,
+ JSON.parse(localStorage.getItem('tasks') || '[]'),
+ )
+ case 'POST':
+ const tasks = JSON.parse(localStorage.getItem('tasks') || '[]')
+ tasks.push(req.body)
+ localStorage.setItem('tasks', JSON.stringify(tasks))
+ return respondWith(201, {
+ status: 'success',
+ task: req.body,
+ })
+ case 'DELETE':
+ localStorage.removeItem('tasks')
+ return respondWith(200, { status: 'success' })
+ }
+ }
+ return next(req)
+}
diff --git a/examples/angular/auto-refetching/src/app/services/tasks.service.ts b/examples/angular/auto-refetching/src/app/services/tasks.service.ts
new file mode 100644
index 0000000000..7fd8ce9a29
--- /dev/null
+++ b/examples/angular/auto-refetching/src/app/services/tasks.service.ts
@@ -0,0 +1,59 @@
+import { HttpClient } from '@angular/common/http'
+import { Injectable, inject } from '@angular/core'
+import {
+ QueryClient,
+ mutationOptions,
+ queryOptions,
+} from '@tanstack/angular-query-experimental'
+
+import { lastValueFrom } from 'rxjs'
+
+@Injectable({
+ providedIn: 'root',
+})
+export class TasksService {
+ #queryClient = inject(QueryClient) // Manages query state and caching
+ #http = inject(HttpClient) // Handles HTTP requests
+
+ /**
+ * Fetches all tasks from the API.
+ * Returns an observable containing an array of task strings.
+ */
+ allTasks = (intervalMs: number) =>
+ queryOptions({
+ queryKey: ['tasks'],
+ queryFn: () => {
+ return lastValueFrom(this.#http.get>('/api/tasks'))
+ },
+ refetchInterval: intervalMs,
+ })
+
+ /**
+ * Creates a mutation for adding a task.
+ * On success, invalidates and refetches the "tasks" query cache to update the task list.
+ */
+ addTask() {
+ return mutationOptions({
+ mutationFn: (task: string) =>
+ lastValueFrom(this.#http.post('/api/tasks', task)),
+ mutationKey: ['tasks'],
+ onSuccess: () => {
+ this.#queryClient.invalidateQueries({ queryKey: ['tasks'] })
+ },
+ })
+ }
+
+ /**
+ * Creates a mutation for clearing all tasks.
+ * On success, invalidates and refetches the "tasks" query cache to ensure consistency.
+ */
+ clearAllTasks() {
+ return mutationOptions({
+ mutationFn: () => lastValueFrom(this.#http.delete('/api/tasks')),
+ mutationKey: ['clearTasks'],
+ onSuccess: () => {
+ this.#queryClient.invalidateQueries({ queryKey: ['tasks'] })
+ },
+ })
+ }
+}
diff --git a/examples/angular/auto-refetching/src/favicon.ico b/examples/angular/auto-refetching/src/favicon.ico
new file mode 100644
index 0000000000..57614f9c96
Binary files /dev/null and b/examples/angular/auto-refetching/src/favicon.ico differ
diff --git a/examples/angular/auto-refetching/src/index.html b/examples/angular/auto-refetching/src/index.html
new file mode 100644
index 0000000000..d775852c72
--- /dev/null
+++ b/examples/angular/auto-refetching/src/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+ TanStack Query Angular auto-refetching example
+
+
+
+
+
+
+
+
diff --git a/examples/angular/auto-refetching/src/main.ts b/examples/angular/auto-refetching/src/main.ts
new file mode 100644
index 0000000000..b3963de6e2
--- /dev/null
+++ b/examples/angular/auto-refetching/src/main.ts
@@ -0,0 +1,13 @@
+import { bootstrapApplication } from '@angular/platform-browser'
+import { appConfig } from './app/app.config'
+import { AppComponent } from './app/app.component'
+
+bootstrapApplication(AppComponent, appConfig)
+ .then(() => {
+ // an simple endpoint for getting current list
+ localStorage.setItem(
+ 'tasks',
+ JSON.stringify(['Item 1', 'Item 2', 'Item 3']),
+ )
+ })
+ .catch((err) => console.error(err))
diff --git a/examples/angular/auto-refetching/tsconfig.app.json b/examples/angular/auto-refetching/tsconfig.app.json
new file mode 100644
index 0000000000..5b9d3c5ecb
--- /dev/null
+++ b/examples/angular/auto-refetching/tsconfig.app.json
@@ -0,0 +1,9 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "outDir": "./out-tsc/app",
+ "types": []
+ },
+ "files": ["src/main.ts"],
+ "include": ["src/**/*.d.ts"]
+}
diff --git a/examples/angular/auto-refetching/tsconfig.json b/examples/angular/auto-refetching/tsconfig.json
new file mode 100644
index 0000000000..d0d73c8beb
--- /dev/null
+++ b/examples/angular/auto-refetching/tsconfig.json
@@ -0,0 +1,31 @@
+{
+ "compileOnSave": false,
+ "compilerOptions": {
+ "outDir": "./dist/out-tsc",
+ "forceConsistentCasingInFileNames": true,
+ "strict": true,
+ "noImplicitOverride": true,
+ "noPropertyAccessFromIndexSignature": true,
+ "noImplicitReturns": true,
+ "noFallthroughCasesInSwitch": true,
+ "skipLibCheck": true,
+ "isolatedModules": true,
+ "esModuleInterop": true,
+ "sourceMap": true,
+ "declaration": false,
+ "experimentalDecorators": true,
+ "moduleResolution": "bundler",
+ "importHelpers": true,
+ "target": "ES2022",
+ "module": "ES2022",
+ "useDefineForClassFields": false,
+ "lib": ["ES2022", "dom"]
+ },
+ "angularCompilerOptions": {
+ "enableI18nLegacyMessageIdFormat": false,
+ "strictInjectionParameters": true,
+ "strictInputAccessModifiers": true,
+ "strictStandalone": true,
+ "strictTemplates": true
+ }
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 5973feb493..009d230c58 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -122,6 +122,49 @@ importers:
specifier: ^2.0.4
version: 2.0.5(@types/node@22.9.3)(jsdom@25.0.1)(less@4.2.1)(lightningcss@1.27.0)(sass@1.81.0)(terser@5.31.6)
+ examples/angular/auto-refetching:
+ dependencies:
+ '@angular/common':
+ specifier: ^19.1.0-next.0
+ version: 19.1.0-next.0(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0))(rxjs@7.8.1)
+ '@angular/compiler':
+ specifier: ^19.1.0-next.0
+ version: 19.1.0-next.0(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0))
+ '@angular/core':
+ specifier: ^19.1.0-next.0
+ version: 19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0)
+ '@angular/platform-browser':
+ specifier: ^19.1.0-next.0
+ version: 19.1.0-next.0(@angular/common@19.1.0-next.0(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0))(rxjs@7.8.1))(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0))
+ '@angular/platform-browser-dynamic':
+ specifier: ^19.1.0-next.0
+ version: 19.1.0-next.0(@angular/common@19.1.0-next.0(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0))(rxjs@7.8.1))(@angular/compiler@19.1.0-next.0(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0)))(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0))(@angular/platform-browser@19.1.0-next.0(@angular/common@19.1.0-next.0(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0))(rxjs@7.8.1))(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0)))
+ '@tanstack/angular-query-experimental':
+ specifier: ^5.62.2
+ version: link:../../../packages/angular-query-experimental
+ rxjs:
+ specifier: ^7.8.1
+ version: 7.8.1
+ tslib:
+ specifier: ^2.6.3
+ version: 2.8.1
+ zone.js:
+ specifier: ^0.15.0
+ version: 0.15.0
+ devDependencies:
+ '@angular/build':
+ specifier: ^19.0.2
+ version: 19.0.2(@angular/compiler-cli@19.1.0-next.0(@angular/compiler@19.1.0-next.0(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0)))(typescript@5.7.2))(@angular/compiler@19.1.0-next.0(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0)))(@types/node@22.9.3)(chokidar@4.0.1)(less@4.2.1)(lightningcss@1.27.0)(postcss@8.4.49)(tailwindcss@3.4.7)(terser@5.31.6)(typescript@5.7.2)
+ '@angular/cli':
+ specifier: ^19.0.2
+ version: 19.0.2(@types/node@22.9.3)(chokidar@4.0.1)
+ '@angular/compiler-cli':
+ specifier: ^19.1.0-next.0
+ version: 19.1.0-next.0(@angular/compiler@19.1.0-next.0(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0)))(typescript@5.7.2)
+ typescript:
+ specifier: 5.7.2
+ version: 5.7.2
+
examples/angular/basic:
dependencies:
'@angular/common':
@@ -8948,7 +8991,6 @@ packages:
critters@0.0.24:
resolution: {integrity: sha512-Oyqew0FGM0wYUSNqR0L6AteO5MpMoUU0rhKRieXeiKs+PmRTxiJMyaunYB2KF6fQ3dzChXKCpbFOEJx3OQ1v/Q==}
- deprecated: Ownership of Critters has moved to the Nuxt team, who will be maintaining the project going forward. If you'd like to keep using Critters, please switch to the actively-maintained fork at https://github.com/danielroe/beasties
croner@8.1.0:
resolution: {integrity: sha512-sz990XOUPR8dG/r5BRKMBd15MYDDUu8oeSaxFD5DqvNgHSZw8Psd1s689/IGET7ezxRMiNlCIyGeY1Gvxp/MLg==}
@@ -17021,7 +17063,7 @@ snapshots:
'@babel/helper-split-export-declaration': 7.24.7
'@babel/plugin-syntax-import-attributes': 7.26.0(@babel/core@7.26.0)
'@inquirer/confirm': 5.0.2(@types/node@22.9.3)
- '@vitejs/plugin-basic-ssl': 1.1.0(vite@5.4.11(@types/node@22.9.3)(less@4.2.1)(lightningcss@1.27.0)(sass@1.80.7)(terser@5.31.6))
+ '@vitejs/plugin-basic-ssl': 1.1.0(vite@5.4.11(@types/node@22.9.3)(less@4.2.1)(lightningcss@1.27.0)(sass@1.81.0)(terser@5.31.6))
beasties: 0.1.0
browserslist: 4.24.2
esbuild: 0.24.0
@@ -23419,9 +23461,9 @@ snapshots:
dependencies:
vite: 5.1.7(@types/node@22.9.3)(less@4.2.0)(lightningcss@1.27.0)(sass@1.71.1)(terser@5.29.1)
- '@vitejs/plugin-basic-ssl@1.1.0(vite@5.4.11(@types/node@22.9.3)(less@4.2.1)(lightningcss@1.27.0)(sass@1.80.7)(terser@5.31.6))':
+ '@vitejs/plugin-basic-ssl@1.1.0(vite@5.4.11(@types/node@22.9.3)(less@4.2.1)(lightningcss@1.27.0)(sass@1.81.0)(terser@5.31.6))':
dependencies:
- vite: 5.4.11(@types/node@22.9.3)(less@4.2.1)(lightningcss@1.27.0)(sass@1.80.7)(terser@5.31.6)
+ vite: 5.4.11(@types/node@22.9.3)(less@4.2.1)(lightningcss@1.27.0)(sass@1.81.0)(terser@5.31.6)
'@vitejs/plugin-basic-ssl@1.1.0(vite@5.4.6(@types/node@22.9.3)(less@4.2.0)(lightningcss@1.27.0)(sass@1.77.6)(terser@5.31.6))':
dependencies: