snapshot of https://github.com/TanStack/query.git (main, 2026-09-16; blobs > 131072B omitted)
Some checks failed
autofix.ci / autofix (push) Failing after 4s
Release / Release (push) Has been skipped
GitHub Actions Security Analysis / Run zizmor (push) Failing after 2s

This commit is contained in:
upstream-mirror
2026-09-16 01:13:08 +05:00
commit 3a013ccdfe
3570 changed files with 490194 additions and 0 deletions

View File

@@ -0,0 +1,4 @@
{
"name": "Node.js",
"image": "mcr.microsoft.com/devcontainers/javascript-node:22"
}

View File

@@ -0,0 +1,6 @@
// @ts-check
/** @type {import('eslint').Linter.Config} */
const config = {}
module.exports = config

View File

@@ -0,0 +1,6 @@
# TanStack Query Angular auto-refetching example
To run this example:
- `pnpm install`
- `pnpm start`

View File

@@ -0,0 +1,130 @@
{
"$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"
}
}
}
}
},
"schematics": {
"@schematics/angular:component": {
"type": "component"
},
"@schematics/angular:directive": {
"type": "directive"
},
"@schematics/angular:service": {
"type": "service"
},
"@schematics/angular:guard": {
"typeSeparator": "."
},
"@schematics/angular:interceptor": {
"typeSeparator": "."
},
"@schematics/angular:module": {
"typeSeparator": "."
},
"@schematics/angular:pipe": {
"typeSeparator": "."
},
"@schematics/angular:resolver": {
"typeSeparator": "."
}
}
}

View File

@@ -0,0 +1,27 @@
{
"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": "^20.0.0",
"@angular/compiler": "^20.0.0",
"@angular/core": "^20.0.0",
"@angular/platform-browser": "^20.0.0",
"@tanstack/angular-query-experimental": "^5.102.8",
"rxjs": "^7.8.2",
"tslib": "^2.8.1",
"zone.js": "0.15.0"
},
"devDependencies": {
"@angular/build": "^20.0.0",
"@angular/cli": "^20.0.0",
"@angular/compiler-cli": "^20.0.0",
"typescript": "5.8.3"
}
}

View File

@@ -0,0 +1,10 @@
import { ChangeDetectionStrategy, Component } from '@angular/core'
import { AutoRefetchingExampleComponent } from './components/auto-refetching.component'
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'app-root',
template: `<auto-refetching-example />`,
imports: [AutoRefetchingExampleComponent],
})
export class AppComponent {}

View File

@@ -0,0 +1,28 @@
import {
provideHttpClient,
withFetch,
withInterceptors,
} from '@angular/common/http'
import {
QueryClient,
provideTanStackQuery,
} from '@tanstack/angular-query-experimental'
import { withDevtools } from '@tanstack/angular-query-experimental/devtools'
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(),
),
],
}

View File

@@ -0,0 +1,35 @@
<div>
<h1>Auto Refetch with stale-time set to {{ intervalMs() }}ms</h1>
<p>
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.
</p>
<label>
Query Interval speed (ms):
<input [value]="intervalMs()" (input)="inputChange($event)" />
<span
[ngStyle]="{
display: 'inline-block',
marginLeft: '.5rem',
width: '10px',
height: '10px',
background: tasks.isFetching() ? 'green' : 'transparent',
transition: !tasks.isFetching() ? 'all .3s ease' : 'none',
borderRadius: '100%',
transform: 'scale(2)',
}"
></span>
</label>
<h2>Todo List</h2>
<input placeholder="Enter something" (keydown.enter)="addItem($event)" />
<ul>
@for (item of tasks.data(); track item) {
<li>{{ item }}</li>
}
</ul>
<div>
<button (click)="clearTasks()">Clear All</button>
</div>
</div>

View File

@@ -0,0 +1,49 @@
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',
templateUrl: './auto-refetching.component.html',
imports: [NgStyle],
})
export class AutoRefetchingExampleComponent {
readonly #tasksService = inject(TasksService)
readonly intervalMs = signal(1000)
readonly tasks = injectQuery(() =>
this.#tasksService.allTasks(this.intervalMs()),
)
readonly addMutation = injectMutation(() => this.#tasksService.addTask())
readonly 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 = ''
}
}

View File

@@ -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<unknown>,
next: HttpHandlerFn,
): Observable<HttpEvent<any>> => {
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)
}

View File

@@ -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 {
readonly #queryClient = inject(QueryClient) // Manages query state and caching
readonly #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<Array<string>>('/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'] })
},
})
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>TanStack Query Angular auto-refetching example</title>
<base href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" type="image/x-icon" href="favicon.ico" />
</head>
<body>
<app-root></app-root>
</body>
</html>

View File

@@ -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))

View File

@@ -0,0 +1,9 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/app",
"types": []
},
"files": ["src/main.ts"],
"include": ["src/**/*.d.ts"]
}

View File

@@ -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
}
}

View File

@@ -0,0 +1,4 @@
{
"name": "Node.js",
"image": "mcr.microsoft.com/devcontainers/javascript-node:22"
}

View File

@@ -0,0 +1,6 @@
// @ts-check
/** @type {import('eslint').Linter.Config} */
const config = {}
module.exports = config

View File

@@ -0,0 +1,6 @@
# TanStack Query Angular basic persister example
To run this example:
- `pnpm install`
- `pnpm start`

View File

@@ -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": {
"basic-persister": {
"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/basic-persister",
"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": "basic-persister:build:production"
},
"development": {
"buildTarget": "basic-persister:build:development"
}
},
"defaultConfiguration": "development"
},
"extract-i18n": {
"builder": "@angular/build:extract-i18n",
"options": {
"buildTarget": "basic-persister:build"
}
}
}
}
}
}

View File

