Repository: tanstack/query
Files analyzed: 1000
Estimated tokens: 792.2k
Directory structure:
└── tanstack-query/
├── codecov.yml
├── eslint.config.js
├── knip.json
├── LICENSE
├── nx.json
├── package.json
├── pnpm-workspace.yaml
├── prettier.config.js
├── .editorconfig
├── .npmrc
├── .nvmrc
├── .prettierignore
├── docs/
├── examples/
│ ├── angular/
│ │ ├── auto-refetching/
│ │ │ ├── README.md
│ │ │ ├── angular.json
│ │ │ ├── package.json
│ │ │ ├── tsconfig.app.json
│ │ │ ├── tsconfig.json
│ │ │ ├── .eslintrc.cjs
│ │ │ ├── src/
│ │ │ │ ├── index.html
│ │ │ │ ├── main.ts
│ │ │ │ └── app/
│ │ │ │ ├── app.component.ts
│ │ │ │ ├── app.config.ts
│ │ │ │ ├── components/
│ │ │ │ │ ├── auto-refetching.component.html
│ │ │ │ │ └── auto-refetching.component.ts
│ │ │ │ ├── interceptor/
│ │ │ │ │ └── mock-api.interceptor.ts
│ │ │ │ └── services/
│ │ │ │ └── tasks.service.ts
│ │ │ └── .devcontainer/
│ │ │ └── devcontainer.json
│ │ ├── basic/
│ │ │ ├── README.md
│ │ │ ├── angular.json
│ │ │ ├── package.json
│ │ │ ├── tsconfig.app.json
│ │ │ ├── tsconfig.json
│ │ │ ├── .eslintrc.cjs
│ │ │ ├── src/
│ │ │ │ ├── index.html
│ │ │ │ ├── main.ts
│ │ │ │ └── app/
│ │ │ │ ├── app.component.html
│ │ │ │ ├── app.component.ts
│ │ │ │ ├── app.config.ts
│ │ │ │ ├── components/
│ │ │ │ │ ├── post.component.html
│ │ │ │ │ ├── post.component.ts
│ │ │ │ │ ├── posts.component.html
│ │ │ │ │ └── posts.component.ts
│ │ │ │ └── services/
│ │ │ │ └── posts-service.ts
│ │ │ └── .devcontainer/
│ │ │ └── devcontainer.json
│ │ ├── basic-persister/
│ │ │ ├── README.md
│ │ │ ├── angular.json
│ │ │ ├── package.json
│ │ │ ├── tsconfig.app.json
│ │ │ ├── tsconfig.json
│ │ │ ├── .eslintrc.cjs
│ │ │ ├── src/
│ │ │ │ ├── index.html
│ │ │ │ ├── main.ts
│ │ │ │ └── app/
│ │ │ │ ├── app.component.html
│ │ │ │ ├── app.component.ts
│ │ │ │ ├── app.config.ts
│ │ │ │ ├── components/
│ │ │ │ │ ├── post.component.html
│ │ │ │ │ ├── post.component.ts
│ │ │ │ │ ├── posts.component.html
│ │ │ │ │ └── posts.component.ts
│ │ │ │ └── services/
│ │ │ │ └── posts-service.ts
│ │ │ └── .devcontainer/
│ │ │ └── devcontainer.json
│ │ ├── devtools-panel/
│ │ │ ├── README.md
│ │ │ ├── angular.json
│ │ │ ├── package.json
│ │ │ ├── tsconfig.app.json
│ │ │ ├── tsconfig.json
│ │ │ ├── src/
│ │ │ │ ├── index.html
│ │ │ │ ├── main.ts
│ │ │ │ └── app/
│ │ │ │ ├── app.component.ts
│ │ │ │ ├── app.config.ts
│ │ │ │ ├── app.routes.ts
│ │ │ │ └── components/
│ │ │ │ ├── basic-devtools-panel-example.component.ts
│ │ │ │ ├── example-query.component.ts
│ │ │ │ └── lazy-load-devtools-panel-example.component.ts
│ │ │ └── .devcontainer/
│ │ │ └── devcontainer.json
│ │ ├── infinite-query-with-max-pages/
│ │ │ ├── README.md
│ │ │ ├── angular.json
│ │ │ ├── package.json
│ │ │ ├── tsconfig.app.json
│ │ │ ├── tsconfig.json
│ │ │ ├── .eslintrc.cjs
│ │ │ ├── src/
│ │ │ │ ├── index.html
│ │ │ │ ├── main.ts
│ │ │ │ └── app/
│ │ │ │ ├── app.component.ts
│ │ │ │ ├── app.config.ts
│ │ │ │ ├── api/
│ │ │ │ │ └── projects-mock.interceptor.ts
│ │ │ │ ├── components/
│ │ │ │ │ ├── example.component.html
│ │ │ │ │ └── example.component.ts
│ │ │ │ ├── directives/
│ │ │ │ │ └── project-style.directive.ts
│ │ │ │ └── services/
│ │ │ │ └── projects.service.ts
│ │ │ └── .devcontainer/
│ │ │ └── devcontainer.json
│ │ ├── optimistic-updates/
│ │ │ ├── README.md
│ │ │ ├── angular.json
│ │ │ ├── package.json
│ │ │ ├── tsconfig.app.json
│ │ │ ├── tsconfig.json
│ │ │ ├── .eslintrc.cjs
│ │ │ ├── src/
│ │ │ │ ├── index.html
│ │ │ │ ├── main.ts
│ │ │ │ └── app/
│ │ │ │ ├── app.component.ts
│ │ │ │ ├── app.config.ts
│ │ │ │ ├── components/
│ │ │ │ │ └── optimistic-updates.component.ts
│ │ │ │ ├── interceptor/
│ │ │ │ │ └── mock-api.interceptor.ts
│ │ │ │ └── services/
│ │ │ │ └── tasks.service.ts
│ │ │ └── .devcontainer/
│ │ │ └── devcontainer.json
│ │ ├── pagination/
│ │ │ ├── README.md
│ │ │ ├── angular.json
│ │ │ ├── package.json
│ │ │ ├── tsconfig.app.json
│ │ │ ├── tsconfig.json
│ │ │ ├── src/
│ │ │ │ ├── index.html
│ │ │ │ ├── main.ts
│ │ │ │ └── app/
│ │ │ │ ├── app.component.ts
│ │ │ │ ├── app.config.ts
│ │ │ │ ├── api/
│ │ │ │ │ └── projects-mock.interceptor.ts
│ │ │ │ ├── components/
│ │ │ │ │ ├── example.component.html
│ │ │ │ │ └── example.component.ts
│ │ │ │ └── services/
│ │ │ │ └── projects.service.ts
│ │ │ └── .devcontainer/
│ │ │ └── devcontainer.json
│ │ ├── query-options-from-a-service/
│ │ │ ├── README.md
│ │ │ ├── angular.json
│ │ │ ├── package.json
│ │ │ ├── tsconfig.app.json
│ │ │ ├── tsconfig.json
│ │ │ ├── .eslintrc.cjs
│ │ │ ├── src/
│ │ │ │ ├── index.html
│ │ │ │ ├── main.ts
│ │ │ │ └── app/
│ │ │ │ ├── app.component.html
│ │ │ │ ├── app.component.ts
│ │ │ │ ├── app.config.ts
│ │ │ │ ├── app.routes.ts
│ │ │ │ ├── components/
│ │ │ │ │ ├── post.component.html
│ │ │ │ │ ├── post.component.ts
│ │ │ │ │ ├── posts.component.html
│ │ │ │ │ └── posts.component.ts
│ │ │ │ └── services/
│ │ │ │ └── queries-service.ts
│ │ │ └── .devcontainer/
│ │ │ └── devcontainer.json
│ │ ├── router/
│ │ │ ├── README.md
│ │ │ ├── angular.json
│ │ │ ├── package.json
│ │ │ ├── tsconfig.app.json
│ │ │ ├── tsconfig.json
│ │ │ ├── .eslintrc.cjs
│ │ │ ├── src/
│ │ │ │ ├── index.html
│ │ │ │ ├── main.ts
│ │ │ │ └── app/
│ │ │ │ ├── app.component.html
│ │ │ │ ├── app.component.ts
│ │ │ │ ├── app.config.ts
│ │ │ │ ├── app.routes.ts
│ │ │ │ ├── components/
│ │ │ │ │ ├── post.component.html
│ │ │ │ │ ├── post.component.ts
│ │ │ │ │ ├── posts.component.html
│ │ │ │ │ └── posts.component.ts
│ │ │ │ └── services/
│ │ │ │ └── posts-service.ts
│ │ │ └── .devcontainer/
│ │ │ └── devcontainer.json
│ │ ├── rxjs/
│ │ │ ├── README.md
│ │ │ ├── angular.json
│ │ │ ├── package.json
│ │ │ ├── tsconfig.app.json
│ │ │ ├── tsconfig.json
│ │ │ ├── .eslintrc.cjs
│ │ │ ├── src/
│ │ │ │ ├── index.html
│ │ │ │ ├── main.ts
│ │ │ │ └── app/
│ │ │ │ ├── app.component.ts
│ │ │ │ ├── app.config.ts
│ │ │ │ ├── api/
│ │ │ │ │ └── autocomplete-mock.interceptor.ts
│ │ │ │ ├── components/
│ │ │ │ │ ├── example.component.html
│ │ │ │ │ └── example.component.ts
│ │ │ │ └── services/
│ │ │ │ └── autocomplete-service.ts
│ │ │ └── .devcontainer/
│ │ │ └── devcontainer.json
│ │ └── simple/
│ │ ├── README.md
│ │ ├── angular.json
│ │ ├── package.json
│ │ ├── tsconfig.app.json
│ │ ├── tsconfig.json
│ │ ├── .eslintrc.cjs
│ │ ├── src/
│ │ │ ├── index.html
│ │ │ ├── main.ts
│ │ │ ├── styles.css
│ │ │ └── app/
│ │ │ ├── app.component.ts
│ │ │ ├── app.config.ts
│ │ │ └── components/
│ │ │ ├── simple-example.component.html
│ │ │ └── simple-example.component.ts
│ │ └── .devcontainer/
│ │ └── devcontainer.json
│ ├── react/
│ ├── solid/
│ │ ├── astro/
│ │ │ ├── README.md
│ │ │ ├── astro.config.mjs
│ │ │ ├── package.json
│ │ │ ├── tailwind.config.mjs
│ │ │ ├── tsconfig.json
│ │ │ ├── .gitignore
│ │ │ ├── public/
│ │ │ └── src/
│ │ │ ├── env.d.ts
│ │ │ ├── components/
│ │ │ │ ├── Link.tsx
│ │ │ │ └── SolidApp.tsx
│ │ │ ├── layouts/
│ │ │ │ └── MainLayout.astro
│ │ │ ├── pages/
│ │ │ │ └── index.astro
│ │ │ └── utils/
│ │ │ └── index.ts
│ │ ├── basic/
│ │ │ ├── README.md
│ │ │ ├── index.html
│ │ │ ├── package.json
│ │ │ ├── tsconfig.json
│ │ │ ├── vite.config.ts
│ │ │ ├── .eslintrc.cjs
│ │ │ ├── .gitignore
│ │ │ └── src/
│ │ │ ├── index.tsx
│ │ │ └── assets/
│ │ ├── basic-graphql-request/
│ │ │ ├── README.md
│ │ │ ├── index.html
│ │ │ ├── package.json
│ │ │ ├── tsconfig.json
│ │ │ ├── vite.config.ts
│ │ │ ├── .eslintrc.cjs
│ │ │ ├── .gitignore
│ │ │ └── src/
│ │ │ ├── index.tsx
│ │ │ └── assets/
│ │ ├── default-query-function/
│ │ │ ├── README.md
│ │ │ ├── index.html
│ │ │ ├── package.json
│ │ │ ├── tsconfig.json
│ │ │ ├── vite.config.ts
│ │ │ ├── .eslintrc.cjs
│ │ │ ├── .gitignore
│ │ │ └── src/
│ │ │ ├── index.tsx
│ │ │ └── assets/
│ │ ├── simple/
│ │ │ ├── README.md
│ │ │ ├── index.html
│ │ │ ├── package.json
│ │ │ ├── tsconfig.json
│ │ │ ├── vite.config.ts
│ │ │ ├── .eslintrc.cjs
│ │ │ ├── .gitignore
│ │ │ └── src/
│ │ │ ├── index.tsx
│ │ │ └── assets/
│ │ └── solid-start-streaming/
│ │ ├── README.md
│ │ ├── app.config.ts
│ │ ├── package.json
│ │ ├── tsconfig.json
│ │ ├── .gitignore
│ │ ├── public/
│ │ │ └── imgs/
│ │ └── src/
│ │ ├── app.css
│ │ ├── app.tsx
│ │ ├── entry-client.tsx
│ │ ├── entry-server.tsx
│ │ ├── global.d.ts
│ │ ├── components/
│ │ │ ├── example.tsx
│ │ │ ├── post-viewer.tsx
│ │ │ ├── query-boundary.tsx
│ │ │ └── user-info.tsx
│ │ ├── routes/
│ │ │ ├── [...404].tsx
│ │ │ ├── batch-methods.tsx
│ │ │ ├── deferred.tsx
│ │ │ ├── hydration.tsx
│ │ │ ├── index.tsx
│ │ │ ├── mixed.tsx
│ │ │ ├── prefetch.tsx
│ │ │ ├── streamed.tsx
│ │ │ └── with-error.tsx
│ │ └── utils/
│ │ └── api.ts
│ ├── svelte/
│ │ ├── auto-refetching/
│ │ ├── basic/
│ │ ├── load-more-infinite-scroll/
│ │ │ ├── README.md
│ │ │ ├── package.json
│ │ │ ├── svelte.config.js
│ │ │ ├── tsconfig.json
│ │ │ ├── vite.config.ts
│ │ │ ├── .gitignore
│ │ │ ├── src/
│ │ │ │ ├── app.css
│ │ │ │ ├── app.d.ts
│ │ │ │ ├── app.html
│ │ │ │ ├── lib/
│ │ │ │ │ └── LoadMore.svelte
│ │ │ │ └── routes/
│ │ │ │ ├── +layout.svelte
│ │ │ │ └── +page.svelte
│ │ │ └── static/
│ │ ├── optimistic-updates/
│ │ ├── playground/
│ │ ├── simple/
│ │ ├── ssr/
│ │ └── star-wars/
│ └── vue/
│ ├── 2.6-basic/
│ │ ├── README.md
│ │ ├── index.html
│ │ ├── package.json
│ │ ├── tsconfig.json
│ │ ├── vite.config.ts
│ │ ├── .gitignore
│ │ └── src/
│ │ ├── App.vue
│ │ ├── main.ts
│ │ ├── Post.vue
│ │ ├── Posts.vue
│ │ ├── shims-vue.d.ts
│ │ └── types.d.ts
│ ├── 2.7-basic/
│ │ ├── README.md
│ │ ├── index.html
│ │ ├── package.json
│ │ ├── tsconfig.json
│ │ ├── vite.config.ts
│ │ ├── .gitignore
│ │ └── src/
│ │ ├── App.vue
│ │ ├── main.ts
│ │ ├── Post.vue
│ │ ├── Posts.vue
│ │ ├── shims-vue.d.ts
│ │ └── types.d.ts
│ ├── basic/
│ │ ├── README.md
│ │ ├── index.html
│ │ ├── package.json
│ │ ├── tsconfig.json
│ │ ├── vite.config.ts
│ │ ├── .gitignore
│ │ └── src/
│ │ ├── App.vue
│ │ ├── main.ts
│ │ ├── Post.vue
│ │ ├── Posts.vue
│ │ ├── shims-vue.d.ts
│ │ └── types.d.ts
│ ├── dependent-queries/
│ │ ├── README.md
│ │ ├── index.html
│ │ ├── package.json
│ │ ├── tsconfig.json
│ │ ├── vite.config.ts
│ │ ├── .gitignore
│ │ └── src/
│ │ ├── App.vue
│ │ ├── main.ts
│ │ ├── Post.vue
│ │ ├── Posts.vue
│ │ ├── shims-vue.d.ts
│ │ └── types.d.ts
│ ├── nuxt3/
│ │ ├── README.md
│ │ ├── app.vue
│ │ ├── nuxt.config.ts
│ │ ├── package.json
│ │ ├── tsconfig.json
│ │ ├── .gitignore
│ │ └── plugins/
│ │ └── vue-query.ts
│ ├── persister/
│ │ ├── README.md
│ │ ├── index.html
│ │ ├── package.json
│ │ ├── tsconfig.json
│ │ ├── vite.config.ts
│ │ ├── .gitignore
│ │ └── src/
│ │ ├── App.vue
│ │ ├── main.ts
│ │ ├── Post.vue
│ │ ├── Posts.vue
│ │ ├── shims-vue.d.ts
│ │ └── types.d.ts
│ └── simple/
│ ├── README.md
│ ├── index.html
│ ├── package.json
│ ├── tsconfig.json
│ ├── vite.config.ts
│ ├── .gitignore
│ └── src/
│ ├── App.vue
│ ├── main.ts
│ ├── shims-vue.d.ts
│ └── types.d.ts
├── integrations/
├── media/
│ └── logo.sketch
├── packages/
│ ├── angular-query-experimental/
│ │ ├── README.md
│ │ ├── eslint.config.js
│ │ ├── package.json
│ │ ├── test-setup.ts
│ │ ├── tsconfig.json
│ │ ├── tsconfig.prod.json
│ │ ├── tsup.config.ts
│ │ ├── vite.config.ts
│ │ ├── .attw.json
│ │ ├── root.eslint.config.js -> eslint.config.js
│ │ ├── scripts/
│ │ │ └── prepack.js
│ │ └── src/
│ │ ├── create-base-query.ts
│ │ ├── index.ts
│ │ ├── infinite-query-options.ts
│ │ ├── inject-infinite-query.ts
│ │ ├── inject-is-fetching.ts
│ │ ├── inject-is-mutating.ts
│ │ ├── inject-is-restoring.ts
│ │ ├── inject-mutation-state.ts
│ │ ├── inject-mutation.ts
│ │ ├── inject-queries.ts
│ │ ├── inject-query-client.ts
│ │ ├── inject-query.ts
│ │ ├── mutation-options.ts
│ │ ├── pending-tasks-compat.ts
│ │ ├── providers.ts
│ │ ├── query-options.ts
│ │ ├── signal-proxy.ts
│ │ ├── types.ts
│ │ ├── __tests__/
│ │ │ ├── infinite-query-options.test-d.ts
│ │ │ ├── infinite-query-options.test.ts
│ │ │ ├── inject-devtools-panel.test.ts
│ │ │ ├── inject-infinite-query.test-d.ts
│ │ │ ├── inject-infinite-query.test.ts
│ │ │ ├── inject-is-fetching.test.ts
│ │ │ ├── inject-is-mutating.test.ts
│ │ │ ├── inject-mutation-state.test-d.ts
│ │ │ ├── inject-mutation-state.test.ts
│ │ │ ├── inject-mutation.test-d.ts
│ │ │ ├── inject-mutation.test.ts
│ │ │ ├── inject-queries.test-d.ts
│ │ │ ├── inject-queries.test.ts
│ │ │ ├── inject-query.test-d.ts
│ │ │ ├── inject-query.test.ts
│ │ │ ├── mutation-options.test-d.ts
│ │ │ ├── mutation-options.test.ts
│ │ │ ├── pending-tasks.test.ts
│ │ │ ├── provide-query-client.test.ts
│ │ │ ├── provide-tanstack-query.test.ts
│ │ │ ├── query-options.test-d.ts
│ │ │ ├── query-options.test.ts
│ │ │ ├── signal-proxy.test.ts
│ │ │ ├── test-utils.ts
│ │ │ └── with-devtools.test.ts
│ │ ├── devtools/
│ │ │ ├── index.ts
│ │ │ ├── stub.ts
│ │ │ ├── types.ts
│ │ │ ├── with-devtools.ts
│ │ │ └── production/
│ │ │ └── index.ts
│ │ ├── devtools-panel/
│ │ │ ├── index.ts
│ │ │ ├── inject-devtools-panel.ts
│ │ │ ├── stub.ts
│ │ │ ├── types.ts
│ │ │ └── production/
│ │ │ └── index.ts
│ │ └── inject-queries-experimental/
│ │ └── index.ts
│ ├── angular-query-persist-client/
│ │ ├── eslint.config.js
│ │ ├── package.json
│ │ ├── test-setup.ts
│ │ ├── tsconfig.json
│ │ ├── tsconfig.prod.json
│ │ ├── tsup.config.ts
│ │ ├── vite.config.ts
│ │ ├── .attw.json
│ │ ├── root.eslint.config.js -> eslint.config.js
│ │ └── src/
│ │ ├── index.ts
│ │ ├── with-persist-query-client.ts
│ │ └── __tests__/
│ │ └── with-persist-query-client.test.ts
│ ├── eslint-plugin-query/
│ │ ├── eslint.config.js
│ │ ├── package.json
│ │ ├── root.tsup.config.js
│ │ ├── tsconfig.json
│ │ ├── tsconfig.prod.json
│ │ ├── tsup.config.ts
│ │ ├── vite.config.ts
│ │ ├── .attw.json
│ │ ├── root.eslint.config.js -> eslint.config.js
│ │ └── src/
│ │ ├── index.ts
│ │ ├── rules.ts
│ │ ├── types.ts
│ │ ├── __tests__/
│ │ │ ├── exhaustive-deps.test.ts
│ │ │ ├── infinite-query-property-order.rule.test.ts
│ │ │ ├── mutation-property-order.rule.test.ts
│ │ │ ├── no-rest-destructuring.test.ts
│ │ │ ├── no-unstable-deps.test.ts
│ │ │ ├── no-void-query-fn.test.ts
│ │ │ ├── sort-data-by-order.utils.test.ts
│ │ │ ├── stable-query-client.test.ts
│ │ │ ├── test-utils.test.ts
│ │ │ ├── test-utils.ts
│ │ │ └── ts-fixture/
│ │ │ ├── file.ts
│ │ │ └── tsconfig.json
│ │ ├── rules/
│ │ │ ├── exhaustive-deps/
│ │ │ │ ├── exhaustive-deps.rule.ts
│ │ │ │ └── exhaustive-deps.utils.ts
│ │ │ ├── infinite-query-property-order/
│ │ │ │ ├── constants.ts
│ │ │ │ └── infinite-query-property-order.rule.ts
│ │ │ ├── mutation-property-order/
│ │ │ │ ├── constants.ts
│ │ │ │ └── mutation-property-order.rule.ts
│ │ │ ├── no-rest-destructuring/
│ │ │ │ ├── no-rest-destructuring.rule.ts
│ │ │ │ └── no-rest-destructuring.utils.ts
│ │ │ ├── no-unstable-deps/
│ │ │ │ └── no-unstable-deps.rule.ts
│ │ │ ├── no-void-query-fn/
│ │ │ │ └── no-void-query-fn.rule.ts
│ │ │ └── stable-query-client/
│ │ │ └── stable-query-client.rule.ts
│ │ └── utils/
│ │ ├── ast-utils.ts
│ │ ├── create-property-order-rule.ts
│ │ ├── detect-react-query-imports.ts
│ │ ├── get-docs-url.ts
│ │ ├── sort-data-by-order.ts
│ │ └── unique-by.ts
│ ├── query-async-storage-persister/
│ │ ├── eslint.config.js
│ │ ├── package.json
│ │ ├── tsconfig.json
│ │ ├── tsconfig.prod.json
│ │ ├── tsup.config.ts
│ │ ├── vite.config.ts
│ │ ├── root.eslint.config.js -> eslint.config.js
│ │ ├── root.tsup.config.js -> getTsupConfig.js
│ │ └── src/
│ │ ├── asyncThrottle.ts
│ │ ├── index.ts
│ │ ├── utils.ts
│ │ └── __tests__/
│ │ └── asyncThrottle.test.ts
│ ├── query-broadcast-client-experimental/
│ │ ├── eslint.config.js
│ │ ├── package.json
│ │ ├── test-setup.ts
│ │ ├── tsconfig.json
│ │ ├── tsconfig.prod.json
│ │ ├── tsup.config.ts
│ │ ├── vite.config.ts
│ │ ├── root.eslint.config.js -> eslint.config.js
│ │ ├── root.tsup.config.js -> getTsupConfig.js
│ │ └── src/
│ │ ├── index.ts
│ │ └── __tests__/
│ │ └── index.test.ts
│ ├── query-codemods/
│ │ ├── eslint.config.js
│ │ ├── package.json
│ │ ├── tsconfig.json
│ │ ├── vite.config.ts
│ │ ├── root.eslint.config.js -> eslint.config.js
│ │ └── src/
│ │ ├── utils/
│ │ │ ├── index.cjs
│ │ │ └── transformers/
│ │ │ ├── query-cache-transformer.cjs
│ │ │ ├── query-client-transformer.cjs
│ │ │ └── use-query-like-transformer.cjs
│ │ ├── v4/
│ │ │ ├── key-transformation.cjs
│ │ │ ├── replace-import-specifier.cjs
│ │ │ ├── __testfixtures__/
│ │ │ │ ├── default-import.input.tsx
│ │ │ │ ├── default-import.output.tsx
│ │ │ │ ├── named-import.input.tsx
│ │ │ │ ├── named-import.output.tsx
│ │ │ │ ├── namespaced-import.input.tsx
│ │ │ │ ├── namespaced-import.output.tsx
│ │ │ │ ├── parameter-is-identifier.input.tsx
│ │ │ │ ├── parameter-is-identifier.output.tsx
│ │ │ │ ├── parameter-is-object-expression.input.tsx
│ │ │ │ ├── parameter-is-object-expression.output.tsx
│ │ │ │ ├── replace-import-specifier.input.tsx
│ │ │ │ ├── replace-import-specifier.output.tsx
│ │ │ │ ├── type-arguments.input.tsx
│ │ │ │ └── type-arguments.output.tsx
│ │ │ ├── __tests__/
│ │ │ │ ├── key-transformation.test.cjs
│ │ │ │ └── replace-import-specifier.test.cjs
│ │ │ └── utils/
│ │ │ └── replacers/
│ │ │ └── key-replacer.cjs
│ │ └── v5/
│ │ ├── is-loading/
│ │ │ ├── is-loading.cjs
│ │ │ ├── __testfixtures__/
│ │ │ │ ├── default-import.input.tsx
│ │ │ │ ├── default-import.output.tsx
│ │ │ │ ├── named-import.input.tsx
│ │ │ │ └── named-import.output.tsx
│ │ │ └── __tests__/
│ │ │ └── is-loading.test.cjs
│ │ ├── keep-previous-data/
│ │ │ ├── README.md
│ │ │ ├── keep-previous-data.cjs
│ │ │ ├── __testfixtures__/
│ │ │ │ ├── default.input.tsx
│ │ │ │ ├── default.output.tsx
│ │ │ │ ├── named.input.tsx
│ │ │ │ └── named.output.tsx
│ │ │ ├── __tests__/
│ │ │ │ └── keep-previous-data.test.cjs
│ │ │ └── utils/
│ │ │ └── already-has-placeholder-data-property.cjs
│ │ ├── remove-overloads/
│ │ │ ├── remove-overloads.cjs
│ │ │ ├── __testfixtures__/
│ │ │ │ ├── bug-reports.input.tsx
│ │ │ │ ├── bug-reports.output.tsx
│ │ │ │ ├── default-import.input.tsx
│ │ │ │ └── default-import.output.tsx
│ │ │ ├── __tests__/
│ │ │ │ └── remove-overloads.test.cjs
│ │ │ ├── transformers/
│ │ │ │ ├── filter-aware-usage-transformer.cjs
│ │ │ │ └── query-fn-aware-usage-transformer.cjs
│ │ │ └── utils/
│ │ │ ├── index.cjs
│ │ │ └── unknown-usage-error.cjs
│ │ ├── rename-hydrate/
│ │ │ ├── rename-hydrate.cjs
│ │ │ ├── __testfixtures__/
│ │ │ │ ├── default-import.input.tsx
│ │ │ │ ├── default-import.output.tsx
│ │ │ │ ├── named-import.input.tsx
│ │ │ │ └── named-import.output.tsx
│ │ │ └── __tests__/
│ │ │ └── rename-hydrate.test.cjs
│ │ └── rename-properties/
│ │ ├── rename-properties.cjs
│ │ ├── __testfixtures__/
│ │ │ ├── rename-cache-time.input.tsx
│ │ │ ├── rename-cache-time.output.tsx
│ │ │ ├── rename-use-error-boundary.input.tsx
│ │ │ └── rename-use-error-boundary.output.tsx
│ │ └── __tests__/
│ │ └── rename-properties.test.cjs
│ ├── query-core/
│ │ ├── eslint.config.js
│ │ ├── package.json
│ │ ├── tsconfig.json
│ │ ├── tsconfig.legacy.json
│ │ ├── tsconfig.prod.json
│ │ ├── tsup.config.ts
│ │ ├── vite.config.ts
│ │ ├── root.eslint.config.js -> eslint.config.js
│ │ ├── root.tsup.config.js -> getTsupConfig.js
│ │ └── src/
│ │ ├── focusManager.ts
│ │ ├── hydration.ts
│ │ ├── index.ts
│ │ ├── infiniteQueryBehavior.ts
│ │ ├── infiniteQueryObserver.ts
│ │ ├── mutation.ts
│ │ ├── mutationCache.ts
│ │ ├── mutationObserver.ts
│ │ ├── notifyManager.ts
│ │ ├── onlineManager.ts
│ │ ├── queriesObserver.ts
│ │ ├── query.ts
│ │ ├── queryCache.ts
│ │ ├── queryClient.ts
│ │ ├── queryObserver.ts
│ │ ├── removable.ts
│ │ ├── retryer.ts
│ │ ├── streamedQuery.ts
│ │ ├── subscribable.ts
│ │ ├── thenable.ts
│ │ ├── timeoutManager.ts
│ │ ├── types.ts
│ │ ├── utils.ts
│ │ └── __tests__/
│ │ ├── focusManager.test.tsx
│ │ ├── hydration.test.tsx
│ │ ├── infiniteQueryBehavior.test.tsx
│ │ ├── infiniteQueryObserver.test-d.tsx
│ │ ├── infiniteQueryObserver.test.tsx
│ │ ├── mutationCache.test.tsx
│ │ ├── mutationObserver.test.tsx
│ │ ├── mutations.test.tsx
│ │ ├── notifyManager.test.tsx
│ │ ├── OmitKeyof.test-d.ts
│ │ ├── onlineManager.test.tsx
│ │ ├── queriesObserver.test.tsx
│ │ ├── query.test.tsx
│ │ ├── queryCache.test.tsx
│ │ ├── queryClient.test-d.tsx
│ │ ├── queryClient.test.tsx
│ │ ├── queryObserver.test-d.tsx
│ │ ├── queryObserver.test.tsx
│ │ ├── streamedQuery.test.tsx
│ │ ├── timeoutManager.test.tsx
│ │ ├── utils.test-d.tsx
│ │ ├── utils.test.tsx
│ │ └── utils.ts
│ ├── query-devtools/
│ │ ├── eslint.config.js
│ │ ├── package.json
│ │ ├── tsconfig.json
│ │ ├── tsconfig.prod.json
│ │ ├── tsup.config.ts
│ │ ├── vite.config.ts
│ │ ├── root.eslint.config.js -> eslint.config.js
│ │ └── src/
│ │ ├── constants.ts
│ │ ├── Devtools.tsx
│ │ ├── DevtoolsComponent.tsx
│ │ ├── DevtoolsPanelComponent.tsx
│ │ ├── Explorer.tsx
│ │ ├── index.ts
│ │ ├── TanstackQueryDevtools.tsx
│ │ ├── TanstackQueryDevtoolsPanel.tsx
│ │ ├── theme.ts
│ │ ├── utils.tsx
│ │ ├── __tests__/
│ │ │ ├── devtools.test.tsx
│ │ │ └── utils.test.ts
│ │ ├── contexts/
│ │ │ ├── index.ts
│ │ │ ├── PiPContext.tsx
│ │ │ ├── QueryDevtoolsContext.ts
│ │ │ └── ThemeContext.ts
│ │ └── icons/
│ │ └── index.tsx
│ ├── query-persist-client-core/
│ │ ├── eslint.config.js
│ │ ├── package.json
│ │ ├── tsconfig.json
│ │ ├── tsconfig.prod.json
│ │ ├── tsup.config.ts
│ │ ├── vite.config.ts
│ │ ├── root.eslint.config.js -> eslint.config.js
│ │ ├── root.tsup.config.js -> getTsupConfig.js
│ │ └── src/
│ │ ├── createPersister.ts
│ │ ├── index.ts
│ │ ├── persist.ts
│ │ ├── retryStrategies.ts
│ │ └── __tests__/
│ │ ├── createPersister.test.ts
│ │ ├── persist.test.ts
│ │ └── utils.ts
│ ├── query-sync-storage-persister/
│ │ ├── eslint.config.js
│ │ ├── package.json
│ │ ├── tsconfig.json
│ │ ├── tsconfig.prod.json
│ │ ├── tsup.config.ts
│ │ ├── vite.config.ts
│ │ ├── root.eslint.config.js -> eslint.config.js
│ │ ├── root.tsup.config.js -> getTsupConfig.js
│ │ └── src/
│ │ ├── index.ts
│ │ ├── utils.ts
│ │ └── __tests__/
│ │ └── storageIsFull.test.ts
│ ├── query-test-utils/
│ │ ├── eslint.config.js
│ │ ├── package.json
│ │ ├── tsconfig.json
│ │ ├── vite.config.ts
│ │ ├── root.eslint.config.js -> eslint.config.js
│ │ └── src/
│ │ ├── index.ts
│ │ ├── mockVisibilityState.ts
│ │ ├── queryKey.ts
│ │ ├── sleep.ts
│ │ └── __test__/
│ │ ├── queryKey.test.ts
│ │ └── sleep.test.ts
│ ├── react-query/
│ │ ├── README.md
│ │ ├── eslint.config.js
│ │ ├── package.json
│ │ ├── test-setup.ts
│ │ ├── tsconfig.json
│ │ ├── tsconfig.legacy.json
│ │ ├── tsconfig.prod.json
│ │ ├── tsup.config.ts
│ │ ├── vite.config.ts
│ │ ├── root.eslint.config.js -> eslint.config.js
│ │ ├── root.tsup.config.js -> getTsupConfig.js
│ │ └── src/
│ │ ├── errorBoundaryUtils.ts
│ │ ├── HydrationBoundary.tsx
│ │ ├── index.ts
│ │ ├── infiniteQueryOptions.ts
│ │ ├── IsRestoringProvider.ts
│ │ ├── mutationOptions.ts
│ │ ├── QueryClientProvider.tsx
│ │ ├── QueryErrorResetBoundary.tsx
│ │ ├── queryOptions.ts
│ │ ├── suspense.ts
│ │ ├── types.ts
│ │ ├── useBaseQuery.ts
│ │ ├── useInfiniteQuery.ts
│ │ ├── useIsFetching.ts
│ │ ├── useMutation.ts
│ │ ├── useMutationState.ts
│ │ ├── usePrefetchInfiniteQuery.tsx
│ │ ├── usePrefetchQuery.tsx
│ │ ├── useQueries.ts
│ │ ├── useQuery.ts
│ │ ├── useSuspenseInfiniteQuery.ts
│ │ ├── useSuspenseQueries.ts
│ │ ├── useSuspenseQuery.ts
│ │ └── __tests__/
│ │ ├── fine-grained-persister.test.tsx
│ │ ├── HydrationBoundary.test.tsx
│ │ ├── infiniteQueryOptions.test-d.tsx
│ │ ├── infiniteQueryOptions.test.tsx
│ │ ├── mutationOptions.test-d.tsx
│ │ ├── mutationOptions.test.tsx
│ │ ├── QueryClientProvider.test.tsx
│ │ ├── queryOptions.test-d.tsx
│ │ ├── queryOptions.test.tsx
│ │ ├── QueryResetErrorBoundary.test.tsx
│ │ ├── ssr-hydration.test.tsx
│ │ ├── ssr.test.tsx
│ │ ├── suspense.test.tsx
│ │ ├── useInfiniteQuery.test-d.tsx
│ │ ├── useInfiniteQuery.test.tsx
│ │ ├── useIsFetching.test.tsx
│ │ ├── useMutation.test.tsx
│ │ ├── useMutationState.test-d.tsx
│ │ ├── useMutationState.test.tsx
│ │ ├── usePrefetchInfiniteQuery.test-d.tsx
│ │ ├── usePrefetchInfiniteQuery.test.tsx
│ │ ├── usePrefetchQuery.test-d.tsx
│ │ ├── usePrefetchQuery.test.tsx
│ │ ├── useQueries.test-d.tsx
│ │ ├── useQueries.test.tsx
│ │ ├── useQuery.promise.test.tsx
│ │ ├── useQuery.test-d.tsx
│ │ ├── useQuery.test.tsx
│ │ ├── useSuspenseInfiniteQuery.test-d.tsx
│ │ ├── useSuspenseInfiniteQuery.test.tsx
│ │ ├── useSuspenseQueries.test-d.tsx
│ │ ├── useSuspenseQueries.test.tsx
│ │ ├── useSuspenseQuery.test-d.tsx
│ │ ├── useSuspenseQuery.test.tsx
│ │ └── utils.tsx
│ ├── react-query-devtools/
│ │ ├── eslint.config.js
│ │ ├── package.json
│ │ ├── test-setup.ts
│ │ ├── tsconfig.json
│ │ ├── tsconfig.legacy.json
│ │ ├── tsconfig.prod.json
│ │ ├── tsup.config.ts
│ │ ├── vite.config.ts
│ │ ├── .attw.json
│ │ ├── root.eslint.config.js -> eslint.config.js
│ │ ├── root.tsup.config.js -> getTsupConfig.js
│ │ └── src/
│ │ ├── index.ts
│ │ ├── production.ts
│ │ ├── ReactQueryDevtools.tsx
│ │ ├── ReactQueryDevtoolsPanel.tsx
│ │ └── __tests__/
│ │ ├── devtools.test.tsx
│ │ └── not-development.test.tsx
│ ├── react-query-next-experimental/
│ │ ├── eslint.config.js
│ │ ├── package.json
│ │ ├── tsconfig.json
│ │ ├── tsconfig.legacy.json
│ │ ├── tsconfig.prod.json
│ │ ├── tsup.config.ts
│ │ ├── vite.config.ts
│ │ ├── root.eslint.config.js -> eslint.config.js
│ │ ├── root.tsup.config.js -> getTsupConfig.js
│ │ └── src/
│ │ ├── htmlescape.ts
│ │ ├── HydrationStreamProvider.tsx
│ │ ├── index.ts
│ │ └── ReactQueryStreamedHydration.tsx
│ ├── react-query-persist-client/
│ │ ├── eslint.config.js
│ │ ├── package.json
│ │ ├── test-setup.ts
│ │ ├── tsconfig.json
│ │ ├── tsconfig.legacy.json
│ │ ├── tsconfig.prod.json
│ │ ├── tsup.config.ts
│ │ ├── vite.config.ts
│ │ ├── root.eslint.config.js -> eslint.config.js
│ │ ├── root.tsup.config.js -> getTsupConfig.js
│ │ └── src/
│ │ ├── index.ts
│ │ ├── PersistQueryClientProvider.tsx
│ │ └── __tests__/
│ │ ├── PersistQueryClientProvider.test.tsx
│ │ └── use-queries-with-persist.test.tsx
│ ├── solid-query/
│ │ ├── README.md
│ │ ├── eslint.config.js
│ │ ├── package.json
│ │ ├── test-setup.ts
│ │ ├── tsconfig.json
│ │ ├── tsconfig.legacy.json
│ │ ├── tsconfig.prod.json
│ │ ├── tsup.config.ts
│ │ ├── vite.config.ts
│ │ ├── root.eslint.config.js -> eslint.config.js
│ │ └── src/
│ │ ├── index.ts
│ │ ├── infiniteQueryOptions.ts
│ │ ├── isRestoring.ts
│ │ ├── QueryClient.ts
│ │ ├── QueryClientProvider.tsx
│ │ ├── queryOptions.ts
│ │ ├── types.ts
│ │ ├── useBaseQuery.ts
│ │ ├── useInfiniteQuery.ts
│ │ ├── useIsFetching.ts
│ │ ├── useIsMutating.ts
│ │ ├── useMutation.ts
│ │ ├── useMutationState.ts
│ │ ├── useQueries.ts
│ │ ├── useQuery.ts
│ │ └── __tests__/
│ │ ├── createQueries.test-d.tsx
│ │ ├── QueryClientProvider.test.tsx
│ │ ├── queryOptions.test-d.tsx
│ │ ├── suspense.test.tsx
│ │ ├── transition.test.tsx
│ │ ├── useInfiniteQuery.test.tsx
│ │ ├── useIsFetching.test.tsx
│ │ ├── useIsMutating.test.tsx
│ │ ├── useMutation.test.tsx
│ │ ├── useMutationState.test-d.tsx
│ │ ├── useMutationState.test.tsx
│ │ ├── useQueries.test.tsx
│ │ ├── useQuery.test-d.tsx
│ │ ├── useQuery.test.tsx
│ │ ├── useQueryOptions.test-d.tsx
│ │ └── utils.tsx
│ ├── solid-query-devtools/
│ │ ├── eslint.config.js
│ │ ├── package.json
│ │ ├── tsconfig.json
│ │ ├── tsconfig.prod.json
│ │ ├── tsup.config.ts
│ │ ├── vite.config.ts
│ │ ├── root.eslint.config.js -> eslint.config.js
│ │ └── src/
│ │ ├── clientOnly.tsx
│ │ ├── devtools.tsx
│ │ ├── devtoolsPanel.tsx
│ │ └── index.tsx
│ ├── solid-query-persist-client/
│ │ ├── eslint.config.js
│ │ ├── package.json
│ │ ├── test-setup.ts
│ │ ├── tsconfig.json
│ │ ├── tsconfig.prod.json
│ │ ├── tsup.config.ts
│ │ ├── vite.config.ts
│ │ ├── root.eslint.config.js -> eslint.config.js
│ │ └── src/
│ │ ├── index.ts
│ │ ├── PersistQueryClientProvider.tsx
│ │ └── __tests__/
│ │ └── PersistQueryClientProvider.test.tsx
│ ├── svelte-query/
│ │ ├── eslint.config.js
│ │ ├── package.json
│ │ ├── svelte.config.js
│ │ ├── tsconfig.json
│ │ ├── vite.config.ts
│ │ ├── .attw.json
│ │ ├── root.eslint.config.js -> eslint.config.js
│ │ ├── src/
│ │ │ ├── context.ts
│ │ │ ├── createBaseQuery.ts
│ │ │ ├── createInfiniteQuery.ts
│ │ │ ├── createMutation.ts
│ │ │ ├── createQueries.ts
│ │ │ ├── createQuery.ts
│ │ │ ├── HydrationBoundary.svelte
│ │ │ ├── index.ts
│ │ │ ├── infiniteQueryOptions.ts
│ │ │ ├── QueryClientProvider.svelte
│ │ │ ├── queryOptions.ts
│ │ │ ├── types.ts
│ │ │ ├── useHydrate.ts
│ │ │ ├── useIsFetching.ts
│ │ │ ├── useIsMutating.ts
│ │ │ ├── useIsRestoring.ts
│ │ │ ├── useMutationState.ts
│ │ │ ├── useQueryClient.ts
│ │ │ └── utils.ts
│ │ └── tests/
│ │ ├── test-setup.ts
│ │ ├── context/
│ │ │ ├── BaseExample.svelte
│ │ │ └── context.test.ts
│ │ ├── createInfiniteQuery/
│ │ │ ├── BaseExample.svelte
│ │ │ ├── createInfiniteQuery.test.ts
│ │ │ └── SelectExample.svelte
│ │ ├── createMutation/
│ │ │ ├── createMutation.test.ts
│ │ │ ├── FailureExample.svelte
│ │ │ ├── OnSuccessExample.svelte
│ │ │ └── ResetExample.svelte
│ │ ├── createQueries/
│ │ │ ├── BaseExample.svelte
│ │ │ ├── CombineExample.svelte
│ │ │ ├── createQueries.test-d.ts
│ │ │ └── createQueries.test.ts
│ │ ├── createQuery/
│ │ │ ├── BaseExample.svelte
│ │ │ ├── createQuery.test-d.ts
│ │ │ ├── createQuery.test.ts
│ │ │ ├── DisabledExample.svelte
│ │ │ ├── PlaceholderData.svelte
│ │ │ └── RefetchExample.svelte
│ │ ├── infiniteQueryOptions/
│ │ │ └── infiniteQueryOptions.test-d.ts
│ │ ├── QueryClientProvider/
│ │ │ ├── ChildComponent.svelte
│ │ │ ├── ParentComponent.svelte
│ │ │ └── QueryClientProvider.test.ts
│ │ ├── queryOptions/
│ │ │ └── queryOptions.test-d.ts
│ │ ├── useIsFetching/
│ │ │ ├── BaseExample.svelte
│ │ │ └── useIsFetching.test.ts
│ │ ├── useIsMutating/
│ │ │ ├── BaseExample.svelte
│ │ │ └── useIsMutating.test.ts
│ │ └── useMutationState/
│ │ ├── BaseExample.svelte
│ │ └── useMutationState.test.ts
│ ├── svelte-query-devtools/
│ │ ├── eslint.config.js
│ │ ├── package.json
│ │ ├── svelte.config.js
│ │ ├── tsconfig.json
│ │ ├── vite.config.ts
│ │ ├── .attw.json
│ │ ├── root.eslint.config.js -> eslint.config.js
│ │ └── src/
│ │ ├── Devtools.svelte
│ │ └── index.ts
│ ├── svelte-query-persist-client/
│ │ ├── eslint.config.js
│ │ ├── package.json
│ │ ├── svelte.config.js
│ │ ├── tsconfig.json
│ │ ├── vite.config.ts
│ │ ├── .attw.json
│ │ ├── root.eslint.config.js -> eslint.config.js
│ │ ├── src/
│ │ │ ├── index.ts
│ │ │ └── PersistQueryClientProvider.svelte
│ │ └── tests/
│ │ ├── PersistQueryClientProvider.test.ts
│ │ ├── test-setup.ts
│ │ ├── utils.ts
│ │ ├── AwaitOnSuccess/
│ │ │ ├── AwaitOnSuccess.svelte
│ │ │ └── Provider.svelte
│ │ ├── FreshData/
│ │ │ ├── FreshData.svelte
│ │ │ └── Provider.svelte
│ │ ├── InitialData/
│ │ │ ├── InitialData.svelte
│ │ │ └── Provider.svelte
│ │ ├── OnSuccess/
│ │ │ ├── OnSuccess.svelte
│ │ │ └── Provider.svelte
│ │ ├── RemoveCache/
│ │ │ ├── Provider.svelte
│ │ │ └── RemoveCache.svelte
│ │ ├── RestoreCache/
│ │ │ ├── Provider.svelte
│ │ │ └── RestoreCache.svelte
│ │ └── UseQueries/
│ │ ├── Provider.svelte
│ │ └── UseQueries.svelte
│ ├── vue-query/
│ │ ├── README.md
│ │ ├── eslint.config.js
│ │ ├── package.json
│ │ ├── test-setup.ts
│ │ ├── tsconfig.json
│ │ ├── tsconfig.legacy.json
│ │ ├── tsconfig.prod.json
│ │ ├── tsup.config.ts
│ │ ├── vite.config.ts
│ │ ├── media/
│ │ ├── root.eslint.config.js -> eslint.config.js
│ │ ├── root.tsup.config.js -> getTsupConfig.js
│ │ └── src/
│ │ ├── index.ts
│ │ ├── infiniteQueryOptions.ts
│ │ ├── mutationCache.ts
│ │ ├── queryCache.ts
│ │ ├── queryClient.ts
│ │ ├── queryOptions.ts
│ │ ├── types.ts
│ │ ├── useBaseQuery.ts
│ │ ├── useInfiniteQuery.ts
│ │ ├── useIsFetching.ts
│ │ ├── useMutation.ts
│ │ ├── useMutationState.ts
│ │ ├── useQueries.ts
│ │ ├── useQuery.ts
│ │ ├── useQueryClient.ts
│ │ ├── utils.ts
│ │ ├── vueQueryPlugin.ts
│ │ ├── __mocks__/
│ │ │ ├── useBaseQuery.ts
│ │ │ └── useQueryClient.ts
│ │ ├── __tests__/
│ │ │ ├── infiniteQueryOptions.test-d.ts
│ │ │ ├── mutationCache.test.ts
│ │ │ ├── queryCache.test.ts
│ │ │ ├── queryClient.test-d.ts
│ │ │ ├── queryClient.test.ts
│ │ │ ├── queryOptions.test-d.ts
│ │ │ ├── useInfiniteQuery.test-d.tsx
│ │ │ ├── useInfiniteQuery.test.ts
│ │ │ ├── useIsFetching.test.ts
│ │ │ ├── useIsMutating.test.ts
│ │ │ ├── useMutation.test-d.tsx
│ │ │ ├── useMutation.test.ts
│ │ │ ├── useQueries.test-d.ts
│ │ │ ├── useQueries.test.ts
│ │ │ ├── useQuery.test-d.ts
│ │ │ ├── useQuery.test.ts
│ │ │ ├── useQueryClient.test.ts
│ │ │ ├── utils.test.ts
│ │ │ └── vueQueryPlugin.test.ts
│ │ └── devtools/
│ │ ├── devtools.ts
│ │ └── utils.ts
│ └── vue-query-devtools/
│ ├── eslint.config.js
│ ├── package.json
│ ├── tsconfig.json
│ ├── vite.config.ts
│ ├── .attw.json
│ ├── root.eslint.config.js -> eslint.config.js
│ └── src/
│ ├── devtools.vue
│ ├── index.ts
│ ├── production.ts
│ └── types.ts
├── scripts/
│ ├── generate-labeler-config.ts
│ ├── generateDocs.ts
│ ├── getTsupConfig.js
│ ├── getViteAliases.js
│ ├── publish.ts
│ ├── tsconfig.json
│ └── verify-links.ts
├── .github/
└── .nx/
└── workflows/
└── dynamic-changesets.yaml
================================================
FILE: codecov.yml
================================================
codecov:
max_report_age: off
coverage:
status:
project:
default:
target: auto
threshold: 1%
base: auto
comment:
layout: 'header, reach, diff, flags, components'
behavior: default
require_changes: false
require_base: false
require_head: true
hide_project_coverage: false
component_management:
individual_components:
- component_id: angular-query-experimental
name: '@tanstack/angular-query-experimental'
paths:
- packages/angular-query-experimental/**
- component_id: eslint-plugin-query
name: '@tanstack/eslint-plugin-query'
paths:
- packages/eslint-plugin-query/**
- component_id: query-async-storage-persister
name: '@tanstack/query-async-storage-persister'
paths:
- packages/query-async-storage-persister/**
- component_id: query-broadcast-client-experimental
name: '@tanstack/query-broadcast-client-experimental'
paths:
- packages/query-broadcast-client-experimental/**
- component_id: query-codemods
name: '@tanstack/query-codemods'
paths:
- packages/query-codemods/**
- component_id: query-core
name: '@tanstack/query-core'
paths:
- packages/query-core/**
- component_id: query-devtools
name: '@tanstack/query-devtools'
paths:
- packages/query-devtools/**
- component_id: query-persist-client-core
name: '@tanstack/query-persist-client-core'
paths:
- packages/query-persist-client-core/**
- component_id: query-sync-storage-persister
name: '@tanstack/query-sync-storage-persister'
paths:
- packages/query-sync-storage-persister/**
- component_id: query-test-utils
name: '@tanstack/query-test-utils'
paths:
- packages/query-test-utils/**
- component_id: react-query
name: '@tanstack/react-query'
paths:
- packages/react-query/**
- component_id: react-query-devtools
name: '@tanstack/react-query-devtools'
paths:
- packages/react-query-devtools/**
- component_id: react-query-next-experimental
name: '@tanstack/react-query-next-experimental'
paths:
- packages/react-query-next-experimental/**
- component_id: react-query-persist-client
name: '@tanstack/react-query-persist-client'
paths:
- packages/react-query-persist-client/**
- component_id: solid-query
name: '@tanstack/solid-query'
paths:
- packages/solid-query/**
- component_id: solid-query-devtools
name: '@tanstack/solid-query-devtools'
paths:
- packages/solid-query-devtools/**
- component_id: solid-query-persist-client
name: '@tanstack/solid-query-persist-client'
paths:
- packages/solid-query-persist-client/**
- component_id: svelte-query
name: '@tanstack/svelte-query'
paths:
- packages/svelte-query/**
- component_id: svelte-query-devtools
name: '@tanstack/svelte-query-devtools'
paths:
- packages/svelte-query-devtools/**
- component_id: svelte-query-persist-client
name: '@tanstack/svelte-query-persist-client'
paths:
- packages/svelte-query-persist-client/**
- component_id: vue-query
name: '@tanstack/vue-query'
paths:
- packages/vue-query/**
- component_id: vue-query-devtools
name: '@tanstack/vue-query-devtools'
paths:
- packages/vue-query-devtools/**
================================================
FILE: eslint.config.js
================================================
// @ts-check
// @ts-ignore Needed due to moduleResolution Node vs Bundler
import { tanstackConfig } from '@tanstack/config/eslint'
import pluginCspell from '@cspell/eslint-plugin'
import vitest from '@vitest/eslint-plugin'
export default [
...tanstackConfig,
{
name: 'tanstack/temp',
plugins: {
cspell: pluginCspell,
},
rules: {
'cspell/spellchecker': [
'warn',
{
cspell: {
words: [
'Promisable', // Our public interface
'TSES', // @typescript-eslint package's interface
'codemod', // We support our codemod
'combinate', // Library name
'datatag', // Query options tagging
'extralight', // Our public interface
'jscodeshift',
'refetches', // Query refetch operations
'retryer', // Our public interface
'solidjs', // Our target framework
'tabular-nums', // https://developer.mozilla.org/en-US/docs/Web/CSS/font-variant-numeric
'tanstack', // Our package scope
'todos', // Too general word to be caught as error
'tsqd', // Our public interface (TanStack Query Devtools shorthand)
'tsup', // We use tsup as builder
'typecheck', // Field of vite.config.ts
'vue-demi', // dependency of @tanstack/vue-query
'ɵkind', // Angular specific
'ɵproviders', // Angular specific
],
},
},
],
'@typescript-eslint/no-empty-function': 'off',
'@typescript-eslint/no-unsafe-function-type': 'off',
'no-case-declarations': 'off',
},
},
{
files: ['**/*.spec.ts*', '**/*.test.ts*', '**/*.test-d.ts*'],
plugins: { vitest },
rules: vitest.configs.recommended.rules,
settings: { vitest: { typecheck: true } },
},
]
================================================
FILE: knip.json
================================================
{
"$schema": "https://unpkg.com/knip@5/schema.json",
"ignore": [
".pnpmfile.cjs",
"scripts/*.{j,t}s",
"**/root.*.config.*",
"**/ts-fixture/file.ts"
],
"ignoreDependencies": [
"@types/react",
"@types/react-dom",
"react",
"react-dom",
"markdown-link-extractor"
],
"ignoreWorkspaces": ["examples/**", "integrations/**"],
"workspaces": {
"packages/angular-query-experimental": {
"entry": ["src/index.ts", "src/inject-queries-experimental/index.ts"]
},
"packages/query-codemods": {
"entry": ["src/v4/**/*.cjs", "src/v5/**/*.cjs"],
"ignore": ["**/__testfixtures__/**"]
},
"packages/vue-query": {
"ignore": ["**/__mocks__/**"],
"ignoreDependencies": ["vue2", "vue2.7"]
},
"packages/angular-query-experimental": {
"entry": [
"src/devtools/production/index.ts",
"src/devtools-panel/production/index.ts"
]
}
}
}
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2021-present Tanner Linsley
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: nx.json
================================================
{
"$schema": "./node_modules/nx/schemas/nx-schema.json",
"defaultBase": "main",
"nxCloudAccessToken": "ZDdkNDA4MGEtYjNmYi00MWI4LWE1N2QtYTdlNmYxMGJlZWM2fHJlYWQ=",
"useInferencePlugins": false,
"parallel": 5,
"namedInputs": {
"sharedGlobals": [
"{workspaceRoot}/.nvmrc",
"{workspaceRoot}/package.json",
"{workspaceRoot}/scripts/*.js",
"{workspaceRoot}/tsconfig.json",
"{workspaceRoot}/eslint.config.js",
"{workspaceRoot}/pnpm-workspace.yaml"
],
"default": [
"sharedGlobals",
"{projectRoot}/**/*",
"!{projectRoot}/**/*.md"
],
"production": [
"default",
"!{projectRoot}/tests/**/*",
"!{projectRoot}/eslint.config.js"
]
},
"targetDefaults": {
"compile": {
"cache": true,
"inputs": ["default", "^production"],
"outputs": ["{projectRoot}/dist-ts"]
},
"test:knip": {
"cache": true,
"inputs": ["{workspaceRoot}/**/*"]
},
"test:sherif": {
"cache": true,
"inputs": ["{workspaceRoot}/**/package.json"]
},
"test:eslint": {
"cache": true,
"dependsOn": ["^compile"],
"inputs": ["default", "^production", "{workspaceRoot}/eslint.config.js"]
},
"test:lib": {
"cache": true,
"dependsOn": ["^compile"],
"inputs": ["default", "^production"],
"outputs": ["{projectRoot}/coverage"]
},
"test:types": {
"cache": true,
"inputs": ["default", "^production"]
},
"build": {
"cache": true,
"dependsOn": ["^build"],
"inputs": ["production", "^production"],
"outputs": ["{projectRoot}/build", "{projectRoot}/dist"]
},
"test:build": {
"cache": true,
"dependsOn": ["build"],
"inputs": ["production"]
}
}
}
================================================
FILE: package.json
================================================
{
"name": "root",
"private": true,
"repository": {
"type": "git",
"url": "https://github.com/TanStack/query.git"
},
"packageManager": "pnpm@10.16.1",
"type": "module",
"scripts": {
"clean": "pnpm --filter \"./packages/**\" run clean",
"preinstall": "node -e \"if(process.env.CI == 'true') {console.log('Skipping preinstall...')} else {process.exit(1)}\" || npx -y only-allow pnpm",
"test": "pnpm run test:ci",
"test:pr": "nx affected --targets=test:sherif,test:knip,test:eslint,test:lib,test:types,test:build,build",
"test:ci": "nx run-many --targets=test:sherif,test:knip,test:eslint,test:lib,test:types,test:build,build",
"test:eslint": "nx affected --target=test:eslint",
"test:format": "pnpm run prettier --check",
"test:sherif": "sherif -i typescript -p \"./integrations/*\" -p \"./examples/*\"",
"test:lib": "nx affected --target=test:lib --exclude=examples/**",
"test:lib:dev": "pnpm run test:lib && nx watch --all -- pnpm run test:lib",
"test:build": "nx affected --target=test:build --exclude=examples/**",
"test:types": "nx affected --target=test:types --exclude=examples/**",
"test:knip": "knip",
"build": "nx affected --target=build --exclude=examples/** --exclude=integrations/**",
"build:all": "nx run-many --target=build --exclude=examples/** --exclude=integrations/**",
"watch": "pnpm run build:all && nx watch --all -- pnpm run build:all",
"dev": "pnpm run watch",
"prettier": "prettier --experimental-cli --ignore-unknown '**/*'",
"prettier:write": "pnpm run prettier --write",
"docs:generate": "node scripts/generateDocs.ts",
"verify-links": "node scripts/verify-links.ts",
"cipublish": "node scripts/publish.ts"
},
"nx": {
"includedScripts": [
"test:sherif",
"test:knip"
]
},
"devDependencies": {
"@arethetypeswrong/cli": "^0.15.3",
"@cspell/eslint-plugin": "^9.2.1",
"@eslint-react/eslint-plugin": "^1.53.1",
"@tanstack/config": "^0.20.2",
"@testing-library/jest-dom": "^6.8.0",
"@types/node": "^22.15.3",
"@types/react": "^19.0.1",
"@types/react-dom": "^19.0.2",
"@vitest/coverage-istanbul": "3.2.4",
"@vitest/eslint-plugin": "^1.1.36",
"esbuild-plugin-file-path-extensions": "^2.1.4",
"eslint": "^9.36.0",
"eslint-plugin-react-hooks": "^6.0.0-rc.2",
"jsdom": "^27.0.0",
"knip": "^5.63.1",
"markdown-link-extractor": "^4.0.2",
"nx": "21.5.3",
"premove": "^4.0.0",
"prettier": "^3.6.2",
"prettier-plugin-svelte": "^3.4.0",
"publint": "^0.3.13",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"sherif": "^1.6.1",
"tinyglobby": "^0.2.15",
"tsup": "^8.4.0",
"typescript": "5.8.3",
"typescript50": "npm:typescript@5.0",
"typescript51": "npm:typescript@5.1",
"typescript52": "npm:typescript@5.2",
"typescript53": "npm:typescript@5.3",
"typescript54": "npm:typescript@5.4",
"typescript55": "npm:typescript@5.5",
"typescript56": "npm:typescript@5.6",
"typescript57": "npm:typescript@5.7",
"vite": "^6.3.6",
"vitest": "3.2.4"
},
"pnpm": {
"overrides": {
"@tanstack/angular-query-experimental": "workspace:*",
"@tanstack/eslint-plugin-query": "workspace:*",
"@tanstack/query-async-storage-persister": "workspace:*",
"@tanstack/query-broadcast-client-experimental": "workspace:*",
"@tanstack/query-codemods": "workspace:*",
"@tanstack/query-core": "workspace:*",
"@tanstack/query-devtools": "workspace:*",
"@tanstack/query-persist-client-core": "workspace:*",
"@tanstack/query-sync-storage-persister": "workspace:*",
"@tanstack/query-test-utils": "workspace:*",
"@tanstack/react-query": "workspace:*",
"@tanstack/react-query-devtools": "workspace:*",
"@tanstack/react-query-next-experimental": "workspace:*",
"@tanstack/react-query-persist-client": "workspace:*",
"@tanstack/solid-query": "workspace:*",
"@tanstack/solid-query-devtools": "workspace:*",
"@tanstack/solid-query-persist-client": "workspace:*",
"@tanstack/svelte-query": "workspace:*",
"@tanstack/svelte-query-devtools": "workspace:*",
"@tanstack/svelte-query-persist-client": "workspace:*",
"@tanstack/vue-query": "workspace:*",
"@tanstack/vue-query-devtools": "workspace:*",
"@types/react": "^19.0.1",
"@types/react-dom": "^19.0.2"
}
}
}
================================================
FILE: pnpm-workspace.yaml
================================================
cleanupUnusedCatalogs: true
linkWorkspacePackages: true
preferWorkspacePackages: true
packages:
- 'packages/*'
- 'integrations/*'
- 'examples/angular/*'
- 'examples/react/*'
- 'examples/solid/*'
- 'examples/svelte/*'
- 'examples/vue/*'
- '!examples/vue/2*'
- '!examples/vue/nuxt*'
================================================
FILE: prettier.config.js
================================================
// @ts-check
/** @type {import('prettier').Config} */
const config = {
semi: false,
singleQuote: true,
trailingComma: 'all',
plugins: ['prettier-plugin-svelte'],
overrides: [{ files: '*.svelte', options: { parser: 'svelte' } }],
}
export default config
================================================
FILE: .editorconfig
================================================
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
================================================
FILE: .npmrc
================================================
provenance=true
================================================
FILE: .nvmrc
================================================
24.8.0
================================================
FILE: .prettierignore
================================================
**/.next
**/.nx/cache
**/.svelte-kit
**/build
**/coverage
**/dist
**/query-codemods/**/__testfixtures__
pnpm-lock.yaml
packages/**/tsup.config.bundled*.mjs
**/tsconfig.vitest-temp.json
================================================
FILE: examples/angular/auto-refetching/README.md
================================================
# 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`
================================================
FILE: examples/angular/auto-refetching/angular.json
================================================
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"cli": {
"packageManager": "pnpm",
"analytics": false,
"cache": {
"enabled": false
}
},
"newProjectRoot": "projects",
"projects": {
"auto-refetching": {
"projectType": "application",
"schematics": {
"@schematics/angular:component": {
"inlineTemplate": true,
"inlineStyle": true,
"skipTests": true
},
"@schematics/angular:class": {
"skipTests": true
},
"@schematics/angular:directive": {
"skipTests": true
},
"@schematics/angular:guard": {
"skipTests": true
},
"@schematics/angular:interceptor": {
"skipTests": true
},
"@schematics/angular:pipe": {
"skipTests": true
},
"@schematics/angular:resolver": {
"skipTests": true
},
"@schematics/angular:service": {
"skipTests": true
}
},
"root": "",
"sourceRoot": "src",
"prefix": "app",
"architect": {
"build": {
"builder": "@angular/build:application",
"options": {
"outputPath": "dist/auto-refetching",
"index": "src/index.html",
"browser": "src/main.ts",
"polyfills": ["zone.js"],
"tsConfig": "tsconfig.app.json",
"assets": ["src/favicon.ico", "src/assets"],
"styles": [],
"scripts": []
},
"configurations": {
"production": {
"budgets": [
{
"type": "initial",
"maximumWarning": "500kb",
"maximumError": "1mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "2kb",
"maximumError": "4kb"
}
],
"outputHashing": "all"
},
"development": {
"optimization": false,
"extractLicenses": false,
"sourceMap": true
}
},
"defaultConfiguration": "production"
},
"serve": {
"builder": "@angular/build:dev-server",
"configurations": {
"production": {
"buildTarget": "auto-refetching:build:production"
},
"development": {
"buildTarget": "auto-refetching:build:development"
}
},
"defaultConfiguration": "development"
},
"extract-i18n": {
"builder": "@angular/build:extract-i18n",
"options": {
"buildTarget": "auto-refetching:build"
}
}
}
}
},
"schematics": {
"@schematics/angular:component": {
"type": "component"
},
"@schematics/angular:directive": {
"type": "directive"
},
"@schematics/angular:service": {
"type": "service"
},
"@schematics/angular:guard": {
"typeSeparator": "."
},
"@schematics/angular:interceptor": {
"typeSeparator": "."
},
"@schematics/angular:module": {
"typeSeparator": "."
},
"@schematics/angular:pipe": {
"typeSeparator": "."
},
"@schematics/angular:resolver": {
"typeSeparator": "."
}
}
}
================================================
FILE: examples/angular/auto-refetching/package.json
================================================
{
"name": "@tanstack/query-example-angular-auto-refetching",
"type": "module",
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"watch": "ng build --watch --configuration development"
},
"private": true,
"dependencies": {
"@angular/common": "^20.0.0",
"@angular/compiler": "^20.0.0",
"@angular/core": "^20.0.0",
"@angular/platform-browser": "^20.0.0",
"@tanstack/angular-query-experimental": "^5.90.0",
"rxjs": "^7.8.2",
"tslib": "^2.8.1",
"zone.js": "0.15.0"
},
"devDependencies": {
"@angular/build": "^20.0.0",
"@angular/cli": "^20.0.0",
"@angular/compiler-cli": "^20.0.0",
"typescript": "5.8.3"
}
}
================================================
FILE: examples/angular/auto-refetching/tsconfig.app.json
================================================
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/app",
"types": []
},
"files": ["src/main.ts"],
"include": ["src/**/*.d.ts"]
}
================================================
FILE: examples/angular/auto-refetching/tsconfig.json
================================================
{
"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
}
}
================================================
FILE: examples/angular/auto-refetching/.eslintrc.cjs
================================================
// @ts-check
/** @type {import('eslint').Linter.Config} */
const config = {}
module.exports = config
================================================
FILE: examples/angular/auto-refetching/src/index.html
================================================
TanStack Query Angular auto-refetching example
================================================
FILE: examples/angular/auto-refetching/src/main.ts
================================================
import { bootstrapApplication } from '@angular/platform-browser'
import { appConfig } from './app/app.config'
import { AppComponent } from './app/app.component'
bootstrapApplication(AppComponent, appConfig)
.then(() => {
// an simple endpoint for getting current list
localStorage.setItem(
'tasks',
JSON.stringify(['Item 1', 'Item 2', 'Item 3']),
)
})
.catch((err) => console.error(err))
================================================
FILE: examples/angular/auto-refetching/src/app/app.component.ts
================================================
import { ChangeDetectionStrategy, Component } from '@angular/core'
import { AutoRefetchingExampleComponent } from './components/auto-refetching.component'
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'app-root',
template: ` `,
imports: [AutoRefetchingExampleComponent],
})
export class AppComponent {}
================================================
FILE: examples/angular/auto-refetching/src/app/app.config.ts
================================================
import {
provideHttpClient,
withFetch,
withInterceptors,
} from '@angular/common/http'
import {
QueryClient,
provideTanStackQuery,
} from '@tanstack/angular-query-experimental'
import { withDevtools } from '@tanstack/angular-query-experimental/devtools'
import { mockInterceptor } from './interceptor/mock-api.interceptor'
import type { ApplicationConfig } from '@angular/core'
export const appConfig: ApplicationConfig = {
providers: [
provideHttpClient(withFetch(), withInterceptors([mockInterceptor])),
provideTanStackQuery(
new QueryClient({
defaultOptions: {
queries: {
gcTime: 1000 * 60 * 60 * 24, // 24 hours
},
},
}),
withDevtools(),
),
],
}
================================================
FILE: examples/angular/auto-refetching/src/app/components/auto-refetching.component.html
================================================
================================================
FILE: examples/angular/auto-refetching/src/app/components/auto-refetching.component.ts
================================================
import {
ChangeDetectionStrategy,
Component,
inject,
signal,
} from '@angular/core'
import {
injectMutation,
injectQuery,
} from '@tanstack/angular-query-experimental'
import { NgStyle } from '@angular/common'
import { TasksService } from '../services/tasks.service'
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'auto-refetching-example',
templateUrl: './auto-refetching.component.html',
imports: [NgStyle],
})
export class AutoRefetchingExampleComponent {
readonly #tasksService = inject(TasksService)
readonly intervalMs = signal(1000)
readonly tasks = injectQuery(() =>
this.#tasksService.allTasks(this.intervalMs()),
)
readonly addMutation = injectMutation(() => this.#tasksService.addTask())
readonly clearMutation = injectMutation(() =>
this.#tasksService.clearAllTasks(),
)
clearTasks() {
this.clearMutation.mutate()
}
inputChange($event: Event) {
const target = $event.target as HTMLInputElement
this.intervalMs.set(Number(target.value))
}
addItem($event: Event) {
const target = $event.target as HTMLInputElement
const value = target.value
this.addMutation.mutate(value)
target.value = ''
}
}
================================================
FILE: examples/angular/auto-refetching/src/app/interceptor/mock-api.interceptor.ts
================================================
/**
* 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 === '/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)
}
================================================
FILE: examples/angular/auto-refetching/src/app/services/tasks.service.ts
================================================
import { HttpClient } from '@angular/common/http'
import { Injectable, inject } from '@angular/core'
import {
QueryClient,
mutationOptions,
queryOptions,
} from '@tanstack/angular-query-experimental'
import { lastValueFrom } from 'rxjs'
@Injectable({
providedIn: 'root',
})
export class TasksService {
readonly #queryClient = inject(QueryClient) // Manages query state and caching
readonly #http = inject(HttpClient) // Handles HTTP requests
/**
* Fetches all tasks from the API.
* Returns an observable containing an array of task strings.
*/
allTasks = (intervalMs: number) =>
queryOptions({
queryKey: ['tasks'],
queryFn: () => {
return lastValueFrom(this.#http.get>('/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'] })
},
})
}
}
================================================
FILE: examples/angular/auto-refetching/.devcontainer/devcontainer.json
================================================
{
"name": "Node.js",
"image": "mcr.microsoft.com/devcontainers/javascript-node:22"
}
================================================
FILE: examples/angular/basic/README.md
================================================
# TanStack Query Angular basic 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`
================================================
FILE: examples/angular/basic/angular.json
================================================
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"cli": {
"packageManager": "pnpm",
"analytics": false,
"cache": {
"enabled": false
}
},
"newProjectRoot": "projects",
"projects": {
"basic": {
"projectType": "application",
"schematics": {
"@schematics/angular:component": {
"inlineTemplate": true,
"inlineStyle": true,
"skipTests": true
},
"@schematics/angular:class": {
"skipTests": true
},
"@schematics/angular:directive": {
"skipTests": true
},
"@schematics/angular:guard": {
"skipTests": true
},
"@schematics/angular:interceptor": {
"skipTests": true
},
"@schematics/angular:pipe": {
"skipTests": true
},
"@schematics/angular:resolver": {
"skipTests": true
},
"@schematics/angular:service": {
"skipTests": true
}
},
"root": "",
"sourceRoot": "src",
"prefix": "app",
"architect": {
"build": {
"builder": "@angular/build:application",
"options": {
"outputPath": "dist/basic",
"index": "src/index.html",
"browser": "src/main.ts",
"polyfills": ["zone.js"],
"tsConfig": "tsconfig.app.json",
"assets": ["src/favicon.ico", "src/assets"],
"styles": [],
"scripts": []
},
"configurations": {
"production": {
"budgets": [
{
"type": "initial",
"maximumWarning": "500kb",
"maximumError": "1mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "2kb",
"maximumError": "4kb"
}
],
"outputHashing": "all"
},
"development": {
"optimization": false,
"extractLicenses": false,
"sourceMap": true
}
},
"defaultConfiguration": "production"
},
"serve": {
"builder": "@angular/build:dev-server",
"configurations": {
"production": {
"buildTarget": "basic:build:production"
},
"development": {
"buildTarget": "basic:build:development"
}
},
"defaultConfiguration": "development"
},
"extract-i18n": {
"builder": "@angular/build:extract-i18n",
"options": {
"buildTarget": "basic:build"
}
}
}
}
},
"schematics": {
"@schematics/angular:component": {
"type": "component"
},
"@schematics/angular:directive": {
"type": "directive"
},
"@schematics/angular:service": {
"type": "service"
},
"@schematics/angular:guard": {
"typeSeparator": "."
},
"@schematics/angular:interceptor": {
"typeSeparator": "."
},
"@schematics/angular:module": {
"typeSeparator": "."
},
"@schematics/angular:pipe": {
"typeSeparator": "."
},
"@schematics/angular:resolver": {
"typeSeparator": "."
}
}
}
================================================
FILE: examples/angular/basic/package.json
================================================
{
"name": "@tanstack/query-example-angular-basic",
"type": "module",
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"watch": "ng build --watch --configuration development"
},
"private": true,
"dependencies": {
"@angular/common": "^20.0.0",
"@angular/compiler": "^20.0.0",
"@angular/core": "^20.0.0",
"@angular/platform-browser": "^20.0.0",
"@tanstack/angular-query-experimental": "^5.90.0",
"rxjs": "^7.8.2",
"tslib": "^2.8.1",
"zone.js": "0.15.0"
},
"devDependencies": {
"@angular/build": "^20.0.0",
"@angular/cli": "^20.0.0",
"@angular/compiler-cli": "^20.0.0",
"typescript": "5.8.3"
}
}
================================================
FILE: examples/angular/basic/tsconfig.app.json
================================================
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/app",
"types": []
},
"files": ["src/main.ts"],
"include": ["src/**/*.d.ts"]
}
================================================
FILE: examples/angular/basic/tsconfig.json
================================================
{
"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
}
}
================================================
FILE: examples/angular/basic/.eslintrc.cjs
================================================
// @ts-check
/** @type {import('eslint').Linter.Config} */
const config = {}
module.exports = config
================================================
FILE: examples/angular/basic/src/index.html
================================================
TanStack Query Angular basic example
================================================
FILE: examples/angular/basic/src/main.ts
================================================
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),
)
================================================
FILE: examples/angular/basic/src/app/app.component.html
================================================
As you visit the posts below, you will notice them in a loading state the
first time you load them. However, after you return to this list and click on
any posts you have already visited again, you will see them load instantly and
background refresh right before your eyes!
(You may need to throttle your network speed to simulate longer loading
sequences)
@if (postId() > -1) {
} @else {
}
================================================
FILE: examples/angular/basic/src/app/app.component.ts
================================================
import { ChangeDetectionStrategy, Component, signal } from '@angular/core'
import { PostComponent } from './components/post.component'
import { PostsComponent } from './components/posts.component'
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'basic-example',
templateUrl: './app.component.html',
imports: [PostComponent, PostsComponent],
})
export class BasicExampleComponent {
readonly postId = signal(-1)
}
================================================
FILE: examples/angular/basic/src/app/app.config.ts
================================================
import { provideHttpClient, withFetch } from '@angular/common/http'
import {
QueryClient,
provideTanStackQuery,
} from '@tanstack/angular-query-experimental'
import { withDevtools } from '@tanstack/angular-query-experimental/devtools'
import type { ApplicationConfig } from '@angular/core'
export const appConfig: ApplicationConfig = {
providers: [
provideHttpClient(withFetch()),
provideTanStackQuery(
new QueryClient({
defaultOptions: {
queries: {
gcTime: 1000 * 60 * 60 * 24, // 24 hours
},
},
}),
withDevtools(),
),
],
}
================================================
FILE: examples/angular/basic/src/app/components/post.component.html
================================================
@if (postQuery.isPending()) {
Loading...
} @else if (postQuery.isError()) {
Error: {{ postQuery.error().message }}
}
@if (postQuery.data(); as post) {
{{ post.title }}
@if (postQuery.isFetching()) {
Background Updating...
}
}
================================================
FILE: examples/angular/basic/src/app/components/post.component.ts
================================================
import {
ChangeDetectionStrategy,
Component,
inject,
input,
output,
} from '@angular/core'
import { injectQuery } from '@tanstack/angular-query-experimental'
import { fromEvent, lastValueFrom, takeUntil } from 'rxjs'
import { PostsService } from '../services/posts-service'
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'post',
templateUrl: './post.component.html',
})
export class PostComponent {
readonly #postsService = inject(PostsService)
readonly setPostId = output()
readonly postId = input(0)
readonly postQuery = injectQuery(() => ({
enabled: this.postId() > 0,
queryKey: ['post', this.postId()],
queryFn: (context) => {
// Cancels the request when component is destroyed before the request finishes
const abort$ = fromEvent(context.signal, 'abort')
return lastValueFrom(
this.#postsService.postById$(this.postId()).pipe(takeUntil(abort$)),
)
},
}))
}
================================================
FILE: examples/angular/basic/src/app/components/posts.component.html
================================================
Posts
@if (postsQuery.isPending()) {
Loading...
} @else if (postsQuery.isError()) {
Error: {{ postsQuery.error().message }}
} @else if (postsQuery.isSuccess()) {
}
@if (postsQuery.isFetching()) {
Background Updating...
}
================================================
FILE: examples/angular/basic/src/app/components/posts.component.ts
================================================
import {
ChangeDetectionStrategy,
Component,
inject,
output,
} from '@angular/core'
import { QueryClient, injectQuery } from '@tanstack/angular-query-experimental'
import { lastValueFrom } from 'rxjs'
import { PostsService } from '../services/posts-service'
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'posts',
templateUrl: './posts.component.html',
})
export class PostsComponent {
readonly queryClient = inject(QueryClient)
readonly #postsService = inject(PostsService)
readonly setPostId = output()
readonly postsQuery = injectQuery(() => ({
queryKey: ['posts'],
queryFn: () => lastValueFrom(this.#postsService.allPosts$()),
}))
}
================================================
FILE: examples/angular/basic/src/app/services/posts-service.ts
================================================
import { HttpClient } from '@angular/common/http'
import { Injectable, inject } from '@angular/core'
@Injectable({
providedIn: 'root',
})
export class PostsService {
readonly #http = inject(HttpClient)
postById$ = (postId: number) =>
this.#http.get(`https://jsonplaceholder.typicode.com/posts/${postId}`)
allPosts$ = () =>
this.#http.get>('https://jsonplaceholder.typicode.com/posts')
}
export interface Post {
id: number
title: string
body: string
}
================================================
FILE: examples/angular/basic/.devcontainer/devcontainer.json
================================================
{
"name": "Node.js",
"image": "mcr.microsoft.com/devcontainers/javascript-node:22"
}
================================================
FILE: examples/angular/basic-persister/README.md
================================================
# 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`
================================================
FILE: examples/angular/basic-persister/angular.json
================================================
{
"$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"
}
}
}
}
}
}
================================================
FILE: examples/angular/basic-persister/package.json
================================================
{
"name": "@tanstack/query-example-angular-basic-persister",
"type": "module",
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"watch": "ng build --watch --configuration development"
},
"private": true,
"dependencies": {
"@angular/common": "^20.0.0",
"@angular/compiler": "^20.0.0",
"@angular/core": "^20.0.0",
"@angular/platform-browser": "^20.0.0",
"@tanstack/angular-query-experimental": "^5.90.0",
"@tanstack/angular-query-persist-client": "^5.62.7",
"@tanstack/query-async-storage-persister": "^5.89.0",
"rxjs": "^7.8.2",
"tslib": "^2.8.1",
"zone.js": "0.15.0"
},
"devDependencies": {
"@angular/build": "^20.0.0",
"@angular/cli": "^20.0.0",
"@angular/compiler-cli": "^20.0.0",
"typescript": "5.8.3"
}
}
================================================
FILE: examples/angular/basic-persister/tsconfig.app.json
================================================
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/app",
"types": []
},
"files": ["src/main.ts"],
"include": ["src/**/*.d.ts"]
}
================================================
FILE: examples/angular/basic-persister/tsconfig.json
================================================
{
"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
}
}
================================================
FILE: examples/angular/basic-persister/.eslintrc.cjs
================================================
// @ts-check
/** @type {import('eslint').Linter.Config} */
const config = {}
module.exports = config
================================================
FILE: examples/angular/basic-persister/src/index.html
================================================
TanStack Query Angular basic persister example
================================================
FILE: examples/angular/basic-persister/src/main.ts
================================================
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),
)
================================================
FILE: examples/angular/basic-persister/src/app/app.component.html
================================================
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 {
}
================================================
FILE: examples/angular/basic-persister/src/app/app.component.ts
================================================
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)
}
================================================
FILE: examples/angular/basic-persister/src/app/app.config.ts
================================================
import { provideHttpClient, withFetch } from '@angular/common/http'
import {
QueryClient,
provideTanStackQuery,
} from '@tanstack/angular-query-experimental'
import { withPersistQueryClient } from '@tanstack/angular-query-persist-client'
import { withDevtools } from '@tanstack/angular-query-experimental/devtools'
import { createAsyncStoragePersister } from '@tanstack/query-async-storage-persister'
import type { ApplicationConfig } from '@angular/core'
const localStoragePersister = createAsyncStoragePersister({
storage: window.localStorage,
})
export const appConfig: ApplicationConfig = {
providers: [
provideHttpClient(withFetch()),
provideTanStackQuery(
new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60, // 1 minute
gcTime: 1000 * 60 * 60 * 24, // 24 hours
},
},
}),
withDevtools(),
withPersistQueryClient({
persistOptions: {
persister: localStoragePersister,
},
}),
),
],
}
================================================
FILE: examples/angular/basic-persister/src/app/components/post.component.html
================================================
@if (postQuery.isPending()) {
Loading...
} @else if (postQuery.isError()) {
Error: {{ postQuery.error().message }}
}
@if (postQuery.data(); as post) {
{{ post.title }}
@if (postQuery.isFetching()) {
Background Updating...
}
}
================================================
FILE: examples/angular/basic-persister/src/app/components/post.component.ts
================================================
import {
ChangeDetectionStrategy,
Component,
EventEmitter,
Output,
inject,
input,
} from '@angular/core'
import { QueryClient, injectQuery } from '@tanstack/angular-query-experimental'
import { fromEvent, lastValueFrom, takeUntil } from 'rxjs'
import { PostsService } from '../services/posts-service'
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'post',
templateUrl: './post.component.html',
})
export class PostComponent {
#postsService = inject(PostsService)
@Output() setPostId = new EventEmitter()
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)
}
================================================
FILE: examples/angular/basic-persister/src/app/components/posts.component.html
================================================
Posts
@if (postsQuery.isPending()) {
Loading...
} @else if (postsQuery.isError()) {
Error: {{ postsQuery.error().message }}
} @else if (postsQuery.isSuccess()) {
}
@if (postsQuery.isFetching()) {
Background Updating...
}
================================================
FILE: examples/angular/basic-persister/src/app/components/posts.component.ts
================================================
import {
ChangeDetectionStrategy,
Component,
EventEmitter,
Output,
inject,
} from '@angular/core'
import { QueryClient, injectQuery } from '@tanstack/angular-query-experimental'
import { lastValueFrom } from 'rxjs'
import { PostsService } from '../services/posts-service'
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'posts',
templateUrl: './posts.component.html',
})
export class PostsComponent {
queryClient = inject(QueryClient)
#postsService = inject(PostsService)
@Output() setPostId = new EventEmitter()
postsQuery = injectQuery(() => ({
queryKey: ['posts'],
queryFn: () => lastValueFrom(this.#postsService.allPosts$()),
}))
}
================================================
FILE: examples/angular/basic-persister/src/app/services/posts-service.ts
================================================
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
}
================================================
FILE: examples/angular/basic-persister/.devcontainer/devcontainer.json
================================================
{
"name": "Node.js",
"image": "mcr.microsoft.com/devcontainers/javascript-node:22"
}
================================================
FILE: examples/angular/devtools-panel/README.md
================================================
# TanStack Query Angular devtools panel 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`
================================================
FILE: examples/angular/devtools-panel/angular.json
================================================
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"cli": {
"packageManager": "pnpm",
"analytics": false,
"cache": {
"enabled": false
}
},
"newProjectRoot": "projects",
"projects": {
"devtools-panel": {
"projectType": "application",
"schematics": {
"@schematics/angular:component": {
"inlineTemplate": true,
"inlineStyle": true,
"skipTests": true
},
"@schematics/angular:class": {
"skipTests": true
},
"@schematics/angular:directive": {
"skipTests": true
},
"@schematics/angular:guard": {
"skipTests": true
},
"@schematics/angular:interceptor": {
"skipTests": true
},
"@schematics/angular:pipe": {
"skipTests": true
},
"@schematics/angular:resolver": {
"skipTests": true
},
"@schematics/angular:service": {
"skipTests": true
}
},
"root": "",
"sourceRoot": "src",
"prefix": "app",
"architect": {
"build": {
"builder": "@angular/build:application",
"options": {
"outputPath": "dist/devtools-panel",
"index": "src/index.html",
"browser": "src/main.ts",
"polyfills": ["zone.js"],
"tsConfig": "tsconfig.app.json",
"scripts": []
},
"configurations": {
"production": {
"budgets": [
{
"type": "initial",
"maximumWarning": "500kb",
"maximumError": "1mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "2kb",
"maximumError": "4kb"
}
],
"outputHashing": "all"
},
"development": {
"optimization": false,
"extractLicenses": false,
"sourceMap": true
}
},
"defaultConfiguration": "production"
},
"serve": {
"builder": "@angular/build:dev-server",
"configurations": {
"production": {
"buildTarget": "devtools-panel:build:production"
},
"development": {
"buildTarget": "devtools-panel:build:development"
}
},
"defaultConfiguration": "development"
},
"extract-i18n": {
"builder": "@angular/build:extract-i18n",
"options": {
"buildTarget": "devtools-panel:build"
}
}
}
}
},
"schematics": {
"@schematics/angular:component": {
"type": "component"
},
"@schematics/angular:directive": {
"type": "directive"
},
"@schematics/angular:service": {
"type": "service"
},
"@schematics/angular:guard": {
"typeSeparator": "."
},
"@schematics/angular:interceptor": {
"typeSeparator": "."
},
"@schematics/angular:module": {
"typeSeparator": "."
},
"@schematics/angular:pipe": {
"typeSeparator": "."
},
"@schematics/angular:resolver": {
"typeSeparator": "."
}
}
}
================================================
FILE: examples/angular/devtools-panel/package.json
================================================
{
"name": "@tanstack/query-example-angular-devtools-panel",
"type": "module",
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"watch": "ng build --watch --configuration development"
},
"private": true,
"dependencies": {
"@angular/common": "^20.0.0",
"@angular/compiler": "^20.0.0",
"@angular/core": "^20.0.0",
"@angular/platform-browser": "^20.0.0",
"@angular/router": "^20.0.0",
"@tanstack/angular-query-experimental": "^5.90.0",
"rxjs": "^7.8.2",
"tslib": "^2.8.1",
"zone.js": "0.15.0"
},
"devDependencies": {
"@angular/build": "^20.0.0",
"@angular/cli": "^20.0.0",
"@angular/compiler-cli": "^20.0.0",
"typescript": "5.8.3"
}
}
================================================
FILE: examples/angular/devtools-panel/tsconfig.app.json
================================================
/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/app",
"types": []
},
"files": ["src/main.ts"],
"include": ["src/**/*.d.ts"]
}
================================================
FILE: examples/angular/devtools-panel/tsconfig.json
================================================
{
"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
}
}
================================================
FILE: examples/angular/devtools-panel/src/index.html
================================================
TanStack Query devtools panel example
================================================
FILE: examples/angular/devtools-panel/src/main.ts
================================================
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))
================================================
FILE: examples/angular/devtools-panel/src/app/app.component.ts
================================================
import { ChangeDetectionStrategy, Component } from '@angular/core'
import { RouterLink, RouterOutlet } from '@angular/router'
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'app-root',
template: `
`,
imports: [RouterOutlet, RouterLink],
})
export class AppComponent {}
================================================
FILE: examples/angular/devtools-panel/src/app/app.config.ts
================================================
import { provideHttpClient, withFetch } from '@angular/common/http'
import { provideRouter } from '@angular/router'
import {
QueryClient,
provideTanStackQuery,
} from '@tanstack/angular-query-experimental'
import { routes } from './app.routes'
import type { ApplicationConfig } from '@angular/core'
export const appConfig: ApplicationConfig = {
providers: [
provideHttpClient(withFetch()),
provideRouter(routes),
provideTanStackQuery(new QueryClient()),
],
}
================================================
FILE: examples/angular/devtools-panel/src/app/app.routes.ts
================================================
import type { Route } from '@angular/router'
export const routes: Array = [
{
path: '',
redirectTo: 'basic',
pathMatch: 'full',
},
{
path: 'basic',
loadComponent: () =>
import('./components/basic-devtools-panel-example.component'),
},
{
path: 'lazy',
loadComponent: () =>
import('./components/lazy-load-devtools-panel-example.component'),
},
]
================================================
FILE: examples/angular/devtools-panel/src/app/components/basic-devtools-panel-example.component.ts
================================================
import {
ChangeDetectionStrategy,
Component,
signal,
viewChild,
} from '@angular/core'
import { injectDevtoolsPanel } from '@tanstack/angular-query-experimental/devtools-panel'
import { ExampleQueryComponent } from './example-query.component'
import type { ElementRef } from '@angular/core'
@Component({
selector: 'basic-devtools-panel-example',
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
Basic devtools panel example
In this example, the devtools panel is loaded programmatically when the
button is clicked
{{ isOpen() ? 'Close' : 'Open' }} the devtools panel
@if (isOpen()) {
}
`,
imports: [ExampleQueryComponent],
})
export default class BasicDevtoolsPanelExampleComponent {
readonly isOpen = signal(false)
readonly divEl = viewChild('div')
toggleIsOpen() {
this.isOpen.update((prev) => !prev)
}
readonly devtools = injectDevtoolsPanel(() => ({
hostElement: this.divEl(),
}))
}
================================================
FILE: examples/angular/devtools-panel/src/app/components/example-query.component.ts
================================================
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
import { injectQuery } from '@tanstack/angular-query-experimental'
import { HttpClient } from '@angular/common/http'
import { lastValueFrom } from 'rxjs'
interface Response {
name: string
description: string
subscribers_count: number
stargazers_count: number
forks_count: number
}
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'example-query',
template: `
@if (query.isPending()) {
Loading...
}
@if (query.isError()) {
An error has occurred: {{ query.error().message }}
}
@if (query.isSuccess()) {
@let data = query.data();
{{ data.name }}
{{ data.description }}
👀 {{ data.subscribers_count }}
✨ {{ data.stargazers_count }}
🍴 {{ data.forks_count }}
}
`,
})
export class ExampleQueryComponent {
readonly #http = inject(HttpClient)
readonly query = injectQuery(() => ({
queryKey: ['repoData'],
queryFn: () =>
lastValueFrom(
this.#http.get('https://api.github.com/repos/tanstack/query'),
),
}))
}
================================================
FILE: examples/angular/devtools-panel/src/app/components/lazy-load-devtools-panel-example.component.ts
================================================
import {
ChangeDetectionStrategy,
Component,
Injector,
computed,
effect,
inject,
signal,
viewChild,
} from '@angular/core'
import { ExampleQueryComponent } from './example-query.component'
import type { ElementRef } from '@angular/core'
import type { DevtoolsPanelRef } from '@tanstack/angular-query-experimental/devtools-panel'
@Component({
selector: 'lazy-load-devtools-panel-example',
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
Lazy load devtools panel example
In this example, the devtools panel is loaded programmatically when the
button is clicked. In addition, the code is lazy loaded.
{{ isOpen() ? 'Close' : 'Open' }} the devtools panel
@if (isOpen()) {
}
`,
imports: [ExampleQueryComponent],
})
export default class LazyLoadDevtoolsPanelExampleComponent {
readonly isOpen = signal(false)
readonly devtools = signal | undefined>(undefined)
readonly injector = inject(Injector)
readonly divEl = viewChild('div')
readonly devToolsOptions = computed(() => ({
hostElement: this.divEl(),
}))
toggleIsOpen() {
this.isOpen.update((prev) => !prev)
}
readonly loadDevtoolsEffect = effect(() => {
if (this.devtools()) return
if (this.isOpen()) {
this.devtools.set(
import('@tanstack/angular-query-experimental/devtools-panel').then(
({ injectDevtoolsPanel }) =>
injectDevtoolsPanel(this.devToolsOptions, {
injector: this.injector,
}),
),
)
}
})
}
================================================
FILE: examples/angular/devtools-panel/.devcontainer/devcontainer.json
================================================
{
"name": "Node.js",
"image": "mcr.microsoft.com/devcontainers/javascript-node:22"
}
================================================
FILE: examples/angular/infinite-query-with-max-pages/README.md
================================================
# TanStack Query Angular infinite query 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`
================================================
FILE: examples/angular/infinite-query-with-max-pages/angular.json
================================================
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"cli": {
"packageManager": "pnpm",
"analytics": false,
"cache": {
"enabled": false
}
},
"newProjectRoot": "projects",
"projects": {
"infinite-query-with-max-pages": {
"projectType": "application",
"schematics": {
"@schematics/angular:component": {
"inlineTemplate": true,
"inlineStyle": true,
"skipTests": true
},
"@schematics/angular:class": {
"skipTests": true
},
"@schematics/angular:directive": {
"skipTests": true
},
"@schematics/angular:guard": {
"skipTests": true
},
"@schematics/angular:interceptor": {
"skipTests": true
},
"@schematics/angular:pipe": {
"skipTests": true
},
"@schematics/angular:resolver": {
"skipTests": true
},
"@schematics/angular:service": {
"skipTests": true
}
},
"root": "",
"sourceRoot": "src",
"prefix": "app",
"architect": {
"build": {
"builder": "@angular/build:application",
"options": {
"outputPath": "dist/infinite-query-with-max-pages",
"index": "src/index.html",
"browser": "src/main.ts",
"polyfills": ["zone.js"],
"tsConfig": "tsconfig.app.json",
"assets": ["src/favicon.ico", "src/mockServiceWorker.js"],
"styles": [],
"scripts": []
},
"configurations": {
"production": {
"budgets": [
{
"type": "initial",
"maximumWarning": "500kb",
"maximumError": "1mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "2kb",
"maximumError": "4kb"
}
],
"outputHashing": "all"
},
"development": {
"optimization": false,
"extractLicenses": false,
"sourceMap": true
}
},
"defaultConfiguration": "production"
},
"serve": {
"builder": "@angular/build:dev-server",
"configurations": {
"production": {
"buildTarget": "infinite-query-with-max-pages:build:production"
},
"development": {
"buildTarget": "infinite-query-with-max-pages:build:development"
}
},
"defaultConfiguration": "development"
},
"extract-i18n": {
"builder": "@angular/build:extract-i18n",
"options": {
"buildTarget": "infinite-query-with-max-pages:build"
}
}
}
}
},
"schematics": {
"@schematics/angular:component": {
"type": "component"
},
"@schematics/angular:directive": {
"type": "directive"
},
"@schematics/angular:service": {
"type": "service"
},
"@schematics/angular:guard": {
"typeSeparator": "."
},
"@schematics/angular:interceptor": {
"typeSeparator": "."
},
"@schematics/angular:module": {
"typeSeparator": "."
},
"@schematics/angular:pipe": {
"typeSeparator": "."
},
"@schematics/angular:resolver": {
"typeSeparator": "."
}
}
}
================================================
FILE: examples/angular/infinite-query-with-max-pages/package.json
================================================
{
"name": "@tanstack/query-example-angular-infinite-query-with-max-pages",
"private": true,
"type": "module",
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"watch": "ng build --watch --configuration development"
},
"dependencies": {
"@angular/common": "^20.0.0",
"@angular/compiler": "^20.0.0",
"@angular/core": "^20.0.0",
"@angular/platform-browser": "^20.0.0",
"@tanstack/angular-query-experimental": "^5.90.0",
"rxjs": "^7.8.2",
"tslib": "^2.8.1",
"zone.js": "0.15.0"
},
"devDependencies": {
"@angular/build": "^20.0.0",
"@angular/cli": "^20.0.0",
"@angular/compiler-cli": "^20.0.0",
"typescript": "5.8.3"
}
}
================================================
FILE: examples/angular/infinite-query-with-max-pages/tsconfig.app.json
================================================
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/app",
"types": []
},
"files": ["src/main.ts"],
"include": ["src/**/*.d.ts"]
}
================================================
FILE: examples/angular/infinite-query-with-max-pages/tsconfig.json
================================================
{
"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
}
}
================================================
FILE: examples/angular/infinite-query-with-max-pages/.eslintrc.cjs
================================================
// @ts-check
/** @type {import('eslint').Linter.Config} */
const config = {}
module.exports = config
================================================
FILE: examples/angular/infinite-query-with-max-pages/src/index.html
================================================
TanStack Query Angular infinite query example
================================================
FILE: examples/angular/infinite-query-with-max-pages/src/main.ts
================================================
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))
================================================
FILE: examples/angular/infinite-query-with-max-pages/src/app/app.component.ts
================================================
import { ChangeDetectionStrategy, Component } from '@angular/core'
import { ExampleComponent } from './components/example.component'
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'app-root',
template: ` `,
imports: [ExampleComponent],
})
export class AppComponent {}
================================================
FILE: examples/angular/infinite-query-with-max-pages/src/app/app.config.ts
================================================
import {
provideHttpClient,
withFetch,
withInterceptors,
} from '@angular/common/http'
import {
QueryClient,
provideTanStackQuery,
} from '@tanstack/angular-query-experimental'
import { withDevtools } from '@tanstack/angular-query-experimental/devtools'
import { projectsMockInterceptor } from './api/projects-mock.interceptor'
import type { ApplicationConfig } from '@angular/core'
export const appConfig: ApplicationConfig = {
providers: [
provideHttpClient(withInterceptors([projectsMockInterceptor]), withFetch()),
provideTanStackQuery(
new QueryClient({
defaultOptions: {
queries: {
gcTime: 1000 * 60 * 60 * 24, // 24 hours
},
},
}),
withDevtools(),
),
],
}
================================================
FILE: examples/angular/infinite-query-with-max-pages/src/app/api/projects-mock.interceptor.ts
================================================
import { HttpResponse } from '@angular/common/http'
import { delayWhen, of, timer } from 'rxjs'
import type { Observable } from 'rxjs'
import type { HttpEvent, HttpInterceptorFn } from '@angular/common/http'
export const projectsMockInterceptor: HttpInterceptorFn = (
req,
next,
): Observable> => {
const { url } = req
if (url.includes('/api/projects')) {
const cursor = parseInt(
new URLSearchParams(req.url.split('?')[1]).get('cursor') || '0',
10,
)
const pageSize = 4
const data = Array(pageSize)
.fill(0)
.map((_, i) => {
return {
name: 'Project ' + (i + cursor) + ` (server time: ${Date.now()})`,
id: i + cursor,
}
})
const nextId = cursor < 20 ? data[data.length - 1].id + 1 : null
const previousId = cursor > -20 ? data[0].id - pageSize : null
// Simulate network latency with a random delay between 100ms and 500ms
const delayDuration = Math.random() * (500 - 100) + 100
return of(
new HttpResponse({
status: 200,
body: {
data,
nextId,
previousId,
},
}),
).pipe(delayWhen(() => timer(delayDuration)))
}
return next(req)
}
================================================
FILE: examples/angular/infinite-query-with-max-pages/src/app/components/example.component.html
================================================
Infinite Query with max pages
4 projects per page
3 pages max
@if (query.isPending()) {
Loading...
} @else if (query.isError()) {
Error: {{ query.error().message }}
} @else {
{{ previousButtonText() }}
@for (page of query.data().pages; track $index) {
@for (project of page.data; track project.id) {
{{ project.name }} {{ project.id }}
}
}
{{ nextButtonText() }}
{{
query.isFetching() && !query.isFetchingNextPage()
? 'Background Updating...'
: null
}}
}
================================================
FILE: examples/angular/infinite-query-with-max-pages/src/app/components/example.component.ts
================================================
import {
ChangeDetectionStrategy,
Component,
computed,
inject,
} from '@angular/core'
import { injectInfiniteQuery } from '@tanstack/angular-query-experimental'
import { lastValueFrom } from 'rxjs'
import { ProjectStyleDirective } from '../directives/project-style.directive'
import { ProjectsService } from '../services/projects.service'
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'example',
templateUrl: './example.component.html',
imports: [ProjectStyleDirective],
})
export class ExampleComponent {
readonly projectsService = inject(ProjectsService)
readonly query = injectInfiniteQuery(() => ({
queryKey: ['projects'],
queryFn: ({ pageParam }) => {
return lastValueFrom(this.projectsService.getProjects(pageParam))
},
initialPageParam: 0,
getPreviousPageParam: (firstPage) => firstPage.previousId ?? undefined,
getNextPageParam: (lastPage) => lastPage.nextId ?? undefined,
maxPages: 3,
}))
readonly nextButtonDisabled = computed(
() => !this.#hasNextPage() || this.#isFetchingNextPage(),
)
readonly nextButtonText = computed(() =>
this.#isFetchingNextPage()
? 'Loading more...'
: this.#hasNextPage()
? 'Load newer'
: 'Nothing more to load',
)
readonly previousButtonDisabled = computed(
() => !this.#hasPreviousPage() || this.#isFetchingNextPage(),
)
readonly previousButtonText = computed(() =>
this.#isFetchingPreviousPage()
? 'Loading more...'
: this.#hasPreviousPage()
? 'Load Older'
: 'Nothing more to load',
)
readonly #hasPreviousPage = this.query.hasPreviousPage
readonly #hasNextPage = this.query.hasNextPage
readonly #isFetchingPreviousPage = this.query.isFetchingPreviousPage
readonly #isFetchingNextPage = this.query.isFetchingNextPage
}
================================================
FILE: examples/angular/infinite-query-with-max-pages/src/app/directives/project-style.directive.ts
================================================
import { Directive, computed, input } from '@angular/core'
@Directive({
selector: '[projectStyle]',
host: {
'[style]': 'style()',
},
})
export class ProjectStyleDirective {
readonly projectStyle = input.required()
readonly style = computed(
() =>
`
border: 1px solid gray;
border-radius: 5px;
padding: 8px;
font-size: 14px;
background: hsla(${this.projectStyle() * 30}, 60%, 80%, 1);
`,
)
}
================================================
FILE: examples/angular/infinite-query-with-max-pages/src/app/services/projects.service.ts
================================================
import { HttpClient } from '@angular/common/http'
import { Injectable, inject } from '@angular/core'
interface Project {
id: number
name: string
}
interface ProjectResponse {
data: Array
nextId: number | undefined
previousId: number | undefined
}
@Injectable({
providedIn: 'root',
})
export class ProjectsService {
readonly #http = inject(HttpClient)
getProjects = (page: number) =>
this.#http.get(`/api/projects?cursor=${page}`)
}
================================================
FILE: examples/angular/infinite-query-with-max-pages/.devcontainer/devcontainer.json
================================================
{
"name": "Node.js",
"image": "mcr.microsoft.com/devcontainers/javascript-node:22"
}
================================================
FILE: examples/angular/optimistic-updates/README.md
================================================
# TanStack Query Angular optimistic-updates 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`
================================================
FILE: examples/angular/optimistic-updates/angular.json
================================================
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"cli": {
"packageManager": "pnpm",
"analytics": false,
"cache": {
"enabled": false
}
},
"newProjectRoot": "projects",
"projects": {
"optimistic-updates": {
"projectType": "application",
"schematics": {
"@schematics/angular:component": {
"inlineTemplate": true,
"inlineStyle": true,
"skipTests": true
},
"@schematics/angular:class": {
"skipTests": true
},
"@schematics/angular:directive": {
"skipTests": true
},
"@schematics/angular:guard": {
"skipTests": true
},
"@schematics/angular:interceptor": {
"skipTests": true
},
"@schematics/angular:pipe": {
"skipTests": true
},
"@schematics/angular:resolver": {
"skipTests": true
},
"@schematics/angular:service": {
"skipTests": true
}
},
"root": "",
"sourceRoot": "src",
"prefix": "app",
"architect": {
"build": {
"builder": "@angular/build:application",
"options": {
"outputPath": "dist/optimistic-updates",
"index": "src/index.html",
"browser": "src/main.ts",
"polyfills": ["zone.js"],
"tsConfig": "tsconfig.app.json",
"assets": ["src/favicon.ico", "src/assets"],
"styles": [],
"scripts": []
},
"configurations": {
"production": {
"budgets": [
{
"type": "initial",
"maximumWarning": "500kb",
"maximumError": "1mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "2kb",
"maximumError": "4kb"
}
],
"outputHashing": "all"
},
"development": {
"optimization": false,
"extractLicenses": false,
"sourceMap": true
}
},
"defaultConfiguration": "production"
},
"serve": {
"builder": "@angular/build:dev-server",
"configurations": {
"production": {
"buildTarget": "optimistic-updates:build:production"
},
"development": {
"buildTarget": "optimistic-updates:build:development"
}
},
"defaultConfiguration": "development"
},
"extract-i18n": {
"builder": "@angular/build:extract-i18n",
"options": {
"buildTarget": "optimistic-updates:build"
}
}
}
}
},
"schematics": {
"@schematics/angular:component": {
"type": "component"
},
"@schematics/angular:directive": {
"type": "directive"
},
"@schematics/angular:service": {
"type": "service"
},
"@schematics/angular:guard": {
"typeSeparator": "."
},
"@schematics/angular:interceptor": {
"typeSeparator": "."
},
"@schematics/angular:module": {
"typeSeparator": "."
},
"@schematics/angular:pipe": {
"typeSeparator": "."
},
"@schematics/angular:resolver": {
"typeSeparator": "."
}
}
}
================================================
FILE: examples/angular/optimistic-updates/package.json
================================================
{
"name": "@tanstack/query-example-angular-optimistic-updates",
"type": "module",
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"watch": "ng build --watch --configuration development"
},
"private": true,
"dependencies": {
"@angular/common": "^20.0.0",
"@angular/compiler": "^20.0.0",
"@angular/core": "^20.0.0",
"@angular/forms": "^20.0.0",
"@angular/platform-browser": "^20.0.0",
"@tanstack/angular-query-experimental": "^5.90.0",
"rxjs": "^7.8.2",
"tslib": "^2.8.1",
"zone.js": "0.15.0"
},
"devDependencies": {
"@angular/build": "^20.0.0",
"@angular/cli": "^20.0.0",
"@angular/compiler-cli": "^20.0.0",
"typescript": "5.8.3"
}
}
================================================
FILE: examples/angular/optimistic-updates/tsconfig.app.json
================================================
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/app",
"types": []
},
"files": ["src/main.ts"],
"include": ["src/**/*.d.ts"]
}
================================================
FILE: examples/angular/optimistic-updates/tsconfig.json
================================================
{
"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
}
}
================================================
FILE: examples/angular/optimistic-updates/.eslintrc.cjs
================================================
// @ts-check
/** @type {import('eslint').Linter.Config} */
const config = {}
module.exports = config
================================================
FILE: examples/angular/optimistic-updates/src/index.html
================================================
TanStack Query Angular Optimistic Updates Example
================================================
FILE: examples/angular/optimistic-updates/src/main.ts
================================================
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))
================================================
FILE: examples/angular/optimistic-updates/src/app/app.component.ts
================================================
import { ChangeDetectionStrategy, Component } from '@angular/core'
import { OptimisticUpdatesComponent } from './components/optimistic-updates.component'
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'app-root',
template: ` `,
imports: [OptimisticUpdatesComponent],
})
export class AppComponent {}
================================================
FILE: examples/angular/optimistic-updates/src/app/app.config.ts
================================================
import {
provideHttpClient,
withFetch,
withInterceptors,
} from '@angular/common/http'
import {
QueryClient,
provideTanStackQuery,
} from '@tanstack/angular-query-experimental'
import { withDevtools } from '@tanstack/angular-query-experimental/devtools'
import { mockInterceptor } from './interceptor/mock-api.interceptor'
import type { ApplicationConfig } from '@angular/core'
export const appConfig: ApplicationConfig = {
providers: [
provideHttpClient(withFetch(), withInterceptors([mockInterceptor])),
provideTanStackQuery(
new QueryClient({
defaultOptions: {
queries: {
gcTime: 1000 * 60 * 60 * 24, // 24 hours
},
},
}),
withDevtools(),
),
],
}
================================================
FILE: examples/angular/optimistic-updates/src/app/components/optimistic-updates.component.ts
================================================
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
import {
injectMutation,
injectQuery,
} from '@tanstack/angular-query-experimental'
import { FormsModule } from '@angular/forms'
import { DatePipe } from '@angular/common'
import { TasksService } from '../services/tasks.service'
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'optimistic-updates',
imports: [FormsModule, DatePipe],
template: `
In this example, new items can be created using a mutation. The new item
will be optimistically added to the list in hopes that the server accepts
the item. If it does, the list is refetched with the true items from the
list. Every now and then, the mutation may fail though. When that happens,
the previous list of items is restored and the list is again refetched
from the server.
@if (tasks.isLoading()) {
Loading...
}
Fail Mutation
@if (!tasks.isLoading() && tasks.isFetching()) {
Fetching in background
}
`,
})
export class OptimisticUpdatesComponent {
#tasksService = inject(TasksService)
tasks = injectQuery(() => this.#tasksService.allTasks())
clearMutation = injectMutation(() => this.#tasksService.addTask())
addMutation = injectMutation(() => this.#tasksService.addTask())
newItem = ''
failMutation = false
addItem() {
if (!this.newItem) return
this.addMutation.mutate({
task: this.newItem,
failMutation: this.failMutation,
})
this.newItem = ''
}
}
================================================
FILE: examples/angular/optimistic-updates/src/app/interceptor/mock-api.interceptor.ts
================================================
/**
* MockApiInterceptor is used to simulate API responses for `/api/tasks` endpoints.
* It handles the following operations:
* - GET: Fetches all tasks from sessionStorage.
* - POST: Adds a new task to sessionStorage.
* Simulated responses include a delay to mimic network latency.
*/
import { HttpResponse } from '@angular/common/http'
import { delay, of } from 'rxjs'
import type {
HttpEvent,
HttpHandlerFn,
HttpInterceptorFn,
HttpRequest,
} from '@angular/common/http'
import type { Observable } from 'rxjs'
export const mockInterceptor: HttpInterceptorFn = (
req: HttpRequest,
next: HttpHandlerFn,
): Observable> => {
const respondWith = (status: number, body: any) =>
of(new HttpResponse({ status, body })).pipe(delay(1000))
if (req.url === '/api/tasks') {
switch (req.method) {
case 'GET':
return respondWith(
200,
JSON.parse(
sessionStorage.getItem('optimistic-updates-tasks') || '[]',
),
)
case 'POST':
const tasks = JSON.parse(
sessionStorage.getItem('optimistic-updates-tasks') || '[]',
)
tasks.push(req.body)
sessionStorage.setItem(
'optimistic-updates-tasks',
JSON.stringify(tasks),
)
return respondWith(201, {
status: 'success',
task: req.body,
})
}
}
if (req.url === '/api/tasks-wrong-url') {
return respondWith(500, {
status: 'error',
})
}
return next(req)
}
================================================
FILE: examples/angular/optimistic-updates/src/app/services/tasks.service.ts
================================================
import { HttpClient } from '@angular/common/http'
import { Injectable, inject } from '@angular/core'
import {
QueryClient,
mutationOptions,
queryOptions,
} from '@tanstack/angular-query-experimental'
import { lastValueFrom } from 'rxjs'
@Injectable({
providedIn: 'root',
})
export class TasksService {
#queryClient = inject(QueryClient) // Manages query state and caching
#http = inject(HttpClient) // Handles HTTP requests
/**
* Fetches all tasks from the API.
* Returns an observable containing an array of task strings.
*/
allTasks = () =>
queryOptions({
queryKey: ['tasks'],
queryFn: () => {
return lastValueFrom(this.#http.get>('/api/tasks'))
},
})
/**
* Creates a mutation for adding a task.
* On success, invalidates and refetches the "tasks" query cache to update the task list.
*/
addTask() {
return mutationOptions({
mutationFn: ({
task,
failMutation = false,
}: {
task: string
failMutation: boolean
}) =>
lastValueFrom(
this.#http.post(
`/api/tasks${failMutation ? '-wrong-url' : ''}`,
task,
),
),
mutationKey: ['tasks'],
onSuccess: () => {
this.#queryClient.invalidateQueries({ queryKey: ['tasks'] })
},
onMutate: async ({ task }) => {
// Cancel any outgoing refetches
// (so they don't overwrite our optimistic update)
await this.#queryClient.cancelQueries({ queryKey: ['tasks'] })
// Snapshot the previous value
const previousTodos = this.#queryClient.getQueryData>([
'tasks',
])
// Optimistically update to the new value
if (previousTodos) {
this.#queryClient.setQueryData>(
['tasks'],
[...previousTodos, task],
)
}
return previousTodos
},
onError: (err, variables, context) => {
if (context) {
this.#queryClient.setQueryData>(['tasks'], context)
}
},
// Always refetch after error or success:
onSettled: () => {
this.#queryClient.invalidateQueries({ queryKey: ['tasks'] })
},
})
}
}
================================================
FILE: examples/angular/optimistic-updates/.devcontainer/devcontainer.json
================================================
{
"name": "Node.js",
"image": "mcr.microsoft.com/devcontainers/javascript-node:22"
}
================================================
FILE: examples/angular/pagination/README.md
================================================
# TanStack Query Angular pagination 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`
================================================
FILE: examples/angular/pagination/angular.json
================================================
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"cli": {
"packageManager": "pnpm",
"analytics": false,
"cache": {
"enabled": false
}
},
"newProjectRoot": "projects",
"projects": {
"pagination": {
"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/pagination",
"index": "src/index.html",
"browser": "src/main.ts",
"polyfills": ["zone.js"],
"tsConfig": "tsconfig.app.json",
"assets": ["src/favicon.ico"],
"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": "pagination:build:production"
},
"development": {
"buildTarget": "pagination:build:development"
}
},
"defaultConfiguration": "development"
},
"extract-i18n": {
"builder": "@angular/build:extract-i18n",
"options": {
"buildTarget": "pagination:build"
}
}
}
}
},
"schematics": {
"@schematics/angular:component": {
"type": "component"
},
"@schematics/angular:directive": {
"type": "directive"
},
"@schematics/angular:service": {
"type": "service"
},
"@schematics/angular:guard": {
"typeSeparator": "."
},
"@schematics/angular:interceptor": {
"typeSeparator": "."
},
"@schematics/angular:module": {
"typeSeparator": "."
},
"@schematics/angular:pipe": {
"typeSeparator": "."
},
"@schematics/angular:resolver": {
"typeSeparator": "."
}
}
}
================================================
FILE: examples/angular/pagination/package.json
================================================
{
"name": "@tanstack/query-example-angular-pagination",
"private": true,
"type": "module",
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"watch": "ng build --watch --configuration development"
},
"dependencies": {
"@angular/common": "^20.0.0",
"@angular/compiler": "^20.0.0",
"@angular/core": "^20.0.0",
"@angular/platform-browser": "^20.0.0",
"@tanstack/angular-query-experimental": "^5.90.0",
"rxjs": "^7.8.2",
"tslib": "^2.8.1",
"zone.js": "0.15.0"
},
"devDependencies": {
"@angular/build": "^20.0.0",
"@angular/cli": "^20.0.0",
"@angular/compiler-cli": "^20.0.0",
"typescript": "5.8.3"
}
}
================================================
FILE: examples/angular/pagination/tsconfig.app.json
================================================
/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/app",
"types": []
},
"files": ["src/main.ts"],
"include": ["src/**/*.d.ts"]
}
================================================
FILE: examples/angular/pagination/tsconfig.json
================================================
{
"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
}
}
================================================
FILE: examples/angular/pagination/src/index.html
================================================
TanStack Query Angular pagination example
================================================
FILE: examples/angular/pagination/src/main.ts
================================================
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))
================================================
FILE: examples/angular/pagination/src/app/app.component.ts
================================================
import { ChangeDetectionStrategy, Component } from '@angular/core'
import { ExampleComponent } from './components/example.component'
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'app-root',
template: ` `,
imports: [ExampleComponent],
})
export class AppComponent {}
================================================
FILE: examples/angular/pagination/src/app/app.config.ts
================================================
import {
provideHttpClient,
withFetch,
withInterceptors,
} from '@angular/common/http'
import {
QueryClient,
provideTanStackQuery,
} from '@tanstack/angular-query-experimental'
import { withDevtools } from '@tanstack/angular-query-experimental/devtools'
import { projectsMockInterceptor } from './api/projects-mock.interceptor'
import type { ApplicationConfig } from '@angular/core'
export const appConfig: ApplicationConfig = {
providers: [
provideHttpClient(withInterceptors([projectsMockInterceptor]), withFetch()),
provideTanStackQuery(new QueryClient(), withDevtools()),
],
}
================================================
FILE: examples/angular/pagination/src/app/api/projects-mock.interceptor.ts
================================================
import { HttpResponse } from '@angular/common/http'
import { delay, of } from 'rxjs'
import type { Observable } from 'rxjs'
import type { HttpEvent, HttpInterceptorFn } from '@angular/common/http'
export const projectsMockInterceptor: HttpInterceptorFn = (
req,
next,
): Observable> => {
const { url } = req
if (url.includes('/api/projects')) {
const page = parseInt(
new URLSearchParams(req.url.split('?')[1]).get('page') || '0',
10,
)
const pageSize = 10
const projects = Array(pageSize)
.fill(0)
.map((_, i) => {
const id = page * pageSize + (i + 1)
return {
name: 'Project ' + id,
id,
}
})
return of(
new HttpResponse({
status: 200,
body: {
projects,
hasMore: page < 9,
},
}),
).pipe(delay(1000))
}
return next(req)
}
================================================
FILE: examples/angular/pagination/src/app/components/example.component.html
================================================
In this example, each page of data remains visible as the next page is
fetched. The buttons and capability to proceed to the next page are also
supressed until the next page cursor is known. Each page is cached as a
normal query too, so when going to previous pages, you'll see them
instantaneously while they are also refetched invisibly in the background.
@if (query.isPending()) {
Loading...
} @else if (query.isError()) {
Error: {{ query.error().message }}
} @else if (query.isSuccess()) {
@for (project of query.data().projects; track project.id) {
{{ project.name }}
}
}
Current Page: {{ page() + 1 }}
Previous Page
Next Page
@if (query.isFetching()) {
Loading...
}
================================================
FILE: examples/angular/pagination/src/app/components/example.component.ts
================================================
import {
ChangeDetectionStrategy,
Component,
effect,
inject,
signal,
untracked,
} from '@angular/core'
import {
QueryClient,
injectQuery,
keepPreviousData,
} from '@tanstack/angular-query-experimental'
import { lastValueFrom } from 'rxjs'
import { ProjectsService } from '../services/projects.service'
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'example',
templateUrl: './example.component.html',
})
export class ExampleComponent {
readonly queryClient = inject(QueryClient)
readonly projectsService = inject(ProjectsService)
readonly page = signal(0)
readonly query = injectQuery(() => ({
queryKey: ['projects', this.page()],
queryFn: () => {
return lastValueFrom(this.projectsService.getProjects(this.page()))
},
placeholderData: keepPreviousData,
staleTime: 5000,
}))
readonly prefetchEffect = effect(() => {
const data = this.query.data()
const isPlaceholderData = this.query.isPlaceholderData()
const newPage = this.page() + 1
untracked(() => {
if (!isPlaceholderData && data?.hasMore) {
void this.queryClient.prefetchQuery({
queryKey: ['projects', newPage],
queryFn: () =>
lastValueFrom(this.projectsService.getProjects(newPage)),
})
}
})
})
previousPage() {
this.page.update((currentPage) => {
return Math.max(currentPage - 1, 0)
})
}
nextPage() {
this.page.update((currentPage) => {
return this.query.data()?.hasMore ? currentPage + 1 : currentPage
})
}
}
================================================
FILE: examples/angular/pagination/src/app/services/projects.service.ts
================================================
import { HttpClient } from '@angular/common/http'
import { Injectable, inject } from '@angular/core'
interface Project {
id: number
name: string
}
interface ProjectResponse {
projects: Array
hasMore: boolean
}
@Injectable({
providedIn: 'root',
})
export class ProjectsService {
readonly #http = inject(HttpClient)
getProjects(page: number) {
return this.#http.get(`/api/projects?page=${page}`)
}
}
================================================
FILE: examples/angular/pagination/.devcontainer/devcontainer.json
================================================
{
"name": "Node.js",
"image": "mcr.microsoft.com/devcontainers/javascript-node:22"
}
================================================
FILE: examples/angular/query-options-from-a-service/README.md
================================================
# TanStack Query Angular query options from a service 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`
================================================
FILE: examples/angular/query-options-from-a-service/angular.json
================================================
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"cli": {
"packageManager": "pnpm",
"analytics": false,
"cache": {
"enabled": false
}
},
"newProjectRoot": "projects",
"projects": {
"query-options-from-a-service": {
"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/query-options-from-a-service",
"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": "query-options-from-a-service:build:production"
},
"development": {
"buildTarget": "query-options-from-a-service:build:development"
}
},
"defaultConfiguration": "development"
},
"extract-i18n": {
"builder": "@angular/build:extract-i18n",
"options": {
"buildTarget": "query-options-from-a-service:build"
}
}
}
}
},
"schematics": {
"@schematics/angular:component": {
"type": "component"
},
"@schematics/angular:directive": {
"type": "directive"
},
"@schematics/angular:service": {
"type": "service"
},
"@schematics/angular:guard": {
"typeSeparator": "."
},
"@schematics/angular:interceptor": {
"typeSeparator": "."
},
"@schematics/angular:module": {
"typeSeparator": "."
},
"@schematics/angular:pipe": {
"typeSeparator": "."
},
"@schematics/angular:resolver": {
"typeSeparator": "."
}
}
}
================================================
FILE: examples/angular/query-options-from-a-service/package.json
================================================
{
"name": "@tanstack/query-example-angular-query-options-from-a-service",
"type": "module",
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"watch": "ng build --watch --configuration development"
},
"private": true,
"dependencies": {
"@angular/common": "^20.0.0",
"@angular/compiler": "^20.0.0",
"@angular/core": "^20.0.0",
"@angular/platform-browser": "^20.0.0",
"@angular/router": "^20.0.0",
"@tanstack/angular-query-experimental": "^5.90.0",
"rxjs": "^7.8.2",
"tslib": "^2.8.1",
"zone.js": "0.15.0"
},
"devDependencies": {
"@angular/build": "^20.0.0",
"@angular/cli": "^20.0.0",
"@angular/compiler-cli": "^20.0.0",
"typescript": "5.8.3"
}
}
================================================
FILE: examples/angular/query-options-from-a-service/tsconfig.app.json
================================================
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/app",
"types": []
},
"files": ["src/main.ts"],
"include": ["src/**/*.d.ts"]
}
================================================
FILE: examples/angular/query-options-from-a-service/tsconfig.json
================================================
{
"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
}
}
================================================
FILE: examples/angular/query-options-from-a-service/.eslintrc.cjs
================================================
// @ts-check
/** @type {import('eslint').Linter.Config} */
const config = {}
module.exports = config
================================================
FILE: examples/angular/query-options-from-a-service/src/index.html
================================================
TanStack Query Angular query options from a service example
================================================
FILE: examples/angular/query-options-from-a-service/src/main.ts
================================================
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))
================================================
FILE: examples/angular/query-options-from-a-service/src/app/app.component.html
================================================
As you visit the posts below, you will notice them in a loading state the
first time you load them. However, after you return to this list and click on
any posts you have already visited again, you will see them load instantly and
background refresh right before your eyes!
(You may need to throttle your network speed to simulate longer loading
sequences)
================================================
FILE: examples/angular/query-options-from-a-service/src/app/app.component.ts
================================================
import { Component } from '@angular/core'
import { RouterOutlet } from '@angular/router'
@Component({
selector: 'app-root',
imports: [RouterOutlet],
templateUrl: './app.component.html',
})
export class AppComponent {}
================================================
FILE: examples/angular/query-options-from-a-service/src/app/app.config.ts
================================================
import { provideHttpClient, withFetch } from '@angular/common/http'
import { provideRouter, withComponentInputBinding } from '@angular/router'
import {
QueryClient,
provideTanStackQuery,
} from '@tanstack/angular-query-experimental'
import { withDevtools } from '@tanstack/angular-query-experimental/devtools'
import { routes } from './app.routes'
import type { ApplicationConfig } from '@angular/core'
export const appConfig: ApplicationConfig = {
providers: [
provideHttpClient(withFetch()),
provideRouter(routes, withComponentInputBinding()),
provideTanStackQuery(new QueryClient(), withDevtools()),
],
}
================================================
FILE: examples/angular/query-options-from-a-service/src/app/app.routes.ts
================================================
import type { Route } from '@angular/router'
// loadComponent lazily loads the component
// when the component is the default export, there is no need to handle the promise
export const routes: Array = [
{
path: '',
loadComponent: () => import('./components/posts.component'),
},
{
path: 'post/:postId',
loadComponent: () => import('./components/post.component'),
},
]
================================================
FILE: examples/angular/query-options-from-a-service/src/app/components/post.component.html
================================================
@if (postQuery.isPending()) {
Loading...
} @else if (postQuery.isError()) {
Error: {{ postQuery.error().message }}
}
@if (postQuery.isSuccess()) {
@let post = postQuery.data();
{{ post.title }}
@if (postQuery.isFetching()) {
Background Updating...
}
}
================================================
FILE: examples/angular/query-options-from-a-service/src/app/components/post.component.ts
================================================
import {
ChangeDetectionStrategy,
Component,
inject,
input,
numberAttribute,
} from '@angular/core'
import { RouterLink } from '@angular/router'
import { injectQuery } from '@tanstack/angular-query-experimental'
import { QueriesService } from '../services/queries-service'
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'post',
templateUrl: './post.component.html',
imports: [RouterLink],
})
export default class PostComponent {
private readonly queries = inject(QueriesService)
readonly postId = input.required({
transform: numberAttribute,
})
readonly postQuery = injectQuery(() => this.queries.post(this.postId()))
}
================================================
FILE: examples/angular/query-options-from-a-service/src/app/components/posts.component.html
================================================
Posts
@if (postsQuery.isPending()) {
Loading...
} @else if (postsQuery.isError()) {
Error: {{ postsQuery.error().message }}
} @else if (postsQuery.isSuccess()) {
}
@if (postsQuery.isFetching()) {
Background Updating...
}
================================================
FILE: examples/angular/query-options-from-a-service/src/app/components/posts.component.ts
================================================
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
import { RouterLink } from '@angular/router'
import { QueryClient, injectQuery } from '@tanstack/angular-query-experimental'
import { QueriesService } from '../services/queries-service'
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'posts',
templateUrl: './posts.component.html',
imports: [RouterLink],
})
export default class PostsComponent {
private readonly queries = inject(QueriesService)
readonly postsQuery = injectQuery(() => this.queries.posts())
readonly queryClient = inject(QueryClient)
}
================================================
FILE: examples/angular/query-options-from-a-service/src/app/services/queries-service.ts
================================================
import { Injectable, inject } from '@angular/core'
import { lastValueFrom } from 'rxjs'
import { queryOptions } from '@tanstack/angular-query-experimental'
import { HttpClient } from '@angular/common/http'
export interface Post {
id: number
title: string
body: string
}
@Injectable({
providedIn: 'root',
})
export class QueriesService {
private readonly http = inject(HttpClient)
post(postId: number) {
return queryOptions({
queryKey: ['post', postId],
queryFn: () => {
return lastValueFrom(
this.http.get(
`https://jsonplaceholder.typicode.com/posts/${postId}`,
),
)
},
})
}
posts() {
return queryOptions({
queryKey: ['posts'],
queryFn: () =>
lastValueFrom(
this.http.get>(
'https://jsonplaceholder.typicode.com/posts',
),
),
})
}
}
================================================
FILE: examples/angular/query-options-from-a-service/.devcontainer/devcontainer.json
================================================
{
"name": "Node.js",
"image": "mcr.microsoft.com/devcontainers/javascript-node:22"
}
================================================
FILE: examples/angular/router/README.md
================================================
# TanStack Query Angular router 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`
================================================
FILE: examples/angular/router/angular.json
================================================
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"cli": {
"packageManager": "pnpm",
"analytics": false,
"cache": {
"enabled": false
}
},
"newProjectRoot": "projects",
"projects": {
"router": {
"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/router",
"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": "router:build:production"
},
"development": {
"buildTarget": "router:build:development"
}
},
"defaultConfiguration": "development"
},
"extract-i18n": {
"builder": "@angular/build:extract-i18n",
"options": {
"buildTarget": "router:build"
}
}
}
}
},
"schematics": {
"@schematics/angular:component": {
"type": "component"
},
"@schematics/angular:directive": {
"type": "directive"
},
"@schematics/angular:service": {
"type": "service"
},
"@schematics/angular:guard": {
"typeSeparator": "."
},
"@schematics/angular:interceptor": {
"typeSeparator": "."
},
"@schematics/angular:module": {
"typeSeparator": "."
},
"@schematics/angular:pipe": {
"typeSeparator": "."
},
"@schematics/angular:resolver": {
"typeSeparator": "."
}
}
}
================================================
FILE: examples/angular/router/package.json
================================================
{
"name": "@tanstack/query-example-angular-router",
"type": "module",
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"watch": "ng build --watch --configuration development"
},
"private": true,
"dependencies": {
"@angular/common": "^20.0.0",
"@angular/compiler": "^20.0.0",
"@angular/core": "^20.0.0",
"@angular/platform-browser": "^20.0.0",
"@angular/router": "^20.0.0",
"@tanstack/angular-query-experimental": "^5.90.0",
"rxjs": "^7.8.2",
"tslib": "^2.8.1",
"zone.js": "0.15.0"
},
"devDependencies": {
"@angular/build": "^20.0.0",
"@angular/cli": "^20.0.0",
"@angular/compiler-cli": "^20.0.0",
"typescript": "5.8.3"
}
}
================================================
FILE: examples/angular/router/tsconfig.app.json
================================================
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/app",
"types": []
},
"files": ["src/main.ts"],
"include": ["src/**/*.d.ts"]
}
================================================
FILE: examples/angular/router/tsconfig.json
================================================
{
"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
}
}
================================================
FILE: examples/angular/router/.eslintrc.cjs
================================================
// @ts-check
/** @type {import('eslint').Linter.Config} */
const config = {}
module.exports = config
================================================
FILE: examples/angular/router/src/index.html
================================================
TanStack Query Angular router example
================================================
FILE: examples/angular/router/src/main.ts
================================================
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))
================================================
FILE: examples/angular/router/src/app/app.component.html
================================================
As you visit the posts below, you will notice them in a loading state the
first time you load them. However, after you return to this list and click on
any posts you have already visited again, you will see them load instantly and
background refresh right before your eyes!
(You may need to throttle your network speed to simulate longer loading
sequences)
================================================
FILE: examples/angular/router/src/app/app.component.ts
================================================
import { ChangeDetectionStrategy, Component } from '@angular/core'
import { RouterOutlet } from '@angular/router'
@Component({
selector: 'app-root',
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [RouterOutlet],
templateUrl: './app.component.html',
})
export class AppComponent {}
================================================
FILE: examples/angular/router/src/app/app.config.ts
================================================
import { provideHttpClient, withFetch } from '@angular/common/http'
import { provideRouter, withComponentInputBinding } from '@angular/router'
import {
QueryClient,
provideTanStackQuery,
} from '@tanstack/angular-query-experimental'
import { withDevtools } from '@tanstack/angular-query-experimental/devtools'
import { routes } from './app.routes'
import type { ApplicationConfig } from '@angular/core'
export const appConfig: ApplicationConfig = {
providers: [
provideHttpClient(withFetch()),
provideTanStackQuery(new QueryClient(), withDevtools()),
provideRouter(routes, withComponentInputBinding()),
],
}
================================================
FILE: examples/angular/router/src/app/app.routes.ts
================================================
import type { Route } from '@angular/router'
// loadComponent lazily loads the component
// when the component is the default export, there is no need to handle the promise
export const routes: Array = [
{
path: '',
loadComponent: () => import('./components/posts.component'),
},
{
path: 'post/:postId',
loadComponent: () => import('./components/post.component'),
},
]
================================================
FILE: examples/angular/router/src/app/components/post.component.html
================================================
@if (postQuery.isPending()) {
Loading...
} @else if (postQuery.isError()) {
Error: {{ postQuery.error().message }}
}
@if (postQuery.isSuccess()) {
@let post = postQuery.data();
{{ post.title }}
@if (postQuery.isFetching()) {
Background Updating...
}
}
================================================
FILE: examples/angular/router/src/app/components/post.component.ts
================================================
import {
ChangeDetectionStrategy,
Component,
inject,
input,
numberAttribute,
} from '@angular/core'
import { RouterLink } from '@angular/router'
import { injectQuery } from '@tanstack/angular-query-experimental'
import { lastValueFrom } from 'rxjs'
import { PostsService } from '../services/posts-service'
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'post',
templateUrl: './post.component.html',
imports: [RouterLink],
})
export default class PostComponent {
readonly #postsService = inject(PostsService)
// The Angular router will automatically bind postId
// as `withComponentInputBinding` is added to `provideRouter`.
// See https://angular.dev/api/router/withComponentInputBinding
readonly postId = input.required({
transform: numberAttribute,
})
readonly postQuery = injectQuery(() => ({
queryKey: ['post', this.postId()],
queryFn: () => {
return lastValueFrom(this.#postsService.postById$(this.postId()))
},
}))
}
================================================
FILE: examples/angular/router/src/app/components/posts.component.html
================================================
Posts
@if (postsQuery.isPending()) {
Loading...
} @else if (postsQuery.isError()) {
Error: {{ postsQuery.error().message }}
} @else if (postsQuery.isSuccess()) {
}
@if (postsQuery.isFetching()) {
Background Updating...
}
================================================
FILE: examples/angular/router/src/app/components/posts.component.ts
================================================
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
import { RouterLink } from '@angular/router'
import { QueryClient, injectQuery } from '@tanstack/angular-query-experimental'
import { lastValueFrom } from 'rxjs'
import { PostsService } from '../services/posts-service'
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'posts',
templateUrl: './posts.component.html',
imports: [RouterLink],
})
export default class PostsComponent {
readonly #postsService = inject(PostsService)
readonly postsQuery = injectQuery(() => ({
queryKey: ['posts'],
queryFn: () => lastValueFrom(this.#postsService.allPosts$()),
}))
readonly queryClient = inject(QueryClient)
}
================================================
FILE: examples/angular/router/src/app/services/posts-service.ts
================================================
import { HttpClient } from '@angular/common/http'
import { Injectable, inject } from '@angular/core'
@Injectable({
providedIn: 'root',
})
export class PostsService {
readonly #http = inject(HttpClient)
postById$ = (postId: number) =>
this.#http.get(`https://jsonplaceholder.typicode.com/posts/${postId}`)
allPosts$ = () =>
this.#http.get>('https://jsonplaceholder.typicode.com/posts')
}
export interface Post {
id: number
title: string
body: string
}
================================================
FILE: examples/angular/router/.devcontainer/devcontainer.json
================================================
{
"name": "Node.js",
"image": "mcr.microsoft.com/devcontainers/javascript-node:22"
}
================================================
FILE: examples/angular/rxjs/README.md
================================================
# TanStack Query Angular RxJS 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`
================================================
FILE: examples/angular/rxjs/angular.json
================================================
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"cli": {
"packageManager": "pnpm",
"analytics": false,
"cache": {
"enabled": false
}
},
"newProjectRoot": "projects",
"projects": {
"rxjs": {
"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/rxjs",
"index": "src/index.html",
"browser": "src/main.ts",
"polyfills": ["zone.js"],
"tsConfig": "tsconfig.app.json",
"assets": ["src/favicon.ico", "src/mockServiceWorker.js"],
"styles": [],
"scripts": []
},
"configurations": {
"production": {
"budgets": [
{
"type": "initial",
"maximumWarning": "500kb",
"maximumError": "1mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "2kb",
"maximumError": "4kb"
}
],
"outputHashing": "all"
},
"development": {
"optimization": false,
"extractLicenses": false,
"sourceMap": true
}
},
"defaultConfiguration": "production"
},
"serve": {
"builder": "@angular/build:dev-server",
"configurations": {
"production": {
"buildTarget": "rxjs:build:production"
},
"development": {
"buildTarget": "rxjs:build:development"
}
},
"defaultConfiguration": "development"
},
"extract-i18n": {
"builder": "@angular/build:extract-i18n",
"options": {
"buildTarget": "rxjs:build"
}
}
}
}
},
"schematics": {
"@schematics/angular:component": {
"type": "component"
},
"@schematics/angular:directive": {
"type": "directive"
},
"@schematics/angular:service": {
"type": "service"
},
"@schematics/angular:guard": {
"typeSeparator": "."
},
"@schematics/angular:interceptor": {
"typeSeparator": "."
},
"@schematics/angular:module": {
"typeSeparator": "."
},
"@schematics/angular:pipe": {
"typeSeparator": "."
},
"@schematics/angular:resolver": {
"typeSeparator": "."
}
}
}
================================================
FILE: examples/angular/rxjs/package.json
================================================
{
"name": "@tanstack/query-example-angular-rxjs",
"private": true,
"type": "module",
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"watch": "ng build --watch --configuration development"
},
"dependencies": {
"@angular/common": "^20.0.0",
"@angular/compiler": "^20.0.0",
"@angular/core": "^20.0.0",
"@angular/forms": "^20.0.0",
"@angular/platform-browser": "^20.0.0",
"@tanstack/angular-query-experimental": "^5.90.0",
"rxjs": "^7.8.2",
"tslib": "^2.8.1",
"zone.js": "0.15.0"
},
"devDependencies": {
"@angular/build": "^20.0.0",
"@angular/cli": "^20.0.0",
"@angular/compiler-cli": "^20.0.0",
"typescript": "5.8.3"
}
}
================================================
FILE: examples/angular/rxjs/tsconfig.app.json
================================================
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/app",
"types": []
},
"files": ["src/main.ts"],
"include": ["src/**/*.d.ts"]
}
================================================
FILE: examples/angular/rxjs/tsconfig.json
================================================
{
"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
}
}
================================================
FILE: examples/angular/rxjs/.eslintrc.cjs
================================================
// @ts-check
/** @type {import('eslint').Linter.Config} */
const config = {}
module.exports = config
================================================
FILE: examples/angular/rxjs/src/index.html
================================================
TanStack Query Angular RxJS example
================================================
FILE: examples/angular/rxjs/src/main.ts
================================================
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))
================================================
FILE: examples/angular/rxjs/src/app/app.component.ts
================================================
import { ChangeDetectionStrategy, Component } from '@angular/core'
import { ExampleComponent } from './components/example.component'
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'app-root',
template: ` `,
imports: [ExampleComponent],
})
export class AppComponent {}
================================================
FILE: examples/angular/rxjs/src/app/app.config.ts
================================================
import {
provideHttpClient,
withFetch,
withInterceptors,
} from '@angular/common/http'
import {
QueryClient,
provideTanStackQuery,
} from '@tanstack/angular-query-experimental'
import { withDevtools } from '@tanstack/angular-query-experimental/devtools'
import { autocompleteMockInterceptor } from './api/autocomplete-mock.interceptor'
import type { ApplicationConfig } from '@angular/core'
export const appConfig: ApplicationConfig = {
providers: [
provideHttpClient(
withFetch(),
withInterceptors([autocompleteMockInterceptor]),
),
provideTanStackQuery(
new QueryClient({
defaultOptions: {
queries: {
gcTime: 1000 * 60 * 60 * 24, // 24 hours
},
},
}),
withDevtools(),
),
],
}
================================================
FILE: examples/angular/rxjs/src/app/api/autocomplete-mock.interceptor.ts
================================================
import { HttpResponse } from '@angular/common/http'
import { delayWhen, of, timer } from 'rxjs'
import type { Observable } from 'rxjs'
import type { HttpEvent, HttpInterceptorFn } from '@angular/common/http'
export const autocompleteMockInterceptor: HttpInterceptorFn = (
req,
next,
): Observable> => {
const { url } = req
if (url.includes('/api/autocomplete')) {
const term = new URLSearchParams(req.url.split('?')[1]).get('term') || ''
const data = [
'C#',
'C++',
'Go',
'Java',
'JavaScript',
'Kotlin',
'Lisp',
'Objective-C',
'PHP',
'Perl',
'Python',
'R',
'Ruby',
'Rust',
'SQL',
'Scala',
'Shell',
'Swift',
'TypeScript',
]
// Simulate network latency with a random delay between 100ms and 500ms
const delayDuration = Math.random() * (500 - 100) + 100
return of(
new HttpResponse({
status: 200,
body: {
suggestions: data.filter((item) =>
item.toLowerCase().startsWith(term.toLowerCase()),
),
},
}),
).pipe(delayWhen(() => timer(delayDuration)))
}
return next(req)
}
================================================
FILE: examples/angular/rxjs/src/app/components/example.component.html
================================================
Search for a programming language
================================================
FILE: examples/angular/rxjs/src/app/components/example.component.ts
================================================
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
import { toSignal } from '@angular/core/rxjs-interop'
import { NonNullableFormBuilder, ReactiveFormsModule } from '@angular/forms'
import {
injectQuery,
keepPreviousData,
} from '@tanstack/angular-query-experimental'
import { debounceTime, distinctUntilChanged, lastValueFrom } from 'rxjs'
import { AutocompleteService } from '../services/autocomplete-service'
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'example',
templateUrl: './example.component.html',
imports: [ReactiveFormsModule],
})
export class ExampleComponent {
readonly #autocompleteService = inject(AutocompleteService)
readonly #fb = inject(NonNullableFormBuilder)
readonly form = this.#fb.group({
term: '',
})
readonly term = toSignal(
this.form.controls.term.valueChanges.pipe(
debounceTime(300),
distinctUntilChanged(),
),
{ initialValue: '' },
)
readonly query = injectQuery(() => ({
queryKey: ['suggestions', this.term()],
queryFn: () => {
return lastValueFrom(
this.#autocompleteService.getSuggestions(this.term()),
)
},
placeholderData: keepPreviousData,
staleTime: 1000 * 60 * 5, // 5 minutes
}))
}
================================================
FILE: examples/angular/rxjs/src/app/services/autocomplete-service.ts
================================================
import { HttpClient } from '@angular/common/http'
import { Injectable, inject } from '@angular/core'
import { of } from 'rxjs'
interface Response {
suggestions: Array
}
@Injectable({
providedIn: 'root',
})
export class AutocompleteService {
readonly #http = inject(HttpClient)
getSuggestions = (term: string = '') =>
term.trim() === ''
? of({ suggestions: [] })
: this.#http.get(
`/api/autocomplete?term=${encodeURIComponent(term)}`,
)
}
================================================
FILE: examples/angular/rxjs/.devcontainer/devcontainer.json
================================================
{
"name": "Node.js",
"image": "mcr.microsoft.com/devcontainers/javascript-node:22"
}
================================================
FILE: examples/angular/simple/README.md
================================================
# TanStack Query Angular simple 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`
================================================
FILE: examples/angular/simple/angular.json
================================================
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"cli": {
"packageManager": "pnpm",
"analytics": false,
"cache": {
"enabled": false
}
},
"newProjectRoot": "projects",
"projects": {
"simple": {
"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/simple",
"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": "simple:build:production"
},
"development": {
"buildTarget": "simple:build:development"
}
},
"defaultConfiguration": "development"
},
"extract-i18n": {
"builder": "@angular/build:extract-i18n",
"options": {
"buildTarget": "simple:build"
}
}
}
}
},
"schematics": {
"@schematics/angular:component": {
"type": "component"
},
"@schematics/angular:directive": {
"type": "directive"
},
"@schematics/angular:service": {
"type": "service"
},
"@schematics/angular:guard": {
"typeSeparator": "."
},
"@schematics/angular:interceptor": {
"typeSeparator": "."
},
"@schematics/angular:module": {
"typeSeparator": "."
},
"@schematics/angular:pipe": {
"typeSeparator": "."
},
"@schematics/angular:resolver": {
"typeSeparator": "."
}
}
}
================================================
FILE: examples/angular/simple/package.json
================================================
{
"name": "@tanstack/query-example-angular-simple",
"type": "module",
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"watch": "ng build --watch --configuration development"
},
"private": true,
"dependencies": {
"@angular/common": "^20.0.0",
"@angular/compiler": "^20.0.0",
"@angular/core": "^20.0.0",
"@angular/platform-browser": "^20.0.0",
"@tanstack/angular-query-experimental": "^5.90.0",
"rxjs": "^7.8.2",
"tslib": "^2.8.1",
"zone.js": "0.15.0"
},
"devDependencies": {
"@angular/build": "^20.0.0",
"@angular/cli": "^20.0.0",
"@angular/compiler-cli": "^20.0.0",
"typescript": "5.8.3"
}
}
================================================
FILE: examples/angular/simple/tsconfig.app.json
================================================
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/app",
"types": []
},
"files": ["src/main.ts"],
"include": ["src/**/*.d.ts"]
}
================================================
FILE: examples/angular/simple/tsconfig.json
================================================
{
"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
}
}
================================================
FILE: examples/angular/simple/.eslintrc.cjs
================================================
// @ts-check
/** @type {import('eslint').Linter.Config} */
const config = {}
module.exports = config
================================================
FILE: examples/angular/simple/src/index.html
================================================
TanStack Query Angular simple example
================================================
FILE: examples/angular/simple/src/main.ts
================================================
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))
================================================
FILE: examples/angular/simple/src/styles.css
================================================
/* You can add global styles to this file, and also import other style files */
================================================
FILE: examples/angular/simple/src/app/app.component.ts
================================================
import { ChangeDetectionStrategy, Component } from '@angular/core'
import { SimpleExampleComponent } from './components/simple-example.component'
@Component({
selector: 'app-root',
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [SimpleExampleComponent],
template: ` `,
})
export class AppComponent {}
================================================
FILE: examples/angular/simple/src/app/app.config.ts
================================================
import { provideHttpClient, withFetch } from '@angular/common/http'
import {
QueryClient,
provideTanStackQuery,
} from '@tanstack/angular-query-experimental'
import { withDevtools } from '@tanstack/angular-query-experimental/devtools'
import type { ApplicationConfig } from '@angular/core'
export const appConfig: ApplicationConfig = {
providers: [
provideHttpClient(withFetch()),
provideTanStackQuery(new QueryClient(), withDevtools()),
],
}
================================================
FILE: examples/angular/simple/src/app/components/simple-example.component.html
================================================
@if (query.isPending()) {
Loading...
}
@if (query.isError()) {
An error has occurred: {{ query.error().message }}
}
@if (query.data(); as data) {
{{ data.name }}
{{ data.description }}
👀 {{ data.subscribers_count }}
✨ {{ data.stargazers_count }}
🍴 {{ data.forks_count }}
}
================================================
FILE: examples/angular/simple/src/app/components/simple-example.component.ts
================================================
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
import { injectQuery } from '@tanstack/angular-query-experimental'
import { HttpClient } from '@angular/common/http'
import { lastValueFrom } from 'rxjs'
interface Response {
name: string
description: string
subscribers_count: number
stargazers_count: number
forks_count: number
}
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'simple-example',
templateUrl: './simple-example.component.html',
})
export class SimpleExampleComponent {
readonly #http = inject(HttpClient)
readonly query = injectQuery(() => ({
queryKey: ['repoData'],
queryFn: () =>
lastValueFrom(
this.#http.get('https://api.github.com/repos/tanstack/query'),
),
}))
}
================================================
FILE: examples/angular/simple/.devcontainer/devcontainer.json
================================================
{
"name": "Node.js",
"image": "mcr.microsoft.com/devcontainers/javascript-node:22"
}
================================================
FILE: examples/solid/astro/README.md
================================================
# Astro Starter Kit: Minimal
```sh
npm create astro@latest -- --template minimal
```
[](https://stackblitz.com/github/withastro/astro/tree/latest/examples/minimal)
[](https://codesandbox.io/p/sandbox/github/withastro/astro/tree/latest/examples/minimal)
[](https://codespaces.new/withastro/astro?devcontainer_path=.devcontainer/minimal/devcontainer.json)
> 🧑🚀 **Seasoned astronaut?** Delete this file. Have fun!
## 🚀 Project Structure
Inside of your Astro project, you'll see the following folders and files:
```text
/
├── public/
├── src/
│ └── pages/
│ └── index.astro
└── package.json
```
Astro looks for `.astro` or `.md` files in the `src/pages/` directory. Each page is exposed as a route based on its file name.
There's nothing special about `src/components/`, but that's where we like to put any Astro/React/Vue/Svelte/Preact components.
Any static assets, like images, can be placed in the `public/` directory.
## 🧞 Commands
All commands are run from the root of the project, from a terminal:
| Command | Action |
| :------------------------ | :----------------------------------------------- |
| `npm install` | Installs dependencies |
| `npm run dev` | Starts local dev server at `localhost:4321` |
| `npm run build` | Build your production site to `./dist/` |
| `npm run preview` | Preview your build locally, before deploying |
| `npm run astro ...` | Run CLI commands like `astro add`, `astro check` |
| `npm run astro -- --help` | Get help using the Astro CLI |
## 👀 Want to learn more?
Feel free to check [our documentation](https://docs.astro.build) or jump into our [Discord server](https://astro.build/chat).
================================================
FILE: examples/solid/astro/astro.config.mjs
================================================
import { defineConfig } from 'astro/config'
import solidJs from '@astrojs/solid-js'
import tailwind from '@astrojs/tailwind'
import node from '@astrojs/node'
// https://astro.build/config
export default defineConfig({
output: 'server',
integrations: [solidJs(), tailwind()],
adapter: node({
mode: 'standalone',
}),
})
================================================
FILE: examples/solid/astro/package.json
================================================
{
"name": "@tanstack/query-example-solid-astro",
"private": true,
"type": "module",
"scripts": {
"dev": "astro dev",
"start": "astro dev",
"build": "astro check && astro build",
"preview": "astro preview",
"astro": "astro"
},
"dependencies": {
"@astrojs/check": "^0.9.4",
"@astrojs/node": "^9.1.3",
"@astrojs/solid-js": "^5.0.7",
"@astrojs/tailwind": "^6.0.2",
"@astrojs/vercel": "^8.1.3",
"@tanstack/solid-query": "^5.89.0",
"@tanstack/solid-query-devtools": "^5.89.0",
"astro": "^5.5.6",
"solid-js": "^1.9.7",
"tailwindcss": "^3.4.7",
"typescript": "5.8.3"
}
}
================================================
FILE: examples/solid/astro/tailwind.config.mjs
================================================
/** @type {import('tailwindcss').Config} */
export default {
content: ['./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}'],
theme: {
extend: {},
},
plugins: [],
}
================================================
FILE: examples/solid/astro/tsconfig.json
================================================
{
"extends": "astro/tsconfigs/strict",
"compilerOptions": {
"jsx": "preserve",
"jsxImportSource": "solid-js"
}
}
================================================
FILE: examples/solid/astro/.gitignore
================================================
# build output
dist/
.vercel/
# generated types
.astro/
# dependencies
node_modules/
# logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# environment variables
.env
.env.production
# macOS-specific files
.DS_Store
# jetbrains setting folder
.idea/
================================================
FILE: examples/solid/astro/src/env.d.ts
================================================
///
///
================================================
FILE: examples/solid/astro/src/components/Link.tsx
================================================
export const Link = (props: {
href: string
children: any
class?: string
}) => {
// Links doing client-side navigation
return (
{
e.preventDefault()
history.pushState({}, '', props.href)
window.dispatchEvent(new PopStateEvent('popstate'))
}}
class={props.class}
>
{props.children}
)
}
================================================
FILE: examples/solid/astro/src/components/SolidApp.tsx
================================================
import {
QueryClient,
QueryClientProvider,
keepPreviousData,
useQuery,
} from '@tanstack/solid-query'
import { SolidQueryDevtools } from '@tanstack/solid-query-devtools'
import {
For,
Show,
Suspense,
createContext,
createSignal,
useContext,
} from 'solid-js'
import { isServer } from 'solid-js/web'
import { getSearchParams, properCase } from '../utils'
import { Link } from './Link'
const PokemonIdContext = createContext<() => string>()
const usePokemonID = () => {
const id = useContext(PokemonIdContext)
if (!id) throw new Error('PokemonIdContext not found')
return id
}
const MAX_POKEMONS = 100
export const SolidApp = (props: { pokemon?: string }) => {
const client = new QueryClient()
const search = getSearchParams(props.pokemon || '')
return (
)
}
const App = () => {
return (
)
}
const PokemonDetails = () => {
const id = usePokemonID()
return (
Select a pokemon to see its stats
)
}
const PokemonDex = (props: { id: string }) => {
const pokemon = useQuery(() => ({
queryKey: ['pokemon', props.id],
queryFn: async () => {
const res = await fetch(
`https://pokeapi.co/api/v2/pokemon/${props.id}`,
).then((res) => res.json())
return res
},
placeholderData: keepPreviousData,
}))
const pokemon_stats = useQuery(() => ({
queryKey: ['pokemon', props.id],
queryFn: async () => {
const res = await fetch(
`https://pokeapi.co/api/v2/pokemon/${props.id}`,
).then((res) => res.json())
return res
},
select(data) {
const nameMap = {
hp: 'HP',
attack: 'Attack',
defense: 'Defense',
'special-attack': 'Special Attack',
'special-defense': 'Special Defense',
speed: 'Speed',
}
const stats = data.stats.map((stat: any) => ({
name: nameMap[stat.stat.name as keyof typeof nameMap],
value: stat.base_stat,
}))
return stats as { name: string; value: number }[]
},
placeholderData: keepPreviousData,
reconcile: 'name',
}))
const is_server_rendered = useQuery(() => ({
queryKey: ['is_server_rendered', props.id],
queryFn: () => {
if (isServer) return true
return false
},
placeholderData: keepPreviousData,
}))
return (
{properCase(pokemon.data.name)}
This query was rendered on the{' '}
client . Reload this page to see the page rendered on the
server.
>
}
>
server.
Click on another pokemon to see queries run and render on the
client.
)
}
const SideNav = () => {
const id = usePokemonID()
const pokemonsList = useQuery(() => ({
queryKey: ['pokemons'],
queryFn: async () => {
const res = await fetch(
`https://pokeapi.co/api/v2/pokemon?limit=${MAX_POKEMONS}`,
).then((res) => res.json())
return res as {
results: { name: string; url: string }[]
}
},
select(data) {
return data.results.map((p) => {
const regex = /\/pokemon\/(\d+)\/$/
const match = p.url.match(regex)
const id = match ? match[1] : ''
return {
name: properCase(p.name),
id,
avatar: `https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/${id}.png`,
}
})
},
placeholderData: keepPreviousData,
reconcile: 'id',
}))
const activeClass = (pokemonID: string) =>
id() === pokemonID ? 'bg-gray-50' : 'hover:bg-gray-50'
return (
Pokemons
{(pokemon) => (
{pokemon.name}
)}
)
}
================================================
FILE: examples/solid/astro/src/layouts/MainLayout.astro
================================================
---
import { SolidApp } from "../components/SolidApp";
const { id } = Astro.props;
---
Astro
================================================
FILE: examples/solid/astro/src/pages/index.astro
================================================
---
import MainLayout from '../layouts/MainLayout.astro';
const id = Astro.url.searchParams.get('id') || '';
---
================================================
FILE: examples/solid/astro/src/utils/index.ts
================================================
import { createSignal } from 'solid-js'
export const getSearchParams = (init: string) => {
const [search, setSearch] = createSignal(init)
if (typeof window !== 'undefined') {
window.addEventListener('popstate', () => {
const location = window.location
const params = new URLSearchParams(location.search)
setSearch(params.get('id') || '')
})
}
return search
}
export const properCase = (str: string) =>
str.charAt(0).toUpperCase() + str.slice(1)
================================================
FILE: examples/solid/basic/README.md
================================================
# Example
To run this example:
- `npm install`
- `npm run start`
================================================
FILE: examples/solid/basic/index.html
================================================
Solid App
You need to enable JavaScript to run this app.
================================================
FILE: examples/solid/basic/package.json
================================================
{
"name": "@tanstack/query-example-solid-basic",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"@tanstack/solid-query": "^5.89.0",
"@tanstack/solid-query-devtools": "^5.89.0",
"solid-js": "^1.9.7"
},
"devDependencies": {
"typescript": "5.8.3",
"vite": "^6.3.6",
"vite-plugin-solid": "^2.11.6"
}
}
================================================
FILE: examples/solid/basic/tsconfig.json
================================================
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "Bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "preserve",
"jsxImportSource": "solid-js",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"]
}
================================================
FILE: examples/solid/basic/vite.config.ts
================================================
import { defineConfig } from 'vite'
import solid from 'vite-plugin-solid'
export default defineConfig({
plugins: [solid()],
})
================================================
FILE: examples/solid/basic/.eslintrc.cjs
================================================
// @ts-check
/** @type {import('eslint').Linter.Config} */
const config = {}
module.exports = config
================================================
FILE: examples/solid/basic/.gitignore
================================================
node_modules
dist
.yalc
yalc.lock
================================================
FILE: examples/solid/basic/src/index.tsx
================================================
/* @refresh reload */
import {
QueryClient,
QueryClientProvider,
useQuery,
} from '@tanstack/solid-query'
import { SolidQueryDevtools } from '@tanstack/solid-query-devtools'
import { For, Match, Switch, createSignal } from 'solid-js'
import { render } from 'solid-js/web'
import type { Component, Setter } from 'solid-js'
const queryClient = new QueryClient({
defaultOptions: {
queries: {
gcTime: 1000 * 60 * 60 * 24, // 24 hours
},
},
})
type Post = {
id: number
title: string
body: string
}
function createPosts() {
return useQuery(() => ({
queryKey: ['posts'],
queryFn: async (): Promise> => {
const response = await fetch('https://jsonplaceholder.typicode.com/posts')
return await response.json()
},
}))
}
function Posts(props: { setPostId: Setter }) {
const state = createPosts()
return (
Posts
Loading...
Error: {(state.error as Error).message}
<>
{state.isFetching ? 'Background Updating...' : ' '}
>
)
}
const getPostById = async (id: number): Promise => {
const response = await fetch(
`https://jsonplaceholder.typicode.com/posts/${id}`,
)
return await response.json()
}
function createPost(postId: number) {
return useQuery(() => ({
queryKey: ['post', postId],
queryFn: () => getPostById(postId),
enabled: !!postId,
}))
}
function Post(props: { postId: number; setPostId: Setter }) {
const state = createPost(props.postId)
return (
Loading...
Error: {(state.error as Error).message}
<>
{state.data?.title}
{state.isFetching ? 'Background Updating...' : ' '}
>
)
}
const App: Component = () => {
const [postId, setPostId] = createSignal(-1)
return (
As you visit the posts below, you will notice them in a loading state
the first time you load them. However, after you return to this list and
click on any posts you have already visited again, you will see them
load instantly and background refresh right before your eyes!{' '}
(You may need to throttle your network speed to simulate longer
loading sequences)
{postId() > -1 ? (
) : (
)}
)
}
render(() => , document.getElementById('root') as HTMLElement)
================================================
FILE: examples/solid/basic-graphql-request/README.md
================================================
# Example
To run this example:
- `npm install`
- `npm run start`
================================================
FILE: examples/solid/basic-graphql-request/index.html
================================================
Solid App
You need to enable JavaScript to run this app.
================================================
FILE: examples/solid/basic-graphql-request/package.json
================================================
{
"name": "@tanstack/query-example-solid-basic-graphql-request",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"@tanstack/solid-query": "^5.89.0",
"@tanstack/solid-query-devtools": "^5.89.0",
"graphql": "^16.9.0",
"graphql-request": "^7.1.2",
"solid-js": "^1.9.7"
},
"devDependencies": {
"typescript": "5.8.3",
"vite": "^6.3.6",
"vite-plugin-solid": "^2.11.6"
}
}
================================================
FILE: examples/solid/basic-graphql-request/tsconfig.json
================================================
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "Bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "preserve",
"jsxImportSource": "solid-js",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"]
}
================================================
FILE: examples/solid/basic-graphql-request/vite.config.ts
================================================
import { defineConfig } from 'vite'
import solid from 'vite-plugin-solid'
export default defineConfig({
plugins: [solid()],
})
================================================
FILE: examples/solid/basic-graphql-request/.eslintrc.cjs
================================================
// @ts-check
/** @type {import('eslint').Linter.Config} */
const config = {}
module.exports = config
================================================
FILE: examples/solid/basic-graphql-request/.gitignore
================================================
node_modules
dist
.yalc
yalc.lock
================================================
FILE: examples/solid/basic-graphql-request/src/index.tsx
================================================
/* @refresh reload */
import {
QueryClient,
QueryClientProvider,
useQuery,
} from '@tanstack/solid-query'
import { SolidQueryDevtools } from '@tanstack/solid-query-devtools'
import { For, Match, Switch, createSignal } from 'solid-js'
import { render } from 'solid-js/web'
import { gql, request } from 'graphql-request'
import type { Accessor, Setter } from 'solid-js'
const endpoint = 'https://graphqlzero.almansi.me/api'
const queryClient = new QueryClient()
type Post = {
id: number
title: string
body: string
}
function App() {
const [postId, setPostId] = createSignal(-1)
return (
As you visit the posts below, you will notice them in a loading state
the first time you load them. However, after you return to this list and
click on any posts you have already visited again, you will see them
load instantly and background refresh right before your eyes!{' '}
(You may need to throttle your network speed to simulate longer
loading sequences)
{postId() > -1 ? (
) : (
)}
)
}
function createPosts() {
return useQuery(() => ({
queryKey: ['posts'],
queryFn: async () => {
const {
posts: { data },
} = await request<{ posts: { data: Array } }>(
endpoint,
gql`
query {
posts {
data {
id
title
}
}
}
`,
)
return data
},
}))
}
function Posts(props: { setPostId: Setter }) {
const state = createPosts()
return (
Posts
Loading...
Error: {(state.error as Error).message}
<>
{state.isFetching ? 'Background Updating...' : ' '}
>
)
}
function createPost(postId: Accessor) {
return useQuery(() => ({
queryKey: ['post', postId()],
queryFn: async () => {
const { post } = await request<{ post: Post }>(
endpoint,
gql`
query {
post(id: ${postId()}) {
id
title
body
}
}
`,
)
return post
},
enabled: !!postId(),
}))
}
function Post(props: { postId: number; setPostId: Setter }) {
const state = createPost(() => props.postId)
return (
Loading...
Error: {(state.error as Error).message}
<>
{state.data?.title}
{state.isFetching ? 'Background Updating...' : ' '}
>
)
}
render(() => , document.getElementById('root') as HTMLElement)
================================================
FILE: examples/solid/default-query-function/README.md
================================================
# Example
To run this example:
- `npm install`
- `npm run start`
================================================
FILE: examples/solid/default-query-function/index.html
================================================
Solid App
You need to enable JavaScript to run this app.
================================================
FILE: examples/solid/default-query-function/package.json
================================================
{
"name": "@tanstack/query-example-solid-default-query-function",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"@tanstack/solid-query": "^5.89.0",
"@tanstack/solid-query-devtools": "^5.89.0",
"solid-js": "^1.9.7"
},
"devDependencies": {
"typescript": "5.8.3",
"vite": "^6.3.6",
"vite-plugin-solid": "^2.11.6"
}
}
================================================
FILE: examples/solid/default-query-function/tsconfig.json
================================================
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "Bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "preserve",
"jsxImportSource": "solid-js",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"]
}
================================================
FILE: examples/solid/default-query-function/vite.config.ts
================================================
import { defineConfig } from 'vite'
import solid from 'vite-plugin-solid'
export default defineConfig({
plugins: [solid()],
})
================================================
FILE: examples/solid/default-query-function/.eslintrc.cjs
================================================
// @ts-check
/** @type {import('eslint').Linter.Config} */
const config = {}
module.exports = config
================================================
FILE: examples/solid/default-query-function/.gitignore
================================================
node_modules
dist
.yalc
yalc.lock
================================================
FILE: examples/solid/default-query-function/src/index.tsx
================================================
/* @refresh reload */
import {
QueryClient,
QueryClientProvider,
useQuery,
} from '@tanstack/solid-query'
import { SolidQueryDevtools } from '@tanstack/solid-query-devtools'
import { For, Match, Show, Switch, createSignal } from 'solid-js'
import { render } from 'solid-js/web'
import type { Setter } from 'solid-js'
import type { QueryFunction } from '@tanstack/solid-query'
// Define a default query function that will receive the query key
const defaultQueryFn: QueryFunction = async ({ queryKey }) => {
const response = await fetch(
`https://jsonplaceholder.typicode.com${queryKey[0]}`,
{
method: 'GET',
},
)
return response.json()
}
// provide the default query function to your app via the query client
const queryClient = new QueryClient({
defaultOptions: {
queries: {
queryFn: defaultQueryFn,
},
},
})
function App() {
const [postId, setPostId] = createSignal(-1)
return (
As you visit the posts below, you will notice them in a loading state
the first time you load them. However, after you return to this list and
click on any posts you have already visited again, you will see them
load instantly and background refresh right before your eyes!{' '}
(You may need to throttle your network speed to simulate longer
loading sequences)
-1} fallback={ }>
)
}
function Posts(props: { setPostId: Setter }) {
// All you have to do now is pass a key!
const state = useQuery(() => ({ queryKey: ['/posts'] }))
return (
Posts
Loading...
Error: {(state.error as Error).message}
<>
{state.isFetching ? 'Background Updating...' : ' '}
>
)
}
function Post(props: { postId: number; setPostId: Setter }) {
// You can even leave out the queryFn and just go straight into options
const state = useQuery(() => ({
queryKey: [`/posts/${props.postId}`],
enabled: !!props.postId,
}))
return (
Loading...
Error: {(state.error as Error).message}
<>
{state.data.title}
{state.isFetching ? 'Background Updating...' : ' '}
>
)
}
render(() => , document.getElementById('root') as HTMLElement)
================================================
FILE: examples/solid/simple/README.md
================================================
# Example
To run this example:
- `npm install`
- `npm run start`
================================================
FILE: examples/solid/simple/index.html
================================================
Solid App
You need to enable JavaScript to run this app.
================================================
FILE: examples/solid/simple/package.json
================================================
{
"name": "@tanstack/query-example-solid-simple",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"@tanstack/solid-query": "^5.89.0",
"@tanstack/solid-query-devtools": "^5.89.0",
"solid-js": "^1.9.7"
},
"devDependencies": {
"@tanstack/eslint-plugin-query": "^5.89.0",
"typescript": "5.8.3",
"vite": "^6.3.6",
"vite-plugin-solid": "^2.11.6"
}
}
================================================
FILE: examples/solid/simple/tsconfig.json
================================================
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "Bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "preserve",
"jsxImportSource": "solid-js",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src", ".eslintrc.cjs", "vite.config.ts"]
}
================================================
FILE: examples/solid/simple/vite.config.ts
================================================
import { defineConfig } from 'vite'
import solid from 'vite-plugin-solid'
export default defineConfig({
plugins: [solid()],
})
================================================
FILE: examples/solid/simple/.eslintrc.cjs
================================================
// @ts-check
/** @type {import('eslint').Linter.Config} */
const config = {
extends: ['plugin:@tanstack/eslint-plugin-query/recommended'],
}
module.exports = config
================================================
FILE: examples/solid/simple/.gitignore
================================================
node_modules
dist
.yalc
yalc.lock
================================================
FILE: examples/solid/simple/src/index.tsx
================================================
/* @refresh reload */
import {
QueryClient,
QueryClientProvider,
useQuery,
} from '@tanstack/solid-query'
import { SolidQueryDevtools } from '@tanstack/solid-query-devtools'
import { Match, Switch } from 'solid-js'
import { render } from 'solid-js/web'
const queryClient = new QueryClient()
export default function App() {
return (
)
}
function Example() {
const state = useQuery(() => ({
queryKey: ['repoData'],
queryFn: async () => {
const response = await fetch(
'https://api.github.com/repos/TanStack/query',
)
return await response.json()
},
}))
return (
Loading...
{'An error has occurred: ' + (state.error as Error).message}
{state.data.name}
{state.data.description}
👀 {state.data.subscribers_count} {' '}
✨ {state.data.stargazers_count} {' '}
🍴 {state.data.forks_count}
{state.isFetching ? 'Updating...' : ''}
)
}
render(() => , document.getElementById('root') as HTMLElement)
================================================
FILE: examples/solid/solid-start-streaming/README.md
================================================
# SolidStart
Everything you need to build a Solid project, powered by [`solid-start`](https://start.solidjs.com);
## Creating a project
```bash
# create a new project in the current directory
npm init solid@latest
# create a new project in my-app
npm init solid@latest my-app
```
## Developing
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
```bash
npm run dev
# or start the server and open the app in a new browser tab
npm run dev -- --open
```
## Building
Solid apps are built with _presets_, which optimise your project for deployment to different environments.
By default, `npm run build` will generate a Node app that you can run with `npm start`. To use a different preset, add it to the `devDependencies` in `package.json` and specify in your `app.config.js`.
## This project was created with the [Solid CLI](https://solid-cli.netlify.app)
================================================
FILE: examples/solid/solid-start-streaming/app.config.ts
================================================
import { defineConfig } from '@solidjs/start/config'
export default defineConfig({})
================================================
FILE: examples/solid/solid-start-streaming/package.json
================================================
{
"name": "@tanstack/query-example-solid-start-streaming",
"private": true,
"type": "module",
"scripts": {
"dev": "vinxi dev",
"build": "vinxi build",
"start": "vinxi start",
"version": "vinxi version"
},
"dependencies": {
"@solidjs/meta": "^0.29.4",
"@solidjs/router": "^0.15.3",
"@solidjs/start": "^1.1.3",
"@tanstack/solid-query": "^5.89.0",
"@tanstack/solid-query-devtools": "^5.89.0",
"solid-js": "^1.9.7",
"vinxi": "^0.5.3"
},
"engines": {
"node": ">=18"
}
}
================================================
FILE: examples/solid/solid-start-streaming/tsconfig.json
================================================
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "Bundler",
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"jsx": "preserve",
"jsxImportSource": "solid-js",
"allowJs": true,
"strict": true,
"noEmit": true,
"types": ["vinxi/client", "vite/client"],
"isolatedModules": true,
"paths": {
"~/*": ["./src/*"]
}
}
}
================================================
FILE: examples/solid/solid-start-streaming/.gitignore
================================================
dist
.solid
.output
.vercel
.netlify
netlify
.vinxi
# Environment
.env
.env*.local
# dependencies
/node_modules
# IDEs and editors
/.idea
.project
.classpath
*.launch
.settings/
# Temp
gitignore
# System Files
.DS_Store
Thumbs.db
================================================
FILE: examples/solid/solid-start-streaming/src/app.css
================================================
body {
font-family:
Gordita, Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue',
sans-serif;
line-height: 1.5;
}
a {
margin-right: 1rem;
}
main {
text-align: center;
padding: 1em;
margin: 0 auto;
}
h1 {
color: #335d92;
text-transform: uppercase;
font-size: 2.5rem;
font-weight: 100;
line-height: 1.1;
margin: 2.5rem auto 0 auto;
max-width: 14rem;
}
p {
max-width: 14rem;
margin: 1rem auto;
line-height: 1.35;
}
@media (min-width: 480px) {
h1 {
max-width: none;
}
p {
max-width: none;
}
}
.loader {
color: #888;
margin-top: 1rem;
}
.error {
color: red;
margin-top: 1rem;
}
.description {
max-width: 60rem;
margin: 3rem auto;
}
.description p {
line-height: 1.5;
}
.description *:not(:first-child) {
margin-top: 1rem;
}
.description_img {
max-width: 100%;
border-radius: 4px;
}
.example {
border: 1px solid #ddd;
padding: 1rem;
max-width: 50rem;
text-align: left;
margin: 1rem auto;
}
.example__header {
display: flex;
}
.example__title {
font-size: 1rem;
font-weight: bold;
flex-grow: 1;
}
.example--table {
padding: 0.5rem;
}
.example--table th,
.example--table td {
padding: 4px 12px;
white-space: nowrap;
}
================================================
FILE: examples/solid/solid-start-streaming/src/app.tsx
================================================
// @refresh reload
import { MetaProvider, Title } from '@solidjs/meta'
import { A, Router } from '@solidjs/router'
import { FileRoutes } from '@solidjs/start/router'
import { Suspense } from 'solid-js'
import { SolidQueryDevtools } from '@tanstack/solid-query-devtools'
import { QueryClient, QueryClientProvider } from '@tanstack/solid-query'
import './app.css'
export default function App() {
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
staleTime: 5000,
},
},
})
return (
(
SolidStart - Basic
Home
Streamed
Deferred
Mixed
With Error
Hydration
Prefetch
Batching Methods
{props.children}
)}
>
)
}
================================================
FILE: examples/solid/solid-start-streaming/src/entry-client.tsx
================================================
// @refresh reload
import { mount, StartClient } from '@solidjs/start/client'
mount(() => , document.getElementById('app')!)
================================================
FILE: examples/solid/solid-start-streaming/src/entry-server.tsx
================================================
// @refresh reload
import { createHandler, StartServer } from '@solidjs/start/server'
export default createHandler(() => (
(
{assets}
{children}
{scripts}
)}
/>
))
================================================
FILE: examples/solid/solid-start-streaming/src/global.d.ts
================================================
///
================================================
FILE: examples/solid/solid-start-streaming/src/components/example.tsx
================================================
import type { ParentComponent } from 'solid-js'
export interface ExampleProps {
title: string
deferStream?: boolean
sleep?: number
}
export const Example: ParentComponent = (props) => {
return (
{props.children}
)
}
================================================
FILE: examples/solid/solid-start-streaming/src/components/post-viewer.tsx
================================================
import { useQuery } from '@tanstack/solid-query'
import { resetErrorBoundaries } from 'solid-js'
import { createSignal } from 'solid-js'
import { For } from 'solid-js'
import { Example } from './example'
import { QueryBoundary } from './query-boundary'
import type { Component } from 'solid-js'
import { fetchPost } from '~/utils/api'
export interface PostViewerProps {
deferStream?: boolean
sleep?: number
simulateError?: boolean
}
export const PostViewer: Component = (props) => {
const [simulateError, setSimulateError] = createSignal(props.simulateError)
const [postId, setPostId] = createSignal(1)
const query = useQuery(() => ({
queryKey: ['posts', postId()],
queryFn: () =>
fetchPost({
postId: postId(),
sleep: props.sleep,
simulateError:
simulateError() || (simulateError() !== false && postId() === 5),
}),
deferStream: props.deferStream,
throwOnError: true,
}))
return (
{
setPostId(Math.max(postId() - 1, 1))
resetErrorBoundaries()
}}
>
Previous Post
{
setPostId(Math.min(postId() + 1, 100))
resetErrorBoundaries()
}}
>
Next Post
{/* NOTE: without this extra wrapping div, for some reason solid ends up printing two errors... feels like a bug in solid. */}
loading post...
}
errorFallback={(err, retry) => (
{err.message}
{
setSimulateError(false)
retry()
}}
>
retry
)}
>
{(posts) => (
{(post) => (
[post {postId()}] {post.title}
{post.body}
)}
)}
)
}
================================================
FILE: examples/solid/solid-start-streaming/src/components/query-boundary.tsx
================================================
import { ErrorBoundary, Match, Suspense, Switch, children } from 'solid-js'
import type { UseQueryResult } from '@tanstack/solid-query'
import type { JSX } from 'solid-js'
export interface QueryBoundaryProps {
query: UseQueryResult
/**
* Triggered when the data is initially loading.
*/
loadingFallback?: JSX.Element
/**
* Triggered when fetching is complete, but the returned data was falsey.
*/
notFoundFallback?: JSX.Element
/**
* Triggered when the query results in an error.
*/
errorFallback?: (err: Error, retry: () => void) => JSX.Element
/**
* Triggered when fetching is complete, and the returned data is not falsey.
*/
children: (data: Exclude) => JSX.Element
}
/**
* Convenience wrapper that handles suspense and errors for queries. Makes the results of query.data available to
* children (as a render prop) in a type-safe way.
*/
export function QueryBoundary(props: QueryBoundaryProps) {
return (
props.errorFallback ? (
props.errorFallback(err, async () => {
await props.query.refetch()
reset()
})
) : (
{err.message}
{
await props.query.refetch()
reset()
}}
>
retry
)
}
>
{/*
{props.errorFallback ? (
props.errorFallback
) : (
{props.query.error?.message}
{
props.query.refetch();
}}
>
retry
)}
*/}
{props.notFoundFallback ? (
props.notFoundFallback
) : (
not found
)}
{props.children(
props.query.data as Exclude,
)}
)
}
================================================
FILE: examples/solid/solid-start-streaming/src/components/user-info.tsx
================================================
import { useQuery } from '@tanstack/solid-query'
import { createSignal } from 'solid-js'
import { Example } from './example'
import { QueryBoundary } from './query-boundary'
import type { Component } from 'solid-js'
import { fetchUser } from '~/utils/api'
export interface UserInfoProps {
deferStream?: boolean
sleep?: number
simulateError?: boolean
staleTime?: number
gcTime?: number
}
export const userInfoQueryOpts = (props?: UserInfoProps) => ({
queryKey: ['user'],
queryFn: () => fetchUser(props),
deferStream: props?.deferStream,
staleTime: props?.staleTime,
gcTime: props?.gcTime,
throwOnError: true,
})
export const UserInfo: Component = (props) => {
const [simulateError, setSimulateError] = createSignal(props.simulateError)
const query = useQuery(() =>
userInfoQueryOpts({ ...props, simulateError: simulateError() }),
)
return (
loading user...}
errorFallback={(err, retry) => (
{err.message}
{
setSimulateError(false)
retry()
}}
>
retry
)}
>
{(user) => (
<>
id: {user.id}
name: {user.name}
queryTime: {user.queryTime}
{
query.refetch()
}}
>
refetch
>
)}
)
}
================================================
FILE: examples/solid/solid-start-streaming/src/routes/[...404].tsx
================================================
import { Title } from '@solidjs/meta'
import { HttpStatusCode } from '@solidjs/start'
export default function NotFound() {
return (
Not Found
Page Not Found
Visit{' '}
start.solidjs.com
{' '}
to learn how to build SolidStart apps.
)
}
================================================
FILE: examples/solid/solid-start-streaming/src/routes/batch-methods.tsx
================================================
import { notifyManager, useQuery } from '@tanstack/solid-query'
import { createSignal } from 'solid-js'
import { QueryBoundary } from '~/components/query-boundary'
function sleep(milliseconds: number) {
return new Promise((res) => setTimeout(res, milliseconds))
}
function spin(milliseconds: number) {
const start = performance.now()
while (performance.now() - start <= milliseconds) {
// do nothing
}
}
async function sayHello(name: string) {
console.info('[api] sayHello.start')
await sleep(500)
// make the layout shift more obvious, it doesn't always happen
console.time('[api] sayHello.spin')
spin(20)
console.timeEnd('[api] sayHello.spin')
console.info('[api] sayHello.end')
return `Hello ${name}`
}
export default function BatchMethods() {
const [count, setCount] = createSignal(0)
const hello = useQuery(() => ({
queryKey: ['hello', count()] as const,
queryFn: ({ queryKey: [_, count] }) => sayHello(`solid ${count}`),
}))
return (
(el.value = 'timer')} // browser caches form input
onInput={(e) => {
const type = e.currentTarget.value
if (type === 'raf') notifyManager.setScheduler(requestAnimationFrame)
if (type === 'tick') notifyManager.setScheduler(queueMicrotask)
if (type === 'timer')
notifyManager.setScheduler((cb) => setTimeout(cb, 0))
}}
>
requestAnimationFrame
setTimeout
queueMicrotick
setCount((x) => x + 1)}>
Clicks: {count()}
{(data) => {data}
}
Something below to demonstrate layout shift
Due to the way solidjs handles updates, sometimes the updating of a
query results in DOM modifications triggering a rerender twice. This is
perceived as a glitch in the layout of the webpage that usually lasts
for one frame. By using another batching strategy in the browser,
instead of the default setTimeout one, we can mitigate this issue. Try
out requestAnimationFrame or queueMicrotick.
)
}
================================================
FILE: examples/solid/solid-start-streaming/src/routes/deferred.tsx
================================================
import { Title } from '@solidjs/meta'
import { PostViewer } from '~/components/post-viewer'
import { UserInfo } from '~/components/user-info'
export default function Deferred() {
return (
Solid Query - Deferred
Solid Query - Deferred Example
Both queries are configured with deferStream=true, so the server will
not start streaming HTML to the client until the queries have
resolved. In this case we are not really taking advantage of streaming
- this mimics traditional renderAsync + Suspense behavior. Note how
the green bar in the devtools is much larger now - the client does not
start receiving any information until 2+ seconds (both queries
resolve).
Clients with javascript disabled will see the resolved state for both
queries. (try turning off javascript and reloading this page)
)
}
================================================
FILE: examples/solid/solid-start-streaming/src/routes/hydration.tsx
================================================
import { useQuery } from '@tanstack/solid-query'
import { Suspense, createSignal } from 'solid-js'
import { NoHydration } from 'solid-js/web'
import { Title } from '@solidjs/meta'
import type { UseQueryResult } from '@tanstack/solid-query'
import { fetchUser } from '~/utils/api'
export default function Hydration() {
const query = useQuery(() => ({
queryKey: ['user'],
queryFn: () => fetchUser({ sleep: 500 }),
deferStream: true,
}))
const [initialQueryState] = createSignal(JSON.parse(JSON.stringify(query)))
return (
Solid Query - Hydration
Solid Query - Hydration Example
Lists the query state as seen on the server, initial render on the
client (right after hydration), and current client value. Ideally, if
SSR is setup correctly, these values are exactly the same in all three
contexts.
query.refetch()}>Refetch
Context
data.name
isFetching
isFetched
isPending
isRefetching
isLoading
isStale
isSuccess
isError
error
fetchStatus
dataUpdatedAt
)
}
type QueryState = UseQueryResult<
{
id: string
name: string
queryTime: number
},
Error
>
const QueryStateRow = (props: { context: string; query: QueryState }) => {
return (
{props.context}
{props.query.data?.name}
{String(props.query.isFetching)}
{String(props.query.isFetched)}
{String(props.query.isPending)}
{String(props.query.isRefetching)}
{String(props.query.isLoading)}
{String(props.query.isStale)}
{String(props.query.isSuccess)}
{String(props.query.isError)}
{String(props.query.error)}
{String(props.query.fetchStatus)}
{String(props.query.dataUpdatedAt)}
)
}
================================================
FILE: examples/solid/solid-start-streaming/src/routes/index.tsx
================================================
import { Title } from '@solidjs/meta'
export default function Home() {
return (
Solid Query v5
Solid Query v5
This demo demonstrates how Solid Query can be used in SSR, with
streaming support. Use the links in the top left to navigate between the
various examples.
)
}
================================================
FILE: examples/solid/solid-start-streaming/src/routes/mixed.tsx
================================================
import { Title } from '@solidjs/meta'
import { PostViewer } from '~/components/post-viewer'
import { UserInfo } from '~/components/user-info'
export default function Mixed() {
return (
Solid Query - Mixed
Solid Query - Mixed Example
You may want to require that key queries resolve before the initial
HTML is streamed to the client, while allowing the rest of your
queries to stream to the client as they resolve. A common use case for
this is to populate SEO meta tags and/or social graph information for
unfurl scenarios such as sharing the page as a link in Slack.
In this example, the quick (100ms) user query has deferStream set to
true, while the more expensive post query (1000ms) is streamed to the
client when ready. Clients with javascript disabled will see the
resolved state for the user query, and the loading state for the post
query. (try turning off javascript and reloading this page)
)
}
================================================
FILE: examples/solid/solid-start-streaming/src/routes/prefetch.tsx
================================================
import { useQueryClient } from '@tanstack/solid-query'
import { isServer } from 'solid-js/web'
import { Title } from '@solidjs/meta'
import { UserInfo, userInfoQueryOpts } from '~/components/user-info'
export const route = {
load: () => {
const queryClient = useQueryClient()
// Prefetching the user info and caching it for 15 seconds.
queryClient.prefetchQuery(userInfoQueryOpts({ sleep: 500, gcTime: 15000 }))
},
}
export default function Prefetch() {
return (
Solid Query - Prefetch
Solid Query - Prefetch Example
SolidStart now supports link prefetching. This means that when a user
hovers over a link, the browser can prefetch the data for that page
before the user even clicks on the link. This can make navigating
around your app feel much faster.
To see this in action, go to the home page and reload the page. Then
hover over the "Prefetch" link in the navigation. You should see the
user data prefetch in the background and in the devtools. When you
click on the link, the page should load instantly.
)
}
================================================
FILE: examples/solid/solid-start-streaming/src/routes/streamed.tsx
================================================
import { Title } from '@solidjs/meta'
import { PostViewer } from '~/components/post-viewer'
import { UserInfo } from '~/components/user-info'
export default function Streamed() {
return (
Solid Query - Streamed
Solid Query - Streamed Example
HTML is streamed from the server ASAP, reducing key metrics such as
TTFB and TTI. Suspended queries are streamed to the client when they
resolve on the server. This is represented in your devtools by the
green and blue chunks of the waterfall.
Clients with javascript disabled will see the loading state for both
queries. (try turning off javascript and reloading this page)
)
}
================================================
FILE: examples/solid/solid-start-streaming/src/routes/with-error.tsx
================================================
import { Title } from '@solidjs/meta'
import { PostViewer } from '~/components/post-viewer'
import { UserInfo } from '~/components/user-info'
export default function Streamed() {
return (
Solid Query - Errors
Solid Query - Errors
For more control over error handling, try leveraging the `Switch`
component and watching the reactive `query.isError` property. See
`compoennts/query-boundary.tsx` for one possible approach.
)
}
================================================
FILE: examples/solid/solid-start-streaming/src/utils/api.ts
================================================
interface PostData {
userId: number
id: number
title: string
body: string
}
const doSleep = (ms: number) => new Promise((r) => setTimeout(r, ms))
export const fetchPost = async ({
postId,
simulateError,
sleep,
}: {
postId: number
simulateError?: boolean
sleep?: number
}) => {
console.info('[api] fetchPost.start', { postId, sleep, simulateError })
let response
if (!simulateError) {
response = await fetch(
`https://jsonplaceholder.typicode.com/posts/${postId}`,
).then((res) => res.json())
}
// simulate extra latency to make things like streaming behavior more clear
if (sleep) {
await doSleep(sleep)
}
console.info('[api] fetchPost.done', { postId, sleep, simulateError })
if (simulateError) {
throw new Error('API request to get post was not OK')
}
return [response] as PostData[]
}
export const fetchUser = async ({
sleep,
simulateError,
}: { sleep?: number; simulateError?: boolean } = {}) => {
console.info('[api] fetchUser.start', { sleep, simulateError })
if (sleep) {
await doSleep(sleep)
}
console.info('[api] fetchUser.done', { sleep, simulateError })
if (simulateError) {
throw new Error('API request to get user was not OK')
}
return {
id: 'abc',
name: `john doe`,
queryTime: Date.now(),
}
}
================================================
FILE: examples/svelte/load-more-infinite-scroll/README.md
================================================
# create-svelte
Everything you need to build a Svelte project, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/master/packages/create-svelte).
## Creating a project
If you're seeing this, you've probably already done this step. Congrats!
```bash
# create a new project in the current directory
npm create svelte@latest
# create a new project in my-app
npm create svelte@latest my-app
```
## Developing
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn` or `bun install`), start a development server:
```bash
npm run dev
# or start the server and open the app in a new browser tab
npm run dev -- --open
```
## Building
To create a production version of your app:
```bash
npm run build
```
You can preview the production build with `npm run preview`.
> To deploy your app, you may need to install an [adapter](https://kit.svelte.dev/docs/adapters) for your target environment.
================================================
FILE: examples/svelte/load-more-infinite-scroll/package.json
================================================
{
"name": "@tanstack/query-example-svelte-load-more-infinite-scroll",
"private": true,
"type": "module",
"scripts": {
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"@tanstack/svelte-query": "^5.89.0",
"@tanstack/svelte-query-devtools": "^5.89.0"
},
"devDependencies": {
"@sveltejs/adapter-auto": "^6.1.0",
"@sveltejs/kit": "^2.42.2",
"@sveltejs/vite-plugin-svelte": "^5.1.1",
"svelte": "^5.39.3",
"svelte-check": "^4.3.1",
"typescript": "5.8.3",
"vite": "^6.3.6"
}
}
================================================
FILE: examples/svelte/load-more-infinite-scroll/svelte.config.js
================================================
import adapter from '@sveltejs/adapter-auto'
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'
/** @type {import('@sveltejs/kit').Config} */
const config = {
// Consult https://kit.svelte.dev/docs/integrations#preprocessors
// for more information about preprocessors
preprocess: vitePreprocess(),
kit: {
adapter: adapter(),
},
}
export default config
================================================
FILE: examples/svelte/load-more-infinite-scroll/tsconfig.json
================================================
{
"extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": {
"allowJs": true,
"checkJs": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": true,
"strict": true
}
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias
//
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
// from the referenced tsconfig.json - TypeScript does not merge them in
}
================================================
FILE: examples/svelte/load-more-infinite-scroll/vite.config.ts
================================================
import { sveltekit } from '@sveltejs/kit/vite'
import { defineConfig } from 'vite'
export default defineConfig({
plugins: [sveltekit()],
})
================================================
FILE: examples/svelte/load-more-infinite-scroll/.gitignore
================================================
.DS_Store
node_modules
/build
/.svelte-kit
/package
.env
.env.*
!.env.example
vite.config.js.timestamp-*
vite.config.ts.timestamp-*
================================================
FILE: examples/svelte/load-more-infinite-scroll/src/app.css
================================================
:root {
font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
font-size: 16px;
line-height: 24px;
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-webkit-text-size-adjust: 100%;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}
body {
margin: 0;
display: flex;
place-items: center;
min-width: 320px;
min-height: 100vh;
}
h1 {
font-size: 3.2em;
line-height: 1.1;
}
.card {
padding: 2em;
}
main {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
text-align: center;
}
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
background-color: #1a1a1a;
cursor: pointer;
transition: border-color 0.25s;
}
button:hover {
border-color: #646cff;
}
button:focus,
button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
}
@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
}
}
================================================
FILE: examples/svelte/load-more-infinite-scroll/src/app.d.ts
================================================
// See https://kit.svelte.dev/docs/types#app
// for information about these interfaces
declare global {
namespace App {
// interface Error {}
// interface Locals {}
// interface PageData {}
// interface Platform {}
}
}
export {}
================================================
FILE: examples/svelte/load-more-infinite-scroll/src/app.html
================================================
%sveltekit.head%
%sveltekit.body%
================================================
FILE: examples/svelte/load-more-infinite-scroll/src/lib/LoadMore.svelte
================================================
{#if $query.isPending}
Loading...
{/if}
{#if $query.error}
Error: {$query.error.message}
{/if}
{#if $query.isSuccess}
{#each $query.data.pages as { results }}
{#each results as planet}
Planet Name: {planet.name}
Population: {planet.population}
{/each}
{/each}
$query.fetchNextPage()}
disabled={!$query.hasNextPage || $query.isFetchingNextPage}
>
{#if $query.isFetching}
Loading more...
{:else if $query.hasNextPage}
Load More
{:else}Nothing more to load{/if}
{/if}
================================================
FILE: examples/svelte/load-more-infinite-scroll/src/routes/+layout.svelte
================================================
================================================
FILE: examples/svelte/load-more-infinite-scroll/src/routes/+page.svelte
================================================
Infinite Load More
================================================
FILE: examples/vue/2.6-basic/README.md
================================================
# 2.6 Basic example
To run this example:
- `npm install` or `yarn` or `pnpm i` or `bun i`
- `npm run dev` or `yarn dev` or `pnpm dev` or `bun dev`
================================================
FILE: examples/vue/2.6-basic/index.html
================================================
Vue Query Example
================================================
FILE: examples/vue/2.6-basic/package.json
================================================
{
"name": "@tanstack/query-example-vue-2.6-basic",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"_build": "vite build",
"_preview": "vite preview"
},
"dependencies": {
"@tanstack/vue-query": "^5.89.0",
"@vue/composition-api": "1.7.2",
"vue": "2.6.14",
"vue-template-compiler": "2.6.14"
},
"devDependencies": {
"typescript": "5.8.3",
"vite": "^4.5.3",
"vite-plugin-vue2": "2.0.3"
}
}
================================================
FILE: examples/vue/2.6-basic/tsconfig.json
================================================
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "Bundler",
"strict": true,
"jsx": "preserve",
"sourceMap": true,
"resolveJsonModule": true,
"esModuleInterop": true,
"lib": ["esnext", "dom"],
"types": ["vite/client"]
},
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.vue"]
}
================================================
FILE: examples/vue/2.6-basic/vite.config.ts
================================================
import { defineConfig } from 'vite'
import { createVuePlugin } from 'vite-plugin-vue2'
export default defineConfig({
plugins: [createVuePlugin()],
})
================================================
FILE: examples/vue/2.6-basic/.gitignore
================================================
node_modules
.DS_Store
dist
dist-ssr
*.local
package-lock.json
yarn.lock
pnpm-lock.yaml
================================================
FILE: examples/vue/2.6-basic/src/App.vue
================================================
Vue Query - Basic
As you visit the posts below, you will notice them in a loading state the
first time you load them. However, after you return to this list and click
on any posts you have already visited again, you will see them load
instantly and background refresh right before your eyes!
(You may need to throttle your network speed to simulate longer loading
sequences)
================================================
FILE: examples/vue/2.6-basic/src/main.ts
================================================
import Vue from 'vue'
import VueCompositionApi, { createApp, h } from '@vue/composition-api'
import { VueQueryPlugin } from '@tanstack/vue-query'
import App from './App.vue'
Vue.use(VueCompositionApi)
Vue.use(VueQueryPlugin)
createApp({
render() {
return h(App)
},
}).mount('#app')
================================================
FILE: examples/vue/2.6-basic/src/Post.vue
================================================
Post {{ postId }}
Back
Loading...
An error has occurred: {{ error }}
{{ data.title }}
Background Updating...
================================================
FILE: examples/vue/2.6-basic/src/Posts.vue
================================================
Posts
Loading...
An error has occurred: {{ error }}
================================================
FILE: examples/vue/2.6-basic/src/shims-vue.d.ts
================================================
declare module '*.vue' {
import Vue from 'vue'
export default Vue
}
================================================
FILE: examples/vue/2.6-basic/src/types.d.ts
================================================
export interface Post {
userId: number
id: number
title: string
body: string
}
================================================
FILE: examples/vue/2.7-basic/README.md
================================================
# 2.6 Basic example
To run this example:
- `npm install` or `yarn` or `pnpm i`
- `npm run dev` or `yarn dev` or `pnpm dev`
================================================
FILE: examples/vue/2.7-basic/index.html
================================================
Vue Query Example
================================================
FILE: examples/vue/2.7-basic/package.json
================================================
{
"name": "@tanstack/query-example-vue-2.7-basic",
"private": true,
"scripts": {
"dev": "vite",
"_build": "vite build",
"_build:dev": "vite build -m development",
"_serve": "vite preview"
},
"dependencies": {
"@tanstack/vue-query": "^5.89.0",
"vue": "2.7.16",
"vue-template-compiler": "2.7.16"
},
"devDependencies": {
"typescript": "5.8.3",
"vite": "^4.5.3",
"vite-plugin-vue2": "2.0.3"
}
}
================================================
FILE: examples/vue/2.7-basic/tsconfig.json
================================================
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "Bundler",
"strict": true,
"jsx": "preserve",
"sourceMap": true,
"resolveJsonModule": true,
"esModuleInterop": true,
"lib": ["esnext", "dom"],
"types": ["vite/client"]
},
"include": ["src/*.ts", "src/*.d.ts", "src/*.vue"]
}
================================================
FILE: examples/vue/2.7-basic/vite.config.ts
================================================
import { defineConfig } from 'vite'
import { createVuePlugin } from 'vite-plugin-vue2'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [createVuePlugin()],
})
================================================
FILE: examples/vue/2.7-basic/.gitignore
================================================
node_modules
.DS_Store
dist
dist-ssr
*.local
package-lock.json
yarn.lock
pnpm-lock.yaml
================================================
FILE: examples/vue/2.7-basic/src/App.vue
================================================
Vue Query - Basic
As you visit the posts below, you will notice them in a loading state the
first time you load them. However, after you return to this list and click
on any posts you have already visited again, you will see them load
instantly and background refresh right before your eyes!
(You may need to throttle your network speed to simulate longer loading
sequences)
================================================
FILE: examples/vue/2.7-basic/src/main.ts
================================================
import Vue, { h } from 'vue'
import { VueQueryPlugin } from '@tanstack/vue-query'
import App from './App.vue'
Vue.use(VueQueryPlugin)
var app = new Vue({
el: '#app',
render: () => {
return h(App)
},
})
================================================
FILE: examples/vue/2.7-basic/src/Post.vue
================================================
Post {{ postId }}
Back
Loading...
An error has occurred: {{ error }}
{{ data.title }}
Background Updating...
================================================
FILE: examples/vue/2.7-basic/src/Posts.vue
================================================
Posts
Loading...
An error has occurred: {{ error }}
================================================
FILE: examples/vue/2.7-basic/src/shims-vue.d.ts
================================================
declare module '*.vue' {
import Vue from 'vue'
export default Vue
}
================================================
FILE: examples/vue/2.7-basic/src/types.d.ts
================================================
export interface Post {
userId: number
id: number
title: string
body: string
}
================================================
FILE: examples/vue/basic/README.md
================================================
# Basic example
To run this example:
- `npm install` or `yarn` or `pnpm i` or `bun i`
- `npm run dev` or `yarn dev` or `pnpm dev` or `bun dev`
================================================
FILE: examples/vue/basic/index.html
================================================
Vue Query Example
================================================
FILE: examples/vue/basic/package.json
================================================
{
"name": "@tanstack/query-example-vue-basic",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"@tanstack/vue-query": "^5.89.0",
"@tanstack/vue-query-devtools": "^5.89.0",
"vue": "^3.4.27"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.2.1",
"typescript": "5.8.3",
"vite": "^6.3.6"
}
}
================================================
FILE: examples/vue/basic/tsconfig.json
================================================
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "Bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "preserve",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.vue"]
}
================================================
FILE: examples/vue/basic/vite.config.ts
================================================
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
optimizeDeps: {
exclude: ['@tanstack/vue-query', 'vue-demi'],
},
})
================================================
FILE: examples/vue/basic/.gitignore
================================================
node_modules
.DS_Store
dist
dist-ssr
*.local
package-lock.json
yarn.lock
pnpm-lock.yaml
================================================
FILE: examples/vue/basic/src/App.vue
================================================
Vue Query - Basic
As you visit the posts below, you will notice them in a loading state the
first time you load them. However, after you return to this list and click
on any posts you have already visited again, you will see them load
instantly and background refresh right before your eyes!
(You may need to throttle your network speed to simulate longer loading
sequences)
================================================
FILE: examples/vue/basic/src/main.ts
================================================
import { createApp } from 'vue'
import { VueQueryPlugin } from '@tanstack/vue-query'
import App from './App.vue'
createApp(App).use(VueQueryPlugin).mount('#app')
================================================
FILE: examples/vue/basic/src/Post.vue
================================================
Post {{ postId }}
Back
Loading...
An error has occurred: {{ error }}
{{ data.title }}
Background Updating...
================================================
FILE: examples/vue/basic/src/Posts.vue
================================================
Posts
Loading...
An error has occurred: {{ error }}
================================================
FILE: examples/vue/basic/src/shims-vue.d.ts
================================================
declare module '*.vue' {
import { DefineComponent } from 'vue'
const component: DefineComponent<{}, {}, any>
export default component
}
================================================
FILE: examples/vue/basic/src/types.d.ts
================================================
export interface Post {
userId: number
id: number
title: string
body: string
}
================================================
FILE: examples/vue/dependent-queries/README.md
================================================
# Dependent queries example
To run this example:
- `npm install` or `yarn`
- `npm run dev` or `yarn dev`
================================================
FILE: examples/vue/dependent-queries/index.html
================================================
Vue Query Example
================================================
FILE: examples/vue/dependent-queries/package.json
================================================
{
"name": "@tanstack/query-example-vue-dependent-queries",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"@tanstack/vue-query": "^5.89.0",
"vue": "^3.4.27"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.2.1",
"typescript": "5.8.3",
"vite": "^6.3.6"
}
}
================================================
FILE: examples/vue/dependent-queries/tsconfig.json
================================================
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "Bundler",
"strict": true,
"jsx": "preserve",
"sourceMap": true,
"resolveJsonModule": true,
"esModuleInterop": true,
"lib": ["esnext", "dom"],
"types": ["vite/client"]
},
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"]
}
================================================
FILE: examples/vue/dependent-queries/vite.config.ts
================================================
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
optimizeDeps: {
exclude: ['@tanstack/vue-query', 'vue-demi'],
},
})
================================================
FILE: examples/vue/dependent-queries/.gitignore
================================================
node_modules
.DS_Store
dist
dist-ssr
*.local
package-lock.json
================================================
FILE: examples/vue/dependent-queries/src/App.vue
================================================
Vue Query - Dependent Queries
As you visit the posts below, you will notice them in a loading state the
first time you load them. However, after you return to this list and click
on any posts you have already visited again, you will see them load
instantly and background refresh right before your eyes!
You will also notice, that after some time the name of the author gets
loaded. This Data is not directly included in the
/post/id
response but gets fetched when the post gets loaded
from its userId
.
(You may need to throttle your network speed to simulate longer loading
sequences)
================================================
FILE: examples/vue/dependent-queries/src/main.ts
================================================
import { createApp } from 'vue'
import { VueQueryPlugin } from '@tanstack/vue-query'
import App from './App.vue'
createApp(App).use(VueQueryPlugin).mount('#app')
================================================
FILE: examples/vue/dependent-queries/src/Post.vue
================================================
Post {{ postId }}
Back
Loading...
An error has occurred: {{ error }}
{{ post.title }} by {{ author.name }}
Background Updating...
================================================
FILE: examples/vue/dependent-queries/src/Posts.vue
================================================
Posts
Loading...
An error has occurred: {{ error }}
================================================
FILE: examples/vue/dependent-queries/src/shims-vue.d.ts
================================================
declare module '*.vue' {
import { DefineComponent } from 'vue'
const component: DefineComponent<{}, {}, any>
export default component
}
================================================
FILE: examples/vue/dependent-queries/src/types.d.ts
================================================
export interface Post {
userId: number
id: number
title: string
body: string
}
export interface Author {
id: number
name: string
username: string
email: string
address: {
street: string
suite: string
city: string
zipcode: string
geo: {
lat: string
lng: string
}
}
phone: string
website: string
company: {
name: string
catchPhrase: string
bs: string
}
}
================================================
FILE: examples/vue/nuxt3/README.md
================================================
# Nuxt 3 Minimal Starter
We recommend to look at the [documentation](https://v3.nuxtjs.org).
## Setup
Make sure to install the dependencies
```bash
yarn install
```
## Development
Start the development server on http://localhost:3000
```bash
yarn dev
```
## Production
Build the application for production:
```bash
yarn build
```
Checkout the [deployment documentation](https://v3.nuxtjs.org/docs/deployment).
================================================
FILE: examples/vue/nuxt3/app.vue
================================================
{{ data }}
================================================
FILE: examples/vue/nuxt3/nuxt.config.ts
================================================
import { defineNuxtConfig } from 'nuxt/config'
// https://v3.nuxtjs.org/docs/directory-structure/nuxt.config
export default defineNuxtConfig({})
================================================
FILE: examples/vue/nuxt3/package.json
================================================
{
"name": "@tanstack/query-example-vue-nuxt3",
"private": true,
"scripts": {
"dev": "nuxi dev",
"_build": "nuxi build",
"_start": "node .output/server/index.mjs"
},
"dependencies": {
"@tanstack/vue-query": "^5.89.0"
},
"devDependencies": {
"nuxt": "^3.12.4"
}
}
================================================
FILE: examples/vue/nuxt3/tsconfig.json
================================================
{
// https://v3.nuxtjs.org/concepts/typescript
"extends": "./.nuxt/tsconfig.json"
}
================================================
FILE: examples/vue/nuxt3/.gitignore
================================================
node_modules
*.log
.nuxt
nuxt.d.ts
.output
.env
package-lock.json
yarn.lock
pnpm-lock.yaml
================================================
FILE: examples/vue/nuxt3/plugins/vue-query.ts
================================================
import type {
DehydratedState,
VueQueryPluginOptions,
} from '@tanstack/vue-query'
import {
VueQueryPlugin,
QueryClient,
hydrate,
dehydrate,
} from '@tanstack/vue-query'
// Nuxt 3 app aliases
import { defineNuxtPlugin, useState } from '#imports'
export default defineNuxtPlugin((nuxt) => {
const vueQueryState = useState('vue-query')
// Modify your Vue Query global settings here
const queryClient = new QueryClient({
defaultOptions: { queries: { staleTime: 5000 } },
})
const options: VueQueryPluginOptions = { queryClient }
nuxt.vueApp.use(VueQueryPlugin, options)
if (import.meta.server) {
nuxt.hooks.hook('app:rendered', () => {
vueQueryState.value = dehydrate(queryClient)
})
}
if (import.meta.client) {
nuxt.hooks.hook('app:created', () => {
hydrate(queryClient, vueQueryState.value)
})
}
})
================================================
FILE: examples/vue/persister/README.md
================================================
# Basic example
To run this example:
- `npm install` or `yarn`
- `npm run dev` or `yarn dev`
================================================
FILE: examples/vue/persister/index.html
================================================
Vue Query Example
================================================
FILE: examples/vue/persister/package.json
================================================
{
"name": "@tanstack/query-example-vue-persister",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"@tanstack/query-core": "^5.89.0",
"@tanstack/query-persist-client-core": "^5.89.0",
"@tanstack/query-sync-storage-persister": "^5.89.0",
"@tanstack/vue-query": "^5.89.0",
"idb-keyval": "^6.2.1",
"vue": "^3.4.27"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.2.1",
"typescript": "5.8.3",
"vite": "^6.3.6"
}
}
================================================
FILE: examples/vue/persister/tsconfig.json
================================================
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "Bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "preserve",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.vue"]
}
================================================
FILE: examples/vue/persister/vite.config.ts
================================================
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
optimizeDeps: {
exclude: ['@tanstack/vue-query', 'vue-demi'],
},
})
================================================
FILE: examples/vue/persister/.gitignore
================================================
node_modules
.DS_Store
dist
dist-ssr
*.local
package-lock.json
================================================
FILE: examples/vue/persister/src/App.vue
================================================
Vue Query - Basic
As you visit the posts below, you will notice them in a loading state the
first time you load them. However, after you return to this list and click
on any posts you have already visited again, you will see them load
instantly and background refresh right before your eyes!
(You may need to throttle your network speed to simulate longer loading
sequences)
================================================
FILE: examples/vue/persister/src/main.ts
================================================
import { createApp } from 'vue'
import { VueQueryPlugin } from '@tanstack/vue-query'
import type { VueQueryPluginOptions } from '@tanstack/vue-query'
import App from './App.vue'
const vueQueryOptions: VueQueryPluginOptions = {
queryClientConfig: {
defaultOptions: {
queries: {
gcTime: 1000 * 60 * 60 * 24,
// staleTime: 1000 * 10,
},
},
},
}
createApp(App).use(VueQueryPlugin, vueQueryOptions).mount('#app')
================================================
FILE: examples/vue/persister/src/Post.vue
================================================
Post {{ postId }}
Back
Loading...
An error has occurred: {{ error }}
{{ data.title }}
Background Updating...
================================================
FILE: examples/vue/persister/src/Posts.vue
================================================
Posts
Refetching...
Loading...
An error has occurred: {{ error }}
================================================
FILE: examples/vue/persister/src/shims-vue.d.ts
================================================
declare module '*.vue' {
import { DefineComponent } from 'vue'
const component: DefineComponent<{}, {}, any>
export default component
}
================================================
FILE: examples/vue/persister/src/types.d.ts
================================================
export interface Post {
userId: number
id: number
title: string
body: string
}
================================================
FILE: examples/vue/simple/README.md
================================================
# Basic example
To run this example:
- `npm install` or `yarn` or `pnpm i` or `bun i`
- `npm run dev` or `yarn dev` or `pnpm dev` or `bun dev`
================================================
FILE: examples/vue/simple/index.html
================================================
TanStack Query Vue Simple Example App
================================================
FILE: examples/vue/simple/package.json
================================================
{
"name": "@tanstack/query-example-vue-simple",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"@tanstack/vue-query": "^5.89.0",
"@tanstack/vue-query-devtools": "^5.89.0",
"vue": "^3.4.27"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.2.1",
"typescript": "5.8.3",
"vite": "^6.3.6"
}
}
================================================
FILE: examples/vue/simple/tsconfig.json
================================================
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "Bundler",
"strict": true,
"jsx": "preserve",
"sourceMap": true,
"resolveJsonModule": true,
"esModuleInterop": true,
"lib": ["esnext", "dom"],
"types": ["vite/client"]
},
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.vue"]
}
================================================
FILE: examples/vue/simple/vite.config.ts
================================================
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
optimizeDeps: {
exclude: ['@tanstack/vue-query', 'vue-demi'],
},
})
================================================
FILE: examples/vue/simple/.gitignore
================================================
node_modules
.DS_Store
dist
dist-ssr
*.local
package-lock.json
yarn.lock
pnpm-lock.yaml
================================================
FILE: examples/vue/simple/src/App.vue
================================================
Loading...
'An error has occurred: {{ error.message }}
{{ data.name }}
{{ data.description }}
👀 {{ data.subscribers_count }}
✨ {{ data.stargazers_count }}
🍴 {{ data.forks_count }}
{{ isFetching ? 'Updating...' : '' }}
================================================
FILE: examples/vue/simple/src/main.ts
================================================
import { createApp } from 'vue'
import { VueQueryPlugin } from '@tanstack/vue-query'
import App from './App.vue'
createApp(App).use(VueQueryPlugin).mount('#app')
================================================
FILE: examples/vue/simple/src/shims-vue.d.ts
================================================
declare module '*.vue' {
import { DefineComponent } from 'vue'
const component: DefineComponent<{}, {}, any>
export default component
}
================================================
FILE: examples/vue/simple/src/types.d.ts
================================================
export interface Post {
userId: number
id: number
title: string
body: string
}
================================================
FILE: media/logo.sketch
================================================
[Non-text file]
================================================
FILE: packages/angular-query-experimental/README.md
================================================

[](https://www.npmjs.com/package/@tanstack/angular-query-experimental)
[](https://github.com/TanStack/query/blob/main/LICENSE)
[](https://bundlephobia.com/package/@tanstack/angular-query-experimental)
[](https://www.npmjs.com/package/@tanstack/angular-query-experimental)
# Angular Query
> IMPORTANT: This library is currently in an experimental stage. This means that breaking changes may happen in minor AND patch releases. Upgrade carefully. If you use this in production while in experimental stage, please lock your version to a patch-level version to avoid unexpected breaking changes.
Functions for fetching, caching and updating asynchronous data in Angular
# Documentation
Visit https://tanstack.com/query/latest/docs/framework/angular/overview
## Quick Features
- Transport/protocol/backend agnostic data fetching (REST, GraphQL, promises, whatever!)
- Auto Caching + Refetching (stale-while-revalidate, Window Refocus, Polling/Realtime)
- Parallel + Dependent Queries
- Mutations + Reactive Query Refetching
- Multi-layer Cache + Automatic Garbage Collection
- Paginated + Cursor-based Queries
- Load-More + Infinite Scroll Queries w/ Scroll Recovery
- Request Cancellation
- Dedicated Devtools
# Quick Start
> The Angular adapter for TanStack Query requires Angular 16 or higher.
1. Install `angular-query`
```bash
$ npm i @tanstack/angular-query-experimental
```
or
```bash
$ pnpm add @tanstack/angular-query-experimental
```
or
```bash
$ yarn add @tanstack/angular-query-experimental
```
or
```bash
$ bun add @tanstack/angular-query-experimental
```
2. Initialize **TanStack Query** by adding **provideTanStackQuery** to your application
```ts
import { provideTanStackQuery } from '@tanstack/angular-query-experimental'
import { QueryClient } from '@tanstack/angular-query-experimental'
bootstrapApplication(AppComponent, {
providers: [provideTanStackQuery(new QueryClient())],
})
```
or in a NgModule-based app
```ts
import { provideHttpClient } from '@angular/common/http'
import {
provideTanStackQuery,
QueryClient,
} from '@tanstack/angular-query-experimental'
@NgModule({
declarations: [AppComponent],
imports: [BrowserModule],
providers: [provideTanStackQuery(new QueryClient())],
bootstrap: [AppComponent],
})
```
3. Inject query
```ts
import { injectQuery } from '@tanstack/angular-query-experimental'
import { Component } from '@angular/core'
@Component({...})
export class TodosComponent {
info = injectQuery(() => ({ queryKey: ['todos'], queryFn: fetchTodoList }))
}
```
4. If you need to update options on your query dynamically, make sure to pass them as signals. The query will refetch automatically if data for an updated query key is stale or not present.
[Open in StackBlitz](https://stackblitz.com/github/TanStack/query/tree/main/examples/angular/router)
```ts
@Component({})
export class PostComponent {
#postsService = inject(PostsService)
postId = input.required({
transform: numberAttribute,
})
postQuery = injectQuery(() => ({
queryKey: ['post', this.postId()],
queryFn: () => {
return lastValueFrom(this.#postsService.postById$(this.postId()))
},
}))
}
@Injectable({
providedIn: 'root',
})
export class PostsService {
#http = inject(HttpClient)
postById$ = (postId: number) =>
this.#http.get(`https://jsonplaceholder.typicode.com/posts/${postId}`)
}
export interface Post {
id: number
title: string
body: string
}
```
================================================
FILE: packages/angular-query-experimental/eslint.config.js
================================================
// @ts-check
import pluginJsdoc from 'eslint-plugin-jsdoc'
import vitest from '@vitest/eslint-plugin'
import rootConfig from './root.eslint.config.js'
export default [
...rootConfig,
pluginJsdoc.configs['flat/recommended-typescript'],
{
rules: {
'jsdoc/require-hyphen-before-param-description': 1,
'jsdoc/sort-tags': 1,
'jsdoc/require-throws': 1,
'jsdoc/check-tag-names': ['warn'],
},
},
{
plugins: { vitest },
rules: {
'vitest/expect-expect': [
'error',
{ assertFunctionNames: ['expect', 'expectSignals'] },
],
},
},
{
files: ['**/__tests__/**'],
rules: {
'@typescript-eslint/no-unnecessary-condition': 'off',
'@typescript-eslint/require-await': 'off',
'jsdoc/require-returns': 'off',
},
},
]
================================================
FILE: packages/angular-query-experimental/package.json
================================================
{
"name": "@tanstack/angular-query-experimental",
"version": "5.90.0",
"description": "Signals for managing, caching and syncing asynchronous and remote data in Angular",
"author": "Arnoud de Vries",
"license": "MIT",
"repository": {
"type": "git",
"url": "git+https://github.com/TanStack/query.git",
"directory": "packages/angular-query-experimental"
},
"homepage": "https://tanstack.com/query",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
},
"keywords": [
"angular query",
"angular",
"cache",
"performance",
"reactive",
"rxjs",
"signals",
"state management",
"state",
"tanstack"
],
"scripts": {
"clean": "premove ./dist ./coverage ./dist-ts",
"compile": "tsc --build",
"test:eslint": "eslint --concurrency=auto ./src",
"test:types": "npm-run-all --serial test:types:*",
"test:types:ts50": "node ../../node_modules/typescript50/lib/tsc.js --build",
"test:types:ts51": "node ../../node_modules/typescript51/lib/tsc.js --build",
"test:types:ts52": "node ../../node_modules/typescript52/lib/tsc.js --build",
"test:types:ts53": "node ../../node_modules/typescript53/lib/tsc.js --build",
"test:types:ts54": "node ../../node_modules/typescript54/lib/tsc.js --build",
"test:types:ts55": "node ../../node_modules/typescript55/lib/tsc.js --build",
"test:types:ts56": "node ../../node_modules/typescript56/lib/tsc.js --build",
"test:types:ts57": "node ../../node_modules/typescript57/lib/tsc.js --build",
"test:types:tscurrent": "tsc --build",
"test:lib": "vitest",
"test:lib:dev": "pnpm run test:lib --watch",
"test:build": "pnpm pack && publint ./dist/*.tgz --strict && attw ./dist/*.tgz; premove ./dist/*.tgz",
"build": "vite build",
"prepack": "node scripts/prepack.js"
},
"type": "module",
"types": "dist/index.d.ts",
"module": "dist/index.mjs",
"exports": {
".": {
"@tanstack/custom-condition": "./src/index.ts",
"types": "./dist/index.d.ts",
"default": "./dist/index.mjs"
},
"./package.json": "./package.json",
"./inject-queries-experimental": {
"types": "./dist/inject-queries-experimental/index.d.ts",
"default": "./dist/inject-queries-experimental/index.mjs"
},
"./devtools": {
"types": "./dist/devtools/index.d.ts",
"development": "./dist/devtools/index.mjs",
"default": "./dist/devtools/stub.mjs"
},
"./devtools/production": {
"types": "./dist/devtools/production/index.d.ts",
"default": "./dist/devtools/index.mjs"
},
"./devtools-panel": {
"types": "./dist/devtools-panel/index.d.ts",
"development": "./dist/devtools-panel/index.mjs",
"default": "./dist/devtools-panel/stub.mjs"
},
"./devtools-panel/production": {
"types": "./dist/devtools-panel/production/index.d.ts",
"default": "./dist/devtools-panel/index.mjs"
}
},
"sideEffects": false,
"files": [
"**/*.d.ts",
"**/*.mjs",
"**/*.mjs.map"
],
"dependencies": {
"@tanstack/query-core": "workspace:*"
},
"devDependencies": {
"@angular/common": "^20.0.0",
"@angular/compiler": "^20.0.0",
"@angular/core": "^20.0.0",
"@angular/platform-browser": "^20.0.0",
"@tanstack/query-test-utils": "workspace:*",
"@testing-library/angular": "^18.0.0",
"eslint-plugin-jsdoc": "^50.5.0",
"npm-run-all2": "^5.0.0",
"rxjs": "^7.8.2",
"vite-plugin-dts": "4.2.3",
"vite-plugin-externalize-deps": "^0.9.0",
"vite-tsconfig-paths": "^5.1.4"
},
"optionalDependencies": {
"@tanstack/query-devtools": "workspace:*"
},
"peerDependencies": {
"@angular/common": ">=16.0.0",
"@angular/core": ">=16.0.0"
},
"publishConfig": {
"directory": "dist",
"linkDirectory": false
}
}
================================================
FILE: packages/angular-query-experimental/test-setup.ts
================================================
import { getTestBed } from '@angular/core/testing'
import {
BrowserTestingModule,
platformBrowserTesting,
} from '@angular/platform-browser/testing'
getTestBed().initTestEnvironment(BrowserTestingModule, platformBrowserTesting())
================================================
FILE: packages/angular-query-experimental/tsconfig.json
================================================
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "./dist-ts",
"rootDir": ".",
"noFallthroughCasesInSwitch": true,
"useDefineForClassFields": false,
"target": "ES2022"
},
"include": ["src", "scripts", "test-setup.ts", "*.config.*", "package.json"],
"references": [{ "path": "../query-core" }, { "path": "../query-devtools" }]
}
================================================
FILE: packages/angular-query-experimental/tsconfig.prod.json
================================================
{
"extends": "./tsconfig.json",
"compilerOptions": {
"incremental": false,
"composite": false,
"rootDir": "../../",
"customConditions": null
}
}
================================================
FILE: packages/angular-query-experimental/tsup.config.ts
================================================
import { defineConfig } from 'tsup'
export default defineConfig({
entry: ['src/index.ts'],
sourcemap: true,
clean: true,
format: ['esm'],
dts: true,
outDir: 'build',
outExtension({ format }) {
return format === 'esm' ? { js: '.mjs' } : { js: '.js' }
},
})
================================================
FILE: packages/angular-query-experimental/vite.config.ts
================================================
import { defineConfig, mergeConfig } from 'vitest/config'
import { externalizeDeps } from 'vite-plugin-externalize-deps'
import tsconfigPaths from 'vite-tsconfig-paths'
import dts from 'vite-plugin-dts'
import packageJson from './package.json'
import type { Options } from '@tanstack/config/vite'
function ensureImportFileExtension({
content,
extension,
}: {
content: string
extension: string
}) {
// replace e.g. `import { foo } from './foo'` with `import { foo } from './foo.js'`
content = content.replace(
/(im|ex)port\s[\w{}/*\s,]+from\s['"](?:\.\.?\/)+?[^.'"]+(?=['"];?)/gm,
`$&.${extension}`,
)
// replace e.g. `import('./foo')` with `import('./foo.js')`
content = content.replace(
/import\(['"](?:\.\.?\/)+?[^.'"]+(?=['"];?)/gm,
`$&.${extension}`,
)
return content
}
const config = defineConfig({
// fix from https://github.com/vitest-dev/vitest/issues/6992#issuecomment-2509408660
resolve: {
conditions: ['@tanstack/custom-condition'],
},
environments: {
ssr: {
resolve: {
conditions: ['@tanstack/custom-condition'],
},
},
},
test: {
name: packageJson.name,
dir: './src',
watch: false,
environment: 'jsdom',
setupFiles: ['test-setup.ts'],
coverage: { enabled: true, provider: 'istanbul', include: ['src/**/*'] },
typecheck: { enabled: true },
globals: true,
restoreMocks: true,
},
})
// copy from @tanstack/config/vite with changes:
// - build - lib - fileName: [name.mjs]
// - rollup - output - preserveModulesRoot: src
export const tanstackViteConfig = (options: Options) => {
const outDir = options.outDir ?? 'dist'
const cjs = options.cjs ?? true
return defineConfig({
plugins: [
externalizeDeps({ include: options.externalDeps ?? [] }),
tsconfigPaths({
projects: options.tsconfigPath ? [options.tsconfigPath] : undefined,
}),
dts({
outDir,
entryRoot: options.srcDir,
include: options.srcDir,
exclude: options.exclude,
tsconfigPath: options.tsconfigPath,
compilerOptions: {
module: 99, // ESNext
declarationMap: false,
},
beforeWriteFile: (filePath, content) => {
return {
filePath,
content: ensureImportFileExtension({ content, extension: 'js' }),
}
},
afterDiagnostic: (diagnostics) => {
if (diagnostics.length > 0) {
console.error('Please fix the above type errors')
process.exit(1)
}
},
}),
],
build: {
outDir,
minify: false,
sourcemap: true,
lib: {
entry: options.entry,
formats: cjs ? ['es', 'cjs'] : ['es'],
fileName: () => '[name].mjs',
},
rollupOptions: {
output: {
preserveModules: true,
preserveModulesRoot: 'src',
},
},
},
})
}
export default mergeConfig(
config,
tanstackViteConfig({
cjs: false,
entry: [
'./src/index.ts',
'./src/inject-queries-experimental/index.ts',
'./src/devtools-panel/index.ts',
'./src/devtools-panel/stub.ts',
'./src/devtools/index.ts',
'./src/devtools/stub.ts',
],
exclude: ['src/__tests__'],
srcDir: './src',
tsconfigPath: 'tsconfig.prod.json',
}),
)
================================================
FILE: packages/angular-query-experimental/.attw.json
================================================
{
"ignoreRules": ["cjs-resolves-to-esm"]
}
================================================
SYMLINK: packages/angular-query-experimental/root.eslint.config.js -> eslint.config.js
================================================
================================================
FILE: packages/angular-query-experimental/scripts/prepack.js
================================================
import fs from 'node:fs'
import path from 'node:path'
/**
* Prepack script that prepares the package for publishing by:
* 1. Creating a modified package.json without dev dependencies, publishConfig and build scripts
* 2. Updating file paths to remove 'dist/' prefixes (since files will be at root in published package)
* 3. Writing this modified package.json to the `dist` directory
* 4. Copying additional files like README.md to the dist directory
*
* Type declarations need to be in the package root or corresponding sub-path to support
* sub-path exports in applications still using `moduleResolution: node`.
*/
console.log('Running prepack script')
/**
* Files to copy to the dist directory
* @type {string[]}
*/
const FILES_TO_COPY = ['README.md']
/**
* Fields to remove from the package.json copy
* @type {string[]}
*/
const FIELDS_TO_REMOVE = [
'devDependencies',
'files',
'publishConfig',
'scripts',
]
/**
* Replaces 'dist/' or './dist/' prefix from a file path with './'
* Only matches at the start of the path to avoid false matches
* @param {string} filePath - The file path to process
* @returns {string} The path without dist prefix
*/
function replaceDist(filePath) {
// Only match dist/ at the beginning of the path, followed by a filename
// This prevents matching strings like "distributed/file.js" or "some/dist/path"
return filePath.replace(/^(?:\.\/)?dist\/(?=.+)/, './')
}
/**
* Recursively processes package.json `exports` to remove dist prefixes
* @param {Record} exports - The exports object to process
* @returns {Record} The processed exports object
*/
function processExports(exports) {
return Object.fromEntries(
Object.entries(exports).map(([key, value]) => [
key,
typeof value === 'string'
? replaceDist(value)
: typeof value === 'object' && value !== null
? processExports(value)
: value,
]),
)
}
console.log('Copying modified package.json')
/** @type {Record} */
const packageJson = JSON.parse(fs.readFileSync('package.json', 'utf8'))
const modifiedPackageJson = { ...packageJson }
if (modifiedPackageJson.types) {
modifiedPackageJson.types = replaceDist(modifiedPackageJson.types)
}
if (modifiedPackageJson.module) {
modifiedPackageJson.module = replaceDist(modifiedPackageJson.module)
}
if (modifiedPackageJson.exports) {
modifiedPackageJson.exports = processExports(modifiedPackageJson.exports)
}
for (const field of FIELDS_TO_REMOVE) {
delete modifiedPackageJson[field]
}
if (!fs.existsSync('dist')) {
fs.mkdirSync('dist', { recursive: true })
}
fs.writeFileSync(
path.join('dist', 'package.json'),
JSON.stringify(modifiedPackageJson, null, 2),
)
console.log('Copying other files')
for (const file of FILES_TO_COPY) {
if (fs.existsSync(file)) {
fs.copyFileSync(file, path.join('dist', file))
console.log(`${file}`)
} else {
console.log(`${file} not found, skipping`)
}
}
console.log('prepack complete')
================================================
FILE: packages/angular-query-experimental/src/create-base-query.ts
================================================
import {
NgZone,
VERSION,
computed,
effect,
inject,
signal,
untracked,
} from '@angular/core'
import {
QueryClient,
notifyManager,
shouldThrowError,
} from '@tanstack/query-core'
import { signalProxy } from './signal-proxy'
import { injectIsRestoring } from './inject-is-restoring'
import { PENDING_TASKS } from './pending-tasks-compat'
import type { PendingTaskRef } from './pending-tasks-compat'
import type {
QueryKey,
QueryObserver,
QueryObserverResult,
} from '@tanstack/query-core'
import type { CreateBaseQueryOptions } from './types'
/**
* Base implementation for `injectQuery` and `injectInfiniteQuery`.
* @param optionsFn
* @param Observer
*/
export function createBaseQuery<
TQueryFnData,
TError,
TData,
TQueryData,
TQueryKey extends QueryKey,
>(
optionsFn: () => CreateBaseQueryOptions<
TQueryFnData,
TError,
TData,
TQueryData,
TQueryKey
>,
Observer: typeof QueryObserver,
) {
const ngZone = inject(NgZone)
const pendingTasks = inject(PENDING_TASKS)
const queryClient = inject(QueryClient)
const isRestoring = injectIsRestoring()
/**
* Signal that has the default options from query client applied
* computed() is used so signals can be inserted into the options
* making it reactive. Wrapping options in a function ensures embedded expressions
* are preserved and can keep being applied after signal changes
*/
const defaultedOptionsSignal = computed(() => {
const defaultedOptions = queryClient.defaultQueryOptions(optionsFn())
defaultedOptions._optimisticResults = isRestoring()
? 'isRestoring'
: 'optimistic'
return defaultedOptions
})
const observerSignal = (() => {
let instance: QueryObserver<
TQueryFnData,
TError,
TData,
TQueryData,
TQueryKey
> | null = null
return computed(() => {
return (instance ||= new Observer(queryClient, defaultedOptionsSignal()))
})
})()
const optimisticResultSignal = computed(() =>
observerSignal().getOptimisticResult(defaultedOptionsSignal()),
)
const resultFromSubscriberSignal = signal | null>(null)
effect(
(onCleanup) => {
const observer = observerSignal()
const defaultedOptions = defaultedOptionsSignal()
untracked(() => {
observer.setOptions(defaultedOptions)
})
onCleanup(() => {
ngZone.run(() => resultFromSubscriberSignal.set(null))
})
},
{
// Set allowSignalWrites to support Angular < v19
// Set to undefined to avoid warning on newer versions
allowSignalWrites: VERSION.major < '19' || undefined,
},
)
effect((onCleanup) => {
// observer.trackResult is not used as this optimization is not needed for Angular
const observer = observerSignal()
let pendingTaskRef: PendingTaskRef | null = null
const unsubscribe = isRestoring()
? () => undefined
: untracked(() =>
ngZone.runOutsideAngular(() => {
return observer.subscribe(
notifyManager.batchCalls((state) => {
ngZone.run(() => {
if (state.fetchStatus === 'fetching' && !pendingTaskRef) {
pendingTaskRef = pendingTasks.add()
}
if (state.fetchStatus === 'idle' && pendingTaskRef) {
pendingTaskRef()
pendingTaskRef = null
}
if (
state.isError &&
!state.isFetching &&
shouldThrowError(observer.options.throwOnError, [
state.error,
observer.getCurrentQuery(),
])
) {
ngZone.onError.emit(state.error)
throw state.error
}
resultFromSubscriberSignal.set(state)
})
}),
)
}),
)
onCleanup(() => {
if (pendingTaskRef) {
pendingTaskRef()
pendingTaskRef = null
}
unsubscribe()
})
})
return signalProxy(
computed(() => {
const subscriberResult = resultFromSubscriberSignal()
const optimisticResult = optimisticResultSignal()
const result = subscriberResult ?? optimisticResult
// Wrap methods to ensure observer has latest options before execution
const observer = observerSignal()
const originalRefetch = result.refetch
return {
...result,
refetch: ((...args: Parameters) => {
observer.setOptions(defaultedOptionsSignal())
return originalRefetch(...args)
}) as typeof originalRefetch,
}
}),
)
}
================================================
FILE: packages/angular-query-experimental/src/index.ts
================================================
/* istanbul ignore file */
// Re-export core
export * from '@tanstack/query-core'
export * from './types'
export type {
DefinedInitialDataOptions,
UndefinedInitialDataOptions,
UnusedSkipTokenOptions,
} from './query-options'
export { queryOptions } from './query-options'
export type { CreateMutationOptions } from './types'
export { mutationOptions } from './mutation-options'
export type {
DefinedInitialDataInfiniteOptions,
UndefinedInitialDataInfiniteOptions,
UnusedSkipTokenInfiniteOptions,
} from './infinite-query-options'
export { infiniteQueryOptions } from './infinite-query-options'
export type { InjectInfiniteQueryOptions } from './inject-infinite-query'
export { injectInfiniteQuery } from './inject-infinite-query'
export type { InjectIsFetchingOptions } from './inject-is-fetching'
export { injectIsFetching } from './inject-is-fetching'
export type { InjectIsMutatingOptions } from './inject-is-mutating'
export { injectIsMutating } from './inject-is-mutating'
export { injectIsRestoring, provideIsRestoring } from './inject-is-restoring'
export type { InjectMutationOptions } from './inject-mutation'
export { injectMutation } from './inject-mutation'
export type { InjectMutationStateOptions } from './inject-mutation-state'
export { injectMutationState } from './inject-mutation-state'
export type { QueriesOptions, QueriesResults } from './inject-queries'
export type { InjectQueryOptions } from './inject-query'
export { injectQuery } from './inject-query'
export { injectQueryClient } from './inject-query-client'
export type {
DeveloperToolsFeature,
PersistQueryClientFeature,
QueryFeature,
QueryFeatures,
} from './providers'
export {
provideAngularQuery,
provideQueryClient,
provideTanStackQuery,
queryFeature,
} from './providers'
================================================
FILE: packages/angular-query-experimental/src/infinite-query-options.ts
================================================
import type {
DataTag,
DefaultError,
InfiniteData,
InitialDataFunction,
NonUndefinedGuard,
OmitKeyof,
QueryKey,
SkipToken,
} from '@tanstack/query-core'
import type { CreateInfiniteQueryOptions } from './types'
export type UndefinedInitialDataInfiniteOptions<
TQueryFnData,
TError = DefaultError,
TData = InfiniteData,
TQueryKey extends QueryKey = QueryKey,
TPageParam = unknown,
> = CreateInfiniteQueryOptions<
TQueryFnData,
TError,
TData,
TQueryKey,
TPageParam
> & {
initialData?:
| undefined
| NonUndefinedGuard>
| InitialDataFunction<
NonUndefinedGuard>
>
}
export type UnusedSkipTokenInfiniteOptions<
TQueryFnData,
TError = DefaultError,
TData = InfiniteData,
TQueryKey extends QueryKey = QueryKey,
TPageParam = unknown,
> = OmitKeyof<
CreateInfiniteQueryOptions<
TQueryFnData,
TError,
TData,
TQueryKey,
TPageParam
>,
'queryFn'
> & {
queryFn?: Exclude<
CreateInfiniteQueryOptions<
TQueryFnData,
TError,
TData,
TQueryKey,
TPageParam
>['queryFn'],
SkipToken | undefined
>
}
export type DefinedInitialDataInfiniteOptions<
TQueryFnData,
TError = DefaultError,
TData = InfiniteData,
TQueryKey extends QueryKey = QueryKey,
TPageParam = unknown,
> = CreateInfiniteQueryOptions<
TQueryFnData,
TError,
TData,
TQueryKey,
TPageParam
> & {
initialData:
| NonUndefinedGuard>
| (() => NonUndefinedGuard>)
| undefined
}
/**
* Allows to share and re-use infinite query options in a type-safe way.
*
* The `queryKey` will be tagged with the type from `queryFn`.
* @param options - The infinite query options to tag with the type from `queryFn`.
* @returns The tagged infinite query options.
*/
export function infiniteQueryOptions<
TQueryFnData,
TError = DefaultError,
TData = InfiniteData,
TQueryKey extends QueryKey = QueryKey,
TPageParam = unknown,
>(
options: DefinedInitialDataInfiniteOptions<
TQueryFnData,
TError,
TData,
TQueryKey,
TPageParam
>,
): DefinedInitialDataInfiniteOptions<
TQueryFnData,
TError,
TData,
TQueryKey,
TPageParam
> & {
queryKey: DataTag, TError>
}
/**
* Allows to share and re-use infinite query options in a type-safe way.
*
* The `queryKey` will be tagged with the type from `queryFn`.
* @param options - The infinite query options to tag with the type from `queryFn`.
* @returns The tagged infinite query options.
*/
export function infiniteQueryOptions<
TQueryFnData,
TError = DefaultError,
TData = InfiniteData,
TQueryKey extends QueryKey = QueryKey,
TPageParam = unknown,
>(
options: UnusedSkipTokenInfiniteOptions<
TQueryFnData,
TError,
TData,
TQueryKey,
TPageParam
>,
): UnusedSkipTokenInfiniteOptions<
TQueryFnData,
TError,
TData,
TQueryKey,
TPageParam
> & {
queryKey: DataTag, TError>
}
/**
* Allows to share and re-use infinite query options in a type-safe way.
*
* The `queryKey` will be tagged with the type from `queryFn`.
* @param options - The infinite query options to tag with the type from `queryFn`.
* @returns The tagged infinite query options.
*/
export function infiniteQueryOptions<
TQueryFnData,
TError = DefaultError,
TData = InfiniteData,
TQueryKey extends QueryKey = QueryKey,
TPageParam = unknown,
>(
options: UndefinedInitialDataInfiniteOptions<
TQueryFnData,
TError,
TData,
TQueryKey,
TPageParam
>,
): UndefinedInitialDataInfiniteOptions<
TQueryFnData,
TError,
TData,
TQueryKey,
TPageParam
> & {
queryKey: DataTag, TError>
}
/**
* Allows to share and re-use infinite query options in a type-safe way.
*
* The `queryKey` will be tagged with the type from `queryFn`.
* @param options - The infinite query options to tag with the type from `queryFn`.
* @returns The tagged infinite query options.
*/
export function infiniteQueryOptions(options: unknown) {
return options
}
================================================
FILE: packages/angular-query-experimental/src/inject-infinite-query.ts
================================================
import { InfiniteQueryObserver } from '@tanstack/query-core'
import {
Injector,
assertInInjectionContext,
inject,
runInInjectionContext,
} from '@angular/core'
import { createBaseQuery } from './create-base-query'
import type {
DefaultError,
InfiniteData,
QueryKey,
QueryObserver,
} from '@tanstack/query-core'
import type {
CreateInfiniteQueryOptions,
CreateInfiniteQueryResult,
DefinedCreateInfiniteQueryResult,
} from './types'
import type {
DefinedInitialDataInfiniteOptions,
UndefinedInitialDataInfiniteOptions,
} from './infinite-query-options'
export interface InjectInfiniteQueryOptions {
/**
* The `Injector` in which to create the infinite query.
*
* If this is not provided, the current injection context will be used instead (via `inject`).
*/
injector?: Injector
}
/**
* Injects an infinite query: a declarative dependency on an asynchronous source of data that is tied to a unique key.
* Infinite queries can additively "load more" data onto an existing set of data or "infinite scroll"
* @param injectInfiniteQueryFn - A function that returns infinite query options.
* @param options - Additional configuration.
* @returns The infinite query result.
*/
export function injectInfiniteQuery<
TQueryFnData,
TError = DefaultError,
TData = InfiniteData,
TQueryKey extends QueryKey = QueryKey,
TPageParam = unknown,
>(
injectInfiniteQueryFn: () => DefinedInitialDataInfiniteOptions<
TQueryFnData,
TError,
TData,
TQueryKey,
TPageParam
>,
options?: InjectInfiniteQueryOptions,
): DefinedCreateInfiniteQueryResult
/**
* Injects an infinite query: a declarative dependency on an asynchronous source of data that is tied to a unique key.
* Infinite queries can additively "load more" data onto an existing set of data or "infinite scroll"
* @param injectInfiniteQueryFn - A function that returns infinite query options.
* @param options - Additional configuration.
* @returns The infinite query result.
*/
export function injectInfiniteQuery<
TQueryFnData,
TError = DefaultError,
TData = InfiniteData,
TQueryKey extends QueryKey = QueryKey,
TPageParam = unknown,
>(
injectInfiniteQueryFn: () => UndefinedInitialDataInfiniteOptions<
TQueryFnData,
TError,
TData,
TQueryKey,
TPageParam
>,
options?: InjectInfiniteQueryOptions,
): CreateInfiniteQueryResult
/**
* Injects an infinite query: a declarative dependency on an asynchronous source of data that is tied to a unique key.
* Infinite queries can additively "load more" data onto an existing set of data or "infinite scroll"
* @param injectInfiniteQueryFn - A function that returns infinite query options.
* @param options - Additional configuration.
* @returns The infinite query result.
*/
export function injectInfiniteQuery<
TQueryFnData,
TError = DefaultError,
TData = InfiniteData,
TQueryKey extends QueryKey = QueryKey,
TPageParam = unknown,
>(
injectInfiniteQueryFn: () => CreateInfiniteQueryOptions<
TQueryFnData,
TError,
TData,
TQueryKey,
TPageParam
>,
options?: InjectInfiniteQueryOptions,
): CreateInfiniteQueryResult
/**
* Injects an infinite query: a declarative dependency on an asynchronous source of data that is tied to a unique key.
* Infinite queries can additively "load more" data onto an existing set of data or "infinite scroll"
* @param injectInfiniteQueryFn - A function that returns infinite query options.
* @param options - Additional configuration.
* @returns The infinite query result.
*/
export function injectInfiniteQuery(
injectInfiniteQueryFn: () => CreateInfiniteQueryOptions,
options?: InjectInfiniteQueryOptions,
) {
!options?.injector && assertInInjectionContext(injectInfiniteQuery)
const injector = options?.injector ?? inject(Injector)
return runInInjectionContext(injector, () =>
createBaseQuery(
injectInfiniteQueryFn,
InfiniteQueryObserver as typeof QueryObserver,
),
)
}
================================================
FILE: packages/angular-query-experimental/src/inject-is-fetching.ts
================================================
import {
DestroyRef,
Injector,
NgZone,
assertInInjectionContext,
inject,
signal,
} from '@angular/core'
import { QueryClient, notifyManager } from '@tanstack/query-core'
import type { QueryFilters } from '@tanstack/query-core'
import type { Signal } from '@angular/core'
export interface InjectIsFetchingOptions {
/**
* The `Injector` in which to create the isFetching signal.
*
* If this is not provided, the current injection context will be used instead (via `inject`).
*/
injector?: Injector
}
/**
* Injects a signal that tracks the number of queries that your application is loading or
* fetching in the background.
*
* Can be used for app-wide loading indicators
* @param filters - The filters to apply to the query.
* @param options - Additional configuration
* @returns signal with number of loading or fetching queries.
*/
export function injectIsFetching(
filters?: QueryFilters,
options?: InjectIsFetchingOptions,
): Signal {
!options?.injector && assertInInjectionContext(injectIsFetching)
const injector = options?.injector ?? inject(Injector)
const destroyRef = injector.get(DestroyRef)
const ngZone = injector.get(NgZone)
const queryClient = injector.get(QueryClient)
const cache = queryClient.getQueryCache()
// isFetching is the prev value initialized on mount *
let isFetching = queryClient.isFetching(filters)
const result = signal(isFetching)
const unsubscribe = ngZone.runOutsideAngular(() =>
cache.subscribe(
notifyManager.batchCalls(() => {
const newIsFetching = queryClient.isFetching(filters)
if (isFetching !== newIsFetching) {
// * and update with each change
isFetching = newIsFetching
ngZone.run(() => {
result.set(isFetching)
})
}
}),
),
)
destroyRef.onDestroy(unsubscribe)
return result
}
================================================
FILE: packages/angular-query-experimental/src/inject-is-mutating.ts
================================================
import {
DestroyRef,
Injector,
NgZone,
assertInInjectionContext,
inject,
signal,
} from '@angular/core'
import { QueryClient, notifyManager } from '@tanstack/query-core'
import type { MutationFilters } from '@tanstack/query-core'
import type { Signal } from '@angular/core'
export interface InjectIsMutatingOptions {
/**
* The `Injector` in which to create the isMutating signal.
*
* If this is not provided, the current injection context will be used instead (via `inject`).
*/
injector?: Injector
}
/**
* Injects a signal that tracks the number of mutations that your application is fetching.
*
* Can be used for app-wide loading indicators
* @param filters - The filters to apply to the query.
* @param options - Additional configuration
* @returns signal with number of fetching mutations.
*/
export function injectIsMutating(
filters?: MutationFilters,
options?: InjectIsMutatingOptions,
): Signal {
!options?.injector && assertInInjectionContext(injectIsMutating)
const injector = options?.injector ?? inject(Injector)
const destroyRef = injector.get(DestroyRef)
const ngZone = injector.get(NgZone)
const queryClient = injector.get(QueryClient)
const cache = queryClient.getMutationCache()
// isMutating is the prev value initialized on mount *
let isMutating = queryClient.isMutating(filters)
const result = signal(isMutating)
const unsubscribe = ngZone.runOutsideAngular(() =>
cache.subscribe(
notifyManager.batchCalls(() => {
const newIsMutating = queryClient.isMutating(filters)
if (isMutating !== newIsMutating) {
// * and update with each change
isMutating = newIsMutating
ngZone.run(() => {
result.set(isMutating)
})
}
}),
),
)
destroyRef.onDestroy(unsubscribe)
return result
}
================================================
FILE: packages/angular-query-experimental/src/inject-is-restoring.ts
================================================
import {
InjectionToken,
Injector,
assertInInjectionContext,
inject,
signal,
} from '@angular/core'
import type { Provider, Signal } from '@angular/core'
/**
* Internal token used to track isRestoring state, accessible in public API through `injectIsRestoring` and set via `provideIsRestoring`
*/
const IS_RESTORING = new InjectionToken('', {
// Default value when not provided
factory: () => signal(false).asReadonly(),
})
interface InjectIsRestoringOptions {
/**
* The `Injector` to use to get the isRestoring signal.
*
* If this is not provided, the current injection context will be used instead (via `inject`).
*/
injector?: Injector
}
/**
* 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 initializing queries.
* @param options - Options for injectIsRestoring.
* @returns readonly signal with boolean that indicates whether a restore is in progress.
*/
export function injectIsRestoring(options?: InjectIsRestoringOptions) {
!options?.injector && assertInInjectionContext(injectIsRestoring)
const injector = options?.injector ?? inject(Injector)
return injector.get(IS_RESTORING)
}
/**
* 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
*/
export function provideIsRestoring(isRestoring: Signal): Provider {
return {
provide: IS_RESTORING,
useValue: isRestoring,
}
}
================================================
FILE: packages/angular-query-experimental/src/inject-mutation-state.ts
================================================
import {
DestroyRef,
Injector,
NgZone,
assertInInjectionContext,
computed,
inject,
signal,
} from '@angular/core'
import {
QueryClient,
notifyManager,
replaceEqualDeep,
} from '@tanstack/query-core'
import type { Signal } from '@angular/core'
import type {
Mutation,
MutationCache,
MutationFilters,
MutationState,
} from '@tanstack/query-core'
type MutationStateOptions = {
filters?: MutationFilters
select?: (mutation: Mutation) => TResult
}
/**
*
* @param mutationCache
* @param options
*/
function getResult(
mutationCache: MutationCache,
options: MutationStateOptions,
): Array {
return mutationCache
.findAll(options.filters)
.map(
(mutation): TResult =>
(options.select ? options.select(mutation) : mutation.state) as TResult,
)
}
export interface InjectMutationStateOptions {
/**
* The `Injector` in which to create the mutation state signal.
*
* If this is not provided, the current injection context will be used instead (via `inject`).
*/
injector?: Injector
}
/**
* Injects a signal that tracks the state of all mutations.
* @param injectMutationStateFn - A function that returns mutation state options.
* @param options - The Angular injector to use.
* @returns The signal that tracks the state of all mutations.
*/
export function injectMutationState(
injectMutationStateFn: () => MutationStateOptions = () => ({}),
options?: InjectMutationStateOptions,
): Signal> {
!options?.injector && assertInInjectionContext(injectMutationState)
const injector = options?.injector ?? inject(Injector)
const destroyRef = injector.get(DestroyRef)
const ngZone = injector.get(NgZone)
const queryClient = injector.get(QueryClient)
const mutationCache = queryClient.getMutationCache()
/**
* Computed signal that gets result from mutation cache based on passed options
* First element is the result, second element is the time when the result was set
*/
const resultFromOptionsSignal = computed(() => {
return [
getResult(mutationCache, injectMutationStateFn()),
performance.now(),
] as const
})
/**
* Signal that contains result set by subscriber
* First element is the result, second element is the time when the result was set
*/
const resultFromSubscriberSignal = signal<[Array, number] | null>(
null,
)
/**
* Returns the last result by either subscriber or options
*/
const effectiveResultSignal = computed(() => {
const optionsResult = resultFromOptionsSignal()
const subscriberResult = resultFromSubscriberSignal()
return subscriberResult && subscriberResult[1] > optionsResult[1]
? subscriberResult[0]
: optionsResult[0]
})
const unsubscribe = ngZone.runOutsideAngular(() =>
mutationCache.subscribe(
notifyManager.batchCalls(() => {
const [lastResult] = effectiveResultSignal()
const nextResult = replaceEqualDeep(
lastResult,
getResult(mutationCache, injectMutationStateFn()),
)
if (lastResult !== nextResult) {
ngZone.run(() => {
resultFromSubscriberSignal.set([nextResult, performance.now()])
})
}
}),
),
)
destroyRef.onDestroy(unsubscribe)
return effectiveResultSignal
}
================================================
FILE: packages/angular-query-experimental/src/inject-mutation.ts
================================================
import {
Injector,
NgZone,
assertInInjectionContext,
computed,
effect,
inject,
signal,
untracked,
} from '@angular/core'
import {
MutationObserver,
QueryClient,
noop,
notifyManager,
shouldThrowError,
} from '@tanstack/query-core'
import { signalProxy } from './signal-proxy'
import { PENDING_TASKS } from './pending-tasks-compat'
import type { PendingTaskRef } from './pending-tasks-compat'
import type { DefaultError, MutationObserverResult } from '@tanstack/query-core'
import type {
CreateMutateFunction,
CreateMutationOptions,
CreateMutationResult,
} from './types'
export interface InjectMutationOptions {
/**
* The `Injector` in which to create the mutation.
*
* If this is not provided, the current injection context will be used instead (via `inject`).
*/
injector?: Injector
}
/**
* Injects a mutation: an imperative function that can be invoked which typically performs server side effects.
*
* Unlike queries, mutations are not run automatically.
* @param injectMutationFn - A function that returns mutation options.
* @param options - Additional configuration
* @returns The mutation.
*/
export function injectMutation<
TData = unknown,
TError = DefaultError,
TVariables = void,
TOnMutateResult = unknown,
>(
injectMutationFn: () => CreateMutationOptions<
TData,
TError,
TVariables,
TOnMutateResult
>,
options?: InjectMutationOptions,
): CreateMutationResult {
!options?.injector && assertInInjectionContext(injectMutation)
const injector = options?.injector ?? inject(Injector)
const ngZone = injector.get(NgZone)
const pendingTasks = injector.get(PENDING_TASKS)
const queryClient = injector.get(QueryClient)
/**
* computed() is used so signals can be inserted into the options
* making it reactive. Wrapping options in a function ensures embedded expressions
* are preserved and can keep being applied after signal changes
*/
const optionsSignal = computed(injectMutationFn)
const observerSignal = (() => {
let instance: MutationObserver<
TData,
TError,
TVariables,
TOnMutateResult
> | null = null
return computed(() => {
return (instance ||= new MutationObserver(queryClient, optionsSignal()))
})
})()
const mutateFnSignal = computed<
CreateMutateFunction
>(() => {
const observer = observerSignal()
return (variables, mutateOptions) => {
observer.mutate(variables, mutateOptions).catch(noop)
}
})
/**
* Computed signal that gets result from mutation cache based on passed options
*/
const resultFromInitialOptionsSignal = computed(() => {
const observer = observerSignal()
return observer.getCurrentResult()
})
/**
* Signal that contains result set by subscriber
*/
const resultFromSubscriberSignal = signal | null>(null)
effect(
() => {
const observer = observerSignal()
const observerOptions = optionsSignal()
untracked(() => {
observer.setOptions(observerOptions)
})
},
{
injector,
},
)
effect(
(onCleanup) => {
// observer.trackResult is not used as this optimization is not needed for Angular
const observer = observerSignal()
let pendingTaskRef: PendingTaskRef | null = null
untracked(() => {
const unsubscribe = ngZone.runOutsideAngular(() =>
observer.subscribe(
notifyManager.batchCalls((state) => {
ngZone.run(() => {
// Track pending task when mutation is pending
if (state.isPending && !pendingTaskRef) {
pendingTaskRef = pendingTasks.add()
}
// Clear pending task when mutation is no longer pending
if (!state.isPending && pendingTaskRef) {
pendingTaskRef()
pendingTaskRef = null
}
if (
state.isError &&
shouldThrowError(observer.options.throwOnError, [state.error])
) {
ngZone.onError.emit(state.error)
throw state.error
}
resultFromSubscriberSignal.set(state)
})
}),
),
)
onCleanup(() => {
// Clean up any pending task on destroy
if (pendingTaskRef) {
pendingTaskRef()
pendingTaskRef = null
}
unsubscribe()
})
})
},
{
injector,
},
)
const resultSignal = computed(() => {
const resultFromSubscriber = resultFromSubscriberSignal()
const resultFromInitialOptions = resultFromInitialOptionsSignal()
const result = resultFromSubscriber ?? resultFromInitialOptions
return {
...result,
mutate: mutateFnSignal(),
mutateAsync: result.mutate,
}
})
return signalProxy(resultSignal) as CreateMutationResult<
TData,
TError,
TVariables,
TOnMutateResult
>
}
================================================
FILE: packages/angular-query-experimental/src/inject-queries.ts
================================================
import {
QueriesObserver,
QueryClient,
notifyManager,
} from '@tanstack/query-core'
import {
DestroyRef,
Injector,
NgZone,
assertInInjectionContext,
computed,
effect,
inject,
runInInjectionContext,
signal,
untracked,
} from '@angular/core'
import { signalProxy } from './signal-proxy'
import { injectIsRestoring } from './inject-is-restoring'
import type {
DefaultError,
OmitKeyof,
QueriesObserverOptions,
QueriesPlaceholderDataFunction,
QueryFunction,
QueryKey,
QueryObserverOptions,
ThrowOnError,
} from '@tanstack/query-core'
import type {
CreateQueryOptions,
CreateQueryResult,
DefinedCreateQueryResult,
} from './types'
import type { Signal } from '@angular/core'
// This defines the `CreateQueryOptions` that are accepted in `QueriesOptions` & `GetOptions`.
// `placeholderData` function always gets undefined passed
type QueryObserverOptionsForCreateQueries<
TQueryFnData = unknown,
TError = DefaultError,
TData = TQueryFnData,
TQueryKey extends QueryKey = QueryKey,
> = OmitKeyof<
CreateQueryOptions,
'placeholderData'
> & {
placeholderData?: TQueryFnData | QueriesPlaceholderDataFunction
}
// Avoid TS depth-limit error in case of large array literal
type MAXIMUM_DEPTH = 20
// Widen the type of the symbol to enable type inference even if skipToken is not immutable.
type SkipTokenForCreateQueries = symbol
type GetCreateQueryOptionsForCreateQueries =
// Part 1: responsible for applying explicit type parameter to function arguments, if object { queryFnData: TQueryFnData, error: TError, data: TData }
T extends {
queryFnData: infer TQueryFnData
error?: infer TError
data: infer TData
}
? QueryObserverOptionsForCreateQueries
: T extends { queryFnData: infer TQueryFnData; error?: infer TError }
? QueryObserverOptionsForCreateQueries
: T extends { data: infer TData; error?: infer TError }
? QueryObserverOptionsForCreateQueries
: // Part 2: responsible for applying explicit type parameter to function arguments, if tuple [TQueryFnData, TError, TData]
T extends [infer TQueryFnData, infer TError, infer TData]
? QueryObserverOptionsForCreateQueries
: T extends [infer TQueryFnData, infer TError]
? QueryObserverOptionsForCreateQueries
: T extends [infer TQueryFnData]
? QueryObserverOptionsForCreateQueries
: // Part 3: responsible for inferring and enforcing type if no explicit parameter was provided
T extends {
queryFn?:
| QueryFunction
| SkipTokenForCreateQueries
select?: (data: any) => infer TData
throwOnError?: ThrowOnError
}
? QueryObserverOptionsForCreateQueries<
TQueryFnData,
unknown extends TError ? DefaultError : TError,
unknown extends TData ? TQueryFnData : TData,
TQueryKey
>
: // Fallback
QueryObserverOptionsForCreateQueries
// A defined initialData setting should return a DefinedCreateQueryResult rather than CreateQueryResult
type GetDefinedOrUndefinedQueryResult = T extends {
initialData?: infer TInitialData
}
? unknown extends TInitialData
? CreateQueryResult
: TInitialData extends TData
? DefinedCreateQueryResult
: TInitialData extends () => infer TInitialDataResult
? unknown extends TInitialDataResult
? CreateQueryResult
: TInitialDataResult extends TData
? DefinedCreateQueryResult
: CreateQueryResult
: CreateQueryResult
: CreateQueryResult
type GetCreateQueryResult =
// Part 1: responsible for mapping explicit type parameter to function result, if object
T extends { queryFnData: any; error?: infer TError; data: infer TData }
? GetDefinedOrUndefinedQueryResult
: T extends { queryFnData: infer TQueryFnData; error?: infer TError }
? GetDefinedOrUndefinedQueryResult
: T extends { data: infer TData; error?: infer TError }
? GetDefinedOrUndefinedQueryResult
: // Part 2: responsible for mapping explicit type parameter to function result, if tuple
T extends [any, infer TError, infer TData]
? GetDefinedOrUndefinedQueryResult
: T extends [infer TQueryFnData, infer TError]
? GetDefinedOrUndefinedQueryResult
: T extends [infer TQueryFnData]
? GetDefinedOrUndefinedQueryResult
: // Part 3: responsible for mapping inferred type to results, if no explicit parameter was provided
T extends {
queryFn?:
| QueryFunction
| SkipTokenForCreateQueries
select?: (data: any) => infer TData
throwOnError?: ThrowOnError
}
? GetDefinedOrUndefinedQueryResult<
T,
unknown extends TData ? TQueryFnData : TData,
unknown extends TError ? DefaultError : TError
>
: // Fallback
CreateQueryResult
/**
* QueriesOptions reducer recursively unwraps function arguments to infer/enforce type param
*/
export type QueriesOptions<
T extends Array,
TResults extends Array = [],
TDepth extends ReadonlyArray = [],
> = TDepth['length'] extends MAXIMUM_DEPTH
? Array
: T extends []
? []
: T extends [infer Head]
? [...TResults, GetCreateQueryOptionsForCreateQueries]
: T extends [infer Head, ...infer Tails]
? QueriesOptions<
[...Tails],
[...TResults, GetCreateQueryOptionsForCreateQueries],
[...TDepth, 1]
>
: ReadonlyArray extends T
? T
: // If T is *some* array but we couldn't assign unknown[] to it, then it must hold some known/homogenous type!
// use this to infer the param types in the case of Array.map() argument
T extends Array<
QueryObserverOptionsForCreateQueries<
infer TQueryFnData,
infer TError,
infer TData,
infer TQueryKey
>
>
? Array<
QueryObserverOptionsForCreateQueries<
TQueryFnData,
TError,
TData,
TQueryKey
>
>
: // Fallback
Array
/**
* QueriesResults reducer recursively maps type param to results
*/
export type QueriesResults<
T extends Array,
TResults extends Array = [],
TDepth extends ReadonlyArray = [],
> = TDepth['length'] extends MAXIMUM_DEPTH
? Array
: T extends []
? []
: T extends [infer Head]
? [...TResults, GetCreateQueryResult]
: T extends [infer Head, ...infer Tails]
? QueriesResults<
[...Tails],
[...TResults, GetCreateQueryResult],
[...TDepth, 1]
>
: { [K in keyof T]: GetCreateQueryResult }
export interface InjectQueriesOptions<
T extends Array,
TCombinedResult = QueriesResults,
> {
queries:
| readonly [...QueriesOptions]
| readonly [
...{ [K in keyof T]: GetCreateQueryOptionsForCreateQueries },
]
combine?: (result: QueriesResults) => TCombinedResult
}
/**
* @param optionsFn - A function that returns queries' options.
* @param injector - The Angular injector to use.
*/
export function injectQueries<
T extends Array,
TCombinedResult = QueriesResults,
>(
optionsFn: () => InjectQueriesOptions,
injector?: Injector,
): Signal {
!injector && assertInInjectionContext(injectQueries)
return runInInjectionContext(injector ?? inject(Injector), () => {
const destroyRef = inject(DestroyRef)
const ngZone = inject(NgZone)
const queryClient = inject(QueryClient)
const isRestoring = injectIsRestoring()
/**
* Signal that has the default options from query client applied
* computed() is used so signals can be inserted into the options
* making it reactive. Wrapping options in a function ensures embedded expressions
* are preserved and can keep being applied after signal changes
*/
const optionsSignal = computed(() => {
return optionsFn()
})
const defaultedQueries = computed(() => {
return optionsSignal().queries.map((opts) => {
const defaultedOptions = queryClient.defaultQueryOptions(
opts as QueryObserverOptions,
)
// Make sure the results are already in fetching state before subscribing or updating options
defaultedOptions._optimisticResults = isRestoring()
? 'isRestoring'
: 'optimistic'
return defaultedOptions as QueryObserverOptions
})
})
const observerSignal = (() => {
let instance: QueriesObserver | null = null
return computed(() => {
return (instance ||= new QueriesObserver(
queryClient,
defaultedQueries(),
optionsSignal() as QueriesObserverOptions,
))
})
})()
const optimisticResultSignal = computed(() =>
observerSignal().getOptimisticResult(
defaultedQueries(),
(optionsSignal() as QueriesObserverOptions).combine,
),
)
// Do not notify on updates because of changes in the options because
// these changes should already be reflected in the optimistic result.
effect(() => {
observerSignal().setQueries(
defaultedQueries(),
optionsSignal() as QueriesObserverOptions,
)
})
const optimisticCombinedResultSignal = computed(() => {
const [_optimisticResult, getCombinedResult, trackResult] =
optimisticResultSignal()
return getCombinedResult(trackResult())
})
const resultFromSubscriberSignal = signal(null)
effect(() => {
const observer = observerSignal()
const [_optimisticResult, getCombinedResult] = optimisticResultSignal()
untracked(() => {
const unsubscribe = isRestoring()
? () => undefined
: ngZone.runOutsideAngular(() =>
observer.subscribe(
notifyManager.batchCalls((state) => {
resultFromSubscriberSignal.set(getCombinedResult(state))
}),
),
)
destroyRef.onDestroy(unsubscribe)
})
})
const resultSignal = computed(() => {
const subscriberResult = resultFromSubscriberSignal()
const optimisticResult = optimisticCombinedResultSignal()
return subscriberResult ?? optimisticResult
})
return computed(() => {
const result = resultSignal()
const { combine } = optionsSignal()
return combine
? result
: (result as QueriesResults).map((query) =>
signalProxy(signal(query)),
)
})
}) as unknown as Signal
}
================================================
FILE: packages/angular-query-experimental/src/inject-query-client.ts
================================================
import { Injector, inject } from '@angular/core'
import { QueryClient } from '@tanstack/query-core'
import type { InjectOptions } from '@angular/core'
/**
* Injects a `QueryClient` instance and allows passing a custom injector.
* @param injectOptions - Type of the options argument to inject and optionally a custom injector.
* @returns The `QueryClient` instance.
* @deprecated Use `inject(QueryClient)` instead.
* If you need to get a `QueryClient` from a custom injector, use `injector.get(QueryClient)`.
*
*
* **Example**
* ```ts
* const queryClient = injectQueryClient();
* ```
*/
export function injectQueryClient(
injectOptions: InjectOptions & { injector?: Injector } = {},
) {
return (injectOptions.injector ?? inject(Injector)).get(QueryClient)
}
================================================
FILE: packages/angular-query-experimental/src/inject-query.ts
================================================
import { QueryObserver } from '@tanstack/query-core'
import {
Injector,
assertInInjectionContext,
inject,
runInInjectionContext,
} from '@angular/core'
import { createBaseQuery } from './create-base-query'
import type { DefaultError, QueryKey } from '@tanstack/query-core'
import type {
CreateQueryOptions,
CreateQueryResult,
DefinedCreateQueryResult,
} from './types'
import type {
DefinedInitialDataOptions,
UndefinedInitialDataOptions,
} from './query-options'
export interface InjectQueryOptions {
/**
* The `Injector` in which to create the query.
*
* If this is not provided, the current injection context will be used instead (via `inject`).
*/
injector?: Injector
}
/**
* Injects a query: a declarative dependency on an asynchronous source of data that is tied to a unique key.
*
* **Basic example**
* ```ts
* class ServiceOrComponent {
* query = injectQuery(() => ({
* queryKey: ['repoData'],
* queryFn: () =>
* this.#http.get('https://api.github.com/repos/tanstack/query'),
* }))
* }
* ```
*
* Similar to `computed` from Angular, the function passed to `injectQuery` will be run in the reactive context.
* In the example below, the query will be automatically enabled and executed when the filter signal changes
* to a truthy value. When the filter signal changes back to a falsy value, the query will be disabled.
*
* **Reactive example**
* ```ts
* class ServiceOrComponent {
* filter = signal('')
*
* todosQuery = injectQuery(() => ({
* queryKey: ['todos', this.filter()],
* queryFn: () => fetchTodos(this.filter()),
* // Signals can be combined with expressions
* enabled: !!this.filter(),
* }))
* }
* ```
* @param injectQueryFn - A function that returns query options.
* @param options - Additional configuration
* @returns The query result.
* @see https://tanstack.com/query/latest/docs/framework/angular/guides/queries
*/
export function injectQuery<
TQueryFnData = unknown,
TError = DefaultError,
TData = TQueryFnData,
TQueryKey extends QueryKey = QueryKey,
>(
injectQueryFn: () => DefinedInitialDataOptions<
TQueryFnData,
TError,
TData,
TQueryKey
>,
options?: InjectQueryOptions,
): DefinedCreateQueryResult
/**
* Injects a query: a declarative dependency on an asynchronous source of data that is tied to a unique key.
*
* **Basic example**
* ```ts
* class ServiceOrComponent {
* query = injectQuery(() => ({
* queryKey: ['repoData'],
* queryFn: () =>
* this.#http.get('https://api.github.com/repos/tanstack/query'),
* }))
* }
* ```
*
* Similar to `computed` from Angular, the function passed to `injectQuery` will be run in the reactive context.
* In the example below, the query will be automatically enabled and executed when the filter signal changes
* to a truthy value. When the filter signal changes back to a falsy value, the query will be disabled.
*
* **Reactive example**
* ```ts
* class ServiceOrComponent {
* filter = signal('')
*
* todosQuery = injectQuery(() => ({
* queryKey: ['todos', this.filter()],
* queryFn: () => fetchTodos(this.filter()),
* // Signals can be combined with expressions
* enabled: !!this.filter(),
* }))
* }
* ```
* @param injectQueryFn - A function that returns query options.
* @param options - Additional configuration
* @returns The query result.
* @see https://tanstack.com/query/latest/docs/framework/angular/guides/queries
*/
export function injectQuery<
TQueryFnData = unknown,
TError = DefaultError,
TData = TQueryFnData,
TQueryKey extends QueryKey = QueryKey,
>(
injectQueryFn: () => UndefinedInitialDataOptions<
TQueryFnData,
TError,
TData,
TQueryKey
>,
options?: InjectQueryOptions,
): CreateQueryResult
/**
* Injects a query: a declarative dependency on an asynchronous source of data that is tied to a unique key.
*
* **Basic example**
* ```ts
* class ServiceOrComponent {
* query = injectQuery(() => ({
* queryKey: ['repoData'],
* queryFn: () =>
* this.#http.get('https://api.github.com/repos/tanstack/query'),
* }))
* }
* ```
*
* Similar to `computed` from Angular, the function passed to `injectQuery` will be run in the reactive context.
* In the example below, the query will be automatically enabled and executed when the filter signal changes
* to a truthy value. When the filter signal changes back to a falsy value, the query will be disabled.
*
* **Reactive example**
* ```ts
* class ServiceOrComponent {
* filter = signal('')
*
* todosQuery = injectQuery(() => ({
* queryKey: ['todos', this.filter()],
* queryFn: () => fetchTodos(this.filter()),
* // Signals can be combined with expressions
* enabled: !!this.filter(),
* }))
* }
* ```
* @param injectQueryFn - A function that returns query options.
* @param options - Additional configuration
* @returns The query result.
* @see https://tanstack.com/query/latest/docs/framework/angular/guides/queries
*/
export function injectQuery<
TQueryFnData = unknown,
TError = DefaultError,
TData = TQueryFnData,
TQueryKey extends QueryKey = QueryKey,
>(
injectQueryFn: () => CreateQueryOptions<
TQueryFnData,
TError,
TData,
TQueryKey
>,
options?: InjectQueryOptions,
): CreateQueryResult
/**
* Injects a query: a declarative dependency on an asynchronous source of data that is tied to a unique key.
*
* **Basic example**
* ```ts
* class ServiceOrComponent {
* query = injectQuery(() => ({
* queryKey: ['repoData'],
* queryFn: () =>
* this.#http.get('https://api.github.com/repos/tanstack/query'),
* }))
* }
* ```
*
* Similar to `computed` from Angular, the function passed to `injectQuery` will be run in the reactive context.
* In the example below, the query will be automatically enabled and executed when the filter signal changes
* to a truthy value. When the filter signal changes back to a falsy value, the query will be disabled.
*
* **Reactive example**
* ```ts
* class ServiceOrComponent {
* filter = signal('')
*
* todosQuery = injectQuery(() => ({
* queryKey: ['todos', this.filter()],
* queryFn: () => fetchTodos(this.filter()),
* // Signals can be combined with expressions
* enabled: !!this.filter(),
* }))
* }
* ```
* @param injectQueryFn - A function that returns query options.
* @param options - Additional configuration
* @returns The query result.
* @see https://tanstack.com/query/latest/docs/framework/angular/guides/queries
*/
export function injectQuery(
injectQueryFn: () => CreateQueryOptions,
options?: InjectQueryOptions,
) {
!options?.injector && assertInInjectionContext(injectQuery)
return runInInjectionContext(options?.injector ?? inject(Injector), () =>
createBaseQuery(injectQueryFn, QueryObserver),
) as unknown as CreateQueryResult
}
================================================
FILE: packages/angular-query-experimental/src/mutation-options.ts
================================================
import type { DefaultError, WithRequired } from '@tanstack/query-core'
import type { CreateMutationOptions } from './types'
/**
* Allows to share and re-use mutation options in a type-safe way.
*
* **Example**
*
* ```ts
* export class QueriesService {
* private http = inject(HttpClient)
* private queryClient = inject(QueryClient)
*
* updatePost(id: number) {
* return mutationOptions({
* mutationFn: (post: Post) => Promise.resolve(post),
* mutationKey: ["updatePost", id],
* onSuccess: (newPost) => {
* // ^? newPost: Post
* this.queryClient.setQueryData(["posts", id], newPost)
* },
* });
* }
* }
*
* class ComponentOrService {
* queries = inject(QueriesService)
* id = signal(0)
* mutation = injectMutation(() => this.queries.updatePost(this.id()))
*
* save() {
* this.mutation.mutate({ title: 'New Title' })
* }
* }
* ```
* @param options - The mutation options.
* @returns Mutation options.
*/
export function mutationOptions<
TData = unknown,
TError = DefaultError,
TVariables = void,
TOnMutateResult = unknown,
>(
options: WithRequired<
CreateMutationOptions,
'mutationKey'
>,
): WithRequired<
CreateMutationOptions,
'mutationKey'
>
export function mutationOptions<
TData = unknown,
TError = DefaultError,
TVariables = void,
TOnMutateResult = unknown,
>(
options: Omit<
CreateMutationOptions,
'mutationKey'
>,
): Omit<
CreateMutationOptions,
'mutationKey'
>
/**
* Allows to share and re-use mutation options in a type-safe way.
*
* **Example**
*
* ```ts
* export class QueriesService {
* private http = inject(HttpClient)
* private queryClient = inject(QueryClient)
*
* updatePost(id: number) {
* return mutationOptions({
* mutationFn: (post: Post) => Promise.resolve(post),
* mutationKey: ["updatePost", id],
* onSuccess: (newPost) => {
* // ^? newPost: Post
* this.queryClient.setQueryData(["posts", id], newPost)
* },
* });
* }
* }
*
* class ComponentOrService {
* queries = inject(QueriesService)
* id = signal(0)
* mutation = injectMutation(() => this.queries.updatePost(this.id()))
*
* save() {
* this.mutation.mutate({ title: 'New Title' })
* }
* }
* ```
* @param options - The mutation options.
* @returns Mutation options.
*/
export function mutationOptions<
TData = unknown,
TError = DefaultError,
TVariables = void,
TOnMutateResult = unknown,
>(
options: CreateMutationOptions,
): CreateMutationOptions {
return options
}
================================================
FILE: packages/angular-query-experimental/src/pending-tasks-compat.ts
================================================
import { InjectionToken, inject } from '@angular/core'
import * as ng from '@angular/core'
import { noop } from '@tanstack/query-core'
type PendingTasksCompat = { add: () => PendingTaskRef }
export type PendingTaskRef = () => void
export const PENDING_TASKS = new InjectionToken(
'PENDING_TASKS',
{
factory: (): PendingTasksCompat => {
// Access via Reflect so bundlers stay quiet when the token is absent (Angular < 19).
const token = Reflect.get(ng, 'PendingTasks') as unknown as
| Parameters[0]
| undefined
const svc: PendingTasksCompat | null = token
? (inject(token, { optional: true }) as PendingTasksCompat | null)
: null
// Without PendingTasks we fall back to a stable no-op shim.
return {
add: svc ? () => svc.add() : () => noop,
}
},
},
)
================================================
FILE: packages/angular-query-experimental/src/providers.ts
================================================
import { DestroyRef, InjectionToken, inject } from '@angular/core'
import { QueryClient } from '@tanstack/query-core'
import type { Provider } from '@angular/core'
/**
* Usually {@link provideTanStackQuery} is used once to set up TanStack Query and the
* {@link https://tanstack.com/query/latest/docs/reference/QueryClient|QueryClient}
* for the entire application. Internally it calls `provideQueryClient`.
* You can use `provideQueryClient` to provide a different `QueryClient` instance for a part
* of the application or for unit testing purposes.
* @param queryClient - A `QueryClient` instance, or an `InjectionToken` which provides a `QueryClient`.
* @returns a provider object that can be used to provide the `QueryClient` instance.
*/
export function provideQueryClient(
queryClient: QueryClient | InjectionToken,
): Provider {
return {
provide: QueryClient,
useFactory: () => {
const client =
queryClient instanceof InjectionToken
? inject(queryClient)
: queryClient
// Unmount the query client on injector destroy
inject(DestroyRef).onDestroy(() => client.unmount())
client.mount()
return client
},
}
}
/**
* Sets up providers necessary to enable TanStack Query functionality for Angular applications.
*
* Allows to configure a `QueryClient` and optional features such as developer tools.
*
* **Example - standalone**
*
* ```ts
* import {
* provideTanStackQuery,
* QueryClient,
* } from '@tanstack/angular-query-experimental'
*
* bootstrapApplication(AppComponent, {
* providers: [provideTanStackQuery(new QueryClient())],
* })
* ```
*
* **Example - NgModule-based**
*
* ```ts
* import {
* provideTanStackQuery,
* QueryClient,
* } from '@tanstack/angular-query-experimental'
*
* @NgModule({
* declarations: [AppComponent],
* imports: [BrowserModule],
* providers: [provideTanStackQuery(new QueryClient())],
* bootstrap: [AppComponent],
* })
* export class AppModule {}
* ```
*
* You can also enable optional developer tools by adding `withDevtools`. By
* default the tools will then be loaded when your app is in development mode.
* ```ts
* import {
* provideTanStackQuery,
* withDevtools
* QueryClient,
* } from '@tanstack/angular-query-experimental'
*
* bootstrapApplication(AppComponent,
* {
* providers: [
* provideTanStackQuery(new QueryClient(), withDevtools())
* ]
* }
* )
* ```
*
* **Example: using an InjectionToken**
*
* ```ts
* export const MY_QUERY_CLIENT = new InjectionToken('', {
* factory: () => new QueryClient(),
* })
*
* // In a lazy loaded route or lazy loaded component's providers array:
* providers: [provideTanStackQuery(MY_QUERY_CLIENT)]
* ```
* Using an InjectionToken for the QueryClient is an advanced optimization which allows TanStack Query to be absent from the main application bundle.
* This can be beneficial if you want to include TanStack Query on lazy loaded routes only while still sharing a `QueryClient`.
*
* Note that this is a small optimization and for most applications it's preferable to provide the `QueryClient` in the main application config.
* @param queryClient - A `QueryClient` instance, or an `InjectionToken` which provides a `QueryClient`.
* @param features - Optional features to configure additional Query functionality.
* @returns A set of providers to set up TanStack Query.
* @see https://tanstack.com/query/v5/docs/framework/angular/quick-start
* @see withDevtools
*/
export function provideTanStackQuery(
queryClient: QueryClient | InjectionToken,
...features: Array
): Array {
return [
provideQueryClient(queryClient),
features.map((feature) => feature.ɵproviders),
]
}
/**
* Sets up providers necessary to enable TanStack Query functionality for Angular applications.
*
* Allows to configure a `QueryClient`.
* @param queryClient - A `QueryClient` instance.
* @returns A set of providers to set up TanStack Query.
* @see https://tanstack.com/query/v5/docs/framework/angular/quick-start
* @deprecated Use `provideTanStackQuery` instead.
*/
export function provideAngularQuery(queryClient: QueryClient): Array {
return provideTanStackQuery(queryClient)
}
const queryFeatures = ['Devtools', 'PersistQueryClient'] as const
type QueryFeatureKind = (typeof queryFeatures)[number]
/**
* Helper type to represent a Query feature.
*/
export interface QueryFeature {
ɵkind: TFeatureKind
ɵproviders: Array
}
/**
* Helper function to create an object that represents a Query feature.
* @param kind -
* @param providers -
* @returns A Query feature.
*/
export function queryFeature(
kind: TFeatureKind,
providers: Array,
): QueryFeature {
return { ɵkind: kind, ɵproviders: providers }
}
/**
* A type alias that represents a feature which enables developer tools.
* The type is used to describe the return value of the `withDevtools` function.
* @see {@link withDevtools}
*/
export type DeveloperToolsFeature = QueryFeature<'Devtools'>
/**
* A type alias that represents a feature which enables persistence.
* The type is used to describe the return value of the `withPersistQueryClient` function.
*/
export type PersistQueryClientFeature = QueryFeature<'PersistQueryClient'>
/**
* A type alias that represents all Query features available for use with `provideTanStackQuery`.
* Features can be enabled by adding special functions to the `provideTanStackQuery` call.
* See documentation for each symbol to find corresponding function name. See also `provideTanStackQuery`
* documentation on how to use those functions.
* @see {@link provideTanStackQuery}
*/
export type QueryFeatures = DeveloperToolsFeature | PersistQueryClientFeature
================================================
FILE: packages/angular-query-experimental/src/query-options.ts
================================================
import type {
DataTag,
DefaultError,
InitialDataFunction,
NonUndefinedGuard,
OmitKeyof,
QueryFunction,
QueryKey,
SkipToken,
} from '@tanstack/query-core'
import type { CreateQueryOptions } from './types'
export type UndefinedInitialDataOptions<
TQueryFnData = unknown,
TError = DefaultError,
TData = TQueryFnData,
TQueryKey extends QueryKey = QueryKey,
> = CreateQueryOptions & {
initialData?:
| undefined
| InitialDataFunction>
| NonUndefinedGuard
}
export type UnusedSkipTokenOptions<
TQueryFnData = unknown,
TError = DefaultError,
TData = TQueryFnData,
TQueryKey extends QueryKey = QueryKey,
> = OmitKeyof<
CreateQueryOptions,
'queryFn'
> & {
queryFn?: Exclude<
CreateQueryOptions['queryFn'],
SkipToken | undefined
>
}
export type DefinedInitialDataOptions<
TQueryFnData = unknown,
TError = DefaultError,
TData = TQueryFnData,
TQueryKey extends QueryKey = QueryKey,
> = Omit<
CreateQueryOptions,
'queryFn'
> & {
initialData:
| NonUndefinedGuard
| (() => NonUndefinedGuard)
queryFn?: QueryFunction
}
/**
* Allows to share and re-use query options in a type-safe way.
*
* The `queryKey` will be tagged with the type from `queryFn`.
*
* **Example**
*
* ```ts
* const { queryKey } = queryOptions({
* queryKey: ['key'],
* queryFn: () => Promise.resolve(5),
* // ^? Promise
* })
*
* const queryClient = new QueryClient()
* const data = queryClient.getQueryData(queryKey)
* // ^? number | undefined
* ```
* @param options - The query options to tag with the type from `queryFn`.
* @returns The tagged query options.
*/
export function queryOptions<
TQueryFnData = unknown,
TError = DefaultError,
TData = TQueryFnData,
TQueryKey extends QueryKey = QueryKey,
>(
options: DefinedInitialDataOptions,
): DefinedInitialDataOptions & {
queryKey: DataTag
}
/**
* Allows to share and re-use query options in a type-safe way.
*
* The `queryKey` will be tagged with the type from `queryFn`.
*
* **Example**
*
* ```ts
* const { queryKey } = queryOptions({
* queryKey: ['key'],
* queryFn: () => Promise.resolve(5),
* // ^? Promise
* })
*
* const queryClient = new QueryClient()
* const data = queryClient.getQueryData(queryKey)
* // ^? number | undefined
* ```
* @param options - The query options to tag with the type from `queryFn`.
* @returns The tagged query options.
*/
export function queryOptions<
TQueryFnData = unknown,
TError = DefaultError,
TData = TQueryFnData,
TQueryKey extends QueryKey = QueryKey,
>(
options: UnusedSkipTokenOptions,
): UnusedSkipTokenOptions & {
queryKey: DataTag
}
/**
* Allows to share and re-use query options in a type-safe way.
*
* The `queryKey` will be tagged with the type from `queryFn`.
*
* **Example**
*
* ```ts
* const { queryKey } = queryOptions({
* queryKey: ['key'],
* queryFn: () => Promise.resolve(5),
* // ^? Promise
* })
*
* const queryClient = new QueryClient()
* const data = queryClient.getQueryData(queryKey)
* // ^? number | undefined
* ```
* @param options - The query options to tag with the type from `queryFn`.
* @returns The tagged query options.
*/
export function queryOptions<
TQueryFnData = unknown,
TError = DefaultError,
TData = TQueryFnData,
TQueryKey extends QueryKey = QueryKey,
>(
options: UndefinedInitialDataOptions,
): UndefinedInitialDataOptions & {
queryKey: DataTag
}
/**
* Allows to share and re-use query options in a type-safe way.
*
* The `queryKey` will be tagged with the type from `queryFn`.
*
* **Example**
*
* ```ts
* const { queryKey } = queryOptions({
* queryKey: ['key'],
* queryFn: () => Promise.resolve(5),
* // ^? Promise
* })
*
* const queryClient = new QueryClient()
* const data = queryClient.getQueryData(queryKey)
* // ^? number | undefined
* ```
* @param options - The query options to tag with the type from `queryFn`.
* @returns The tagged query options.
*/
export function queryOptions(options: unknown) {
return options
}
================================================
FILE: packages/angular-query-experimental/src/signal-proxy.ts
================================================
import { computed, untracked } from '@angular/core'
import type { Signal } from '@angular/core'
export type MapToSignals = {
[K in keyof T]: T[K] extends Function ? T[K] : Signal
}
/**
* Exposes fields of an object passed via an Angular `Signal` as `Computed` signals.
* Functions on the object are passed through as-is.
* @param inputSignal - `Signal` that must return an object.
* @returns A proxy object with the same fields as the input object, but with each field wrapped in a `Computed` signal.
*/
export function signalProxy>(
inputSignal: Signal,
) {
const internalState = {} as MapToSignals
return new Proxy>(internalState, {
get(target, prop) {
// first check if we have it in our internal state and return it
const computedField = target[prop]
if (computedField) return computedField
// then, check if it's a function on the resultState and return it
const targetField = untracked(inputSignal)[prop]
if (typeof targetField === 'function') return targetField
// finally, create a computed field, store it and return it
// @ts-expect-error
return (target[prop] = computed(() => inputSignal()[prop]))
},
has(_, prop) {
return !!untracked(inputSignal)[prop]
},
ownKeys() {
return Reflect.ownKeys(untracked(inputSignal))
},
getOwnPropertyDescriptor() {
return {
enumerable: true,
configurable: true,
}
},
})
}
================================================
FILE: packages/angular-query-experimental/src/types.ts
================================================
/* istanbul ignore file */
import type {
DefaultError,
DefinedInfiniteQueryObserverResult,
DefinedQueryObserverResult,
InfiniteQueryObserverOptions,
InfiniteQueryObserverResult,
MutateFunction,
MutationObserverOptions,
MutationObserverResult,
OmitKeyof,
Override,
QueryKey,
QueryObserverOptions,
QueryObserverResult,
} from '@tanstack/query-core'
import type { Signal } from '@angular/core'
import type { MapToSignals } from './signal-proxy'
export interface CreateBaseQueryOptions<
TQueryFnData = unknown,
TError = DefaultError,
TData = TQueryFnData,
TQueryData = TQueryFnData,
TQueryKey extends QueryKey = QueryKey,
> extends QueryObserverOptions<
TQueryFnData,
TError,
TData,
TQueryData,
TQueryKey
> {}
export interface CreateQueryOptions<
TQueryFnData = unknown,
TError = DefaultError,
TData = TQueryFnData,
TQueryKey extends QueryKey = QueryKey,
> extends OmitKeyof<
CreateBaseQueryOptions<
TQueryFnData,
TError,
TData,
TQueryFnData,
TQueryKey
>,
'suspense'
> {}
type CreateStatusBasedQueryResult<
TStatus extends QueryObserverResult['status'],
TData = unknown,
TError = DefaultError,
> = Extract, { status: TStatus }>
export interface BaseQueryNarrowing {
isSuccess: (
this: CreateBaseQueryResult,
) => this is CreateBaseQueryResult<
TData,
TError,
CreateStatusBasedQueryResult<'success', TData, TError>
>
isError: (
this: CreateBaseQueryResult,
) => this is CreateBaseQueryResult<
TData,
TError,
CreateStatusBasedQueryResult<'error', TData, TError>
>
isPending: (
this: CreateBaseQueryResult,
) => this is CreateBaseQueryResult<
TData,
TError,
CreateStatusBasedQueryResult<'pending', TData, TError>
>
}
export interface CreateInfiniteQueryOptions<
TQueryFnData = unknown,
TError = DefaultError,
TData = TQueryFnData,
TQueryKey extends QueryKey = QueryKey,
TPageParam = unknown,
> extends OmitKeyof<
InfiniteQueryObserverOptions<
TQueryFnData,
TError,
TData,
TQueryKey,
TPageParam
>,
'suspense'
> {}
export type CreateBaseQueryResult<
TData = unknown,
TError = DefaultError,
TState = QueryObserverResult,
> = BaseQueryNarrowing &
MapToSignals>
export type CreateQueryResult<
TData = unknown,
TError = DefaultError,
> = CreateBaseQueryResult
export type DefinedCreateQueryResult<
TData = unknown,
TError = DefaultError,
TState = DefinedQueryObserverResult,
> = BaseQueryNarrowing &
MapToSignals>
export type CreateInfiniteQueryResult<
TData = unknown,
TError = DefaultError,
> = BaseQueryNarrowing &
MapToSignals>
export type DefinedCreateInfiniteQueryResult<
TData = unknown,
TError = DefaultError,
TDefinedInfiniteQueryObserver = DefinedInfiniteQueryObserverResult<
TData,
TError
>,
> = MapToSignals
export interface CreateMutationOptions<
TData = unknown,
TError = DefaultError,
TVariables = void,
TOnMutateResult = unknown,
> extends OmitKeyof<
MutationObserverOptions,
'_defaulted'
> {}
export type CreateMutateFunction<
TData = unknown,
TError = DefaultError,
TVariables = void,
TOnMutateResult = unknown,
> = (
...args: Parameters<
MutateFunction
>
) => void
export type CreateMutateAsyncFunction<
TData = unknown,
TError = DefaultError,
TVariables = void,
TOnMutateResult = unknown,
> = MutateFunction
export type CreateBaseMutationResult<
TData = unknown,
TError = DefaultError,
TVariables = unknown,
TOnMutateResult = unknown,
> = Override<
MutationObserverResult,
{ mutate: CreateMutateFunction }
> & {
mutateAsync: CreateMutateAsyncFunction<
TData,
TError,
TVariables,
TOnMutateResult
>
}
type CreateStatusBasedMutationResult<
TStatus extends CreateBaseMutationResult['status'],
TData = unknown,
TError = DefaultError,
TVariables = unknown,
TOnMutateResult = unknown,
> = Extract<
CreateBaseMutationResult,
{ status: TStatus }
>
type SignalFunction any> = T & Signal>
export interface BaseMutationNarrowing<
TData = unknown,
TError = DefaultError,
TVariables = unknown,
TOnMutateResult = unknown,
> {
isSuccess: SignalFunction<
(
this: CreateMutationResult,
) => this is CreateMutationResult<
TData,
TError,
TVariables,
TOnMutateResult,
CreateStatusBasedMutationResult<
'success',
TData,
TError,
TVariables,
TOnMutateResult
>
>
>
isError: SignalFunction<
(
this: CreateMutationResult,
) => this is CreateMutationResult<
TData,
TError,
TVariables,
TOnMutateResult,
CreateStatusBasedMutationResult<
'error',
TData,
TError,
TVariables,
TOnMutateResult
>
>
>
isPending: SignalFunction<
(
this: CreateMutationResult,
) => this is CreateMutationResult<
TData,
TError,
TVariables,
TOnMutateResult,
CreateStatusBasedMutationResult<
'pending',
TData,
TError,
TVariables,
TOnMutateResult
>
>
>
isIdle: SignalFunction<
(
this: CreateMutationResult,
) => this is CreateMutationResult<
TData,
TError,
TVariables,
TOnMutateResult,
CreateStatusBasedMutationResult<
'idle',
TData,
TError,
TVariables,
TOnMutateResult
>
>
>
}
export type CreateMutationResult<
TData = unknown,
TError = DefaultError,
TVariables = unknown,
TOnMutateResult = unknown,
TState = CreateStatusBasedMutationResult<
CreateBaseMutationResult['status'],
TData,
TError,
TVariables,
TOnMutateResult
>,
> = BaseMutationNarrowing &
MapToSignals>
================================================
FILE: packages/angular-query-experimental/src/__tests__/infinite-query-options.test-d.ts
================================================
import { assertType, describe, expectTypeOf, it, test } from 'vitest'
import { QueryClient, dataTagSymbol } from '@tanstack/query-core'
import { infiniteQueryOptions } from '../infinite-query-options'
import { injectInfiniteQuery } from '../inject-infinite-query'
import { injectQuery } from '../inject-query'
import type {
DataTag,
InfiniteData,
InitialDataFunction,
} from '@tanstack/query-core'
describe('infiniteQueryOptions', () => {
it('should not allow excess properties', () => {
assertType(
infiniteQueryOptions({
queryKey: ['key'],
queryFn: () => Promise.resolve('data'),
getNextPageParam: () => 1,
initialPageParam: 1,
// @ts-expect-error this is a good error, because stallTime does not exist!
stallTime: 1000,
}),
)
})
it('should infer types for callbacks', () => {
infiniteQueryOptions({
queryKey: ['key'],
queryFn: () => Promise.resolve('data'),
staleTime: 1000,
getNextPageParam: () => 1,
initialPageParam: 1,
select: (data) => {
expectTypeOf(data).toEqualTypeOf>()
},
})
})
it('should work when passed to useInfiniteQuery', () => {
const options = infiniteQueryOptions({
queryKey: ['key'],
queryFn: () => Promise.resolve('string'),
getNextPageParam: () => 1,
initialPageParam: 1,
})
const { data } = injectInfiniteQuery(() => options)
// known issue: type of pageParams is unknown when returned from useInfiniteQuery
expectTypeOf(data()).toEqualTypeOf<
InfiniteData | undefined
>()
})
it('should work when passed to fetchInfiniteQuery', async () => {
const options = infiniteQueryOptions({
queryKey: ['key'],
queryFn: () => Promise.resolve('string'),
getNextPageParam: () => 1,
initialPageParam: 1,
})
const data = await new QueryClient().fetchInfiniteQuery(options)
expectTypeOf(data).toEqualTypeOf>()
})
it('should tag the queryKey with the result type of the QueryFn', () => {
const { queryKey } = infiniteQueryOptions({
queryKey: ['key'],
queryFn: () => Promise.resolve('string'),
getNextPageParam: () => 1,
initialPageParam: 1,
})
expectTypeOf(queryKey[dataTagSymbol]).toEqualTypeOf>()
})
it('should tag the queryKey even if no promise is returned', () => {
const { queryKey } = infiniteQueryOptions({
queryKey: ['key'],
queryFn: () => 'string',
getNextPageParam: () => 1,
initialPageParam: 1,
})
expectTypeOf(queryKey[dataTagSymbol]).toEqualTypeOf>()
})
it('should tag the queryKey with the result type of the QueryFn if select is used', () => {
const { queryKey } = infiniteQueryOptions({
queryKey: ['key'],
queryFn: () => Promise.resolve('string'),
select: (data) => data.pages,
getNextPageParam: () => 1,
initialPageParam: 1,
})
expectTypeOf(queryKey[dataTagSymbol]).toEqualTypeOf>()
})
it('should return the proper type when passed to getQueryData', () => {
const { queryKey } = infiniteQueryOptions({
queryKey: ['key'],
queryFn: () => Promise.resolve('string'),
getNextPageParam: () => 1,
initialPageParam: 1,
})
const queryClient = new QueryClient()
const data = queryClient.getQueryData(queryKey)
expectTypeOf(data).toEqualTypeOf<
InfiniteData | undefined
>()
})
it('should properly type when passed to setQueryData', () => {
const { queryKey } = infiniteQueryOptions({
queryKey: ['key'],
queryFn: () => Promise.resolve('string'),
getNextPageParam: () => 1,
initialPageParam: 1,
})
const queryClient = new QueryClient()
const data = queryClient.setQueryData(queryKey, (prev) => {
expectTypeOf(prev).toEqualTypeOf<
InfiniteData | undefined
>()
return prev
})
expectTypeOf(data).toEqualTypeOf<
InfiniteData | undefined
>()
})
test('should not be allowed to be passed to non-infinite query functions', () => {
const queryClient = new QueryClient()
const options = infiniteQueryOptions({
queryKey: ['key'],
queryFn: () => Promise.resolve('string'),
getNextPageParam: () => 1,
initialPageParam: 1,
})
assertType(
// @ts-expect-error cannot pass infinite options to non-infinite query functions
injectQuery(() => options),
)
assertType(
// @ts-expect-error cannot pass infinite options to non-infinite query functions
queryClient.ensureQueryData(options),
)
assertType(
// @ts-expect-error cannot pass infinite options to non-infinite query functions
queryClient.fetchQuery(options),
)
assertType(
// @ts-expect-error cannot pass infinite options to non-infinite query functions
queryClient.prefetchQuery(options),
)
})
test('allow optional initialData function', () => {
const initialData: { example: boolean } | undefined = { example: true }
const queryOptions = infiniteQueryOptions({
queryKey: ['example'],
queryFn: () => initialData,
initialData: initialData
? () => ({ pages: [initialData], pageParams: [] })
: undefined,
getNextPageParam: () => 1,
initialPageParam: 1,
})
expectTypeOf(queryOptions.initialData).toMatchTypeOf<
| InitialDataFunction>
| InfiniteData<{ example: boolean }, number>
| undefined
>()
})
test('allow optional initialData object', () => {
const initialData: { example: boolean } | undefined = { example: true }
const queryOptions = infiniteQueryOptions({
queryKey: ['example'],
queryFn: () => initialData,
initialData: initialData
? { pages: [initialData], pageParams: [] }
: undefined,
getNextPageParam: () => 1,
initialPageParam: 1,
})
expectTypeOf(queryOptions.initialData).toMatchTypeOf<
| InitialDataFunction>
| InfiniteData<{ example: boolean }, number>
| undefined
>()
})
it('should return a custom query key type', () => {
type MyQueryKey = [Array, { type: 'foo' }]
const options = infiniteQueryOptions({
queryKey: [['key'], { type: 'foo' }] as MyQueryKey,
queryFn: () => Promise.resolve(1),
getNextPageParam: () => 1,
initialPageParam: 1,
})
expectTypeOf(options.queryKey).toEqualTypeOf<
DataTag, Error>
>()
})
it('should return a custom query key type with datatag', () => {
type MyQueryKey = DataTag<
[Array, { type: 'foo' }],
number,
Error & { myMessage: string }
>
const options = infiniteQueryOptions({
queryKey: [['key'], { type: 'foo' }] as MyQueryKey,
queryFn: () => Promise.resolve(1),
getNextPageParam: () => 1,
initialPageParam: 1,
})
expectTypeOf(options.queryKey).toEqualTypeOf<
DataTag