Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(Angular-query): add Angular auto-refetching example #8371

Open
wants to merge 15 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"name": "Node.js",
"image": "mcr.microsoft.com/devcontainers/javascript-node:22"
}
6 changes: 6 additions & 0 deletions examples/angular/auto-refetching/.eslintrc.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// @ts-check

/** @type {import('eslint').Linter.Config} */
const config = {}

module.exports = config
6 changes: 6 additions & 0 deletions examples/angular/auto-refetching/README.md
Original file line number Diff line number Diff line change
@@ -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`
104 changes: 104 additions & 0 deletions examples/angular/auto-refetching/angular.json
Original file line number Diff line number Diff line change
@@ -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-devkit/build-angular: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-devkit/build-angular:dev-server",
"configurations": {
"production": {
"buildTarget": "auto-refetching:build:production"
},
"development": {
"buildTarget": "auto-refetching:build:development"
}
},
"defaultConfiguration": "development"
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"options": {
"buildTarget": "auto-refetching:build"
}
}
}
}
}
}
29 changes: 29 additions & 0 deletions examples/angular/auto-refetching/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"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.0.0",
"@angular/compiler": "^19.0.0",
"@angular/core": "^19.0.0",
"@angular/platform-browser": "^19.0.0",
"@angular/platform-browser-dynamic": "^19.0.0",
"@tanstack/angular-query-experimental": "^5.61.6",
"rxjs": "^7.8.1",
"tslib": "^2.6.3",
"zone.js": "^0.15.0"
},
"devDependencies": {
"@angular-devkit/build-angular": "^19.0.2",
"@angular/build": "^19.0.0",
"@angular/cli": "^19.0.2",
"@angular/compiler-cli": "^19.0.0",
"typescript": "5.6.3"
}
}
11 changes: 11 additions & 0 deletions examples/angular/auto-refetching/src/app/app.component.ts
Original file line number Diff line number Diff line change
@@ -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: `<auto-refetching-example />`,
imports: [AutoRefetchingExampleComponent],
})
export class AppComponent {}
31 changes: 31 additions & 0 deletions examples/angular/auto-refetching/src/app/app.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
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(),
),
],
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<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)" />
</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>
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import {
ChangeDetectionStrategy,
Component,
inject,
signal,
} from '@angular/core'
import {
injectMutation,
injectQuery,
QueryFunctionContext,
} from '@tanstack/angular-query-experimental'
import { fromEvent, lastValueFrom, takeUntil } from 'rxjs'
import { TasksService } from '../services/tasks.service'

@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'auto-refetching-example',
standalone: true,
templateUrl: './auto-refetching.component.html',
imports: [],
})
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 = ''
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/**
* 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 { Injectable } from '@angular/core'
import { HttpResponse } from '@angular/common/http'
import { delay, of } from 'rxjs'
import type {
HttpEvent,
HttpHandler,
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)
}
56 changes: 56 additions & 0 deletions examples/angular/auto-refetching/src/app/services/tasks.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { HttpClient } from '@angular/common/http'
import { Injectable, inject } from '@angular/core'
import { QueryClient, QueryFunctionContext, mutationOptions, queryOptions } from '@tanstack/angular-query-experimental'

import { fromEvent, lastValueFrom, takeUntil } 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<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 added examples/angular/auto-refetching/src/favicon.ico
Binary file not shown.
13 changes: 13 additions & 0 deletions examples/angular/auto-refetching/src/index.html
Original file line number Diff line number Diff line change
@@ -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>
Loading
Loading