@@ -0,0 +1,29 @@
{
"name": "@tanstack/query-example-angular-basic-persister",
"type": "module",
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"watch": "ng build --watch --configuration development"
},
"private": true,
"dependencies": {
"@angular/common": "^20.0.0",
"@angular/compiler": "^20.0.0",
"@angular/core": "^20.0.0",
"@angular/platform-browser": "^20.0.0",
"@tanstack/angular-query-experimental": "^5.102.8",
"@tanstack/angular-query-persist-client": "^5.102.8",
"@tanstack/query-async-storage-persister": "^5.102.8",
"rxjs": "^7.8.2",
"tslib": "^2.8.1",
"zone.js": "0.15.0"
},
"devDependencies": {
"@angular/build": "^20.0.0",
"@angular/cli": "^20.0.0",
"@angular/compiler-cli": "^20.0.0",
"typescript": "5.8.3"
}
}

View File

@@ -0,0 +1,10 @@
<p>
Try to mock offline behavior with the button in the devtools. You can navigate
around as long as there is already data in the cache. You'll get a refetch as
soon as you go "online" again.
</p>
@if (postId() > -1) {
<post [postId]="postId()" (setPostId)="postId.set($event)"></post>
} @else {
<posts (setPostId)="postId.set($event)" />
}

View File

@@ -0,0 +1,13 @@
import { ChangeDetectionStrategy, Component, signal } from '@angular/core'
import { PostComponent } from './components/post.component'
import { PostsComponent } from './components/posts.component'
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'basic-example',
templateUrl: './app.component.html',
imports: [PostComponent, PostsComponent],
})
export class BasicExampleComponent {
postId = signal(-1)
}

View File

@@ -0,0 +1,35 @@
import { provideHttpClient, withFetch } from '@angular/common/http'
import {
QueryClient,
provideTanStackQuery,
} from '@tanstack/angular-query-experimental'
import { withPersistQueryClient } from '@tanstack/angular-query-persist-client'
import { withDevtools } from '@tanstack/angular-query-experimental/devtools'
import { createAsyncStoragePersister } from '@tanstack/query-async-storage-persister'
import type { ApplicationConfig } from '@angular/core'
const localStoragePersister = createAsyncStoragePersister({
storage: window.localStorage,
})
export const appConfig: ApplicationConfig = {
providers: [
provideHttpClient(withFetch()),
provideTanStackQuery(
new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60, // 1 minute
gcTime: 1000 * 60 * 60 * 24, // 24 hours
},
},
}),
withDevtools(),
withPersistQueryClient({
persistOptions: {
persister: localStoragePersister,
},
}),
),
],
}

View File

@@ -0,0 +1,19 @@
<div>
<div>
<a (click)="setPostId.emit(-1)" href="#"> Back </a>
</div>
@if (postQuery.isPending()) {
Loading...
} @else if (postQuery.isError()) {
Error: {{ postQuery.error().message }}
}
@if (postQuery.data(); as post) {
<h1>{{ post.title }}</h1>
<div>
<p>{{ post.body }}</p>
</div>
@if (postQuery.isFetching()) {
Background Updating...
}
}
</div>

View File

@@ -0,0 +1,38 @@
import {
ChangeDetectionStrategy,
Component,
EventEmitter,
Output,
inject,
input,
} from '@angular/core'
import { QueryClient, injectQuery } from '@tanstack/angular-query-experimental'
import { fromEvent, lastValueFrom, takeUntil } from 'rxjs'
import { PostsService } from '../services/posts-service'
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'post',
templateUrl: './post.component.html',
})
export class PostComponent {
#postsService = inject(PostsService)
@Output() setPostId = new EventEmitter<number>()
postId = input(0)
postQuery = injectQuery(() => ({
enabled: this.postId() > 0,
queryKey: ['post', this.postId()],
queryFn: async (context) => {
// Cancels the request when component is destroyed before the request finishes
const abort$ = fromEvent(context.signal, 'abort')
return lastValueFrom(
this.#postsService.postById$(this.postId()).pipe(takeUntil(abort$)),
)
},
}))
queryClient = inject(QueryClient)
}

View File

@@ -0,0 +1,35 @@
<div>
<h1>Posts</h1>
@if (postsQuery.isPending()) {
Loading...
} @else if (postsQuery.isError()) {
Error: {{ postsQuery.error().message }}
} @else if (postsQuery.isSuccess()) {
<div class="todo-container">
@for (post of postsQuery.data(); track post.id) {
<p>
<!-- We can access the query data here to show bold links for-->
<!-- ones that are cached-->
<a
href="#"
(click)="setPostId.emit(post.id)"
[style]="
queryClient.getQueryData(['post', post.id])
? {
fontWeight: 'bold',
color: 'green',
}
: {}
"
>{{ post.title }}</a
>
</p>
}
</div>
}
<div>
@if (postsQuery.isFetching()) {
Background Updating...
}
</div>
</div>

View File

@@ -0,0 +1,27 @@
import {
ChangeDetectionStrategy,
Component,
EventEmitter,
Output,
inject,
} from '@angular/core'
import { QueryClient, injectQuery } from '@tanstack/angular-query-experimental'
import { lastValueFrom } from 'rxjs'
import { PostsService } from '../services/posts-service'
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'posts',
templateUrl: './posts.component.html',
})
export class PostsComponent {
queryClient = inject(QueryClient)
#postsService = inject(PostsService)
@Output() setPostId = new EventEmitter<number>()
postsQuery = injectQuery(() => ({
queryKey: ['posts'],
queryFn: () => lastValueFrom(this.#postsService.allPosts$()),
}))
}

View File

@@ -0,0 +1,21 @@
import { HttpClient } from '@angular/common/http'
import { Injectable, inject } from '@angular/core'
@Injectable({
providedIn: 'root',
})
export class PostsService {
#http = inject(HttpClient)
postById$ = (postId: number) =>
this.#http.get<Post>(`https://jsonplaceholder.typicode.com/posts/${postId}`)
allPosts$ = () =>
this.#http.get<Array<Post>>('https://jsonplaceholder.typicode.com/posts')
}
export interface Post {
id: number
title: string
body: string
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>TanStack Query Angular basic persister example</title>
<base href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" type="image/x-icon" href="favicon.ico" />
</head>
<body>
<basic-example></basic-example>
</body>
</html>

View File

@@ -0,0 +1,7 @@
import { bootstrapApplication } from '@angular/platform-browser'
import { appConfig } from './app/app.config'
import { BasicExampleComponent } from './app/app.component'
bootstrapApplication(BasicExampleComponent, appConfig).catch((err) =>
console.error(err),
)

View File

@@ -0,0 +1,9 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/app",
"types": []
},
"files": ["src/main.ts"],
"include": ["src/**/*.d.ts"]
}

