Skip to content

Commit d07a3db

Browse files
MatanShushanautofix-ci[bot]arnoud-dv
authored
docs(angular-query): Add Optimistic Updates Example for Angular (#8439)
* feat: add optimistic updates example for Angular - Implemented OptimisticUpdatesComponent with Angular Query for managing optimistic UI updates. - Added TasksService to handle query and mutation logic with , , and handlers. - Included mock API interceptor for simulating backend responses. * Remove comment * add lock file * ci: apply automated fixes * fix build, add example to docs * ci: apply automated fixes --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Arnoud <[email protected]>
1 parent c1ea870 commit d07a3db

17 files changed

+508
-0
lines changed

docs/config.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1050,6 +1050,10 @@
10501050
"label": "Auto Refetching / Polling / Realtime",
10511051
"to": "framework/angular/examples/auto-refetching"
10521052
},
1053+
{
1054+
"label": "Optimistic Updates",
1055+
"to": "framework/angular/examples/optimistic-updates"
1056+
},
10531057
{
10541058
"label": "Pagination",
10551059
"to": "framework/angular/examples/pagination"
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"name": "Node.js",
3+
"image": "mcr.microsoft.com/devcontainers/javascript-node:22"
4+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
// @ts-check
2+
3+
/** @type {import('eslint').Linter.Config} */
4+
const config = {}
5+
6+
module.exports = config
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# TanStack Query Angular optimistic-updates example
2+
3+
To run this example:
4+
5+
- `npm install` or `yarn` or `pnpm i` or `bun i`
6+
- `npm run start` or `yarn start` or `pnpm start` or `bun start`
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
{
2+
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
3+
"version": 1,
4+
"cli": {
5+
"packageManager": "pnpm",
6+
"analytics": false,
7+
"cache": {
8+
"enabled": false
9+
}
10+
},
11+
"newProjectRoot": "projects",
12+
"projects": {
13+
"optimistic-updates": {
14+
"projectType": "application",
15+
"schematics": {
16+
"@schematics/angular:component": {
17+
"inlineTemplate": true,
18+
"inlineStyle": true,
19+
"skipTests": true
20+
},
21+
"@schematics/angular:class": {
22+
"skipTests": true
23+
},
24+
"@schematics/angular:directive": {
25+
"skipTests": true
26+
},
27+
"@schematics/angular:guard": {
28+
"skipTests": true
29+
},
30+
"@schematics/angular:interceptor": {
31+
"skipTests": true
32+
},
33+
"@schematics/angular:pipe": {
34+
"skipTests": true
35+
},
36+
"@schematics/angular:resolver": {
37+
"skipTests": true
38+
},
39+
"@schematics/angular:service": {
40+
"skipTests": true
41+
}
42+
},
43+
"root": "",
44+
"sourceRoot": "src",
45+
"prefix": "app",
46+
"architect": {
47+
"build": {
48+
"builder": "@angular/build:application",
49+
"options": {
50+
"outputPath": "dist/optimistic-updates",
51+
"index": "src/index.html",
52+
"browser": "src/main.ts",
53+
"polyfills": ["zone.js"],
54+
"tsConfig": "tsconfig.app.json",
55+
"assets": ["src/favicon.ico", "src/assets"],
56+
"styles": [],
57+
"scripts": []
58+
},
59+
"configurations": {
60+
"production": {
61+
"budgets": [
62+
{
63+
"type": "initial",
64+
"maximumWarning": "500kb",
65+
"maximumError": "1mb"
66+
},
67+
{
68+
"type": "anyComponentStyle",
69+
"maximumWarning": "2kb",
70+
"maximumError": "4kb"
71+
}
72+
],
73+
"outputHashing": "all"
74+
},
75+
"development": {
76+
"optimization": false,
77+
"extractLicenses": false,
78+
"sourceMap": true
79+
}
80+
},
81+
"defaultConfiguration": "production"
82+
},
83+
"serve": {
84+
"builder": "@angular/build:dev-server",
85+
"configurations": {
86+
"production": {
87+
"buildTarget": "optimistic-updates:build:production"
88+
},
89+
"development": {
90+
"buildTarget": "optimistic-updates:build:development"
91+
}
92+
},
93+
"defaultConfiguration": "development"
94+
},
95+
"extract-i18n": {
96+
"builder": "@angular/build:extract-i18n",
97+
"options": {
98+
"buildTarget": "optimistic-updates:build"
99+
}
100+
}
101+
}
102+
}
103+
}
104+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
{
2+
"name": "@tanstack/query-example-angular-optimistic-updates",
3+
"type": "module",
4+
"scripts": {
5+
"ng": "ng",
6+
"start": "ng serve",
7+
"build": "ng build",
8+
"watch": "ng build --watch --configuration development"
9+
},
10+
"private": true,
11+
"dependencies": {
12+
"@angular/common": "^19.1.0-next.0",
13+
"@angular/compiler": "^19.1.0-next.0",
14+
"@angular/core": "^19.1.0-next.0",
15+
"@angular/forms": "19.1.0-next.0",
16+
"@angular/platform-browser": "^19.1.0-next.0",
17+
"@angular/platform-browser-dynamic": "^19.1.0-next.0",
18+
"@tanstack/angular-query-experimental": "^5.66.4",
19+
"rxjs": "^7.8.1",
20+
"tslib": "^2.6.3",
21+
"zone.js": "^0.15.0"
22+
},
23+
"devDependencies": {
24+
"@angular/build": "^19.0.2",
25+
"@angular/cli": "^19.0.2",
26+
"@angular/compiler-cli": "^19.1.0-next.0",
27+
"typescript": "5.7.2"
28+
}
29+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { ChangeDetectionStrategy, Component } from '@angular/core'
2+
import { OptimisticUpdatesComponent } from './components/optimistic-updates.component'
3+
4+
@Component({
5+
changeDetection: ChangeDetectionStrategy.OnPush,
6+
selector: 'app-root',
7+
standalone: true,
8+
template: `<optimistic-updates />`,
9+
imports: [OptimisticUpdatesComponent],
10+
})
11+
export class AppComponent {}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import {
2+
provideHttpClient,
3+
withFetch,
4+
withInterceptors,
5+
} from '@angular/common/http'
6+
import {
7+
QueryClient,
8+
provideTanStackQuery,
9+
withDevtools,
10+
} from '@tanstack/angular-query-experimental'
11+
import { mockInterceptor } from './interceptor/mock-api.interceptor'
12+
import type { ApplicationConfig } from '@angular/core'
13+
14+
export const appConfig: ApplicationConfig = {
15+
providers: [
16+
provideHttpClient(withFetch(), withInterceptors([mockInterceptor])),
17+
provideTanStackQuery(
18+
new QueryClient({
19+
defaultOptions: {
20+
queries: {
21+
gcTime: 1000 * 60 * 60 * 24, // 24 hours
22+
},
23+
},
24+
}),
25+
withDevtools(),
26+
),
27+
],
28+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
2+
import {
3+
injectMutation,
4+
injectQuery,
5+
} from '@tanstack/angular-query-experimental'
6+
import { FormsModule } from '@angular/forms'
7+
import { DatePipe } from '@angular/common'
8+
import { TasksService } from '../services/tasks.service'
9+
10+
@Component({
11+
changeDetection: ChangeDetectionStrategy.OnPush,
12+
selector: 'optimistic-updates',
13+
imports: [FormsModule, DatePipe],
14+
template: `
15+
<p>
16+
In this example, new items can be created using a mutation. The new item
17+
will be optimistically added to the list in hopes that the server accepts
18+
the item. If it does, the list is refetched with the true items from the
19+
list. Every now and then, the mutation may fail though. When that happens,
20+
the previous list of items is restored and the list is again refetched
21+
from the server.
22+
</p>
23+
24+
<hr />
25+
@if (tasks.isLoading()) {
26+
<p>Loading...</p>
27+
}
28+
29+
<div class="container">
30+
<label>
31+
<input type="checkbox" [(ngModel)]="failMutation" />
32+
Fail Mutation
33+
</label>
34+
35+
<div class="input-container">
36+
<input type="text" [(ngModel)]="newItem" placeholder="Enter text" />
37+
<button (click)="addItem()">Create</button>
38+
<ul>
39+
@for (task of tasks.data(); track task) {
40+
<li>{{ task }}</li>
41+
}
42+
</ul>
43+
44+
<div>
45+
Updated At: {{ tasks.dataUpdatedAt() | date: 'MMMM d, h:mm:ss a ' }}
46+
</div>
47+
</div>
48+
@if (!tasks.isLoading() && tasks.isFetching()) {
49+
<p>Fetching in background</p>
50+
}
51+
</div>
52+
`,
53+
})
54+
export class OptimisticUpdatesComponent {
55+
#tasksService = inject(TasksService)
56+
57+
tasks = injectQuery(() => this.#tasksService.allTasks())
58+
clearMutation = injectMutation(() => this.#tasksService.addTask())
59+
addMutation = injectMutation(() => this.#tasksService.addTask())
60+
61+
newItem = ''
62+
failMutation = false
63+
64+
addItem() {
65+
if (!this.newItem) return
66+
67+
this.addMutation.mutate({
68+
task: this.newItem,
69+
failMutation: this.failMutation,
70+
})
71+
this.newItem = ''
72+
}
73+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/**
2+
* MockApiInterceptor is used to simulate API responses for `/api/tasks` endpoints.
3+
* It handles the following operations:
4+
* - GET: Fetches all tasks from sessionStorage.
5+
* - POST: Adds a new task to sessionStorage.
6+
* Simulated responses include a delay to mimic network latency.
7+
*/
8+
import { HttpResponse } from '@angular/common/http'
9+
import { delay, of } from 'rxjs'
10+
import type {
11+
HttpEvent,
12+
HttpHandlerFn,
13+
HttpInterceptorFn,
14+
HttpRequest,
15+
} from '@angular/common/http'
16+
import type { Observable } from 'rxjs'
17+
18+
export const mockInterceptor: HttpInterceptorFn = (
19+
req: HttpRequest<unknown>,
20+
next: HttpHandlerFn,
21+
): Observable<HttpEvent<any>> => {
22+
const respondWith = (status: number, body: any) =>
23+
of(new HttpResponse({ status, body })).pipe(delay(1000))
24+
if (req.url === '/api/tasks') {
25+
switch (req.method) {
26+
case 'GET':
27+
return respondWith(
28+
200,
29+
JSON.parse(
30+
sessionStorage.getItem('optimistic-updates-tasks') || '[]',
31+
),
32+
)
33+
case 'POST':
34+
const tasks = JSON.parse(
35+
sessionStorage.getItem('optimistic-updates-tasks') || '[]',
36+
)
37+
tasks.push(req.body)
38+
sessionStorage.setItem(
39+
'optimistic-updates-tasks',
40+
JSON.stringify(tasks),
41+
)
42+
return respondWith(201, {
43+
status: 'success',
44+
task: req.body,
45+
})
46+
}
47+
}
48+
if (req.url === '/api/tasks-wrong-url') {
49+
return respondWith(500, {
50+
status: 'error',
51+
})
52+
}
53+
54+
return next(req)
55+
}

0 commit comments

Comments
 (0)