diff --git a/examples/angular/basic-persister/.devcontainer/devcontainer.json b/examples/angular/basic-persister/.devcontainer/devcontainer.json
new file mode 100644
index 0000000000..365adf8f4c
--- /dev/null
+++ b/examples/angular/basic-persister/.devcontainer/devcontainer.json
@@ -0,0 +1,4 @@
+{
+ "name": "Node.js",
+ "image": "mcr.microsoft.com/devcontainers/javascript-node:22"
+}
diff --git a/examples/angular/basic-persister/.eslintrc.cjs b/examples/angular/basic-persister/.eslintrc.cjs
new file mode 100644
index 0000000000..cca134ce16
--- /dev/null
+++ b/examples/angular/basic-persister/.eslintrc.cjs
@@ -0,0 +1,6 @@
+// @ts-check
+
+/** @type {import('eslint').Linter.Config} */
+const config = {}
+
+module.exports = config
diff --git a/examples/angular/basic-persister/README.md b/examples/angular/basic-persister/README.md
new file mode 100644
index 0000000000..47d5931979
--- /dev/null
+++ b/examples/angular/basic-persister/README.md
@@ -0,0 +1,6 @@
+# TanStack Query Angular basic persister example
+
+To run this example:
+
+- `npm install` or `yarn` or `pnpm i` or `bun i`
+- `npm run start` or `yarn start` or `pnpm start` or `bun start`
diff --git a/examples/angular/basic-persister/angular.json b/examples/angular/basic-persister/angular.json
new file mode 100644
index 0000000000..64adfea7c4
--- /dev/null
+++ b/examples/angular/basic-persister/angular.json
@@ -0,0 +1,104 @@
+{
+ "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
+ "version": 1,
+ "cli": {
+ "packageManager": "pnpm",
+ "analytics": false,
+ "cache": {
+ "enabled": false
+ }
+ },
+ "newProjectRoot": "projects",
+ "projects": {
+ "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"
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/examples/angular/basic-persister/package.json b/examples/angular/basic-persister/package.json
new file mode 100644
index 0000000000..aadaabaf0e
--- /dev/null
+++ b/examples/angular/basic-persister/package.json
@@ -0,0 +1,30 @@
+{
+ "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": "^19.1.0-next.0",
+ "@angular/compiler": "^19.1.0-next.0",
+ "@angular/core": "^19.1.0-next.0",
+ "@angular/platform-browser": "^19.1.0-next.0",
+ "@angular/platform-browser-dynamic": "^19.1.0-next.0",
+ "@tanstack/angular-query-experimental": "^5.62.7",
+ "@tanstack/angular-query-persist-client-experimental": "^5.62.7",
+ "@tanstack/query-sync-storage-persister": "^5.62.3",
+ "rxjs": "^7.8.1",
+ "tslib": "^2.6.3",
+ "zone.js": "^0.15.0"
+ },
+ "devDependencies": {
+ "@angular/build": "^19.0.2",
+ "@angular/cli": "^19.0.2",
+ "@angular/compiler-cli": "^19.1.0-next.0",
+ "typescript": "5.7.2"
+ }
+}
diff --git a/examples/angular/basic-persister/src/app/app.component.html b/examples/angular/basic-persister/src/app/app.component.html
new file mode 100644
index 0000000000..4c21f9e879
--- /dev/null
+++ b/examples/angular/basic-persister/src/app/app.component.html
@@ -0,0 +1,10 @@
+
+ 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.
+
+@if (postId() > -1) {
+
+} @else {
+
+}
diff --git a/examples/angular/basic-persister/src/app/app.component.ts b/examples/angular/basic-persister/src/app/app.component.ts
new file mode 100644
index 0000000000..5958a1b5e7
--- /dev/null
+++ b/examples/angular/basic-persister/src/app/app.component.ts
@@ -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)
+}
diff --git a/examples/angular/basic-persister/src/app/app.config.ts b/examples/angular/basic-persister/src/app/app.config.ts
new file mode 100644
index 0000000000..9d7b34fbd8
--- /dev/null
+++ b/examples/angular/basic-persister/src/app/app.config.ts
@@ -0,0 +1,37 @@
+import { provideHttpClient, withFetch } from '@angular/common/http'
+import {
+ QueryClient,
+ provideTanStackQuery,
+ withDevtools,
+} from '@tanstack/angular-query-experimental'
+import { withPersistQueryClient } from '@tanstack/angular-query-persist-client-experimental'
+import { createSyncStoragePersister } from '@tanstack/query-sync-storage-persister'
+import type { ApplicationConfig } from '@angular/core'
+
+const localStoragePersister = createSyncStoragePersister({
+ storage: window.localStorage,
+})
+
+export const appConfig: ApplicationConfig = {
+ providers: [
+ provideHttpClient(withFetch()),
+ provideTanStackQuery(
+ new QueryClient({
+ defaultOptions: {
+ queries: {
+ staleTime: 1000 * 60,
+ gcTime: 1000 * 60 * 60 * 24, // 24 hours
+ },
+ },
+ }),
+ withDevtools(),
+ withPersistQueryClient([
+ {
+ persistOptions: {
+ persister: localStoragePersister,
+ },
+ },
+ ]),
+ ),
+ ],
+}
diff --git a/examples/angular/basic-persister/src/app/components/post.component.html b/examples/angular/basic-persister/src/app/components/post.component.html
new file mode 100644
index 0000000000..34b36e94fc
--- /dev/null
+++ b/examples/angular/basic-persister/src/app/components/post.component.html
@@ -0,0 +1,19 @@
+
+
+ @if (postQuery.isPending()) {
+ Loading...
+ } @else if (postQuery.isError()) {
+ Error: {{ postQuery.error().message }}
+ }
+ @if (postQuery.data(); as post) {
+
{{ post.title }}
+
+ @if (postQuery.isFetching()) {
+ Background Updating...
+ }
+ }
+
diff --git a/examples/angular/basic-persister/src/app/components/post.component.ts b/examples/angular/basic-persister/src/app/components/post.component.ts
new file mode 100644
index 0000000000..d77e707e99
--- /dev/null
+++ b/examples/angular/basic-persister/src/app/components/post.component.ts
@@ -0,0 +1,39 @@
+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',
+ standalone: true,
+ templateUrl: './post.component.html',
+})
+export class PostComponent {
+ #postsService = inject(PostsService)
+
+ @Output() setPostId = new EventEmitter()
+
+ 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)
+}
diff --git a/examples/angular/basic-persister/src/app/components/posts.component.html b/examples/angular/basic-persister/src/app/components/posts.component.html
new file mode 100644
index 0000000000..568a6a4df4
--- /dev/null
+++ b/examples/angular/basic-persister/src/app/components/posts.component.html
@@ -0,0 +1,39 @@
+
+
Posts
+ @switch (postsQuery.status()) {
+ @case ('pending') {
+ Loading...
+ }
+ @case ('error') {
+ Error: {{ postsQuery.error()?.message }}
+ }
+ @default {
+
+ @for (post of postsQuery.data(); track post.id) {
+
+
+
+ {{ post.title }}
+
+ }
+
+ }
+ }
+
+ @if (postsQuery.isFetching()) {
+ Background Updating...
+ }
+
+
diff --git a/examples/angular/basic-persister/src/app/components/posts.component.ts b/examples/angular/basic-persister/src/app/components/posts.component.ts
new file mode 100644
index 0000000000..3c8bf7c79d
--- /dev/null
+++ b/examples/angular/basic-persister/src/app/components/posts.component.ts
@@ -0,0 +1,28 @@
+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',
+ standalone: true,
+ templateUrl: './posts.component.html',
+})
+export class PostsComponent {
+ queryClient = inject(QueryClient)
+ #postsService = inject(PostsService)
+
+ @Output() setPostId = new EventEmitter()
+
+ postsQuery = injectQuery(() => ({
+ queryKey: ['posts'],
+ queryFn: () => lastValueFrom(this.#postsService.allPosts$()),
+ }))
+}
diff --git a/examples/angular/basic-persister/src/app/services/posts-service.ts b/examples/angular/basic-persister/src/app/services/posts-service.ts
new file mode 100644
index 0000000000..fed2a1b11c
--- /dev/null
+++ b/examples/angular/basic-persister/src/app/services/posts-service.ts
@@ -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(`https://jsonplaceholder.typicode.com/posts/${postId}`)
+
+ allPosts$ = () =>
+ this.#http.get>('https://jsonplaceholder.typicode.com/posts')
+}
+
+export interface Post {
+ id: number
+ title: string
+ body: string
+}
diff --git a/examples/angular/basic-persister/src/favicon.ico b/examples/angular/basic-persister/src/favicon.ico
new file mode 100644
index 0000000000..57614f9c96
Binary files /dev/null and b/examples/angular/basic-persister/src/favicon.ico differ
diff --git a/examples/angular/basic-persister/src/index.html b/examples/angular/basic-persister/src/index.html
new file mode 100644
index 0000000000..2e262442e1
--- /dev/null
+++ b/examples/angular/basic-persister/src/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+ TanStack Query Angular basic persister example
+
+
+
+
+
+
+
+
diff --git a/examples/angular/basic-persister/src/main.ts b/examples/angular/basic-persister/src/main.ts
new file mode 100644
index 0000000000..aa33a0b9ff
--- /dev/null
+++ b/examples/angular/basic-persister/src/main.ts
@@ -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),
+)
diff --git a/examples/angular/basic-persister/tsconfig.app.json b/examples/angular/basic-persister/tsconfig.app.json
new file mode 100644
index 0000000000..5b9d3c5ecb
--- /dev/null
+++ b/examples/angular/basic-persister/tsconfig.app.json
@@ -0,0 +1,9 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "outDir": "./out-tsc/app",
+ "types": []
+ },
+ "files": ["src/main.ts"],
+ "include": ["src/**/*.d.ts"]
+}
diff --git a/examples/angular/basic-persister/tsconfig.json b/examples/angular/basic-persister/tsconfig.json
new file mode 100644
index 0000000000..d0d73c8beb
--- /dev/null
+++ b/examples/angular/basic-persister/tsconfig.json
@@ -0,0 +1,31 @@
+{
+ "compileOnSave": false,
+ "compilerOptions": {
+ "outDir": "./dist/out-tsc",
+ "forceConsistentCasingInFileNames": true,
+ "strict": true,
+ "noImplicitOverride": true,
+ "noPropertyAccessFromIndexSignature": true,
+ "noImplicitReturns": true,
+ "noFallthroughCasesInSwitch": true,
+ "skipLibCheck": true,
+ "isolatedModules": true,
+ "esModuleInterop": true,
+ "sourceMap": true,
+ "declaration": false,
+ "experimentalDecorators": true,
+ "moduleResolution": "bundler",
+ "importHelpers": true,
+ "target": "ES2022",
+ "module": "ES2022",
+ "useDefineForClassFields": false,
+ "lib": ["ES2022", "dom"]
+ },
+ "angularCompilerOptions": {
+ "enableI18nLegacyMessageIdFormat": false,
+ "strictInjectionParameters": true,
+ "strictInputAccessModifiers": true,
+ "strictStandalone": true,
+ "strictTemplates": true
+ }
+}
diff --git a/examples/angular/multiple-persisters/.devcontainer/devcontainer.json b/examples/angular/multiple-persisters/.devcontainer/devcontainer.json
new file mode 100644
index 0000000000..365adf8f4c
--- /dev/null
+++ b/examples/angular/multiple-persisters/.devcontainer/devcontainer.json
@@ -0,0 +1,4 @@
+{
+ "name": "Node.js",
+ "image": "mcr.microsoft.com/devcontainers/javascript-node:22"
+}
diff --git a/examples/angular/multiple-persisters/.eslintrc.cjs b/examples/angular/multiple-persisters/.eslintrc.cjs
new file mode 100644
index 0000000000..cca134ce16
--- /dev/null
+++ b/examples/angular/multiple-persisters/.eslintrc.cjs
@@ -0,0 +1,6 @@
+// @ts-check
+
+/** @type {import('eslint').Linter.Config} */
+const config = {}
+
+module.exports = config
diff --git a/examples/angular/multiple-persisters/README.md b/examples/angular/multiple-persisters/README.md
new file mode 100644
index 0000000000..d0551cf9de
--- /dev/null
+++ b/examples/angular/multiple-persisters/README.md
@@ -0,0 +1,6 @@
+# TanStack Query Angular multiple persisters example
+
+To run this example:
+
+- `npm install` or `yarn` or `pnpm i` or `bun i`
+- `npm run start` or `yarn start` or `pnpm start` or `bun start`
diff --git a/examples/angular/multiple-persisters/angular.json b/examples/angular/multiple-persisters/angular.json
new file mode 100644
index 0000000000..e9ada92785
--- /dev/null
+++ b/examples/angular/multiple-persisters/angular.json
@@ -0,0 +1,104 @@
+{
+ "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
+ "version": 1,
+ "cli": {
+ "packageManager": "pnpm",
+ "analytics": false,
+ "cache": {
+ "enabled": false
+ }
+ },
+ "newProjectRoot": "projects",
+ "projects": {
+ "multiple-persisters": {
+ "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/multiple-persisters",
+ "index": "src/index.html",
+ "browser": "src/main.ts",
+ "polyfills": ["zone.js"],
+ "tsConfig": "tsconfig.app.json",
+ "assets": ["src/favicon.ico", "src/assets"],
+ "styles": ["src/styles.css"],
+ "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": "multiple-persisters:build:production"
+ },
+ "development": {
+ "buildTarget": "multiple-persisters:build:development"
+ }
+ },
+ "defaultConfiguration": "development"
+ },
+ "extract-i18n": {
+ "builder": "@angular/build:extract-i18n",
+ "options": {
+ "buildTarget": "multiple-persisters:build"
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/examples/angular/multiple-persisters/package.json b/examples/angular/multiple-persisters/package.json
new file mode 100644
index 0000000000..4a1d978ba4
--- /dev/null
+++ b/examples/angular/multiple-persisters/package.json
@@ -0,0 +1,33 @@
+{
+ "name": "@tanstack/query-example-angular-multiple-persisters",
+ "type": "module",
+ "scripts": {
+ "ng": "ng",
+ "start": "ng serve",
+ "build": "ng build",
+ "watch": "ng build --watch --configuration development"
+ },
+ "private": true,
+ "dependencies": {
+ "@angular/common": "^19.1.0-next.0",
+ "@angular/compiler": "^19.1.0-next.0",
+ "@angular/core": "^19.1.0-next.0",
+ "@angular/platform-browser": "^19.1.0-next.0",
+ "@angular/platform-browser-dynamic": "^19.1.0-next.0",
+ "@tanstack/angular-query-experimental": "^5.62.7",
+ "@tanstack/angular-query-persist-client-experimental": "^5.62.7",
+ "@tanstack/query-sync-storage-persister": "^5.62.3",
+ "rxjs": "^7.8.1",
+ "tslib": "^2.6.3",
+ "zone.js": "^0.15.0"
+ },
+ "devDependencies": {
+ "@angular/build": "^19.0.2",
+ "@angular/cli": "^19.0.2",
+ "@angular/compiler-cli": "^19.1.0-next.0",
+ "autoprefixer": "^10.4.20",
+ "postcss": "^8.4.49",
+ "tailwindcss": "^3.4.7",
+ "typescript": "5.7.2"
+ }
+}
diff --git a/examples/angular/multiple-persisters/src/app/app.component.ts b/examples/angular/multiple-persisters/src/app/app.component.ts
new file mode 100644
index 0000000000..9ad189c91b
--- /dev/null
+++ b/examples/angular/multiple-persisters/src/app/app.component.ts
@@ -0,0 +1,30 @@
+import { ChangeDetectionStrategy, Component } from '@angular/core'
+import { UserPreferencesComponent } from './components/user-preferences.component'
+import { SessionDataComponent } from './components/session-data.component'
+
+@Component({
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ selector: 'app-root',
+ standalone: true,
+ template: `
+
+
+
+ TanStack Query Persistence Demo
+
+
+ This demo illustrates how to selectively persist queries to different
+ persisters. By leveraging shouldDehydrateQuery, it is possible to
+ strategically cache data in multiple persisters based on specific
+ query requirements.
+
+
+
+
+
+
+
+ `,
+ imports: [UserPreferencesComponent, SessionDataComponent],
+})
+export class AppComponent {}
diff --git a/examples/angular/multiple-persisters/src/app/app.config.ts b/examples/angular/multiple-persisters/src/app/app.config.ts
new file mode 100644
index 0000000000..544da92c30
--- /dev/null
+++ b/examples/angular/multiple-persisters/src/app/app.config.ts
@@ -0,0 +1,60 @@
+import {
+ provideHttpClient,
+ withFetch,
+ withInterceptors,
+} from '@angular/common/http'
+import {
+ QueryClient,
+ provideTanStackQuery,
+ withDevtools,
+} from '@tanstack/angular-query-experimental'
+import { createSyncStoragePersister } from '@tanstack/query-sync-storage-persister'
+import { withPersistQueryClient } from '@tanstack/angular-query-persist-client-experimental'
+import { mockInterceptor } from './interceptor/mock-api.interceptor'
+import type { ApplicationConfig } from '@angular/core'
+
+const localStoragePersister = createSyncStoragePersister({
+ storage: window.localStorage,
+})
+
+const sessionStoragePersister = createSyncStoragePersister({
+ storage: window.sessionStorage,
+})
+
+export const appConfig: ApplicationConfig = {
+ providers: [
+ provideHttpClient(withFetch(), withInterceptors([mockInterceptor])),
+ provideTanStackQuery(
+ new QueryClient({
+ defaultOptions: {
+ queries: {
+ gcTime: 1000 * 60 * 60 * 24, // 24 hours
+ },
+ },
+ }),
+ withDevtools(),
+ withPersistQueryClient([
+ {
+ persistOptions: {
+ persister: localStoragePersister,
+ dehydrateOptions: {
+ shouldDehydrateQuery: (query) =>
+ query.state.status === 'success' &&
+ query.queryKey[0] === 'preferences',
+ },
+ },
+ },
+ {
+ persistOptions: {
+ persister: sessionStoragePersister,
+ dehydrateOptions: {
+ shouldDehydrateQuery: (query) =>
+ query.state.status === 'success' &&
+ query.queryKey[0] === 'session',
+ },
+ },
+ },
+ ]),
+ ),
+ ],
+}
diff --git a/examples/angular/multiple-persisters/src/app/components/session-data.component.ts b/examples/angular/multiple-persisters/src/app/components/session-data.component.ts
new file mode 100644
index 0000000000..88e9f85b6e
--- /dev/null
+++ b/examples/angular/multiple-persisters/src/app/components/session-data.component.ts
@@ -0,0 +1,75 @@
+import { Component, inject } from '@angular/core'
+import { injectQuery } from '@tanstack/angular-query-experimental'
+import { HttpClient } from '@angular/common/http'
+import { firstValueFrom } from 'rxjs'
+import { DatePipe } from '@angular/common'
+
+interface SessionData {
+ lastActive: string
+ currentView: string
+ activeFilters: Array
+ temporaryNotes: string
+}
+
+@Component({
+ selector: 'session-data',
+ template: `
+ @if (sessionData.isLoading()) {
+
+ } @else if (sessionData.isError()) {
+
+ Error loading session data: {{ sessionData.error() }}
+
+ } @else {
+
+
+ 🔑
+
+ Session Data
+ (stored in sessionStorage)
+
+
+
+
+ Last Active:
+
+ {{ sessionData.data()?.lastActive | date }}
+
+
+
+ Current View:
+ {{
+ sessionData.data()?.currentView
+ }}
+
+
+ Active Filters:
+
+ {{ sessionData.data()?.activeFilters?.join(', ') }}
+
+
+
+ Temporary Notes:
+ {{
+ sessionData.data()?.temporaryNotes
+ }}
+
+
+
+ }
+ `,
+ standalone: true,
+ imports: [DatePipe],
+})
+export class SessionDataComponent {
+ #http = inject(HttpClient)
+
+ sessionData = injectQuery(() => ({
+ queryKey: ['session'],
+ queryFn: () => firstValueFrom(this.#http.get('/session')),
+ staleTime: 1000 * 60 * 60, // 1 hour
+ }))
+}
diff --git a/examples/angular/multiple-persisters/src/app/components/user-preferences.component.ts b/examples/angular/multiple-persisters/src/app/components/user-preferences.component.ts
new file mode 100644
index 0000000000..c499ccd27e
--- /dev/null
+++ b/examples/angular/multiple-persisters/src/app/components/user-preferences.component.ts
@@ -0,0 +1,73 @@
+import { Component, inject } from '@angular/core'
+import { injectQuery } from '@tanstack/angular-query-experimental'
+import { HttpClient } from '@angular/common/http'
+import { firstValueFrom } from 'rxjs'
+
+interface UserPreferences {
+ theme: string
+ language: string
+ notifications: boolean
+ fontSize: string
+}
+
+@Component({
+ selector: 'user-preferences',
+ template: `
+ @if (userPreferences.isLoading()) {
+
+ } @else if (userPreferences.isError()) {
+
+ Error loading preferences: {{ userPreferences.error() }}
+
+ } @else {
+
+
+ ⚙️
+
+ User Preferences
+ (stored in localStorage)
+
+
+
+
+ Theme:
+ {{ userPreferences.data()?.theme }}
+
+
+ Language:
+ {{
+ userPreferences.data()?.language
+ }}
+
+
+ Notifications:
+ {{
+ userPreferences.data()?.notifications ? 'Enabled' : 'Disabled'
+ }}
+
+
+ Font Size:
+ {{
+ userPreferences.data()?.fontSize
+ }}
+
+
+
+ }
+ `,
+ standalone: true,
+ imports: [],
+})
+export class UserPreferencesComponent {
+ #http = inject(HttpClient)
+
+ userPreferences = injectQuery(() => ({
+ queryKey: ['preferences'],
+ queryFn: () =>
+ firstValueFrom(this.#http.get('/preferences')),
+ staleTime: 1000 * 60 * 60, // 1 hour
+ }))
+}
diff --git a/examples/angular/multiple-persisters/src/app/interceptor/mock-api.interceptor.ts b/examples/angular/multiple-persisters/src/app/interceptor/mock-api.interceptor.ts
new file mode 100644
index 0000000000..74b3ee0e50
--- /dev/null
+++ b/examples/angular/multiple-persisters/src/app/interceptor/mock-api.interceptor.ts
@@ -0,0 +1,43 @@
+/**
+ * MockApiInterceptor is used to simulate API responses for `/api/tasks` endpoints.
+ * It handles the following operations:
+ * - GET: Fetches all tasks from localStorage.
+ * - POST: Adds a new task to localStorage.
+ * - DELETE: Clears all tasks from localStorage.
+ * Simulated responses include a delay to mimic network latency.
+ */
+import { HttpResponse } from '@angular/common/http'
+import { delay, of } from 'rxjs'
+import type {
+ HttpEvent,
+ HttpHandlerFn,
+ HttpInterceptorFn,
+ HttpRequest,
+} from '@angular/common/http'
+import type { Observable } from 'rxjs'
+
+export const mockInterceptor: HttpInterceptorFn = (
+ req: HttpRequest,
+ next: HttpHandlerFn,
+): Observable> => {
+ const respondWith = (status: number, body: any) =>
+ of(new HttpResponse({ status, body })).pipe(delay(100))
+ if (req.url === '/preferences') {
+ return respondWith(200, {
+ theme: 'dark',
+ language: 'en',
+ notifications: true,
+ fontSize: 'medium',
+ })
+ }
+
+ if (req.url === '/session') {
+ return respondWith(200, {
+ lastActive: '2024-02-28T12:00:00Z',
+ currentView: 'dashboard',
+ activeFilters: ['recent', 'important'],
+ temporaryNotes: 'Meeting at 3PM',
+ })
+ }
+ return next(req)
+}
diff --git a/examples/angular/multiple-persisters/src/favicon.ico b/examples/angular/multiple-persisters/src/favicon.ico
new file mode 100644
index 0000000000..57614f9c96
Binary files /dev/null and b/examples/angular/multiple-persisters/src/favicon.ico differ
diff --git a/examples/angular/multiple-persisters/src/index.html b/examples/angular/multiple-persisters/src/index.html
new file mode 100644
index 0000000000..94e60f2df0
--- /dev/null
+++ b/examples/angular/multiple-persisters/src/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+ TanStack Query Angular multiple persisters example
+
+
+
+
+
+
+
+
diff --git a/examples/angular/multiple-persisters/src/main.ts b/examples/angular/multiple-persisters/src/main.ts
new file mode 100644
index 0000000000..c3d8f9af99
--- /dev/null
+++ b/examples/angular/multiple-persisters/src/main.ts
@@ -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))
diff --git a/examples/angular/multiple-persisters/src/styles.css b/examples/angular/multiple-persisters/src/styles.css
new file mode 100644
index 0000000000..b5c61c9567
--- /dev/null
+++ b/examples/angular/multiple-persisters/src/styles.css
@@ -0,0 +1,3 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
diff --git a/examples/angular/multiple-persisters/tailwind.config.js b/examples/angular/multiple-persisters/tailwind.config.js
new file mode 100644
index 0000000000..7d52777e05
--- /dev/null
+++ b/examples/angular/multiple-persisters/tailwind.config.js
@@ -0,0 +1,8 @@
+/** @type {import('tailwindcss').Config} */
+export default {
+ content: ['./src/**/*.{html,ts}'],
+ theme: {
+ extend: {},
+ },
+ plugins: [],
+}
diff --git a/examples/angular/multiple-persisters/tsconfig.app.json b/examples/angular/multiple-persisters/tsconfig.app.json
new file mode 100644
index 0000000000..5b9d3c5ecb
--- /dev/null
+++ b/examples/angular/multiple-persisters/tsconfig.app.json
@@ -0,0 +1,9 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "outDir": "./out-tsc/app",
+ "types": []
+ },
+ "files": ["src/main.ts"],
+ "include": ["src/**/*.d.ts"]
+}
diff --git a/examples/angular/multiple-persisters/tsconfig.json b/examples/angular/multiple-persisters/tsconfig.json
new file mode 100644
index 0000000000..d0d73c8beb
--- /dev/null
+++ b/examples/angular/multiple-persisters/tsconfig.json
@@ -0,0 +1,31 @@
+{
+ "compileOnSave": false,
+ "compilerOptions": {
+ "outDir": "./dist/out-tsc",
+ "forceConsistentCasingInFileNames": true,
+ "strict": true,
+ "noImplicitOverride": true,
+ "noPropertyAccessFromIndexSignature": true,
+ "noImplicitReturns": true,
+ "noFallthroughCasesInSwitch": true,
+ "skipLibCheck": true,
+ "isolatedModules": true,
+ "esModuleInterop": true,
+ "sourceMap": true,
+ "declaration": false,
+ "experimentalDecorators": true,
+ "moduleResolution": "bundler",
+ "importHelpers": true,
+ "target": "ES2022",
+ "module": "ES2022",
+ "useDefineForClassFields": false,
+ "lib": ["ES2022", "dom"]
+ },
+ "angularCompilerOptions": {
+ "enableI18nLegacyMessageIdFormat": false,
+ "strictInjectionParameters": true,
+ "strictInputAccessModifiers": true,
+ "strictStandalone": true,
+ "strictTemplates": true
+ }
+}
diff --git a/packages/angular-persist-query-client-experimental/.attw.json b/packages/angular-persist-query-client-experimental/.attw.json
new file mode 100644
index 0000000000..ce409e67a8
--- /dev/null
+++ b/packages/angular-persist-query-client-experimental/.attw.json
@@ -0,0 +1,3 @@
+{
+ "ignoreRules": ["cjs-resolves-to-esm", "no-resolution"]
+}
diff --git a/packages/angular-persist-query-client-experimental/config/api-extractor.json b/packages/angular-persist-query-client-experimental/config/api-extractor.json
new file mode 100644
index 0000000000..dd9a7a854f
--- /dev/null
+++ b/packages/angular-persist-query-client-experimental/config/api-extractor.json
@@ -0,0 +1,24 @@
+{
+ "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json",
+
+ "mainEntryPointFilePath": "/build/index.d.ts",
+
+ "newlineKind": "lf",
+
+ "apiReport": {
+ "enabled": true
+ },
+
+ "docModel": {
+ "enabled": false
+ },
+
+ "dtsRollup": {
+ "enabled": true,
+ "untrimmedFilePath": "/build/rollup.d.ts"
+ },
+
+ "tsdocMetadata": {
+ "enabled": false
+ }
+}
diff --git a/packages/angular-persist-query-client-experimental/eslint.config.js b/packages/angular-persist-query-client-experimental/eslint.config.js
new file mode 100644
index 0000000000..b0ec381452
--- /dev/null
+++ b/packages/angular-persist-query-client-experimental/eslint.config.js
@@ -0,0 +1,31 @@
+// @ts-check
+
+import pluginJsdoc from 'eslint-plugin-jsdoc'
+import rootConfig from '../../eslint.config.js'
+
+export default [
+ ...rootConfig,
+ pluginJsdoc.configs['flat/recommended-typescript'],
+ {
+ rules: {
+ 'cspell/spellchecker': [
+ 'warn',
+ {
+ cspell: {
+ ignoreRegExpList: ['\\ɵ.+'],
+ },
+ },
+ ],
+ 'jsdoc/require-hyphen-before-param-description': 1,
+ 'jsdoc/sort-tags': 1,
+ 'jsdoc/require-throws': 1,
+ 'jsdoc/check-tag-names': [
+ 'warn',
+ {
+ // Not compatible with Api Extractor @public
+ typed: false,
+ },
+ ],
+ },
+ },
+]
diff --git a/packages/angular-persist-query-client-experimental/package.json b/packages/angular-persist-query-client-experimental/package.json
new file mode 100644
index 0000000000..9c1a1c2e87
--- /dev/null
+++ b/packages/angular-persist-query-client-experimental/package.json
@@ -0,0 +1,73 @@
+{
+ "name": "@tanstack/angular-query-persist-client-experimental",
+ "version": "5.62.7",
+ "description": "Angular bindings to work with persisters in TanStack/angular-query",
+ "author": "Omer Gronich",
+ "license": "MIT",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/TanStack/query.git",
+ "directory": "packages/angular-query-persist-client-experimental"
+ },
+ "homepage": "https://tanstack.com/query",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ },
+ "scripts": {
+ "clean": "rimraf ./build ./coverage",
+ "test:eslint": "eslint ./src",
+ "test:types": "pnpm run \"/^test:types:ts[0-9]{2}$/\"",
+ "test:types:ts50": "node ../../node_modules/typescript50/lib/tsc.js",
+ "test:types:ts51": "node ../../node_modules/typescript51/lib/tsc.js",
+ "test:types:ts52": "node ../../node_modules/typescript52/lib/tsc.js",
+ "test:types:ts53": "tsc",
+ "test:lib": "vitest",
+ "test:lib:dev": "pnpm run test:lib --watch",
+ "test:build": "publint --strict && attw --pack",
+ "build": "pnpm build:tsup",
+ "build:tsup": "tsup"
+ },
+ "type": "module",
+ "types": "build/index.d.ts",
+ "module": "build/index.js",
+ "exports": {
+ ".": {
+ "import": {
+ "types": "./build/index.d.ts",
+ "default": "./build/index.js"
+ }
+ },
+ "./package.json": {
+ "default": "./package.json"
+ }
+ },
+ "sideEffects": false,
+ "files": [
+ "build",
+ "src",
+ "!src/__tests__"
+ ],
+ "dependencies": {
+ "@tanstack/query-persist-client-core": "workspace:*"
+ },
+ "devDependencies": {
+ "@analogjs/vite-plugin-angular": "^1.6.4",
+ "@angular/compiler": "^19.1.0-next.0",
+ "@angular/core": "^19.1.0-next.0",
+ "@angular/platform-browser": "^19.1.0-next.0",
+ "@angular/platform-browser-dynamic": "^19.1.0-next.0",
+ "@microsoft/api-extractor": "^7.48.0",
+ "@tanstack/angular-query-experimental": "workspace:*",
+ "@testing-library/angular": "^17.3.2",
+ "@testing-library/dom": "^10.4.0",
+ "eslint-plugin-jsdoc": "^50.5.0",
+ "tsup": "8.0.2",
+ "typescript": "5.7.2"
+ },
+ "peerDependencies": {
+ "@angular/common": ">=16.0.0",
+ "@angular/core": ">=16.0.0",
+ "@tanstack/angular-query-experimental": "workspace:*"
+ }
+}
diff --git a/packages/angular-persist-query-client-experimental/src/__tests__/utils.ts b/packages/angular-persist-query-client-experimental/src/__tests__/utils.ts
new file mode 100644
index 0000000000..73fb0ed26f
--- /dev/null
+++ b/packages/angular-persist-query-client-experimental/src/__tests__/utils.ts
@@ -0,0 +1,12 @@
+let queryKeyCount = 0
+
+export function queryKey(): Array {
+ queryKeyCount++
+ return [`query_${queryKeyCount}`]
+}
+
+export function sleep(timeout: number): Promise {
+ return new Promise((resolve, _reject) => {
+ setTimeout(resolve, timeout)
+ })
+}
diff --git a/packages/angular-persist-query-client-experimental/src/__tests__/with-persist-query-client.test.ts b/packages/angular-persist-query-client-experimental/src/__tests__/with-persist-query-client.test.ts
new file mode 100644
index 0000000000..8ffd261988
--- /dev/null
+++ b/packages/angular-persist-query-client-experimental/src/__tests__/with-persist-query-client.test.ts
@@ -0,0 +1,501 @@
+import { describe, expect, test, vi } from 'vitest'
+import {
+ QueryClient,
+ injectQuery,
+ provideTanStackQuery,
+} from '@tanstack/angular-query-experimental'
+import { persistQueryClientSave } from '@tanstack/query-persist-client-core'
+import { Component, effect } from '@angular/core'
+import { render, screen, waitFor } from '@testing-library/angular'
+import { withPersistQueryClient } from '../with-persist-query-client'
+import { queryKey, sleep } from './utils'
+import type {
+ PersistedClient,
+ Persister,
+} from '@tanstack/query-persist-client-core'
+
+const createMockPersister = (): Persister => {
+ let storedState: PersistedClient | undefined
+
+ return {
+ persistClient(persistClient: PersistedClient) {
+ storedState = persistClient
+ },
+ async restoreClient() {
+ await sleep(10)
+ return storedState
+ },
+ removeClient() {
+ storedState = undefined
+ },
+ }
+}
+
+const createMockErrorPersister = (
+ removeClient: Persister['removeClient'],
+): [Error, Persister] => {
+ const error = new Error('restore failed')
+ return [
+ error,
+ {
+ async persistClient() {
+ // noop
+ },
+ async restoreClient() {
+ await sleep(10)
+ throw error
+ },
+ removeClient,
+ },
+ ]
+}
+
+describe('withPersistQueryClient', () => {
+ test('restores cache from persister', async () => {
+ const key = queryKey()
+ const states: Array<{
+ status: string
+ fetchStatus: string
+ data: string | undefined
+ }> = []
+
+ const queryClient = new QueryClient()
+ await queryClient.prefetchQuery({
+ queryKey: key,
+ queryFn: () => Promise.resolve('hydrated'),
+ })
+
+ const persister = createMockPersister()
+
+ await persistQueryClientSave({ queryClient, persister })
+
+ queryClient.clear()
+
+ @Component({
+ template: `
+
+
{{ state.data() }}
+ fetchStatus: {{ state.fetchStatus() }}
+
+ `,
+ })
+ class Page {
+ state = injectQuery(() => ({
+ queryKey: key,
+ queryFn: async () => {
+ await sleep(10)
+ return 'fetched'
+ },
+ }))
+ _ = effect(() => {
+ states.push({
+ status: this.state.status(),
+ fetchStatus: this.state.fetchStatus(),
+ data: this.state.data(),
+ })
+ })
+ }
+
+ render(Page, {
+ providers: [
+ provideTanStackQuery(
+ queryClient,
+ withPersistQueryClient([{ persistOptions: { persister } }]),
+ ),
+ ],
+ })
+
+ await waitFor(() => screen.getByText('fetchStatus: idle'))
+ await waitFor(() => screen.getByText('hydrated'))
+ await waitFor(() => screen.getByText('fetched'))
+
+ expect(states).toHaveLength(3)
+
+ expect(states[0]).toMatchObject({
+ status: 'pending',
+ fetchStatus: 'idle',
+ data: undefined,
+ })
+
+ expect(states[1]).toMatchObject({
+ status: 'success',
+ fetchStatus: 'fetching',
+ data: 'hydrated',
+ })
+
+ expect(states[2]).toMatchObject({
+ status: 'success',
+ fetchStatus: 'idle',
+ data: 'fetched',
+ })
+ })
+
+ test.todo(
+ '(Write this test after injectQueries is working) should also put injectQueries into idle state',
+ )
+
+ test('should show initialData while restoring', async () => {
+ const key = queryKey()
+ const states: Array<{
+ status: string
+ fetchStatus: string
+ data: string | undefined
+ }> = []
+
+ const queryClient = new QueryClient()
+ await queryClient.prefetchQuery({
+ queryKey: key,
+ queryFn: () => Promise.resolve('hydrated'),
+ })
+
+ const persister = createMockPersister()
+
+ await persistQueryClientSave({ queryClient, persister })
+
+ queryClient.clear()
+
+ @Component({
+ template: `
+
+
{{ state.data() }}
+ fetchStatus: {{ state.fetchStatus() }}
+
+ `,
+ })
+ class Page {
+ state = injectQuery(() => ({
+ queryKey: key,
+ queryFn: async () => {
+ await sleep(10)
+ return 'fetched'
+ },
+ initialData: 'initial',
+ // make sure that initial data is older than the hydration data
+ // otherwise initialData would be newer and takes precedence
+ initialDataUpdatedAt: 1,
+ }))
+ _ = effect(() => {
+ states.push({
+ status: this.state.status(),
+ fetchStatus: this.state.fetchStatus(),
+ data: this.state.data(),
+ })
+ })
+ }
+
+ render(Page, {
+ providers: [
+ provideTanStackQuery(
+ queryClient,
+ withPersistQueryClient([{ persistOptions: { persister } }]),
+ ),
+ ],
+ })
+
+ await waitFor(() => screen.getByText('fetched'))
+
+ expect(states).toHaveLength(3)
+
+ expect(states[0]).toMatchObject({
+ status: 'success',
+ fetchStatus: 'idle',
+ data: 'initial',
+ })
+
+ expect(states[1]).toMatchObject({
+ status: 'success',
+ fetchStatus: 'fetching',
+ data: 'hydrated',
+ })
+
+ expect(states[2]).toMatchObject({
+ status: 'success',
+ fetchStatus: 'idle',
+ data: 'fetched',
+ })
+ })
+
+ test('should not refetch after restoring when data is fresh', async () => {
+ const key = queryKey()
+ const states: Array<{
+ status: string
+ fetchStatus: string
+ data: string | undefined
+ }> = []
+
+ const queryClient = new QueryClient()
+ await queryClient.prefetchQuery({
+ queryKey: key,
+ queryFn: () => Promise.resolve('hydrated'),
+ })
+
+ const persister = createMockPersister()
+
+ await persistQueryClientSave({ queryClient, persister })
+
+ queryClient.clear()
+
+ let fetched = false
+
+ @Component({
+ template: `
+
+
data: {{ state.data() ?? 'null' }}
+ fetchStatus: {{ state.fetchStatus() }}
+
+ `,
+ })
+ class Page {
+ state = injectQuery(() => ({
+ queryKey: key,
+ queryFn: async () => {
+ fetched = true
+ await sleep(10)
+ return 'fetched'
+ },
+ staleTime: Infinity,
+ }))
+ _ = effect(() => {
+ states.push({
+ status: this.state.status(),
+ fetchStatus: this.state.fetchStatus(),
+ data: this.state.data(),
+ })
+ })
+ }
+
+ render(Page, {
+ providers: [
+ provideTanStackQuery(
+ queryClient,
+ withPersistQueryClient([{ persistOptions: { persister } }]),
+ ),
+ ],
+ })
+
+ await waitFor(() => screen.getByText('data: null'))
+ await waitFor(() => screen.getByText('data: hydrated'))
+
+ expect(states).toHaveLength(2)
+
+ expect(fetched).toBe(false)
+
+ expect(states[0]).toMatchObject({
+ status: 'pending',
+ fetchStatus: 'idle',
+ data: undefined,
+ })
+
+ expect(states[1]).toMatchObject({
+ status: 'success',
+ fetchStatus: 'idle',
+ data: 'hydrated',
+ })
+ })
+
+ test('should call onSuccess after successful restoring', async () => {
+ const key = queryKey()
+ const queryClient = new QueryClient()
+ await queryClient.prefetchQuery({
+ queryKey: key,
+ queryFn: () => Promise.resolve('hydrated'),
+ })
+
+ const persister = createMockPersister()
+ await persistQueryClientSave({ queryClient, persister })
+
+ queryClient.clear()
+
+ @Component({
+ template: `
+
+
{{ state.data() }}
+ fetchStatus: {{ state.fetchStatus() }}
+
+ `,
+ })
+ class Page {
+ state = injectQuery(() => ({
+ queryKey: key,
+ queryFn: async () => {
+ await sleep(10)
+ return 'fetched'
+ },
+ }))
+ }
+
+ const onSuccess = vi.fn()
+
+ render(Page, {
+ providers: [
+ provideTanStackQuery(
+ queryClient,
+ withPersistQueryClient([
+ {
+ persistOptions: { persister },
+ onSuccess,
+ },
+ ]),
+ ),
+ ],
+ })
+
+ expect(onSuccess).toHaveBeenCalledTimes(0)
+ await waitFor(() => screen.getByText('fetched'))
+ expect(onSuccess).toHaveBeenCalledTimes(1)
+ })
+
+ test('should remove cache after non-successful restoring', async () => {
+ const key = queryKey()
+ const onErrorMock = vi
+ .spyOn(console, 'error')
+ .mockImplementation(() => undefined)
+ const queryClient = new QueryClient()
+ const removeClient = vi.fn()
+ const [error, persister] = createMockErrorPersister(removeClient)
+
+ @Component({
+ template: `
+
+
{{ state.data() }}
+ fetchStatus: {{ state.fetchStatus() }}
+
+ `,
+ })
+ class Page {
+ state = injectQuery(() => ({
+ queryKey: key,
+ queryFn: async () => {
+ await sleep(10)
+ return 'fetched'
+ },
+ }))
+ }
+
+ render(Page, {
+ providers: [
+ provideTanStackQuery(
+ queryClient,
+ withPersistQueryClient([
+ {
+ persistOptions: { persister },
+ },
+ ]),
+ ),
+ ],
+ })
+
+ await waitFor(() => screen.getByText('fetched'))
+ expect(removeClient).toHaveBeenCalledTimes(1)
+ expect(onErrorMock).toHaveBeenCalledTimes(1)
+ expect(onErrorMock).toHaveBeenNthCalledWith(1, error)
+ onErrorMock.mockRestore()
+ })
+
+ test('should be able to support multiple persisters', async () => {
+ const key1 = queryKey()
+ const key2 = queryKey()
+ const states1: Array<{
+ status: string
+ fetchStatus: string
+ data: string | undefined
+ }> = []
+ const states2: Array<{
+ status: string
+ fetchStatus: string
+ data: string | undefined
+ }> = []
+
+ const queryClient = new QueryClient()
+ await queryClient.prefetchQuery({
+ queryKey: key1,
+ queryFn: () => Promise.resolve('hydrated 1'),
+ })
+
+ const persister1 = createMockPersister()
+ await persistQueryClientSave({ queryClient, persister: persister1 })
+ queryClient.clear()
+
+ const persister2 = createMockPersister()
+ await queryClient.prefetchQuery({
+ queryKey: key2,
+ queryFn: () => Promise.resolve('hydrated 2'),
+ })
+ await persistQueryClientSave({ queryClient, persister: persister2 })
+ queryClient.clear()
+
+ @Component({
+ template: `
+
+
{{ query1.data() }}
+ fetchStatus: {{ query1.fetchStatus() }}
+
+
+
{{ query2.data() }}
+ fetchStatus: {{ query2.fetchStatus() }}
+
+ `,
+ })
+ class Page {
+ query1 = injectQuery(() => ({
+ queryKey: key1,
+ queryFn: async () => {
+ await sleep(10)
+ return 'fetched 1'
+ },
+ }))
+ query2 = injectQuery(() => ({
+ queryKey: key2,
+ queryFn: async () => {
+ await sleep(10)
+ return 'fetched 2'
+ },
+ }))
+
+ _ = effect(() => {
+ states1.push({
+ status: this.query1.status(),
+ fetchStatus: this.query1.fetchStatus(),
+ data: this.query1.data(),
+ })
+ states2.push({
+ status: this.query2.status(),
+ fetchStatus: this.query2.fetchStatus(),
+ data: this.query2.data(),
+ })
+ })
+ }
+
+ const onSuccess1 = vi.fn()
+ const onSuccess2 = vi.fn()
+
+ render(Page, {
+ providers: [
+ provideTanStackQuery(
+ queryClient,
+ withPersistQueryClient([
+ {
+ persistOptions: {
+ persister: persister1,
+ },
+ onSuccess: onSuccess1,
+ },
+ {
+ persistOptions: {
+ persister: persister2,
+ },
+ onSuccess: onSuccess2,
+ },
+ ]),
+ ),
+ ],
+ })
+
+ expect(onSuccess1).toHaveBeenCalledTimes(0)
+ expect(onSuccess2).toHaveBeenCalledTimes(0)
+ await waitFor(() => screen.getByText('fetched 1'))
+ await waitFor(() => screen.getByText('fetched 2'))
+ expect(onSuccess1).toHaveBeenCalledTimes(1)
+ expect(onSuccess2).toHaveBeenCalledTimes(1)
+ })
+})
diff --git a/packages/angular-persist-query-client-experimental/src/index.ts b/packages/angular-persist-query-client-experimental/src/index.ts
new file mode 100644
index 0000000000..2f7546d196
--- /dev/null
+++ b/packages/angular-persist-query-client-experimental/src/index.ts
@@ -0,0 +1,4 @@
+// Re-export core
+export * from '@tanstack/query-persist-client-core'
+
+export * from './with-persist-query-client'
diff --git a/packages/angular-persist-query-client-experimental/src/test-setup.ts b/packages/angular-persist-query-client-experimental/src/test-setup.ts
new file mode 100644
index 0000000000..cb5fd340f3
--- /dev/null
+++ b/packages/angular-persist-query-client-experimental/src/test-setup.ts
@@ -0,0 +1,12 @@
+import '@analogjs/vite-plugin-angular/setup-vitest'
+
+import {
+ BrowserDynamicTestingModule,
+ platformBrowserDynamicTesting,
+} from '@angular/platform-browser-dynamic/testing'
+import { getTestBed } from '@angular/core/testing'
+
+getTestBed().initTestEnvironment(
+ BrowserDynamicTestingModule,
+ platformBrowserDynamicTesting(),
+)
diff --git a/packages/angular-persist-query-client-experimental/src/with-persist-query-client.ts b/packages/angular-persist-query-client-experimental/src/with-persist-query-client.ts
new file mode 100644
index 0000000000..049c05abf5
--- /dev/null
+++ b/packages/angular-persist-query-client-experimental/src/with-persist-query-client.ts
@@ -0,0 +1,93 @@
+import {
+ injectQueryClient,
+ provideIsRestoring,
+ queryFeature,
+} from '@tanstack/angular-query-experimental'
+import {
+ DestroyRef,
+ ENVIRONMENT_INITIALIZER,
+ PLATFORM_ID,
+ inject,
+ signal,
+} from '@angular/core'
+import { isPlatformBrowser } from '@angular/common'
+import {
+ persistQueryClientRestore,
+ persistQueryClientSubscribe,
+} from '@tanstack/query-persist-client-core'
+import type { PersistQueryClientOptions as PersistQueryClientOptionsCore } from '@tanstack/query-persist-client-core'
+import type { PersistQueryClientFeature } from '@tanstack/angular-query-experimental'
+
+type PersistQueryClientOptions = {
+ persistOptions: Omit
+ onSuccess?: () => Promise | unknown
+}
+
+/**
+ * Enables persistence.
+ *
+ * **Example**
+ *
+ * ```ts
+ * const localStoragePersister = createSyncStoragePersister({
+ * storage: window.localStorage,
+ * })
+ *
+ * export const appConfig: ApplicationConfig = {
+ * providers: [
+ * provideTanStackQuery(
+ * new QueryClient(),
+ * withPersistQueryClient([
+ * {
+ * persistOptions: {
+ * persister: localStoragePersister,
+ * },
+ * onSuccess: () => console.log('Restoration completed successfully.'),
+ * },
+ * ])
+ * )
+ * ]
+ * }
+ * ```
+ * @param persistQueryClientOptions - An array of objects containing persistOptions and an onSuccess callback which gets called when the restoration process is complete.
+ * @returns A set of providers for use with `provideTanStackQuery`.
+ * @public
+ */
+export function withPersistQueryClient(
+ persistQueryClientOptions: Array,
+): PersistQueryClientFeature {
+ const isRestoring = signal(false)
+ const providers = [
+ provideIsRestoring(isRestoring.asReadonly()),
+ {
+ provide: ENVIRONMENT_INITIALIZER,
+ multi: true,
+ useValue: () => {
+ if (!isPlatformBrowser(inject(PLATFORM_ID))) return
+ const destroyRef = inject(DestroyRef)
+ const queryClient = injectQueryClient()
+
+ isRestoring.set(true)
+ const restorations = persistQueryClientOptions.map(
+ ({ onSuccess, persistOptions }) => {
+ const options = { queryClient, ...persistOptions }
+ return persistQueryClientRestore(options).then(async () => {
+ try {
+ if (onSuccess) {
+ await onSuccess()
+ }
+ } finally {
+ const cleanup = persistQueryClientSubscribe(options)
+ destroyRef.onDestroy(cleanup)
+ }
+ })
+ },
+ )
+ Promise.all(restorations).finally(() => {
+ isRestoring.set(false)
+ })
+ },
+ },
+ ]
+ return queryFeature('PersistQueryClient', providers)
+}
diff --git a/packages/angular-persist-query-client-experimental/tsconfig.json b/packages/angular-persist-query-client-experimental/tsconfig.json
new file mode 100644
index 0000000000..c7fce5ae9d
--- /dev/null
+++ b/packages/angular-persist-query-client-experimental/tsconfig.json
@@ -0,0 +1,13 @@
+{
+ "extends": "../../tsconfig.json",
+ "compilerOptions": {
+ "moduleResolution": "Bundler",
+ "noImplicitOverride": true,
+ "noPropertyAccessFromIndexSignature": true,
+ "noFallthroughCasesInSwitch": true,
+ "useDefineForClassFields": false,
+ "target": "ES2022",
+ "types": ["vitest/globals"]
+ },
+ "include": ["src", "eslint.config.js", "tsup.config.js", "vite.config.ts"]
+}
diff --git a/packages/angular-persist-query-client-experimental/tsup.config.js b/packages/angular-persist-query-client-experimental/tsup.config.js
new file mode 100644
index 0000000000..eadb7ad63d
--- /dev/null
+++ b/packages/angular-persist-query-client-experimental/tsup.config.js
@@ -0,0 +1,10 @@
+import { defineConfig } from 'tsup'
+
+export default defineConfig({
+ entry: ['src/index.ts'],
+ sourcemap: true,
+ clean: true,
+ format: ['esm'],
+ dts: true,
+ outDir: 'build',
+})
diff --git a/packages/angular-persist-query-client-experimental/vite.config.ts b/packages/angular-persist-query-client-experimental/vite.config.ts
new file mode 100644
index 0000000000..3793a2137c
--- /dev/null
+++ b/packages/angular-persist-query-client-experimental/vite.config.ts
@@ -0,0 +1,16 @@
+import { defineConfig } from 'vitest/config'
+import packageJson from './package.json'
+
+export default defineConfig({
+ test: {
+ name: packageJson.name,
+ dir: './src',
+ watch: false,
+ environment: 'jsdom',
+ setupFiles: ['src/test-setup.ts'],
+ coverage: { enabled: true, provider: 'istanbul', include: ['src/**/*'] },
+ typecheck: { enabled: true },
+ globals: true,
+ restoreMocks: true,
+ },
+})
diff --git a/packages/angular-query-experimental/src/create-base-query.ts b/packages/angular-query-experimental/src/create-base-query.ts
index d53a387087..a78a8eb0c1 100644
--- a/packages/angular-query-experimental/src/create-base-query.ts
+++ b/packages/angular-query-experimental/src/create-base-query.ts
@@ -1,5 +1,4 @@
import {
- DestroyRef,
Injector,
NgZone,
VERSION,
@@ -13,6 +12,7 @@ import {
import { QueryClient, notifyManager } from '@tanstack/query-core'
import { signalProxy } from './signal-proxy'
import { shouldThrowError } from './util'
+import { injectIsRestoring } from './inject-is-restoring'
import type {
QueryKey,
QueryObserver,
@@ -41,8 +41,8 @@ export function createBaseQuery<
) {
const injector = inject(Injector)
const ngZone = injector.get(NgZone)
- const destroyRef = injector.get(DestroyRef)
const queryClient = injector.get(QueryClient)
+ const isRestoring = injectIsRestoring(injector)
/**
* Signal that has the default options from query client applied
@@ -53,7 +53,9 @@ export function createBaseQuery<
const defaultedOptionsSignal = computed(() => {
const options = runInInjectionContext(injector, () => optionsFn())
const defaultedOptions = queryClient.defaultQueryOptions(options)
- defaultedOptions._optimisticResults = 'optimistic'
+ defaultedOptions._optimisticResults = isRestoring()
+ ? 'isRestoring'
+ : 'optimistic'
return defaultedOptions
})
@@ -104,33 +106,33 @@ export function createBaseQuery<
},
)
- effect(() => {
+ effect((onCleanup) => {
// observer.trackResult is not used as this optimization is not needed for Angular
const observer = observerSignal()
-
- untracked(() => {
- const unsubscribe = ngZone.runOutsideAngular(() =>
- observer.subscribe(
- notifyManager.batchCalls((state) => {
- ngZone.run(() => {
- if (
- state.isError &&
- !state.isFetching &&
- // !isRestoring() && // todo: enable when client persistence is implemented
- shouldThrowError(observer.options.throwOnError, [
- state.error,
- observer.getCurrentQuery(),
- ])
- ) {
- throw state.error
- }
- resultFromSubscriberSignal.set(state)
- })
- }),
- ),
- )
- destroyRef.onDestroy(unsubscribe)
- })
+ const unsubscribe = isRestoring()
+ ? () => undefined
+ : untracked(() =>
+ ngZone.runOutsideAngular(() =>
+ observer.subscribe(
+ notifyManager.batchCalls((state) => {
+ ngZone.run(() => {
+ if (
+ state.isError &&
+ !state.isFetching &&
+ shouldThrowError(observer.options.throwOnError, [
+ state.error,
+ observer.getCurrentQuery(),
+ ])
+ ) {
+ throw state.error
+ }
+ resultFromSubscriberSignal.set(state)
+ })
+ }),
+ ),
+ ),
+ )
+ onCleanup(unsubscribe)
})
return signalProxy(
diff --git a/packages/angular-query-experimental/src/index.ts b/packages/angular-query-experimental/src/index.ts
index aa6292d4b5..da68067d33 100644
--- a/packages/angular-query-experimental/src/index.ts
+++ b/packages/angular-query-experimental/src/index.ts
@@ -22,6 +22,7 @@ export { infiniteQueryOptions } from './infinite-query-options'
export * from './inject-infinite-query'
export * from './inject-is-fetching'
export * from './inject-is-mutating'
+export * from './inject-is-restoring'
export * from './inject-mutation'
export * from './inject-mutation-state'
export * from './inject-queries'
diff --git a/packages/angular-query-experimental/src/inject-is-restoring.ts b/packages/angular-query-experimental/src/inject-is-restoring.ts
new file mode 100644
index 0000000000..56753a5485
--- /dev/null
+++ b/packages/angular-query-experimental/src/inject-is-restoring.ts
@@ -0,0 +1,32 @@
+import { InjectionToken, computed, inject } from '@angular/core'
+import { assertInjector } from './util/assert-injector/assert-injector'
+import type { Injector, Provider, Signal } from '@angular/core'
+
+const IsRestoring = new InjectionToken>('IsRestoring')
+
+/**
+ * Injects a signal that tracks whether a restore is currently in progress. {@link injectQuery} and friends also check this internally to avoid race conditions between the restore and mounting queries.
+ * @param injector - The Angular injector to use.
+ * @returns signal with boolean that indicates whether a restore is in progress.
+ * @public
+ */
+export function injectIsRestoring(injector?: Injector): Signal {
+ return assertInjector(
+ injectIsRestoring,
+ injector,
+ () => inject(IsRestoring, { optional: true }) ?? computed(() => false),
+ )
+}
+
+/**
+ * Used by TanStack Query Angular persist client plugin to provide the signal that tracks the restore state
+ * @param isRestoring - a readonly signal that returns a boolean
+ * @returns Provider for the `isRestoring` signal
+ * @public
+ */
+export function provideIsRestoring(isRestoring: Signal): Provider {
+ return {
+ provide: IsRestoring,
+ useValue: isRestoring,
+ }
+}
diff --git a/packages/angular-query-experimental/src/inject-queries.ts b/packages/angular-query-experimental/src/inject-queries.ts
index 8f969993d6..345d511924 100644
--- a/packages/angular-query-experimental/src/inject-queries.ts
+++ b/packages/angular-query-experimental/src/inject-queries.ts
@@ -12,6 +12,7 @@ import {
signal,
} from '@angular/core'
import { assertInjector } from './util/assert-injector/assert-injector'
+import { injectIsRestoring } from './inject-is-restoring'
import type { Injector, Signal } from '@angular/core'
import type {
DefaultError,
@@ -212,12 +213,15 @@ export function injectQueries<
const destroyRef = inject(DestroyRef)
const ngZone = inject(NgZone)
const queryClient = inject(QueryClient)
+ const isRestoring = injectIsRestoring(injector)
const defaultedQueries = computed(() => {
return queries().map((opts) => {
const defaultedOptions = queryClient.defaultQueryOptions(opts)
// Make sure the results are already in fetching state before subscribing or updating options
- defaultedOptions._optimisticResults = 'optimistic'
+ defaultedOptions._optimisticResults = isRestoring()
+ ? 'isRestoring'
+ : 'optimistic'
return defaultedOptions as QueryObserverOptions
})
@@ -246,10 +250,14 @@ export function injectQueries<
const result = signal(getCombinedResult() as any)
- const unsubscribe = ngZone.runOutsideAngular(() =>
- observer.subscribe(notifyManager.batchCalls(result.set)),
- )
- destroyRef.onDestroy(unsubscribe)
+ effect(() => {
+ const unsubscribe = isRestoring()
+ ? () => undefined
+ : ngZone.runOutsideAngular(() =>
+ observer.subscribe(notifyManager.batchCalls(result.set)),
+ )
+ destroyRef.onDestroy(unsubscribe)
+ })
return result
})
diff --git a/packages/angular-query-experimental/src/providers.ts b/packages/angular-query-experimental/src/providers.ts
index 42645b1b85..87adfa581e 100644
--- a/packages/angular-query-experimental/src/providers.ts
+++ b/packages/angular-query-experimental/src/providers.ts
@@ -142,7 +142,7 @@ export interface QueryFeature {
* @param providers -
* @returns A Query feature.
*/
-function queryFeature(
+export function queryFeature(
kind: TFeatureKind,
providers: Array,
): QueryFeature {
@@ -157,6 +157,13 @@ function queryFeature(
*/
export type DeveloperToolsFeature = QueryFeature<'DeveloperTools'>
+/**
+ * A type alias that represents a feature which enables persistence.
+ * The type is used to describe the return value of the `withPersistQueryClient` function.
+ * @public
+ */
+export type PersistQueryClientFeature = QueryFeature<'PersistQueryClient'>
+
/**
* Options for configuring the TanStack Query devtools.
* @public
@@ -346,8 +353,8 @@ export function withDevtools(
* @public
* @see {@link provideTanStackQuery}
*/
-export type QueryFeatures = DeveloperToolsFeature // Union type of features but just one now
+export type QueryFeatures = DeveloperToolsFeature | PersistQueryClientFeature
-export const queryFeatures = ['DeveloperTools'] as const
+export const queryFeatures = ['DeveloperTools', 'PersistQueryClient'] as const
export type QueryFeatureKind = (typeof queryFeatures)[number]
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index b6f76d18bb..9f38072b75 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -208,6 +208,55 @@ importers:
specifier: 5.7.2
version: 5.7.2
+ examples/angular/basic-persister:
+ dependencies:
+ '@angular/common':
+ specifier: ^19.1.0-next.0
+ version: 19.1.0-next.0(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0))(rxjs@7.8.1)
+ '@angular/compiler':
+ specifier: ^19.1.0-next.0
+ version: 19.1.0-next.0(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0))
+ '@angular/core':
+ specifier: ^19.1.0-next.0
+ version: 19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0)
+ '@angular/platform-browser':
+ specifier: ^19.1.0-next.0
+ version: 19.1.0-next.0(@angular/common@19.1.0-next.0(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0))(rxjs@7.8.1))(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0))
+ '@angular/platform-browser-dynamic':
+ specifier: ^19.1.0-next.0
+ version: 19.1.0-next.0(@angular/common@19.1.0-next.0(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0))(rxjs@7.8.1))(@angular/compiler@19.1.0-next.0(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0)))(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0))(@angular/platform-browser@19.1.0-next.0(@angular/common@19.1.0-next.0(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0))(rxjs@7.8.1))(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0)))
+ '@tanstack/angular-query-experimental':
+ specifier: ^5.62.7
+ version: link:../../../packages/angular-query-experimental
+ '@tanstack/angular-query-persist-client-experimental':
+ specifier: ^5.62.7
+ version: link:../../../packages/angular-persist-query-client-experimental
+ '@tanstack/query-sync-storage-persister':
+ specifier: ^5.62.3
+ version: link:../../../packages/query-sync-storage-persister
+ rxjs:
+ specifier: ^7.8.1
+ version: 7.8.1
+ tslib:
+ specifier: ^2.6.3
+ version: 2.8.1
+ zone.js:
+ specifier: ^0.15.0
+ version: 0.15.0
+ devDependencies:
+ '@angular/build':
+ specifier: ^19.0.2
+ version: 19.0.2(@angular/compiler-cli@19.1.0-next.0(@angular/compiler@19.1.0-next.0(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0)))(typescript@5.7.2))(@angular/compiler@19.1.0-next.0(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0)))(@types/node@22.9.3)(chokidar@4.0.1)(less@4.2.1)(lightningcss@1.27.0)(postcss@8.4.49)(tailwindcss@3.4.7)(terser@5.31.6)(typescript@5.7.2)
+ '@angular/cli':
+ specifier: ^19.0.2
+ version: 19.0.2(@types/node@22.9.3)(chokidar@4.0.1)
+ '@angular/compiler-cli':
+ specifier: ^19.1.0-next.0
+ version: 19.1.0-next.0(@angular/compiler@19.1.0-next.0(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0)))(typescript@5.7.2)
+ typescript:
+ specifier: 5.7.2
+ version: 5.7.2
+
examples/angular/devtools-panel:
dependencies:
'@angular/common':
@@ -300,6 +349,64 @@ importers:
specifier: 5.7.2
version: 5.7.2
+ examples/angular/multiple-persisters:
+ dependencies:
+ '@angular/common':
+ specifier: ^19.1.0-next.0
+ version: 19.1.0-next.0(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0))(rxjs@7.8.1)
+ '@angular/compiler':
+ specifier: ^19.1.0-next.0
+ version: 19.1.0-next.0(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0))
+ '@angular/core':
+ specifier: ^19.1.0-next.0
+ version: 19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0)
+ '@angular/platform-browser':
+ specifier: ^19.1.0-next.0
+ version: 19.1.0-next.0(@angular/common@19.1.0-next.0(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0))(rxjs@7.8.1))(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0))
+ '@angular/platform-browser-dynamic':
+ specifier: ^19.1.0-next.0
+ version: 19.1.0-next.0(@angular/common@19.1.0-next.0(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0))(rxjs@7.8.1))(@angular/compiler@19.1.0-next.0(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0)))(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0))(@angular/platform-browser@19.1.0-next.0(@angular/common@19.1.0-next.0(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0))(rxjs@7.8.1))(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0)))
+ '@tanstack/angular-query-experimental':
+ specifier: ^5.62.7
+ version: link:../../../packages/angular-query-experimental
+ '@tanstack/angular-query-persist-client-experimental':
+ specifier: ^5.62.7
+ version: link:../../../packages/angular-persist-query-client-experimental
+ '@tanstack/query-sync-storage-persister':
+ specifier: ^5.62.3
+ version: link:../../../packages/query-sync-storage-persister
+ rxjs:
+ specifier: ^7.8.1
+ version: 7.8.1
+ tslib:
+ specifier: ^2.6.3
+ version: 2.8.1
+ zone.js:
+ specifier: ^0.15.0
+ version: 0.15.0
+ devDependencies:
+ '@angular/build':
+ specifier: ^19.0.2
+ version: 19.0.2(@angular/compiler-cli@19.1.0-next.0(@angular/compiler@19.1.0-next.0(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0)))(typescript@5.7.2))(@angular/compiler@19.1.0-next.0(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0)))(@types/node@22.9.3)(chokidar@4.0.1)(less@4.2.1)(lightningcss@1.27.0)(postcss@8.4.49)(tailwindcss@3.4.7)(terser@5.31.6)(typescript@5.7.2)
+ '@angular/cli':
+ specifier: ^19.0.2
+ version: 19.0.2(@types/node@22.9.3)(chokidar@4.0.1)
+ '@angular/compiler-cli':
+ specifier: ^19.1.0-next.0
+ version: 19.1.0-next.0(@angular/compiler@19.1.0-next.0(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0)))(typescript@5.7.2)
+ autoprefixer:
+ specifier: ^10.4.20
+ version: 10.4.20(postcss@8.4.49)
+ postcss:
+ specifier: ^8.4.49
+ version: 8.4.49
+ tailwindcss:
+ specifier: ^3.4.7
+ version: 3.4.7
+ typescript:
+ specifier: 5.7.2
+ version: 5.7.2
+
examples/angular/pagination:
dependencies:
'@angular/common':
@@ -2085,6 +2192,52 @@ importers:
specifier: ^2.1.10
version: 2.1.10(typescript@5.6.3)
+ packages/angular-persist-query-client-experimental:
+ dependencies:
+ '@angular/common':
+ specifier: '>=16.0.0'
+ version: 17.3.12(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0))(rxjs@7.8.1)
+ '@tanstack/query-persist-client-core':
+ specifier: workspace:*
+ version: link:../query-persist-client-core
+ devDependencies:
+ '@analogjs/vite-plugin-angular':
+ specifier: ^1.6.4
+ version: 1.6.4(@angular-devkit/build-angular@18.2.12(@angular/compiler-cli@19.1.0-next.0(@angular/compiler@19.1.0-next.0(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0)))(typescript@5.7.2))(@types/node@22.9.3)(chokidar@3.6.0)(html-webpack-plugin@5.6.3(webpack@5.94.0(esbuild@0.19.12)))(lightningcss@1.27.0)(tailwindcss@3.4.7)(typescript@5.7.2))(@ngtools/webpack@18.2.12(@angular/compiler-cli@19.1.0-next.0(@angular/compiler@19.1.0-next.0(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0)))(typescript@5.7.2))(typescript@5.7.2)(webpack@5.94.0(esbuild@0.23.0)))
+ '@angular/compiler':
+ specifier: ^19.1.0-next.0
+ version: 19.1.0-next.0(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0))
+ '@angular/core':
+ specifier: ^19.1.0-next.0
+ version: 19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0)
+ '@angular/platform-browser':
+ specifier: ^19.1.0-next.0
+ version: 19.1.0-next.0(@angular/common@17.3.12(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0))(rxjs@7.8.1))(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0))
+ '@angular/platform-browser-dynamic':
+ specifier: ^19.1.0-next.0
+ version: 19.1.0-next.0(@angular/common@17.3.12(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0))(rxjs@7.8.1))(@angular/compiler@19.1.0-next.0(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0)))(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0))(@angular/platform-browser@19.1.0-next.0(@angular/common@17.3.12(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0))(rxjs@7.8.1))(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0)))
+ '@microsoft/api-extractor':
+ specifier: ^7.48.0
+ version: 7.48.0(@types/node@22.9.3)
+ '@tanstack/angular-query-experimental':
+ specifier: workspace:*
+ version: link:../angular-query-experimental
+ '@testing-library/angular':
+ specifier: ^17.3.2
+ version: 17.3.4(@angular/common@17.3.12(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0))(rxjs@7.8.1))(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0))(@angular/platform-browser@19.1.0-next.0(@angular/common@17.3.12(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0))(rxjs@7.8.1))(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0)))(@angular/router@19.1.0-next.0(@angular/common@17.3.12(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0))(rxjs@7.8.1))(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0))(@angular/platform-browser@19.1.0-next.0(@angular/common@17.3.12(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0))(rxjs@7.8.1))(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0)))(rxjs@7.8.1))(@testing-library/dom@10.4.0)
+ '@testing-library/dom':
+ specifier: ^10.4.0
+ version: 10.4.0
+ eslint-plugin-jsdoc:
+ specifier: ^50.5.0
+ version: 50.5.0(eslint@9.15.0(jiti@2.4.0))
+ tsup:
+ specifier: 8.0.2
+ version: 8.0.2(@microsoft/api-extractor@7.48.0(@types/node@22.9.3))(postcss@8.4.41)(typescript@5.7.2)
+ typescript:
+ specifier: 5.7.2
+ version: 5.7.2
+
packages/angular-query-devtools-experimental:
dependencies:
'@angular/common':
@@ -2096,7 +2249,7 @@ importers:
devDependencies:
'@analogjs/vite-plugin-angular':
specifier: ^1.6.4
- version: 1.6.4(@angular-devkit/build-angular@18.2.12(@angular/compiler-cli@19.1.0-next.0(@angular/compiler@19.1.0-next.0(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0)))(typescript@5.7.2))(@types/node@22.9.3)(chokidar@3.6.0)(html-webpack-plugin@5.6.3(webpack@5.94.0(esbuild@0.19.12)))(lightningcss@1.27.0)(tailwindcss@3.4.7)(typescript@5.7.2))(@ngtools/webpack@18.2.12(@angular/compiler-cli@19.1.0-next.0(@angular/compiler@19.1.0-next.0(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0)))(typescript@5.7.2))(typescript@5.7.2)(webpack@5.94.0(esbuild@0.23.0)))
+ version: 1.6.4(@angular-devkit/build-angular@18.2.12(@angular/compiler-cli@19.1.0-next.0(@angular/compiler@19.1.0-next.0(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0)))(typescript@5.7.2))(@types/node@22.9.3)(chokidar@3.6.0)(html-webpack-plugin@5.6.3(webpack@5.96.1(esbuild@0.24.0)))(lightningcss@1.27.0)(tailwindcss@3.4.7)(typescript@5.7.2))(@ngtools/webpack@18.2.12(@angular/compiler-cli@19.1.0-next.0(@angular/compiler@19.1.0-next.0(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0)))(typescript@5.7.2))(typescript@5.7.2)(webpack@5.96.1(esbuild@0.24.0)))
'@angular/core':
specifier: ^19.1.0-next.0
version: 19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0)
@@ -2111,7 +2264,7 @@ importers:
version: 50.5.0(eslint@9.15.0(jiti@2.4.0))
tsup:
specifier: 8.0.2
- version: 8.0.2(@microsoft/api-extractor@7.48.0(@types/node@22.9.3))(postcss@8.4.41)(typescript@5.7.2)
+ version: 8.0.2(@microsoft/api-extractor@7.48.0(@types/node@22.9.3))(postcss@8.4.49)(typescript@5.7.2)
typescript:
specifier: 5.7.2
version: 5.7.2
@@ -7039,6 +7192,15 @@ packages:
react: '>=16'
react-dom: '>=16'
+ '@testing-library/angular@17.3.4':
+ resolution: {integrity: sha512-QqBcRaVb4VJO66/5oboJXaME1PugM+y/tOpSTpgB7QwjcWgvcW63CQsX8JbSJQPgthk7gwhhgiHJAqyDUITp6Q==}
+ peerDependencies:
+ '@angular/common': '>= 17.0.0'
+ '@angular/core': '>= 17.0.0'
+ '@angular/platform-browser': '>= 17.0.0'
+ '@angular/router': '>= 17.0.0'
+ '@testing-library/dom': ^10.0.0
+
'@testing-library/dom@10.4.0':
resolution: {integrity: sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==}
engines: {node: '>=18'}
@@ -17201,6 +17363,12 @@ snapshots:
rxjs: 7.8.1
tslib: 2.8.1
+ '@angular/common@17.3.12(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0))(rxjs@7.8.1)':
+ dependencies:
+ '@angular/core': 19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0)
+ rxjs: 7.8.1
+ tslib: 2.8.1
+
'@angular/common@19.0.0(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0))(rxjs@7.8.1)':
dependencies:
'@angular/core': 19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0)
@@ -17283,6 +17451,14 @@ snapshots:
'@angular/platform-browser': 17.3.12(@angular/animations@17.3.12(@angular/core@17.3.12(rxjs@7.8.1)(zone.js@0.14.8)))(@angular/common@17.3.12(@angular/core@17.3.12(rxjs@7.8.1)(zone.js@0.14.8))(rxjs@7.8.1))(@angular/core@17.3.12(rxjs@7.8.1)(zone.js@0.14.8))
tslib: 2.8.1
+ '@angular/platform-browser-dynamic@19.1.0-next.0(@angular/common@17.3.12(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0))(rxjs@7.8.1))(@angular/compiler@19.1.0-next.0(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0)))(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0))(@angular/platform-browser@19.1.0-next.0(@angular/common@17.3.12(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0))(rxjs@7.8.1))(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0)))':
+ dependencies:
+ '@angular/common': 17.3.12(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0))(rxjs@7.8.1)
+ '@angular/compiler': 19.1.0-next.0(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0))
+ '@angular/core': 19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0)
+ '@angular/platform-browser': 19.1.0-next.0(@angular/common@17.3.12(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0))(rxjs@7.8.1))(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0))
+ tslib: 2.8.1
+
'@angular/platform-browser-dynamic@19.1.0-next.0(@angular/common@19.0.0(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0))(rxjs@7.8.1))(@angular/compiler@19.1.0-next.0(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0)))(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0))(@angular/platform-browser@19.1.0-next.0(@angular/common@19.0.0(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0))(rxjs@7.8.1))(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0)))':
dependencies:
'@angular/common': 19.0.0(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0))(rxjs@7.8.1)
@@ -17307,6 +17483,12 @@ snapshots:
optionalDependencies:
'@angular/animations': 17.3.12(@angular/core@17.3.12(rxjs@7.8.1)(zone.js@0.14.8))
+ '@angular/platform-browser@19.1.0-next.0(@angular/common@17.3.12(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0))(rxjs@7.8.1))(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0))':
+ dependencies:
+ '@angular/common': 17.3.12(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0))(rxjs@7.8.1)
+ '@angular/core': 19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0)
+ tslib: 2.8.1
+
'@angular/platform-browser@19.1.0-next.0(@angular/common@19.0.0(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0))(rxjs@7.8.1))(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0))':
dependencies:
'@angular/common': 19.0.0(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0))(rxjs@7.8.1)
@@ -17319,6 +17501,14 @@ snapshots:
'@angular/core': 19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0)
tslib: 2.8.1
+ '@angular/router@19.1.0-next.0(@angular/common@17.3.12(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0))(rxjs@7.8.1))(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0))(@angular/platform-browser@19.1.0-next.0(@angular/common@17.3.12(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0))(rxjs@7.8.1))(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0)))(rxjs@7.8.1)':
+ dependencies:
+ '@angular/common': 17.3.12(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0))(rxjs@7.8.1)
+ '@angular/core': 19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0)
+ '@angular/platform-browser': 19.1.0-next.0(@angular/common@17.3.12(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0))(rxjs@7.8.1))(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0))
+ rxjs: 7.8.1
+ tslib: 2.8.1
+
'@angular/router@19.1.0-next.0(@angular/common@19.1.0-next.0(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0))(rxjs@7.8.1))(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0))(@angular/platform-browser@19.1.0-next.0(@angular/common@19.1.0-next.0(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0))(rxjs@7.8.1))(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0)))(rxjs@7.8.1)':
dependencies:
'@angular/common': 19.1.0-next.0(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0))(rxjs@7.8.1)
@@ -23015,6 +23205,15 @@ snapshots:
react: 19.0.0
react-dom: 19.0.0(react@19.0.0)
+ '@testing-library/angular@17.3.4(@angular/common@17.3.12(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0))(rxjs@7.8.1))(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0))(@angular/platform-browser@19.1.0-next.0(@angular/common@17.3.12(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0))(rxjs@7.8.1))(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0)))(@angular/router@19.1.0-next.0(@angular/common@17.3.12(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0))(rxjs@7.8.1))(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0))(@angular/platform-browser@19.1.0-next.0(@angular/common@17.3.12(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0))(rxjs@7.8.1))(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0)))(rxjs@7.8.1))(@testing-library/dom@10.4.0)':
+ dependencies:
+ '@angular/common': 17.3.12(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0))(rxjs@7.8.1)
+ '@angular/core': 19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0)
+ '@angular/platform-browser': 19.1.0-next.0(@angular/common@17.3.12(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0))(rxjs@7.8.1))(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0))
+ '@angular/router': 19.1.0-next.0(@angular/common@17.3.12(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0))(rxjs@7.8.1))(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0))(@angular/platform-browser@19.1.0-next.0(@angular/common@17.3.12(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0))(rxjs@7.8.1))(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0)))(rxjs@7.8.1)
+ '@testing-library/dom': 10.4.0
+ tslib: 2.8.1
+
'@testing-library/dom@10.4.0':
dependencies:
'@babel/code-frame': 7.26.2
diff --git a/scripts/publish.js b/scripts/publish.js
index 9bfa1e5a35..3b6d770f1f 100644
--- a/scripts/publish.js
+++ b/scripts/publish.js
@@ -92,6 +92,10 @@ await publish({
name: '@tanstack/angular-query-experimental',
packageDir: 'packages/angular-query-experimental',
},
+ {
+ name: '@tanstack/angular-query-persist-client-experimental',
+ packageDir: 'packages/angular-persist-query-client-experimental',
+ },
],
branchConfigs: {
main: {