View File

@@ -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
}
}

View File

@@ -0,0 +1,4 @@
{
"name": "Node.js",
"image": "mcr.microsoft.com/devcontainers/javascript-node:22"
}

View File

@@ -0,0 +1,6 @@
// @ts-check
/** @type {import('eslint').Linter.Config} */
const config = {}
module.exports = config

View File

@@ -0,0 +1,6 @@
# TanStack Query Angular basic example
To run this example:
- `pnpm install`
- `pnpm start`

View File

@@ -0,0 +1,130 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"cli": {
"packageManager": "pnpm",
"analytics": false,
"cache": {
"enabled": false
}
},
"newProjectRoot": "projects",
"projects": {
"basic": {
"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/basic",
"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": "basic:build:production"
},
"development": {
"buildTarget": "basic:build:development"
}
},
"defaultConfiguration": "development"
},
"extract-i18n": {
"builder": "@angular/build:extract-i18n",
"options": {
"buildTarget": "basic:build"
}
}
}
}
},
"schematics": {
"@schematics/angular:component": {
"type": "component"
},
"@schematics/angular:directive": {
"type": "directive"
},
"@schematics/angular:service": {
"type": "service"
},
"@schematics/angular:guard": {
"typeSeparator": "."
},
"@schematics/angular:interceptor": {
"typeSeparator": "."
},
"@schematics/angular:module": {
"typeSeparator": "."
},
"@schematics/angular:pipe": {
"typeSeparator": "."
},
"@schematics/angular:resolver": {
"typeSeparator": "."
}
}
}

View File

@@ -0,0 +1,27 @@
{
"name": "@tanstack/query-example-angular-basic",
"type": "module",
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"watch": "ng build --watch --configuration development"
},
"private": true,
"dependencies": {
"@angular/common": "^20.0.0",
"@angular/compiler": "^20.0.0",
"@angular/core": "^20.0.0",
"@angular/platform-browser": "^20.0.0",
"@tanstack/angular-query-experimental": "^5.102.8",
"rxjs": "^7.8.2",
"tslib": "^2.8.1",
"zone.js": "0.15.0"
},
"devDependencies": {
"@angular/build": "^20.0.0",
"@angular/cli": "^20.0.0",
"@angular/compiler-cli": "^20.0.0",
"typescript": "5.8.3"
}
}

View File

@@ -0,0 +1,15 @@
<p>
As you visit the posts below, you will notice them in a loading state the
first time you load them. However, after you return to this list and click on
any posts you have already visited again, you will see them load instantly and
background refresh right before your eyes!
<strong>
(You may need to throttle your network speed to simulate longer loading
sequences)
</strong>
</p>
@if (postId() > -1) {
<post [postId]="postId()" (setPostId)="postId.set($event)"></post>
} @else {
<posts (setPostId)="postId.set($event)" />
}

View File

@@ -0,0 +1,13 @@
import { ChangeDetectionStrategy, Component, signal } from '@angular/core'
import { PostComponent } from './components/post.component'
import { PostsComponent } from './components/posts.component'
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'basic-example',
templateUrl: './app.component.html',
imports: [PostComponent, PostsComponent],
})
export class BasicExampleComponent {
readonly postId = signal(-1)
}

View File

@@ -0,0 +1,23 @@
import { provideHttpClient, withFetch } from '@angular/common/http'
import {
QueryClient,
provideTanStackQuery,
} from '@tanstack/angular-query-experimental'
import { withDevtools } from '@tanstack/angular-query-experimental/devtools'
import type { ApplicationConfig } from '@angular/core'
export const appConfig: ApplicationConfig = {
providers: [
provideHttpClient(withFetch()),
provideTanStackQuery(
new QueryClient({
defaultOptions: {
queries: {
gcTime: 1000 * 60 * 60 * 24, // 24 hours
},
},
}),
withDevtools(),
),
],
}

View File

@@ -0,0 +1,19 @@
<div>
<div>
<a (click)="setPostId.emit(-1)" href="#"> Back </a>
</div>
@if (postQuery.isPending()) {
Loading...
} @else if (postQuery.isError()) {
Error: {{ postQuery.error().message }}
}
@if (postQuery.data(); as post) {
<h1>{{ post.title }}</h1>
<div>
<p>{{ post.body }}</p>
</div>
@if (postQuery.isFetching()) {
Background Updating...
}
}
</div>

View File

@@ -0,0 +1,34 @@
import {
ChangeDetectionStrategy,
Component,
inject,
input,
output,
} from '@angular/core'
import { injectQuery } from '@tanstack/angular-query-experimental'
import { fromEvent, lastValueFrom, takeUntil } from 'rxjs'
import { PostsService } from '../services/posts-service'
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'post',
templateUrl: './post.component.html',
})
export class PostComponent {
readonly #postsService = inject(PostsService)
readonly setPostId = output<number>()
readonly postId = input(0)
readonly postQuery = injectQuery(() => ({
enabled: this.postId() > 0,
queryKey: ['post', this.postId()],
queryFn: (context) => {
// Cancels the request when component is destroyed before the request finishes
const abort$ = fromEvent(context.signal, 'abort')
return lastValueFrom(
this.#postsService.postById$(this.postId()).pipe(takeUntil(abort$)),
)
},
}))
}

