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