View File

@@ -0,0 +1,35 @@
<div>
<h1>Posts</h1>
@if (postsQuery.isPending()) {
Loading...
} @else if (postsQuery.isError()) {
Error: {{ postsQuery.error().message }}
} @else if (postsQuery.isSuccess()) {
<div class="todo-container">
@for (post of postsQuery.data(); track post.id) {
<p>
<!-- We can access the query data here to show bold links for-->
<!-- ones that are cached-->
<a
href="#"
(click)="setPostId.emit(post.id)"
[style]="
queryClient.getQueryData(['post', post.id])
? {
fontWeight: 'bold',
color: 'green',
}
: {}
"
>{{ post.title }}</a
>
</p>
}
</div>
}
<div>
@if (postsQuery.isFetching()) {
Background Updating...
}
</div>
</div>

View File

@@ -0,0 +1,25 @@
import {
ChangeDetectionStrategy,
Component,
inject,
output,
} from '@angular/core'
import { QueryClient, injectQuery } from '@tanstack/angular-query-experimental'
import { lastValueFrom } from 'rxjs'
import { PostsService } from '../services/posts-service'
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'posts',
templateUrl: './posts.component.html',
})
export class PostsComponent {
readonly queryClient = inject(QueryClient)
readonly #postsService = inject(PostsService)
readonly setPostId = output<number>()
readonly postsQuery = injectQuery(() => ({
queryKey: ['posts'],
queryFn: () => lastValueFrom(this.#postsService.allPosts$()),
}))
}

View File

@@ -0,0 +1,21 @@
import { HttpClient } from '@angular/common/http'
import { Injectable, inject } from '@angular/core'
@Injectable({
providedIn: 'root',
})
export class PostsService {
readonly #http = inject(HttpClient)
postById$ = (postId: number) =>
this.#http.get<Post>(`https://jsonplaceholder.typicode.com/posts/${postId}`)
allPosts$ = () =>
this.#http.get<Array<Post>>('https://jsonplaceholder.typicode.com/posts')
}
export interface Post {
id: number
title: string
body: string
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>TanStack Query Angular basic example</title>
<base href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" type="image/x-icon" href="favicon.ico" />
</head>
<body>
<basic-example></basic-example>
</body>
</html>

View File

@@ -0,0 +1,7 @@
import { bootstrapApplication } from '@angular/platform-browser'
import { appConfig } from './app/app.config'
import { BasicExampleComponent } from './app/app.component'
bootstrapApplication(BasicExampleComponent, appConfig).catch((err) =>
console.error(err),
)

View File

@@ -0,0 +1,9 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/app",
"types": []
},
"files": ["src/main.ts"],
"include": ["src/**/*.d.ts"]
}

View File

@@ -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
}
}

View File

@@ -0,0 +1,4 @@
{
"name": "Node.js",
"image": "mcr.microsoft.com/devcontainers/javascript-node:22"
}

View File

@@ -0,0 +1,6 @@
# TanStack Query Angular devtools panel example
To run this example:
- `pnpm install`
- `pnpm start`

View File

@@ -0,0 +1,128 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"cli": {
"packageManager": "pnpm",
"analytics": false,
"cache": {
"enabled": false
}
},
"newProjectRoot": "projects",
"projects": {
"devtools-panel": {
"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/devtools-panel",
"index": "src/index.html",
"browser": "src/main.ts",
"polyfills": ["zone.js"],
"tsConfig": "tsconfig.app.json",
"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": "devtools-panel:build:production"
},
"development": {
"buildTarget": "devtools-panel:build:development"
}
},
"defaultConfiguration": "development"
},
"extract-i18n": {
"builder": "@angular/build:extract-i18n",
"options": {
"buildTarget": "devtools-panel:build"
}
}
}
}
},
"schematics": {
"@schematics/angular:component": {
"type": "component"
},
"@schematics/angular:directive": {
"type": "directive"
},
"@schematics/angular:service": {
"type": "service"
},
"@schematics/angular:guard": {
"typeSeparator": "."
},
"@schematics/angular:interceptor": {
"typeSeparator": "."
},
"@schematics/angular:module": {
"typeSeparator": "."
},
"@schematics/angular:pipe": {
"typeSeparator": "."
},
"@schematics/angular:resolver": {
"typeSeparator": "."
}
}
}

View File

@@ -0,0 +1,28 @@
{
"name": "@tanstack/query-example-angular-devtools-panel",
"type": "module",
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"watch": "ng build --watch --configuration development"
},
"private": true,
"dependencies": {
"@angular/common": "^20.0.0",
"@angular/compiler": "^20.0.0",
"@angular/core": "^20.0.0",
"@angular/platform-browser": "^20.0.0",
"@angular/router": "^20.0.0",
"@tanstack/angular-query-experimental": "^5.102.8",
"rxjs": "^7.8.2",
"tslib": "^2.8.1",
"zone.js": "0.15.0"
},
"devDependencies": {
"@angular/build": "^20.0.0",
"@angular/cli": "^20.0.0",
"@angular/compiler-cli": "^20.0.0",
"typescript": "5.8.3"
}
}

View File

@@ -0,0 +1,21 @@
import { ChangeDetectionStrategy, Component } from '@angular/core'
import { RouterLink, RouterOutlet } from '@angular/router'
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'app-root',
template: `
<ul>
<li>
<a routerLink="basic">Basic devtools panel example</a>
</li>
<li>
<a routerLink="lazy">Lazy load devtools panel example</a>
</li>
</ul>
<router-outlet />
`,
imports: [RouterOutlet, RouterLink],
})
export class AppComponent {}

View File

@@ -0,0 +1,17 @@
import { provideHttpClient, withFetch } from '@angular/common/http'
import { provideRouter } from '@angular/router'
import {
QueryClient,
provideTanStackQuery,
} from '@tanstack/angular-query-experimental'
import { routes } from './app.routes'
import type { ApplicationConfig } from '@angular/core'
export const appConfig: ApplicationConfig = {
providers: [
provideHttpClient(withFetch()),
provideRouter(routes),
provideTanStackQuery(new QueryClient()),
],
}

View File

@@ -0,0 +1,19 @@
import type { Route } from '@angular/router'
export const routes: Array<Route> = [
{
path: '',
redirectTo: 'basic',
pathMatch: 'full',
},
{
path: 'basic',
loadComponent: () =>
import('./components/basic-devtools-panel-example.component'),
},
{
path: 'lazy',
loadComponent: () =>
import('./components/lazy-load-devtools-panel-example.component'),
},
]

View File

@@ -0,0 +1,41 @@
import {
ChangeDetectionStrategy,
Component,
signal,
viewChild,
} from '@angular/core'
import { injectDevtoolsPanel } from '@tanstack/angular-query-experimental/devtools-panel'
import { ExampleQueryComponent } from './example-query.component'
import type { ElementRef } from '@angular/core'
@Component({
selector: 'basic-devtools-panel-example',
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<example-query />
<h1>Basic devtools panel example</h1>
<p>
In this example, the devtools panel is loaded programmatically when the
button is clicked
</p>
<button type="button" (click)="toggleIsOpen()">
{{ isOpen() ? 'Close' : 'Open' }} the devtools panel
</button>
@if (isOpen()) {
<div #div style="height: 500px"></div>
}
`,
imports: [ExampleQueryComponent],
})
export default class BasicDevtoolsPanelExampleComponent {
readonly isOpen = signal(false)
readonly divEl = viewChild<ElementRef>('div')
toggleIsOpen() {
this.isOpen.update((prev) => !prev)
}
readonly devtools = injectDevtoolsPanel(() => ({
hostElement: this.divEl(),
}))
}

View File

@@ -0,0 +1,46 @@
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
import { injectQuery } from '@tanstack/angular-query-experimental'
import { HttpClient } from '@angular/common/http'
import { lastValueFrom } from 'rxjs'
interface Response {
name: string
description: string
subscribers_count: number
stargazers_count: number
forks_count: number
}
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'example-query',
template: `
<div style="padding-bottom: 20px">
@if (query.isPending()) {
<div>Loading...</div>
}
@if (query.isError()) {
<div>An error has occurred: {{ query.error().message }}</div>
}
@if (query.isSuccess()) {
@let data = query.data();
<h1>{{ data.name }}</h1>
<p>{{ data.description }}</p>
<strong>👀 {{ data.subscribers_count }}</strong>
<strong>✨ {{ data.stargazers_count }}</strong>
<strong>🍴 {{ data.forks_count }}</strong>
}
</div>
`,
})
export class ExampleQueryComponent {
readonly #http = inject(HttpClient)
readonly query = injectQuery(() => ({
queryKey: ['repoData'],
queryFn: () =>
lastValueFrom(
this.#http.get<Response>('https://api.github.com/repos/tanstack/query'),
),
}))
}

View File

@@ -0,0 +1,61 @@
import {
ChangeDetectionStrategy,
Component,
Injector,
computed,
effect,
inject,
signal,
viewChild,
} from '@angular/core'
import { ExampleQueryComponent } from './example-query.component'
import type { ElementRef } from '@angular/core'
import type { DevtoolsPanelRef } from '@tanstack/angular-query-experimental/devtools-panel'
@Component({
selector: 'lazy-load-devtools-panel-example',
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<example-query />
<h1>Lazy load devtools panel example</h1>
<p>
In this example, the devtools panel is loaded programmatically when the
button is clicked. In addition, the code is lazy loaded.
</p>
<button type="button" (click)="toggleIsOpen()">
{{ isOpen() ? 'Close' : 'Open' }} the devtools panel
</button>
@if (isOpen()) {
<div #div style="height: 500px"></div>
}
`,
imports: [ExampleQueryComponent],
})
export default class LazyLoadDevtoolsPanelExampleComponent {
readonly isOpen = signal(false)
readonly devtools = signal<Promise<DevtoolsPanelRef> | undefined>(undefined)
readonly injector = inject(Injector)
readonly divEl = viewChild<ElementRef>('div')
readonly devToolsOptions = computed(() => ({
hostElement: this.divEl(),
}))
toggleIsOpen() {
this.isOpen.update((prev) => !prev)
}
readonly loadDevtoolsEffect = effect(() => {
if (this.devtools()) return
if (this.isOpen()) {
this.devtools.set(
import('@tanstack/angular-query-experimental/devtools-panel').then(
({ injectDevtoolsPanel }) =>
injectDevtoolsPanel(this.devToolsOptions, {
injector: this.injector,
}),
),
)
}
})
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>TanStack Query devtools panel example</title>
<base href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" type="image/x-icon" href="favicon.ico" />
</head>
<body>
<app-root></app-root>
</body>
</html>

View File

@@ -0,0 +1,5 @@
import { bootstrapApplication } from '@angular/platform-browser'
import { appConfig } from './app/app.config'
import { AppComponent } from './app/app.component'
bootstrapApplication(AppComponent, appConfig).catch((err) => console.error(err))

View File

@@ -0,0 +1,10 @@
/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/app",
"types": []
},
"files": ["src/main.ts"],
"include": ["src/**/*.d.ts"]
}

View File

@@ -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
}
}

View File

@@ -0,0 +1,4 @@
{
"name": "Node.js",
"image": "mcr.microsoft.com/devcontainers/javascript-node:22"
}

View File

@@ -0,0 +1,6 @@
// @ts-check
/** @type {import('eslint').Linter.Config} */
const config = {}
module.exports = config

View File

@@ -0,0 +1,6 @@
# TanStack Query Angular infinite query example
To run this example:
- `pnpm install`
- `pnpm start`

View File

@@ -0,0 +1,130 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"cli": {
"packageManager": "pnpm",
"analytics": false,
"cache": {
"enabled": false
}
},
"newProjectRoot": "projects",
"projects": {
"infinite-query-with-max-pages": {
"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/infinite-query-with-max-pages",
"index": "src/index.html",
"browser": "src/main.ts",
"polyfills": ["zone.js"],
"tsConfig": "tsconfig.app.json",
"assets": ["src/favicon.ico", "src/mockServiceWorker.js"],
"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": "infinite-query-with-max-pages:build:production"
},
"development": {
"buildTarget": "infinite-query-with-max-pages:build:development"
}
},
"defaultConfiguration": "development"
},
"extract-i18n": {
"builder": "@angular/build:extract-i18n",
"options": {
"buildTarget": "infinite-query-with-max-pages:build"
}
}
}
}
},
"schematics": {
"@schematics/angular:component": {
"type": "component"
},
"@schematics/angular:directive": {
"type": "directive"
},
"@schematics/angular:service": {
"type": "service"
},
"@schematics/angular:guard": {
"typeSeparator": "."
},
"@schematics/angular:interceptor": {
"typeSeparator": "."
},
"@schematics/angular:module": {
"typeSeparator": "."
},
"@schematics/angular:pipe": {
"typeSeparator": "."
},
"@schematics/angular:resolver": {
"typeSeparator": "."
}
}
}

View File

@@ -0,0 +1,27 @@
{
"name": "@tanstack/query-example-angular-infinite-query-with-max-pages",
"private": true,
"type": "module",
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"watch": "ng build --watch --configuration development"
},
"dependencies": {
"@angular/common": "^20.0.0",
"@angular/compiler": "^20.0.0",
"@angular/core": "^20.0.0",
"@angular/platform-browser": "^20.0.0",
"@tanstack/angular-query-experimental": "^5.102.8",
"rxjs": "^7.8.2",
"tslib": "^2.8.1",
"zone.js": "0.15.0"
},
"devDependencies": {
"@angular/build": "^20.0.0",
"@angular/cli": "^20.0.0",
"@angular/compiler-cli": "^20.0.0",
"typescript": "5.8.3"
}
}

View File

@@ -0,0 +1,45 @@
import { HttpResponse } from '@angular/common/http'
import { delayWhen, of, timer } from 'rxjs'
import type { Observable } from 'rxjs'
import type { HttpEvent, HttpInterceptorFn } from '@angular/common/http'
export const projectsMockInterceptor: HttpInterceptorFn = (
req,
next,
): Observable<HttpEvent<any>> => {
const { url } = req
if (url.includes('/api/projects')) {
const cursor = parseInt(
new URLSearchParams(req.url.split('?')[1]).get('cursor') || '0',
10,
)
const pageSize = 4
const data = Array(pageSize)
.fill(0)
.map((_, i) => {
return {
name: 'Project ' + (i + cursor) + ` (server time: ${Date.now()})`,
id: i + cursor,
}
})
const nextId = cursor < 20 ? data[data.length - 1].id + 1 : null
const previousId = cursor > -20 ? data[0].id - pageSize : null
// Simulate network latency with a random delay between 100ms and 500ms
const delayDuration = Math.random() * (500 - 100) + 100
return of(
new HttpResponse({
status: 200,
body: {
data,
nextId,
previousId,
},
}),
).pipe(delayWhen(() => timer(delayDuration)))
}
return next(req)
}

View File

@@ -0,0 +1,10 @@
import { ChangeDetectionStrategy, Component } from '@angular/core'
import { ExampleComponent } from './components/example.component'
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'app-root',
template: `<example />`,
imports: [ExampleComponent],
})
export class AppComponent {}

View File

@@ -0,0 +1,28 @@
import {
provideHttpClient,
withFetch,
withInterceptors,
} from '@angular/common/http'
import {
QueryClient,
provideTanStackQuery,
} from '@tanstack/angular-query-experimental'
import { withDevtools } from '@tanstack/angular-query-experimental/devtools'
import { projectsMockInterceptor } from './api/projects-mock.interceptor'
import type { ApplicationConfig } from '@angular/core'
export const appConfig: ApplicationConfig = {
providers: [
provideHttpClient(withInterceptors([projectsMockInterceptor]), withFetch()),
provideTanStackQuery(
new QueryClient({
defaultOptions: {
queries: {
gcTime: 1000 * 60 * 60 * 24, // 24 hours
},
},
}),
withDevtools(),
),
],
}

View File

@@ -0,0 +1,37 @@
<div>
<h1>Infinite Query with max pages</h1>
<h3>4 projects per page</h3>
<h3>3 pages max</h3>
@if (query.isPending()) {
<p>Loading...</p>
} @else if (query.isError()) {
<span>Error: {{ query.error().message }}</span>
} @else {
<div>
<button
(click)="query.fetchPreviousPage()"
[disabled]="previousButtonDisabled()"
>
{{ previousButtonText() }}
</button>
</div>
@for (page of query.data().pages; track $index) {
@for (project of page.data; track project.id) {
<p [projectStyle]="project.id">{{ project.name }} {{ project.id }}</p>
}
}
<div>
<button (click)="query.fetchNextPage()" [disabled]="nextButtonDisabled()">
{{ nextButtonText() }}
</button>
</div>
<div>
{{
query.isFetching() && !query.isFetchingNextPage()
? 'Background Updating...'
: null
}}
</div>
}
<hr />
</div>

View File

@@ -0,0 +1,59 @@
import {
ChangeDetectionStrategy,
Component,
computed,
inject,
} from '@angular/core'
import { injectInfiniteQuery } from '@tanstack/angular-query-experimental'
import { lastValueFrom } from 'rxjs'
import { ProjectStyleDirective } from '../directives/project-style.directive'
import { ProjectsService } from '../services/projects.service'
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'example',
templateUrl: './example.component.html',
imports: [ProjectStyleDirective],
})
export class ExampleComponent {
readonly projectsService = inject(ProjectsService)
readonly query = injectInfiniteQuery(() => ({
queryKey: ['projects'],
queryFn: ({ pageParam }) => {
return lastValueFrom(this.projectsService.getProjects(pageParam))
},
initialPageParam: 0,
getPreviousPageParam: (firstPage) => firstPage.previousId ?? undefined,
getNextPageParam: (lastPage) => lastPage.nextId ?? undefined,
maxPages: 3,
}))
readonly nextButtonDisabled = computed(
() => !this.#hasNextPage() || this.#isFetchingNextPage(),
)
readonly nextButtonText = computed(() =>
this.#isFetchingNextPage()
? 'Loading more...'
: this.#hasNextPage()
? 'Load newer'
: 'Nothing more to load',
)
readonly previousButtonDisabled = computed(
() => !this.#hasPreviousPage() || this.#isFetchingNextPage(),
)
readonly previousButtonText = computed(() =>
this.#isFetchingPreviousPage()
? 'Loading more...'
: this.#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
}

View File

@@ -0,0 +1,22 @@
import { Directive, computed, input } from '@angular/core'
@Directive({
selector: '[projectStyle]',
host: {
'[style]': 'style()',
},
})
export class ProjectStyleDirective {
readonly projectStyle = input.required<number>()
readonly style = computed(
() =>
`
border: 1px solid gray;
border-radius: 5px;
padding: 8px;
font-size: 14px;
background: hsla(${this.projectStyle() * 30}, 60%, 80%, 1);
`,
)
}

View File

@@ -0,0 +1,22 @@
import { HttpClient } from '@angular/common/http'
import { Injectable, inject } from '@angular/core'
interface Project {
id: number
name: string
}
interface ProjectResponse {
data: Array<Project>
nextId: number | undefined
previousId: number | undefined
}
@Injectable({
providedIn: 'root',
})
export class ProjectsService {
readonly #http = inject(HttpClient)
getProjects = (page: number) =>
this.#http.get<ProjectResponse>(`/api/projects?cursor=${page}`)
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>TanStack Query Angular infinite query example</title>
<base href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" type="image/x-icon" href="favicon.ico" />
</head>
<body>
<app-root></app-root>
</body>
</html>

View File

@@ -0,0 +1,5 @@
import { bootstrapApplication } from '@angular/platform-browser'
import { appConfig } from './app/app.config'
import { AppComponent } from './app/app.component'
bootstrapApplication(AppComponent, appConfig).catch((err) => console.error(err))

View File

@@ -0,0 +1,9 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/app",
"types": []
},
"files": ["src/main.ts"],
"include": ["src/**/*.d.ts"]
}

View File

@@ -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
}
}

View File

@@ -0,0 +1,4 @@
{
"name": "Node.js",
"image": "mcr.microsoft.com/devcontainers/javascript-node:22"
}

View File

@@ -0,0 +1,6 @@
// @ts-check
/** @type {import('eslint').Linter.Config} */
const config = {}
module.exports = config

View File

@@ -0,0 +1,6 @@
# TanStack Query Angular optimistic-updates example
To run this example:
- `pnpm install`
- `pnpm start`

View File

@@ -0,0 +1,130 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"cli": {
"packageManager": "pnpm",
"analytics": false,
"cache": {
"enabled": false
}
},
"newProjectRoot": "projects",
"projects": {
"optimistic-updates": {
"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/optimistic-updates",
"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": "optimistic-updates:build:production"
},
"development": {
"buildTarget": "optimistic-updates:build:development"
}
},
"defaultConfiguration": "development"
},
"extract-i18n": {
"builder": "@angular/build:extract-i18n",
"options": {
"buildTarget": "optimistic-updates:build"
}
}
}
}
},
"schematics": {
"@schematics/angular:component": {
"type": "component"
},
"@schematics/angular:directive": {
"type": "directive"
},
"@schematics/angular:service": {
"type": "service"
},
"@schematics/angular:guard": {
"typeSeparator": "."
},
"@schematics/angular:interceptor": {
"typeSeparator": "."
},
"@schematics/angular:module": {
"typeSeparator": "."
},
"@schematics/angular:pipe": {
"typeSeparator": "."
},
"@schematics/angular:resolver": {
"typeSeparator": "."
}
}
}

View File

@@ -0,0 +1,28 @@
{
"name": "@tanstack/query-example-angular-optimistic-updates",
"type": "module",
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"watch": "ng build --watch --configuration development"
},
"private": true,
"dependencies": {
"@angular/common": "^20.0.0",
"@angular/compiler": "^20.0.0",
"@angular/core": "^20.0.0",
"@angular/forms": "^20.0.0",
"@angular/platform-browser": "^20.0.0",
"@tanstack/angular-query-experimental": "^5.102.8",
"rxjs": "^7.8.2",
"tslib": "^2.8.1",
"zone.js": "0.15.0"
},
"devDependencies": {
"@angular/build": "^20.0.0",
"@angular/cli": "^20.0.0",
"@angular/compiler-cli": "^20.0.0",
"typescript": "5.8.3"
}
}

View File

@@ -0,0 +1,10 @@
import { ChangeDetectionStrategy, Component } from '@angular/core'
import { OptimisticUpdatesComponent } from './components/optimistic-updates.component'
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'app-root',
template: `<optimistic-updates />`,
imports: [OptimisticUpdatesComponent],
})
export class AppComponent {}

View File

@@ -0,0 +1,28 @@
import {
provideHttpClient,
withFetch,
withInterceptors,
} from '@angular/common/http'
import {
QueryClient,
provideTanStackQuery,
} from '@tanstack/angular-query-experimental'
import { withDevtools } from '@tanstack/angular-query-experimental/devtools'
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(),
),
],
}

View File

@@ -0,0 +1,73 @@
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
import {
injectMutation,
injectQuery,
} from '@tanstack/angular-query-experimental'
import { FormsModule } from '@angular/forms'
import { DatePipe } from '@angular/common'
import { TasksService } from '../services/tasks.service'
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'optimistic-updates',
imports: [FormsModule, DatePipe],
template: `
<p>
In this example, new items can be created using a mutation. The new item
will be optimistically added to the list in hopes that the server accepts
the item. If it does, the list is refetched with the true items from the
list. Every now and then, the mutation may fail though. When that happens,
the previous list of items is restored and the list is again refetched
from the server.
</p>
<hr />
@if (tasks.isLoading()) {
<p>Loading...</p>
}
<div class="container">
<label>
<input type="checkbox" [(ngModel)]="failMutation" />
Fail Mutation
</label>
<div class="input-container">
<input type="text" [(ngModel)]="newItem" placeholder="Enter text" />
<button (click)="addItem()">Create</button>
<ul>
@for (task of tasks.data(); track task) {
<li>{{ task }}</li>
}
</ul>
<div>
Updated At: {{ tasks.dataUpdatedAt() | date: 'MMMM d, h:mm:ss a ' }}
</div>
</div>
@if (!tasks.isLoading() && tasks.isFetching()) {
<p>Fetching in background</p>
}
</div>
`,
})
export class OptimisticUpdatesComponent {
#tasksService = inject(TasksService)
tasks = injectQuery(() => this.#tasksService.allTasks())
clearMutation = injectMutation(() => this.#tasksService.addTask())
addMutation = injectMutation(() => this.#tasksService.addTask())
newItem = ''
failMutation = false
addItem() {
if (!this.newItem) return
this.addMutation.mutate({
task: this.newItem,
failMutation: this.failMutation,
})
this.newItem = ''
}
}

View File

@@ -0,0 +1,55 @@
/**
* MockApiInterceptor is used to simulate API responses for `/api/tasks` endpoints.
* It handles the following operations:
* - GET: Fetches all tasks from sessionStorage.
* - POST: Adds a new task to sessionStorage.
* 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<unknown>,
next: HttpHandlerFn,
): Observable<HttpEvent<any>> => {
const respondWith = (status: number, body: any) =>
of(new HttpResponse({ status, body })).pipe(delay(1000))
if (req.url === '/api/tasks') {
switch (req.method) {
case 'GET':
return respondWith(
200,
JSON.parse(
sessionStorage.getItem('optimistic-updates-tasks') || '[]',
),
)
case 'POST':
const tasks = JSON.parse(
sessionStorage.getItem('optimistic-updates-tasks') || '[]',
)
tasks.push(req.body)
sessionStorage.setItem(
'optimistic-updates-tasks',
JSON.stringify(tasks),
)
return respondWith(201, {
status: 'success',
task: req.body,
})
}
}
if (req.url === '/api/tasks-wrong-url') {
return respondWith(500, {
status: 'error',
})
}
return next(req)
}

View File

@@ -0,0 +1,84 @@
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 = () =>
queryOptions({
queryKey: ['tasks'],
queryFn: () => {
return lastValueFrom(this.#http.get<Array<string>>('/api/tasks'))
},
})
/**
* 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,
failMutation = false,
}: {
task: string
failMutation: boolean
}) =>
lastValueFrom(
this.#http.post(
`/api/tasks${failMutation ? '-wrong-url' : ''}`,
task,
),
),
mutationKey: ['tasks'],
onSuccess: () => {
this.#queryClient.invalidateQueries({ queryKey: ['tasks'] })
},
onMutate: async ({ task }) => {
// Cancel any outgoing refetches
// (so they don't overwrite our optimistic update)
await this.#queryClient.cancelQueries({ queryKey: ['tasks'] })
// Snapshot the previous value
const previousTodos = this.#queryClient.getQueryData<Array<string>>([
'tasks',
])
// Optimistically update to the new value
if (previousTodos) {
this.#queryClient.setQueryData<Array<string>>(
['tasks'],
[...previousTodos, task],
)
}
return previousTodos
},
onError: (err, variables, context) => {
if (context) {
this.#queryClient.setQueryData<Array<string>>(['tasks'], context)
}
},
// Always refetch after error or success:
onSettled: () => {
this.#queryClient.invalidateQueries({ queryKey: ['tasks'] })
},
})
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>TanStack Query Angular Optimistic Updates Example</title>
<base href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" type="image/x-icon" href="favicon.ico" />
</head>
<body>
<app-root></app-root>
</body>
</html>

View File

@@ -0,0 +1,5 @@
import { bootstrapApplication } from '@angular/platform-browser'
import { appConfig } from './app/app.config'
import { AppComponent } from './app/app.component'
bootstrapApplication(AppComponent, appConfig).catch((err) => console.error(err))

View File

@@ -0,0 +1,9 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/app",
"types": []
},
"files": ["src/main.ts"],
"include": ["src/**/*.d.ts"]
}

View File

@@ -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
}
}

View File

@@ -0,0 +1,4 @@
{
"name": "Node.js",
"image": "mcr.microsoft.com/devcontainers/javascript-node:22"
}

Some files were not shown because too many files have changed in this diff Show More