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

Auto Refetch with stale-time set to {{ intervalMs() }}ms

This example is best experienced on your own machine, where you can open multiple tabs to the same localhost server and see your changes propagate between the two.

Todo List

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

{{ post.body }}

@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()) {
@for (post of postsQuery.data(); track post.id) {

{{ post.title }}

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

{{ post.body }}

@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()) {
@for (post of postsQuery.data(); track post.id) {

{{ post.title }}

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

@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.

@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 {
@for (page of query.data().pages; track $index) { @for (project of page.data; track project.id) {

{{ project.name }} {{ project.id }}

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

}
    @for (task of tasks.data(); track task) {
  • {{ task }}
  • }
Updated At: {{ tasks.dataUpdatedAt() | date: 'MMMM d, h:mm:ss a ' }}
@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 }}
@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 }}

{{ post.body }}

@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()) {
@for (post of postsQuery.data(); track post.id) {

{{ post.title }}

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

{{ post.body }}

@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()) {
@for (post of postsQuery.data(); track post.id) {

{{ post.title }}

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

@if (query.isSuccess() && query.data().suggestions.length) {
    @for (suggestion of query.data().suggestions; track suggestion) {
  • {{ suggestion }}
  • }
}
================================================ 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 ``` [![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/withastro/astro/tree/latest/examples/minimal) [![Open with CodeSandbox](https://assets.codesandbox.io/github/button-edit-lime.svg)](https://codesandbox.io/p/sandbox/github/withastro/astro/tree/latest/examples/minimal) [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](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)}
{properCase(pokemon.data?.name {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.
{(stat) => (
{stat.name}
{stat.value}
)}
) } 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} {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
================================================ 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 ( ) } 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.data?.body}

{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
================================================ 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 ( ) } 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.data?.body}

{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
================================================ 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 ( ) } 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.data.body}

{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
================================================ 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.title}
[deferStream={String(props.deferStream || false)}]
[simulated sleep: {props.sleep || 0}ms]
{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 (
{/* 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}
)} > {(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}
) } > {/* {props.errorFallback ? ( props.errorFallback ) : (
{props.query.error?.message}
)}
*/} {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}
)} > {(user) => ( <>
id: {user.id}
name: {user.name}
queryTime: {user.queryTime}
)}
) } ================================================ 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 (

{(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)

devtools deferred requests
) } ================================================ 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.

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)

devtools streaming request
) } ================================================ 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}
{/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 ================================================ ================================================ 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 ================================================ ================================================ FILE: examples/vue/2.6-basic/src/Posts.vue ================================================ ================================================ 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 ================================================ ================================================ 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 ================================================ ================================================ FILE: examples/vue/2.7-basic/src/Posts.vue ================================================ ================================================ 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 ================================================ ================================================ 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 ================================================ ================================================ FILE: examples/vue/basic/src/Posts.vue ================================================ ================================================ 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 ================================================ ================================================ 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 ================================================ ================================================ FILE: examples/vue/dependent-queries/src/Posts.vue ================================================ ================================================ 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 ================================================ ================================================ 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 ================================================ ================================================ 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 ================================================ ================================================ FILE: examples/vue/persister/src/Posts.vue ================================================ ================================================ 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 ================================================ ================================================ 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 ================================================ ![TanStack Query Header](https://github.com/TanStack/query/raw/main/media/repo-header.png) [![npm version](https://img.shields.io/npm/v/@tanstack/angular-query-experimental)](https://www.npmjs.com/package/@tanstack/angular-query-experimental) [![npm license](https://img.shields.io/npm/l/@tanstack/angular-query-experimental)](https://github.com/TanStack/query/blob/main/LICENSE) [![bundle size](https://img.shields.io/bundlephobia/minzip/@tanstack/angular-query-experimental)](https://bundlephobia.com/package/@tanstack/angular-query-experimental) [![npm](https://img.shields.io/npm/dm/@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, Error & { myMessage: string }> >() }) }) ================================================ FILE: packages/angular-query-experimental/src/__tests__/infinite-query-options.test.ts ================================================ import { describe, expect, it } from 'vitest' import { infiniteQueryOptions } from '../infinite-query-options' import type { CreateInfiniteQueryOptions } from '../types' describe('infiniteQueryOptions', () => { it('should return the object received as a parameter without any modification.', () => { const object: CreateInfiniteQueryOptions = { queryKey: ['key'], queryFn: () => Promise.resolve(5), getNextPageParam: () => null, initialPageParam: null, } expect(infiniteQueryOptions(object)).toStrictEqual(object) }) }) ================================================ FILE: packages/angular-query-experimental/src/__tests__/inject-devtools-panel.test.ts ================================================ import { ElementRef, provideZonelessChangeDetection, signal, } from '@angular/core' import { TestBed } from '@angular/core/testing' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { QueryClient } from '@tanstack/query-core' import { provideTanStackQuery } from '../providers' import { injectDevtoolsPanel } from '../devtools-panel' const mockDevtoolsPanelInstance = { mount: vi.fn(), unmount: vi.fn(), setClient: vi.fn(), setErrorTypes: vi.fn(), setOnClose: vi.fn(), } const mocks = vi.hoisted(() => { return { mockTanstackQueryDevtoolsPanel: vi.fn(() => mockDevtoolsPanelInstance), } }) vi.mock('@tanstack/query-devtools', () => ({ TanstackQueryDevtoolsPanel: mocks.mockTanstackQueryDevtoolsPanel, })) describe('injectDevtoolsPanel', () => { let queryClient: QueryClient let mockElementRef: ElementRef const waitForDevtoolsToBeCreated = async () => { await vi.waitFor(() => { expect(mocks.mockTanstackQueryDevtoolsPanel).toHaveBeenCalledTimes(1) }) } beforeEach(() => { queryClient = new QueryClient() mockElementRef = new ElementRef(document.createElement('div')) TestBed.configureTestingModule({ providers: [ provideZonelessChangeDetection(), provideTanStackQuery(queryClient), { provide: ElementRef, useValue: signal(mockElementRef) }, ], }) }) afterEach(() => { vi.restoreAllMocks() }) it('should return a DevtoolsPanelRef', () => { const result = TestBed.runInInjectionContext(() => { return injectDevtoolsPanel(() => ({ hostElement: TestBed.inject(ElementRef), })) }) expect(result).toEqual({ destroy: expect.any(Function), }) }) it('should initialize TanstackQueryDevtoolsPanel', async () => { TestBed.runInInjectionContext(() => { injectDevtoolsPanel(() => ({ hostElement: TestBed.inject(ElementRef), })) }) TestBed.tick() await waitForDevtoolsToBeCreated() expect(mockDevtoolsPanelInstance.mount).toHaveBeenCalledTimes(1) }) it('should destroy TanstackQueryDevtoolsPanel', async () => { const result = TestBed.runInInjectionContext(() => { return injectDevtoolsPanel(() => ({ hostElement: TestBed.inject(ElementRef), })) }) TestBed.tick() await waitForDevtoolsToBeCreated() result.destroy() expect(mockDevtoolsPanelInstance.unmount).toHaveBeenCalledTimes(1) }) it('should destroy TanstackQueryDevtoolsPanel when hostElement is removed', async () => { const hostElement = signal(mockElementRef) TestBed.runInInjectionContext(() => { return injectDevtoolsPanel(() => ({ hostElement: hostElement(), })) }) TestBed.tick() await waitForDevtoolsToBeCreated() expect(mockDevtoolsPanelInstance.unmount).toHaveBeenCalledTimes(0) hostElement.set(null as unknown as ElementRef) TestBed.tick() expect(mockDevtoolsPanelInstance.unmount).toHaveBeenCalledTimes(1) }) it('should update client', async () => { const client = signal(new QueryClient()) TestBed.runInInjectionContext(() => { return injectDevtoolsPanel(() => ({ hostElement: TestBed.inject(ElementRef), client: client(), })) }) TestBed.tick() await waitForDevtoolsToBeCreated() expect(mockDevtoolsPanelInstance.setClient).toHaveBeenCalledTimes(0) client.set(new QueryClient()) TestBed.tick() expect(mockDevtoolsPanelInstance.setClient).toHaveBeenCalledTimes(1) }) it('should update error types', async () => { const errorTypes = signal([]) TestBed.runInInjectionContext(() => { return injectDevtoolsPanel(() => ({ hostElement: TestBed.inject(ElementRef), errorTypes: errorTypes(), })) }) TestBed.tick() await waitForDevtoolsToBeCreated() expect(mockDevtoolsPanelInstance.setErrorTypes).toHaveBeenCalledTimes(0) errorTypes.set([]) TestBed.tick() expect(mockDevtoolsPanelInstance.setErrorTypes).toHaveBeenCalledTimes(1) }) it('should update onclose', async () => { const functionA = () => {} const functionB = () => {} const onClose = signal(functionA) TestBed.runInInjectionContext(() => { return injectDevtoolsPanel(() => ({ hostElement: TestBed.inject(ElementRef), onClose: onClose(), })) }) TestBed.tick() await waitForDevtoolsToBeCreated() expect(mockDevtoolsPanelInstance.setOnClose).toHaveBeenCalledTimes(0) onClose.set(functionB) TestBed.tick() expect(mockDevtoolsPanelInstance.setOnClose).toHaveBeenCalledTimes(1) }) }) ================================================ FILE: packages/angular-query-experimental/src/__tests__/inject-infinite-query.test-d.ts ================================================ import { TestBed } from '@angular/core/testing' import { afterEach, beforeEach, describe, expectTypeOf, test, vi } from 'vitest' import { provideZonelessChangeDetection } from '@angular/core' import { sleep } from '@tanstack/query-test-utils' import { QueryClient, injectInfiniteQuery, provideTanStackQuery } from '..' import type { InfiniteData } from '@tanstack/query-core' describe('injectInfiniteQuery', () => { let queryClient: QueryClient beforeEach(() => { queryClient = new QueryClient() vi.useFakeTimers() TestBed.configureTestingModule({ providers: [ provideZonelessChangeDetection(), provideTanStackQuery(queryClient), ], }) }) afterEach(() => { vi.useRealTimers() }) test('should narrow type after isSuccess', () => { const query = TestBed.runInInjectionContext(() => { return injectInfiniteQuery(() => ({ queryKey: ['infiniteQuery'], queryFn: ({ pageParam }) => sleep(0).then(() => 'data on page ' + pageParam), initialPageParam: 0, getNextPageParam: () => 12, })) }) if (query.isSuccess()) { const data = query.data() expectTypeOf(data).toEqualTypeOf>() } }) }) ================================================ FILE: packages/angular-query-experimental/src/__tests__/inject-infinite-query.test.ts ================================================ import { TestBed } from '@angular/core/testing' import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest' import { Injector, provideZonelessChangeDetection } from '@angular/core' import { sleep } from '@tanstack/query-test-utils' import { QueryClient, injectInfiniteQuery, provideTanStackQuery } from '..' import { expectSignals } from './test-utils' describe('injectInfiniteQuery', () => { let queryClient: QueryClient beforeEach(() => { queryClient = new QueryClient() vi.useFakeTimers() TestBed.configureTestingModule({ providers: [ provideZonelessChangeDetection(), provideTanStackQuery(queryClient), ], }) }) afterEach(() => { vi.useRealTimers() }) test('should properly execute infinite query', async () => { const query = TestBed.runInInjectionContext(() => { return injectInfiniteQuery(() => ({ queryKey: ['infiniteQuery'], queryFn: ({ pageParam }) => sleep(10).then(() => 'data on page ' + pageParam), initialPageParam: 0, getNextPageParam: () => 12, })) }) expectSignals(query, { data: undefined, status: 'pending', }) await vi.advanceTimersByTimeAsync(11) expectSignals(query, { data: { pageParams: [0], pages: ['data on page 0'], }, status: 'success', }) void query.fetchNextPage() await vi.advanceTimersByTimeAsync(11) expectSignals(query, { data: { pageParams: [0, 12], pages: ['data on page 0', 'data on page 12'], }, status: 'success', }) }) describe('injection context', () => { test('throws NG0203 with descriptive error outside injection context', () => { expect(() => { injectInfiniteQuery(() => ({ queryKey: ['injectionContextError'], queryFn: ({ pageParam }) => sleep(0).then(() => 'data on page ' + pageParam), initialPageParam: 0, getNextPageParam: () => 12, })) }).toThrowError(/NG0203(.*?)injectInfiniteQuery/) }) test('can be used outside injection context when passing an injector', () => { const query = injectInfiniteQuery( () => ({ queryKey: ['manualInjector'], queryFn: ({ pageParam }) => sleep(0).then(() => 'data on page ' + pageParam), initialPageParam: 0, getNextPageParam: () => 12, }), { injector: TestBed.inject(Injector), }, ) expect(query.status()).toBe('pending') }) }) }) ================================================ FILE: packages/angular-query-experimental/src/__tests__/inject-is-fetching.test.ts ================================================ import { TestBed } from '@angular/core/testing' import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest' import { Injector, provideZonelessChangeDetection } from '@angular/core' import { sleep } from '@tanstack/query-test-utils' import { QueryClient, injectIsFetching, injectQuery, provideTanStackQuery, } from '..' describe('injectIsFetching', () => { let queryClient: QueryClient beforeEach(() => { vi.useFakeTimers() queryClient = new QueryClient() TestBed.configureTestingModule({ providers: [ provideZonelessChangeDetection(), provideTanStackQuery(queryClient), ], }) }) afterEach(() => { vi.useRealTimers() }) test('Returns number of fetching queries', async () => { const isFetching = TestBed.runInInjectionContext(() => { injectQuery(() => ({ queryKey: ['isFetching1'], queryFn: () => sleep(100).then(() => 'Some data'), })) return injectIsFetching() }) expect(isFetching()).toStrictEqual(0) await vi.advanceTimersByTimeAsync(1) expect(isFetching()).toStrictEqual(1) await vi.advanceTimersByTimeAsync(100) expect(isFetching()).toStrictEqual(0) }) describe('injection context', () => { test('throws NG0203 with descriptive error outside injection context', () => { expect(() => { injectIsFetching() }).toThrowError(/NG0203(.*?)injectIsFetching/) }) test('can be used outside injection context when passing an injector', () => { expect( injectIsFetching(undefined, { injector: TestBed.inject(Injector), }), ).not.toThrow() }) }) }) ================================================ FILE: packages/angular-query-experimental/src/__tests__/inject-is-mutating.test.ts ================================================ import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest' import { TestBed } from '@angular/core/testing' import { Injector, provideZonelessChangeDetection } from '@angular/core' import { sleep } from '@tanstack/query-test-utils' import { QueryClient, injectIsMutating, injectMutation, provideTanStackQuery, } from '..' describe('injectIsMutating', () => { let queryClient: QueryClient beforeEach(() => { vi.useFakeTimers() queryClient = new QueryClient() TestBed.configureTestingModule({ providers: [ provideZonelessChangeDetection(), provideTanStackQuery(queryClient), ], }) }) afterEach(() => { vi.useRealTimers() }) test('should properly return isMutating state', async () => { const [mutation, isMutating] = TestBed.runInInjectionContext(() => [ injectMutation(() => ({ mutationKey: ['isMutating1'], mutationFn: (params: { par1: string }) => sleep(10).then(() => params), })), injectIsMutating(), ]) expect(isMutating()).toBe(0) mutation.mutate({ par1: 'par1', }) expect(isMutating()).toBe(0) await vi.advanceTimersByTimeAsync(0) expect(isMutating()).toBe(1) await vi.advanceTimersByTimeAsync(11) expect(isMutating()).toBe(0) }) describe('injection context', () => { test('throws NG0203 with descriptive error outside injection context', () => { expect(() => { injectIsMutating() }).toThrowError(/NG0203(.*?)injectIsMutating/) }) test('can be used outside injection context when passing an injector', () => { expect( injectIsMutating(undefined, { injector: TestBed.inject(Injector), }), ).not.toThrow() }) }) }) ================================================ FILE: packages/angular-query-experimental/src/__tests__/inject-mutation-state.test-d.ts ================================================ import { describe, expectTypeOf, it } from 'vitest' import { injectMutationState } from '..' import type { MutationState, MutationStatus } from '..' describe('injectMutationState', () => { it('should default to QueryState', () => { const result = injectMutationState(() => ({ filters: { status: 'pending' }, })) expectTypeOf(result()).toEqualTypeOf>() }) it('should infer with select', () => { const result = injectMutationState(() => ({ filters: { status: 'pending' }, select: (mutation) => mutation.state.status, })) expectTypeOf(result()).toEqualTypeOf>() }) }) ================================================ FILE: packages/angular-query-experimental/src/__tests__/inject-mutation-state.test.ts ================================================ import { Component, Injector, input, provideZonelessChangeDetection, signal, } from '@angular/core' import { TestBed } from '@angular/core/testing' import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest' import { By } from '@angular/platform-browser' import { sleep } from '@tanstack/query-test-utils' import { QueryClient, injectMutation, injectMutationState, provideTanStackQuery, } from '..' import { setFixtureSignalInputs } from './test-utils' describe('injectMutationState', () => { let queryClient: QueryClient beforeEach(() => { queryClient = new QueryClient() vi.useFakeTimers() TestBed.configureTestingModule({ providers: [ provideZonelessChangeDetection(), provideTanStackQuery(queryClient), ], }) }) afterEach(() => { vi.useRealTimers() }) describe('injectMutationState', () => { test('should return variables after calling mutate 1', () => { const mutationKey = ['mutation'] const variables = 'foo123' const mutation = TestBed.runInInjectionContext(() => { return injectMutation(() => ({ mutationKey: mutationKey, mutationFn: (params: string) => sleep(0).then(() => params), })) }) mutation.mutate(variables) const mutationState = TestBed.runInInjectionContext(() => { return injectMutationState(() => ({ filters: { mutationKey, status: 'pending' }, select: (m) => m.state.variables, })) }) expect(mutationState()).toEqual([variables]) }) test('reactive options should update injectMutationState', () => { const mutationKey1 = ['mutation1'] const mutationKey2 = ['mutation2'] const variables1 = 'foo123' const variables2 = 'bar234' const [mutation1, mutation2] = TestBed.runInInjectionContext(() => { return [ injectMutation(() => ({ mutationKey: mutationKey1, mutationFn: (params: string) => sleep(0).then(() => params), })), injectMutation(() => ({ mutationKey: mutationKey2, mutationFn: (params: string) => sleep(0).then(() => params), })), ] }) mutation1.mutate(variables1) mutation2.mutate(variables2) const filterKey = signal(mutationKey1) const mutationState = TestBed.runInInjectionContext(() => { return injectMutationState(() => ({ filters: { mutationKey: filterKey(), status: 'pending' }, select: (m) => m.state.variables, })) }) expect(mutationState()).toEqual([variables1]) filterKey.set(mutationKey2) expect(mutationState()).toEqual([variables2]) }) test('should return variables after calling mutate 2', () => { queryClient.clear() const mutationKey = ['mutation'] const variables = 'bar234' const mutation = TestBed.runInInjectionContext(() => { return injectMutation(() => ({ mutationKey: mutationKey, mutationFn: (params: string) => sleep(0).then(() => params), })) }) mutation.mutate(variables) const mutationState = TestBed.runInInjectionContext(() => { return injectMutationState() }) expect(mutationState()[0]?.variables).toEqual(variables) }) test('should support required signal inputs', async () => { queryClient.clear() const fakeName = 'name1' const mutationKey1 = ['fake', fakeName] const mutations = TestBed.runInInjectionContext(() => { return [ injectMutation(() => ({ mutationKey: mutationKey1, mutationFn: () => sleep(10).then(() => 'myValue'), })), injectMutation(() => ({ mutationKey: mutationKey1, mutationFn: () => sleep(10).then(() => Promise.reject(new Error('myValue2'))), })), ] }) mutations.forEach((mutation) => mutation.mutate()) @Component({ selector: 'app-fake', template: ` @for (mutation of mutationState(); track $index) { {{ mutation.status }} } `, }) class FakeComponent { name = input.required() mutationState = injectMutationState(() => ({ filters: { mutationKey: ['fake', this.name()], exact: true, }, })) } const fixture = TestBed.createComponent(FakeComponent) const { debugElement } = fixture setFixtureSignalInputs(fixture, { name: fakeName }) await vi.advanceTimersByTimeAsync(0) let spans = debugElement .queryAll(By.css('span')) .map((span) => span.nativeNode.textContent) expect(spans).toEqual(['pending', 'pending']) await vi.advanceTimersByTimeAsync(11) fixture.detectChanges() spans = debugElement .queryAll(By.css('span')) .map((span) => span.nativeNode.textContent) expect(spans).toEqual(['success', 'error']) }) describe('injection context', () => { test('throws NG0203 with descriptive error outside injection context', () => { expect(() => { injectMutationState() }).toThrowError(/NG0203(.*?)injectMutationState/) }) test('can be used outside injection context when passing an injector', () => { const injector = TestBed.inject(Injector) expect( injectMutationState(undefined, { injector, }), ).not.toThrow() }) }) }) }) ================================================ FILE: packages/angular-query-experimental/src/__tests__/inject-mutation.test-d.ts ================================================ import { describe, expectTypeOf, test } from 'vitest' import { sleep } from '@tanstack/query-test-utils' import { injectMutation } from '..' import type { Signal } from '@angular/core' describe('Discriminated union return type', () => { test('data should be possibly undefined by default', () => { const mutation = injectMutation(() => ({ mutationFn: () => sleep(0).then(() => 'string'), })) expectTypeOf(mutation.data).toEqualTypeOf>() }) test('data should be defined when mutation is success', () => { const mutation = injectMutation(() => ({ mutationFn: () => sleep(0).then(() => 'string'), })) if (mutation.isSuccess()) { expectTypeOf(mutation.data).toEqualTypeOf>() } }) test('error should be null when mutation is success', () => { const mutation = injectMutation(() => ({ mutationFn: () => sleep(0).then(() => 'string'), })) if (mutation.isSuccess()) { expectTypeOf(mutation.error).toEqualTypeOf>() } }) test('data should be undefined when mutation is pending', () => { const mutation = injectMutation(() => ({ mutationFn: () => sleep(0).then(() => 'string'), })) if (mutation.isPending()) { expectTypeOf(mutation.data).toEqualTypeOf>() } }) test('error should be defined when mutation is error', () => { const mutation = injectMutation(() => ({ mutationFn: () => sleep(0).then(() => 'string'), })) if (mutation.isError()) { expectTypeOf(mutation.error).toEqualTypeOf>() } }) test('should narrow variables', () => { const mutation = injectMutation(() => ({ mutationFn: (_variables: string) => sleep(0).then(() => 'string'), })) if (mutation.isIdle()) { expectTypeOf(mutation.variables).toEqualTypeOf>() } if (mutation.isPending()) { expectTypeOf(mutation.variables).toEqualTypeOf>() } if (mutation.isSuccess()) { expectTypeOf(mutation.variables).toEqualTypeOf>() } expectTypeOf(mutation.variables).toEqualTypeOf>() }) }) ================================================ FILE: packages/angular-query-experimental/src/__tests__/inject-mutation.test.ts ================================================ import { ApplicationRef, Component, Injector, input, provideZonelessChangeDetection, signal, } from '@angular/core' import { TestBed } from '@angular/core/testing' import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest' import { By } from '@angular/platform-browser' import { sleep } from '@tanstack/query-test-utils' import { QueryClient, injectMutation, provideTanStackQuery } from '..' import { expectSignals, setFixtureSignalInputs } from './test-utils' describe('injectMutation', () => { let queryClient: QueryClient beforeEach(() => { queryClient = new QueryClient() vi.useFakeTimers() TestBed.configureTestingModule({ providers: [ provideZonelessChangeDetection(), provideTanStackQuery(queryClient), ], }) }) afterEach(() => { vi.useRealTimers() }) test('should be in idle state initially', () => { const mutation = TestBed.runInInjectionContext(() => { return injectMutation(() => ({ mutationFn: (params) => sleep(0).then(() => params), })) }) expectSignals(mutation, { isIdle: true, isPending: false, isError: false, isSuccess: false, }) }) test('should change state after invoking mutate', async () => { const result = 'Mock data' const mutation = TestBed.runInInjectionContext(() => { return injectMutation(() => ({ mutationFn: (params: string) => sleep(10).then(() => params), })) }) TestBed.tick() mutation.mutate(result) await vi.advanceTimersByTimeAsync(0) expectSignals(mutation, { isIdle: false, isPending: true, isError: false, isSuccess: false, data: undefined, error: null, }) }) test('should return error when request fails', async () => { const mutation = TestBed.runInInjectionContext(() => { return injectMutation(() => ({ mutationFn: () => sleep(10).then(() => Promise.reject(new Error('Some error'))), })) }) mutation.mutate() await vi.advanceTimersByTimeAsync(11) expectSignals(mutation, { isIdle: false, isPending: false, isError: true, isSuccess: false, data: undefined, error: Error('Some error'), }) }) test('should return data when request succeeds', async () => { const result = 'Mock data' const mutation = TestBed.runInInjectionContext(() => { return injectMutation(() => ({ mutationFn: (params: string) => sleep(10).then(() => params), })) }) mutation.mutate(result) await vi.advanceTimersByTimeAsync(11) expectSignals(mutation, { isIdle: false, isPending: false, isError: false, isSuccess: true, data: result, error: null, }) }) test('reactive options should update mutation', () => { const mutationCache = queryClient.getMutationCache() // Signal will be updated before the mutation is called // this test confirms that the mutation uses the updated value const mutationKey = signal(['1']) const mutation = TestBed.runInInjectionContext(() => { return injectMutation(() => ({ mutationKey: mutationKey(), mutationFn: (params: string) => sleep(0).then(() => params), })) }) mutationKey.set(['2']) mutation.mutate('xyz') const mutations = mutationCache.find({ mutationKey: ['2'] }) expect(mutations?.options.mutationKey).toEqual(['2']) }) test('should reset state after invoking mutation.reset', async () => { const mutation = TestBed.runInInjectionContext(() => { return injectMutation(() => ({ mutationFn: () => sleep(10).then(() => Promise.reject(new Error('Some error'))), })) }) mutation.mutate() await vi.advanceTimersByTimeAsync(11) expect(mutation.isError()).toBe(true) mutation.reset() await vi.advanceTimersByTimeAsync(0) expectSignals(mutation, { isIdle: true, isPending: false, isError: false, isSuccess: false, data: undefined, error: null, }) }) describe('side effects', () => { beforeEach(() => { vi.clearAllMocks() }) test('should call onMutate when passed as an option', async () => { const onMutate = vi.fn() const mutation = TestBed.runInInjectionContext(() => { return injectMutation(() => ({ mutationFn: (params: string) => sleep(10).then(() => params), onMutate, })) }) mutation.mutate('') await vi.advanceTimersByTimeAsync(0) expect(onMutate).toHaveBeenCalledTimes(1) }) test('should call onError when passed as an option', async () => { const onError = vi.fn() const mutation = TestBed.runInInjectionContext(() => { return injectMutation(() => ({ mutationFn: (_params: string) => sleep(10).then(() => Promise.reject(new Error('Some error'))), onError, })) }) mutation.mutate('') await vi.advanceTimersByTimeAsync(11) expect(onError).toHaveBeenCalledTimes(1) }) test('should call onSuccess when passed as an option', async () => { const onSuccess = vi.fn() const mutation = TestBed.runInInjectionContext(() => { return injectMutation(() => ({ mutationFn: (params: string) => sleep(10).then(() => params), onSuccess, })) }) mutation.mutate('') await vi.advanceTimersByTimeAsync(11) expect(onSuccess).toHaveBeenCalledTimes(1) }) test('should call onSettled when passed as an option', async () => { const onSettled = vi.fn() const mutation = TestBed.runInInjectionContext(() => { return injectMutation(() => ({ mutationFn: (params: string) => sleep(10).then(() => params), onSettled, })) }) mutation.mutate('') await vi.advanceTimersByTimeAsync(11) expect(onSettled).toHaveBeenCalledTimes(1) }) test('should call onError when passed as an argument of mutate function', async () => { const onError = vi.fn() const mutation = TestBed.runInInjectionContext(() => { return injectMutation(() => ({ mutationFn: (_params: string) => sleep(10).then(() => Promise.reject(new Error('Some error'))), })) }) mutation.mutate('', { onError }) await vi.advanceTimersByTimeAsync(11) expect(onError).toHaveBeenCalledTimes(1) }) test('should call onSuccess when passed as an argument of mutate function', async () => { const onSuccess = vi.fn() const mutation = TestBed.runInInjectionContext(() => { return injectMutation(() => ({ mutationFn: (params: string) => sleep(10).then(() => params), })) }) mutation.mutate('', { onSuccess }) await vi.advanceTimersByTimeAsync(11) expect(onSuccess).toHaveBeenCalledTimes(1) }) test('should call onSettled when passed as an argument of mutate function', async () => { const onSettled = vi.fn() const mutation = TestBed.runInInjectionContext(() => { return injectMutation(() => ({ mutationFn: (params: string) => sleep(10).then(() => params), })) }) mutation.mutate('', { onSettled }) await vi.advanceTimersByTimeAsync(11) expect(onSettled).toHaveBeenCalledTimes(1) }) test('should fire both onSettled functions', async () => { const onSettled = vi.fn() const onSettledOnFunction = vi.fn() const mutation = TestBed.runInInjectionContext(() => { return injectMutation(() => ({ mutationFn: (params: string) => sleep(10).then(() => params), onSettled, })) }) mutation.mutate('', { onSettled: onSettledOnFunction }) await vi.advanceTimersByTimeAsync(11) expect(onSettled).toHaveBeenCalledTimes(1) expect(onSettledOnFunction).toHaveBeenCalledTimes(1) }) }) test('should support required signal inputs', async () => { const mutationCache = queryClient.getMutationCache() @Component({ selector: 'app-fake', template: ` {{ mutation.data() }} `, }) class FakeComponent { name = input.required() mutation = injectMutation(() => ({ mutationKey: ['fake', this.name()], mutationFn: () => sleep(10).then(() => this.name()), })) mutate(): void { this.mutation.mutate() } } const fixture = TestBed.createComponent(FakeComponent) const { debugElement } = fixture setFixtureSignalInputs(fixture, { name: 'value' }) const button = debugElement.query(By.css('button')) button.triggerEventHandler('click') await vi.advanceTimersByTimeAsync(11) fixture.detectChanges() const text = debugElement.query(By.css('span')).nativeElement.textContent expect(text).toEqual('value') const mutation = mutationCache.find({ mutationKey: ['fake', 'value'] }) expect(mutation).toBeDefined() expect(mutation!.options.mutationKey).toStrictEqual(['fake', 'value']) }) test('should update options on required signal input change', async () => { const mutationCache = queryClient.getMutationCache() @Component({ selector: 'app-fake', template: ` {{ mutation.data() }} `, }) class FakeComponent { name = input.required() mutation = injectMutation(() => ({ mutationKey: ['fake', this.name()], mutationFn: () => sleep(10).then(() => this.name()), })) mutate(): void { this.mutation.mutate() } } const fixture = TestBed.createComponent(FakeComponent) const { debugElement } = fixture setFixtureSignalInputs(fixture, { name: 'value' }) const button = debugElement.query(By.css('button')) const span = debugElement.query(By.css('span')) button.triggerEventHandler('click') await vi.advanceTimersByTimeAsync(11) fixture.detectChanges() expect(span.nativeElement.textContent).toEqual('value') setFixtureSignalInputs(fixture, { name: 'updatedValue' }) button.triggerEventHandler('click') await vi.advanceTimersByTimeAsync(11) fixture.detectChanges() expect(span.nativeElement.textContent).toEqual('updatedValue') const mutations = mutationCache.findAll() expect(mutations.length).toBe(2) const [mutation1, mutation2] = mutations expect(mutation1!.options.mutationKey).toEqual(['fake', 'value']) expect(mutation2!.options.mutationKey).toEqual(['fake', 'updatedValue']) }) describe('throwOnError', () => { test('should evaluate throwOnError when mutation is expected to throw', async () => { const err = new Error('Expected mock error. All is well!') const boundaryFn = vi.fn() const { mutate } = TestBed.runInInjectionContext(() => { return injectMutation(() => ({ mutationKey: ['fake'], mutationFn: () => { return Promise.reject(err) }, throwOnError: boundaryFn, })) }) TestBed.tick() mutate() await vi.advanceTimersByTimeAsync(0) expect(boundaryFn).toHaveBeenCalledTimes(1) expect(boundaryFn).toHaveBeenCalledWith(err) }) }) test('should throw when throwOnError is true', async () => { const err = new Error('Expected mock error. All is well!') const { mutateAsync } = TestBed.runInInjectionContext(() => { return injectMutation(() => ({ mutationKey: ['fake'], mutationFn: () => { return Promise.reject(err) }, throwOnError: true, })) }) await expect(() => mutateAsync()).rejects.toThrowError(err) }) test('should throw when throwOnError function returns true', async () => { const err = new Error('Expected mock error. All is well!') const { mutateAsync } = TestBed.runInInjectionContext(() => { return injectMutation(() => ({ mutationKey: ['fake'], mutationFn: () => { return Promise.reject(err) }, throwOnError: () => true, })) }) await expect(() => mutateAsync()).rejects.toThrowError(err) }) describe('injection context', () => { test('throws NG0203 with descriptive error outside injection context', () => { expect(() => { injectMutation(() => ({ mutationKey: ['injectionContextError'], mutationFn: () => Promise.resolve(), })) }).toThrowError(/NG0203(.*?)injectMutation/) }) test('can be used outside injection context when passing an injector', () => { expect(() => { injectMutation( () => ({ mutationKey: ['injectionContextError'], mutationFn: () => Promise.resolve(), }), { injector: TestBed.inject(Injector), }, ) }).not.toThrow() }) test('should complete mutation before whenStable() resolves', async () => { const app = TestBed.inject(ApplicationRef) let mutationStarted = false let mutationCompleted = false const mutation = TestBed.runInInjectionContext(() => injectMutation(() => ({ mutationKey: ['pendingTasksTest'], mutationFn: async (data: string) => { mutationStarted = true await sleep(50) mutationCompleted = true return `processed: ${data}` }, })), ) // Initial state expect(mutation.data()).toBeUndefined() expect(mutationStarted).toBe(false) // Start mutation mutation.mutate('test') // Wait for mutation to start and Angular to be "stable" const stablePromise = app.whenStable() await vi.advanceTimersByTimeAsync(60) await stablePromise // After whenStable(), mutation should be complete expect(mutationStarted).toBe(true) expect(mutationCompleted).toBe(true) expect(mutation.isSuccess()).toBe(true) expect(mutation.data()).toBe('processed: test') }) test('should handle synchronous mutation with retry', async () => { TestBed.resetTestingModule() TestBed.configureTestingModule({ providers: [ provideZonelessChangeDetection(), provideTanStackQuery(queryClient), ], }) const app = TestBed.inject(ApplicationRef) let attemptCount = 0 const mutation = TestBed.runInInjectionContext(() => injectMutation(() => ({ retry: 2, retryDelay: 0, // No delay for synchronous retry mutationFn: async (data: string) => { attemptCount++ if (attemptCount <= 2) { throw new Error(`Sync attempt ${attemptCount} failed`) } return `processed: ${data}` }, })), ) // Start mutation mutation.mutate('retry-test') // Synchronize pending effects for each retry attempt TestBed.tick() await Promise.resolve() await vi.advanceTimersByTimeAsync(10) TestBed.tick() await Promise.resolve() await vi.advanceTimersByTimeAsync(10) TestBed.tick() const stablePromise = app.whenStable() await Promise.resolve() await vi.advanceTimersByTimeAsync(10) await stablePromise expect(mutation.isSuccess()).toBe(true) expect(mutation.data()).toBe('processed: retry-test') expect(attemptCount).toBe(3) // Initial + 2 retries }) test('should handle multiple synchronous mutations on same key', async () => { TestBed.resetTestingModule() TestBed.configureTestingModule({ providers: [ provideZonelessChangeDetection(), provideTanStackQuery(queryClient), ], }) const app = TestBed.inject(ApplicationRef) let callCount = 0 const mutation1 = TestBed.runInInjectionContext(() => injectMutation(() => ({ mutationKey: ['sync-mutation-key'], mutationFn: async (data: string) => { callCount++ return `mutation1: ${data}` }, })), ) const mutation2 = TestBed.runInInjectionContext(() => injectMutation(() => ({ mutationKey: ['sync-mutation-key'], mutationFn: async (data: string) => { callCount++ return `mutation2: ${data}` }, })), ) // Start both mutations mutation1.mutate('test1') mutation2.mutate('test2') // Synchronize pending effects TestBed.tick() const stablePromise = app.whenStable() // Flush microtasks to allow TanStack Query's scheduled notifications to process await Promise.resolve() await vi.advanceTimersByTimeAsync(1) await stablePromise expect(mutation1.isSuccess()).toBe(true) expect(mutation1.data()).toBe('mutation1: test1') expect(mutation2.isSuccess()).toBe(true) expect(mutation2.data()).toBe('mutation2: test2') expect(callCount).toBe(2) }) test('should handle synchronous mutation with optimistic updates', async () => { TestBed.resetTestingModule() TestBed.configureTestingModule({ providers: [ provideZonelessChangeDetection(), provideTanStackQuery(queryClient), ], }) const app = TestBed.inject(ApplicationRef) const testQueryKey = ['sync-optimistic'] let onMutateCalled = false let onSuccessCalled = false // Set initial data queryClient.setQueryData(testQueryKey, 'initial') const mutation = TestBed.runInInjectionContext(() => injectMutation(() => ({ mutationFn: async (data: string) => `final: ${data}`, // Synchronous resolution onMutate: async (variables) => { onMutateCalled = true const previousData = queryClient.getQueryData(testQueryKey) queryClient.setQueryData(testQueryKey, `optimistic: ${variables}`) return { previousData } }, onSuccess: (data) => { onSuccessCalled = true queryClient.setQueryData(testQueryKey, data) }, })), ) // Start mutation mutation.mutate('test') // Synchronize pending effects TestBed.tick() const stablePromise = app.whenStable() // Flush microtasks to allow TanStack Query's scheduled notifications to process await Promise.resolve() await vi.advanceTimersByTimeAsync(1) await stablePromise expect(onMutateCalled).toBe(true) expect(onSuccessCalled).toBe(true) expect(mutation.isSuccess()).toBe(true) expect(mutation.data()).toBe('final: test') expect(queryClient.getQueryData(testQueryKey)).toBe('final: test') }) test('should handle synchronous mutation cancellation', async () => { TestBed.resetTestingModule() TestBed.configureTestingModule({ providers: [ provideZonelessChangeDetection(), provideTanStackQuery(queryClient), ], }) const app = TestBed.inject(ApplicationRef) const mutation = TestBed.runInInjectionContext(() => injectMutation(() => ({ mutationKey: ['cancel-sync'], mutationFn: async (data: string) => `processed: ${data}`, // Synchronous resolution })), ) // Start mutation mutation.mutate('test') // Synchronize pending effects TestBed.tick() const stablePromise = app.whenStable() // Flush microtasks to allow TanStack Query's scheduled notifications to process await Promise.resolve() await vi.advanceTimersByTimeAsync(1) await stablePromise // Synchronous mutations complete immediately expect(mutation.isSuccess()).toBe(true) expect(mutation.data()).toBe('processed: test') }) }) }) ================================================ FILE: packages/angular-query-experimental/src/__tests__/inject-queries.test-d.ts ================================================ import { describe, expectTypeOf, it } from 'vitest' import { skipToken } from '..' import { injectQueries } from '../inject-queries' import { queryOptions } from '../query-options' import type { CreateQueryOptions, CreateQueryResult, OmitKeyof } from '..' import type { Signal } from '@angular/core' describe('InjectQueries config object overload', () => { it('TData should always be defined when initialData is provided as an object', () => { const query1 = { queryKey: ['key1'], queryFn: () => { return { wow: true, } }, initialData: { wow: false, }, } const query2 = { queryKey: ['key2'], queryFn: () => 'Query Data', initialData: 'initial data', } const query3 = { queryKey: ['key2'], queryFn: () => 'Query Data', } const queryResults = injectQueries(() => ({ queries: [query1, query2, query3], })) const query1Data = queryResults()[0].data() const query2Data = queryResults()[1].data() const query3Data = queryResults()[2].data() expectTypeOf(query1Data).toEqualTypeOf<{ wow: boolean }>() expectTypeOf(query2Data).toEqualTypeOf() expectTypeOf(query3Data).toEqualTypeOf() }) it('TData should be defined when passed through queryOptions', () => { const options = queryOptions({ queryKey: ['key'], queryFn: () => { return { wow: true, } }, initialData: { wow: true, }, }) const queryResults = injectQueries(() => ({ queries: [options] })) const data = queryResults()[0].data() expectTypeOf(data).toEqualTypeOf<{ wow: boolean }>() }) it('should be possible to define a different TData than TQueryFnData using select with queryOptions spread into injectQuery', () => { const query1 = queryOptions({ queryKey: ['key'], queryFn: () => Promise.resolve(1), select: (data) => data > 1, }) const query2 = { queryKey: ['key'], queryFn: () => Promise.resolve(1), select: (data: number) => data > 1, } const queryResults = injectQueries(() => ({ queries: [query1, query2] })) const query1Data = queryResults()[0].data() const query2Data = queryResults()[1].data() expectTypeOf(query1Data).toEqualTypeOf() expectTypeOf(query2Data).toEqualTypeOf() }) it('TData should have undefined in the union when initialData is provided as a function which can return undefined', () => { const queryResults = injectQueries(() => ({ queries: [ { queryKey: ['key'], queryFn: () => { return { wow: true, } }, initialData: () => undefined as { wow: boolean } | undefined, }, ], })) const data = queryResults()[0].data() expectTypeOf(data).toEqualTypeOf<{ wow: boolean } | undefined>() }) describe('custom injectable', () => { it('should allow custom hooks using UseQueryOptions', () => { type Data = string const injectCustomQueries = ( options?: OmitKeyof, 'queryKey' | 'queryFn'>, ) => { return injectQueries(() => ({ queries: [ { ...options, queryKey: ['todos-key'], queryFn: () => Promise.resolve('data'), }, ], })) } const queryResults = injectCustomQueries() const data = queryResults()[0].data() expectTypeOf(data).toEqualTypeOf() }) }) it('TData should have correct type when conditional skipToken is passed', () => { const queryResults = injectQueries(() => ({ queries: [ { queryKey: ['withSkipToken'], queryFn: Math.random() > 0.5 ? skipToken : () => Promise.resolve(5), }, ], })) const firstResult = queryResults()[0] expectTypeOf(firstResult).toEqualTypeOf>() expectTypeOf(firstResult.data()).toEqualTypeOf() }) it('should return correct data for dynamic queries with mixed result types', () => { const Queries1 = { get: () => queryOptions({ queryKey: ['key1'], queryFn: () => Promise.resolve(1), }), } const Queries2 = { get: () => queryOptions({ queryKey: ['key2'], queryFn: () => Promise.resolve(true), }), } const queries1List = [1, 2, 3].map(() => ({ ...Queries1.get() })) const result = injectQueries(() => ({ queries: [...queries1List, { ...Queries2.get() }], })) expectTypeOf(result).branded.toEqualTypeOf< Signal< [ ...Array>, CreateQueryResult, ] > >() }) }) ================================================ FILE: packages/angular-query-experimental/src/__tests__/inject-queries.test.ts ================================================ import { beforeEach, describe, expect, it } from 'vitest' import { render } from '@testing-library/angular' import { Component, effect, provideZonelessChangeDetection, } from '@angular/core' import { TestBed } from '@angular/core/testing' import { queryKey } from '@tanstack/query-test-utils' import { QueryClient, provideTanStackQuery } from '..' import { injectQueries } from '../inject-queries' let queryClient: QueryClient beforeEach(() => { queryClient = new QueryClient() TestBed.configureTestingModule({ providers: [ provideZonelessChangeDetection(), provideTanStackQuery(queryClient), ], }) }) describe('injectQueries', () => { it('should return the correct states', async () => { const key1 = queryKey() const key2 = queryKey() const results: Array>> = [] @Component({ template: `
data1: {{ result()[0].data() ?? 'null' }}, data2: {{ result()[1].data() ?? 'null' }}
`, }) class Page { toString(val: any) { return String(val) } result = injectQueries(() => ({ queries: [ { queryKey: key1, queryFn: async () => { await new Promise((r) => setTimeout(r, 10)) return 1 }, }, { queryKey: key2, queryFn: async () => { await new Promise((r) => setTimeout(r, 100)) return 2 }, }, ], })) _pushResults = effect(() => { const snapshot = this.result().map((q) => ({ data: q.data() })) results.push(snapshot) }) } const rendered = await render(Page) await rendered.findByText('data1: 1, data2: 2') expect(results.length).toBe(3) expect(results[0]).toMatchObject([{ data: undefined }, { data: undefined }]) expect(results[1]).toMatchObject([{ data: 1 }, { data: undefined }]) expect(results[2]).toMatchObject([{ data: 1 }, { data: 2 }]) }) }) ================================================ FILE: packages/angular-query-experimental/src/__tests__/inject-query.test-d.ts ================================================ import { describe, expectTypeOf, it, test } from 'vitest' import { sleep } from '@tanstack/query-test-utils' import { injectQuery, queryOptions } from '..' import type { Signal } from '@angular/core' describe('initialData', () => { describe('Config object overload', () => { it('TData should always be defined when initialData is provided as an object', () => { const { data } = injectQuery(() => ({ queryKey: ['key'], queryFn: () => ({ wow: true }), initialData: { wow: true }, })) expectTypeOf(data).toEqualTypeOf>() }) it('TData should be defined when passed through queryOptions', () => { const options = () => queryOptions({ queryKey: ['key'], queryFn: () => { return { wow: true, } }, initialData: { wow: true, }, }) const { data } = injectQuery(options) expectTypeOf(data).toEqualTypeOf>() }) it('should be possible to define a different TData than TQueryFnData using select with queryOptions spread into useQuery', () => { const options = queryOptions({ queryKey: ['key'], queryFn: () => Promise.resolve(1), }) const query = injectQuery(() => ({ ...options, select: (data) => data > 1, })) expectTypeOf(query.data).toEqualTypeOf>() }) it('TData should always be defined when initialData is provided as a function which ALWAYS returns the data', () => { const { data } = injectQuery(() => ({ queryKey: ['key'], queryFn: () => { return { wow: true, } }, initialData: () => ({ wow: true, }), })) expectTypeOf(data).toEqualTypeOf>() }) it('TData should have undefined in the union when initialData is NOT provided', () => { const { data } = injectQuery(() => ({ queryKey: ['key'], queryFn: () => { return { wow: true, } }, })) expectTypeOf(data).toEqualTypeOf>() }) it('TData should have undefined in the union when initialData is provided as a function which can return undefined', () => { const { data } = injectQuery(() => ({ queryKey: ['key'], queryFn: () => { return { wow: true, } }, initialData: () => undefined as { wow: boolean } | undefined, })) expectTypeOf(data).toEqualTypeOf>() }) it('TData should be narrowed after an isSuccess check when initialData is provided as a function which can return undefined', () => { const query = injectQuery(() => ({ queryKey: ['key'], queryFn: () => { return { wow: true, } }, initialData: () => undefined as { wow: boolean } | undefined, })) if (query.isSuccess()) { expectTypeOf(query.data).toEqualTypeOf>() } }) }) describe('structuralSharing', () => { it('should be able to use structuralSharing with unknown types', () => { // https://github.com/TanStack/query/issues/6525#issuecomment-1938411343 injectQuery(() => ({ queryKey: ['key'], queryFn: () => 5, structuralSharing: (oldData, newData) => { expectTypeOf(oldData).toBeUnknown() expectTypeOf(newData).toBeUnknown() return newData }, })) }) }) }) describe('Discriminated union return type', () => { test('data should be possibly undefined by default', () => { const query = injectQuery(() => ({ queryKey: ['key'], queryFn: () => sleep(0).then(() => 'Some data'), })) expectTypeOf(query.data).toEqualTypeOf>() }) test('data should be defined when query is success', () => { const query = injectQuery(() => ({ queryKey: ['key'], queryFn: () => sleep(0).then(() => 'Some data'), })) if (query.isSuccess()) { expectTypeOf(query.data).toEqualTypeOf>() } }) test('error should be null when query is success', () => { const query = injectQuery(() => ({ queryKey: ['key'], queryFn: () => sleep(0).then(() => 'Some data'), })) if (query.isSuccess()) { expectTypeOf(query.error).toEqualTypeOf>() } }) test('data should be undefined when query is pending', () => { const query = injectQuery(() => ({ queryKey: ['key'], queryFn: () => sleep(0).then(() => 'Some data'), })) if (query.isPending()) { expectTypeOf(query.data).toEqualTypeOf>() } }) test('error should be defined when query is error', () => { const query = injectQuery(() => ({ queryKey: ['key'], queryFn: () => sleep(0).then(() => 'Some data'), })) if (query.isError()) { expectTypeOf(query.error).toEqualTypeOf>() } }) }) ================================================ FILE: packages/angular-query-experimental/src/__tests__/inject-query.test.ts ================================================ import { ApplicationRef, Component, Injector, computed, effect, input, provideZonelessChangeDetection, signal, } from '@angular/core' import { TestBed } from '@angular/core/testing' import { HttpClient, provideHttpClient } from '@angular/common/http' import { HttpTestingController, provideHttpClientTesting, } from '@angular/common/http/testing' import { afterEach, beforeEach, describe, expect, expectTypeOf, test, vi, } from 'vitest' import { queryKey, sleep } from '@tanstack/query-test-utils' import { lastValueFrom } from 'rxjs' import { QueryCache, QueryClient, injectQuery, provideTanStackQuery } from '..' import { setSignalInputs } from './test-utils' import type { CreateQueryOptions, OmitKeyof, QueryFunction } from '..' describe('injectQuery', () => { let queryCache: QueryCache let queryClient: QueryClient beforeEach(() => { vi.useFakeTimers() queryCache = new QueryCache() queryClient = new QueryClient({ queryCache }) TestBed.configureTestingModule({ providers: [ provideZonelessChangeDetection(), provideTanStackQuery(queryClient), ], }) }) afterEach(() => { vi.useRealTimers() }) test('should return the correct types', () => { const key = queryKey() // unspecified query function should default to unknown const noQueryFn = TestBed.runInInjectionContext(() => injectQuery(() => ({ queryKey: key, })), ) expectTypeOf(noQueryFn.data()).toEqualTypeOf() expectTypeOf(noQueryFn.error()).toEqualTypeOf() // it should infer the result type from the query function const fromQueryFn = TestBed.runInInjectionContext(() => injectQuery(() => ({ queryKey: key, queryFn: () => 'test', })), ) expectTypeOf(fromQueryFn.data()).toEqualTypeOf() expectTypeOf(fromQueryFn.error()).toEqualTypeOf() // it should be possible to specify the result type const withResult = TestBed.runInInjectionContext(() => injectQuery(() => ({ queryKey: key, queryFn: () => 'test', })), ) expectTypeOf(withResult.data()).toEqualTypeOf() expectTypeOf(withResult.error()).toEqualTypeOf() // it should be possible to specify the error type type CustomErrorType = { message: string } const withError = TestBed.runInInjectionContext(() => injectQuery(() => ({ queryKey: key, queryFn: () => 'test', })), ) expectTypeOf(withError.data()).toEqualTypeOf() expectTypeOf(withError.error()).toEqualTypeOf() // it should infer the result type from the configuration const withResultInfer = TestBed.runInInjectionContext(() => injectQuery(() => ({ queryKey: key, queryFn: () => true, })), ) expectTypeOf(withResultInfer.data()).toEqualTypeOf() expectTypeOf(withResultInfer.error()).toEqualTypeOf() // it should be possible to specify a union type as result type const unionTypeSync = TestBed.runInInjectionContext(() => injectQuery(() => ({ queryKey: key, queryFn: () => (Math.random() > 0.5 ? ('a' as const) : ('b' as const)), })), ) expectTypeOf(unionTypeSync.data()).toEqualTypeOf<'a' | 'b' | undefined>() const unionTypeAsync = TestBed.runInInjectionContext(() => injectQuery<'a' | 'b'>(() => ({ queryKey: key, queryFn: () => Promise.resolve(Math.random() > 0.5 ? 'a' : 'b'), })), ) expectTypeOf(unionTypeAsync.data()).toEqualTypeOf<'a' | 'b' | undefined>() // it should error when the query function result does not match with the specified type TestBed.runInInjectionContext(() => // @ts-expect-error injectQuery(() => ({ queryKey: key, queryFn: () => 'test' })), ) // it should infer the result type from a generic query function /** * */ function queryFn(): Promise { return Promise.resolve({} as T) } const fromGenericQueryFn = TestBed.runInInjectionContext(() => injectQuery(() => ({ queryKey: key, queryFn: () => queryFn(), })), ) expectTypeOf(fromGenericQueryFn.data()).toEqualTypeOf() expectTypeOf(fromGenericQueryFn.error()).toEqualTypeOf() // todo use query options? const fromGenericOptionsQueryFn = TestBed.runInInjectionContext(() => injectQuery(() => ({ queryKey: key, queryFn: () => queryFn(), })), ) expectTypeOf(fromGenericOptionsQueryFn.data()).toEqualTypeOf< string | undefined >() expectTypeOf( fromGenericOptionsQueryFn.error(), ).toEqualTypeOf() type MyData = number type MyQueryKey = readonly ['my-data', number] const getMyDataArrayKey: QueryFunction = ({ queryKey: [, n], }) => { return n + 42 } const fromMyDataArrayKeyQueryFn = TestBed.runInInjectionContext(() => injectQuery(() => ({ queryKey: ['my-data', 100] as const, queryFn: getMyDataArrayKey, })), ) expectTypeOf(fromMyDataArrayKeyQueryFn.data()).toEqualTypeOf< number | undefined >() // it should handle query-functions that return Promise const fromPromiseAnyQueryFn = TestBed.runInInjectionContext(() => injectQuery(() => ({ queryKey: key, queryFn: () => fetch('return Promise').then((resp) => resp.json()), })), ) expectTypeOf(fromPromiseAnyQueryFn.data()).toEqualTypeOf() TestBed.runInInjectionContext(() => effect(() => { if (fromPromiseAnyQueryFn.isSuccess()) { expect(fromMyDataArrayKeyQueryFn.data()).toBe(142) } }), ) const getMyDataStringKey: QueryFunction = (context) => { expectTypeOf(context.queryKey).toEqualTypeOf<['1']>() return Number(context.queryKey[0]) + 42 } const fromGetMyDataStringKeyQueryFn = TestBed.runInInjectionContext(() => injectQuery(() => ({ queryKey: ['1'] as ['1'], queryFn: getMyDataStringKey, })), ) expectTypeOf(fromGetMyDataStringKeyQueryFn.data()).toEqualTypeOf< number | undefined >() TestBed.runInInjectionContext(() => effect(() => { if (fromGetMyDataStringKeyQueryFn.isSuccess()) { expect(fromGetMyDataStringKeyQueryFn.data()).toBe(43) } }), ) // handles wrapped queries with custom fetcher passed as inline queryFn const createWrappedQuery = < TQueryKey extends [string, Record?], TQueryFnData, TError, TData = TQueryFnData, >( qk: TQueryKey, fetcher: ( obj: TQueryKey[1], token: string, // return type must be wrapped with TQueryFnReturn ) => Promise, options?: OmitKeyof< CreateQueryOptions, 'queryKey' | 'queryFn' | 'initialData', 'safely' >, ) => injectQuery(() => ({ queryKey: qk, queryFn: () => fetcher(qk[1], 'token'), ...options, })) const fromWrappedQuery = TestBed.runInInjectionContext(() => createWrappedQuery([''], () => Promise.resolve('1')), ) expectTypeOf(fromWrappedQuery.data()).toEqualTypeOf() // handles wrapped queries with custom fetcher passed directly to createQuery const createWrappedFuncStyleQuery = < TQueryKey extends [string, Record?], TQueryFnData, TError, TData = TQueryFnData, >( qk: TQueryKey, fetcher: () => Promise, options?: OmitKeyof< CreateQueryOptions, 'queryKey' | 'queryFn' | 'initialData', 'safely' >, ) => injectQuery(() => ({ queryKey: qk, queryFn: fetcher, ...options })) const fromWrappedFuncStyleQuery = TestBed.runInInjectionContext(() => createWrappedFuncStyleQuery([''], () => Promise.resolve(true)), ) expectTypeOf(fromWrappedFuncStyleQuery.data()).toEqualTypeOf< boolean | undefined >() }) test('should return pending status initially', () => { const query = TestBed.runInInjectionContext(() => { return injectQuery(() => ({ queryKey: ['key1'], queryFn: () => sleep(10).then(() => 'Some data'), })) }) expect(query.status()).toBe('pending') expect(query.isPending()).toBe(true) expect(query.isFetching()).toBe(true) expect(query.isStale()).toBe(true) expect(query.isFetched()).toBe(false) }) test('should resolve to success and update signal: injectQuery()', async () => { const query = TestBed.runInInjectionContext(() => { return injectQuery(() => ({ queryKey: ['key2'], queryFn: () => sleep(10).then(() => 'result2'), })) }) await vi.advanceTimersByTimeAsync(11) expect(query.status()).toBe('success') expect(query.data()).toBe('result2') expect(query.isPending()).toBe(false) expect(query.isFetching()).toBe(false) expect(query.isFetched()).toBe(true) expect(query.isSuccess()).toBe(true) }) test('should reject and update signal', async () => { const query = TestBed.runInInjectionContext(() => { return injectQuery(() => ({ retry: false, queryKey: ['key3'], queryFn: () => sleep(10).then(() => Promise.reject(new Error('Some error'))), })) }) await vi.advanceTimersByTimeAsync(11) expect(query.status()).toBe('error') expect(query.data()).toBe(undefined) expect(query.error()).toMatchObject({ message: 'Some error' }) expect(query.isPending()).toBe(false) expect(query.isFetching()).toBe(false) expect(query.isError()).toBe(true) expect(query.failureCount()).toBe(1) expect(query.failureReason()).toMatchObject({ message: 'Some error' }) }) test('should update query on options contained signal change', async () => { const key = signal(['key6', 'key7']) const spy = vi.fn(() => sleep(10).then(() => 'Some data')) const query = TestBed.runInInjectionContext(() => { return injectQuery(() => ({ queryKey: key(), queryFn: spy, })) }) await vi.advanceTimersByTimeAsync(0) expect(spy).toHaveBeenCalledTimes(1) await vi.advanceTimersByTimeAsync(11) expect(query.status()).toBe('success') key.set(['key8']) TestBed.tick() expect(spy).toHaveBeenCalledTimes(2) // should call queryFn with context containing the new queryKey expect(spy).toBeCalledWith({ client: queryClient, meta: undefined, queryKey: ['key8'], signal: expect.anything(), }) }) test('should only run query once enabled signal is set to true', async () => { const spy = vi.fn(() => sleep(10).then(() => 'Some data')) const enabled = signal(false) const query = TestBed.runInInjectionContext(() => { return injectQuery(() => ({ queryKey: ['key9'], queryFn: spy, enabled: enabled(), })) }) expect(spy).not.toHaveBeenCalled() expect(query.status()).toBe('pending') enabled.set(true) await vi.advanceTimersByTimeAsync(11) expect(spy).toHaveBeenCalledTimes(1) expect(query.status()).toBe('success') }) test('should properly execute dependant queries', async () => { const query1 = TestBed.runInInjectionContext(() => { return injectQuery(() => ({ queryKey: ['dependant1'], queryFn: () => sleep(10).then(() => 'Some data'), })) }) const dependentQueryFn = vi .fn() .mockImplementation(() => sleep(1000).then(() => 'Some data')) const query2 = TestBed.runInInjectionContext(() => { return injectQuery( computed(() => ({ queryKey: ['dependant2'], queryFn: dependentQueryFn, enabled: !!query1.data(), })), ) }) expect(query1.data()).toStrictEqual(undefined) expect(query2.fetchStatus()).toStrictEqual('idle') expect(dependentQueryFn).not.toHaveBeenCalled() await vi.advanceTimersByTimeAsync(11) expect(query1.data()).toStrictEqual('Some data') expect(query2.fetchStatus()).toStrictEqual('fetching') await vi.advanceTimersByTimeAsync(1002) expect(query2.fetchStatus()).toStrictEqual('idle') expect(query2.status()).toStrictEqual('success') expect(dependentQueryFn).toHaveBeenCalledTimes(1) expect(dependentQueryFn).toHaveBeenCalledWith( expect.objectContaining({ queryKey: ['dependant2'] }), ) }) test('should use the current value for the queryKey when refetch is called', async () => { const fetchFn = vi.fn(() => sleep(10).then(() => 'Some data')) const keySignal = signal('key11') const query = TestBed.runInInjectionContext(() => { return injectQuery(() => ({ queryKey: ['key10', keySignal()], queryFn: fetchFn, enabled: false, })) }) expect(fetchFn).not.toHaveBeenCalled() query.refetch().then(() => { expect(fetchFn).toHaveBeenCalledTimes(1) expect(fetchFn).toHaveBeenCalledWith( expect.objectContaining({ queryKey: ['key10', 'key11'], }), ) }) await vi.advanceTimersByTimeAsync(11) keySignal.set('key12') query.refetch().then(() => { expect(fetchFn).toHaveBeenCalledTimes(2) expect(fetchFn).toHaveBeenCalledWith( expect.objectContaining({ queryKey: ['key10', 'key12'], }), ) }) await vi.advanceTimersByTimeAsync(11) }) describe('throwOnError', () => { test('should evaluate throwOnError when query is expected to throw', async () => { const boundaryFn = vi.fn() TestBed.runInInjectionContext(() => { return injectQuery(() => ({ queryKey: ['key12'], queryFn: () => sleep(10).then(() => Promise.reject(new Error('Some error'))), retry: false, throwOnError: boundaryFn, })) }) await vi.advanceTimersByTimeAsync(11) expect(boundaryFn).toHaveBeenCalledTimes(1) expect(boundaryFn).toHaveBeenCalledWith( Error('Some error'), expect.objectContaining({ state: expect.objectContaining({ status: 'error' }), }), ) }) test('should throw when throwOnError is true', async () => { TestBed.runInInjectionContext(() => { return injectQuery(() => ({ queryKey: ['key13'], queryFn: () => sleep(0).then(() => Promise.reject(new Error('Some error'))), throwOnError: true, })) }) await expect(vi.runAllTimersAsync()).rejects.toThrow('Some error') }) test('should throw when throwOnError function returns true', async () => { TestBed.runInInjectionContext(() => { return injectQuery(() => ({ queryKey: ['key14'], queryFn: () => sleep(0).then(() => Promise.reject(new Error('Some error'))), throwOnError: () => true, })) }) await expect(vi.runAllTimersAsync()).rejects.toThrow('Some error') }) }) test('should set state to error when queryFn returns reject promise', async () => { const query = TestBed.runInInjectionContext(() => { return injectQuery(() => ({ retry: false, queryKey: ['key15'], queryFn: () => sleep(10).then(() => Promise.reject(new Error('Some error'))), })) }) expect(query.status()).toBe('pending') await vi.advanceTimersByTimeAsync(11) expect(query.status()).toBe('error') }) test('should render with required signal inputs', async () => { @Component({ selector: 'app-fake', template: `{{ query.data() }}`, }) class FakeComponent { name = input.required() query = injectQuery(() => ({ queryKey: ['fake', this.name()], queryFn: () => this.name(), })) } const fixture = TestBed.createComponent(FakeComponent) setSignalInputs(fixture.componentInstance, { name: 'signal-input-required-test', }) fixture.detectChanges() await vi.advanceTimersByTimeAsync(0) expect(fixture.componentInstance.query.data()).toEqual( 'signal-input-required-test', ) }) describe('injection context', () => { test('throws NG0203 with descriptive error outside injection context', () => { expect(() => { injectQuery(() => ({ queryKey: ['injectionContextError'], queryFn: () => sleep(0).then(() => 'Some data'), })) }).toThrowError(/NG0203(.*?)injectQuery/) }) test('can be used outside injection context when passing an injector', () => { const query = injectQuery( () => ({ queryKey: ['manualInjector'], queryFn: () => sleep(0).then(() => 'Some data'), }), { injector: TestBed.inject(Injector), }, ) expect(query.status()).toBe('pending') }) test('should complete queries before whenStable() resolves', async () => { const app = TestBed.inject(ApplicationRef) const query = TestBed.runInInjectionContext(() => injectQuery(() => ({ queryKey: ['pendingTasksTest'], queryFn: async () => { await sleep(50) return 'test data' }, })), ) expect(query.status()).toBe('pending') expect(query.data()).toBeUndefined() const stablePromise = app.whenStable() await vi.advanceTimersByTimeAsync(60) await stablePromise expect(query.status()).toBe('success') expect(query.data()).toBe('test data') }) test('should complete HttpClient-based queries before whenStable() resolves', async () => { TestBed.resetTestingModule() TestBed.configureTestingModule({ providers: [ provideZonelessChangeDetection(), provideTanStackQuery(queryClient), provideHttpClient(), provideHttpClientTesting(), ], }) const app = TestBed.inject(ApplicationRef) const httpClient = TestBed.inject(HttpClient) const httpTestingController = TestBed.inject(HttpTestingController) // Create a query using HttpClient const query = TestBed.runInInjectionContext(() => injectQuery(() => ({ queryKey: ['httpClientTest'], queryFn: () => lastValueFrom(httpClient.get<{ message: string }>('/api/test')), })), ) // Schedule the HTTP response setTimeout(() => { const req = httpTestingController.expectOne('/api/test') req.flush({ message: 'http test data' }) }, 10) // Initial state expect(query.status()).toBe('pending') // Advance timers and wait for Angular to be "stable" const stablePromise = app.whenStable() await vi.advanceTimersByTimeAsync(20) await stablePromise // Query should be complete after whenStable() thanks to PendingTasks integration expect(query.status()).toBe('success') expect(query.data()).toEqual({ message: 'http test data' }) httpTestingController.verify() }) test('should handle synchronous queryFn with staleTime', async () => { TestBed.resetTestingModule() TestBed.configureTestingModule({ providers: [ provideZonelessChangeDetection(), provideTanStackQuery(queryClient), ], }) const app = TestBed.inject(ApplicationRef) let callCount = 0 const query = TestBed.runInInjectionContext(() => injectQuery(() => ({ queryKey: ['sync-stale'], staleTime: 1000, queryFn: () => { callCount++ return `sync-data-${callCount}` }, })), ) // Synchronize pending effects TestBed.tick() const stablePromise = app.whenStable() await stablePromise expect(query.status()).toBe('success') expect(query.data()).toBe('sync-data-1') expect(callCount).toBe(1) await query.refetch() await Promise.resolve() await vi.runAllTimersAsync() await app.whenStable() expect(query.status()).toBe('success') expect(query.data()).toBe('sync-data-2') expect(callCount).toBe(2) }) test('should handle enabled/disabled transitions with synchronous queryFn', async () => { TestBed.resetTestingModule() TestBed.configureTestingModule({ providers: [ provideZonelessChangeDetection(), provideTanStackQuery(queryClient), ], }) const app = TestBed.inject(ApplicationRef) const enabledSignal = signal(false) let callCount = 0 const query = TestBed.runInInjectionContext(() => injectQuery(() => ({ queryKey: ['sync-enabled'], enabled: enabledSignal(), queryFn: () => { callCount++ return `sync-data-${callCount}` }, })), ) // Initially disabled TestBed.tick() await app.whenStable() expect(query.status()).toBe('pending') expect(query.data()).toBeUndefined() expect(callCount).toBe(0) // Enable the query enabledSignal.set(true) TestBed.tick() await app.whenStable() expect(query.status()).toBe('success') expect(query.data()).toBe('sync-data-1') expect(callCount).toBe(1) }) test('should handle query invalidation with synchronous data', async () => { TestBed.resetTestingModule() TestBed.configureTestingModule({ providers: [ provideZonelessChangeDetection(), provideTanStackQuery(queryClient), ], }) const app = TestBed.inject(ApplicationRef) const testKey = ['sync-invalidate'] let callCount = 0 const query = TestBed.runInInjectionContext(() => injectQuery(() => ({ queryKey: testKey, queryFn: () => { callCount++ return `sync-data-${callCount}` }, })), ) // Synchronize pending effects TestBed.tick() await app.whenStable() expect(query.status()).toBe('success') expect(query.data()).toBe('sync-data-1') expect(callCount).toBe(1) // Invalidate the query queryClient.invalidateQueries({ queryKey: testKey }) TestBed.tick() // Wait for the invalidation to trigger a refetch await Promise.resolve() await vi.advanceTimersByTimeAsync(10) TestBed.tick() await app.whenStable() expect(query.status()).toBe('success') expect(query.data()).toBe('sync-data-2') expect(callCount).toBe(2) }) }) }) ================================================ FILE: packages/angular-query-experimental/src/__tests__/mutation-options.test-d.ts ================================================ import { assertType, describe, expectTypeOf, it } from 'vitest' import { QueryClient } from '@tanstack/query-core' import { injectIsMutating, injectMutation, injectMutationState, mutationOptions, } from '..' import type { DefaultError, MutationFunctionContext, MutationState, WithRequired, } from '@tanstack/query-core' import type { CreateMutationOptions, CreateMutationResult } from '../types' describe('mutationOptions', () => { it('should not allow excess properties', () => { // @ts-expect-error this is a good error, because onMutates does not exist! mutationOptions({ mutationFn: () => Promise.resolve(5), mutationKey: ['key'], onMutates: 1000, onSuccess: (data) => { expectTypeOf(data).toEqualTypeOf() }, }) }) it('should infer types for callbacks', () => { mutationOptions({ mutationFn: () => Promise.resolve(5), mutationKey: ['key'], onSuccess: (data) => { expectTypeOf(data).toEqualTypeOf() }, }) }) it('should infer types for onError callback', () => { mutationOptions({ mutationFn: () => { throw new Error('fail') }, mutationKey: ['key'], onError: (error) => { expectTypeOf(error).toEqualTypeOf() }, }) }) it('should infer types for variables', () => { mutationOptions({ mutationFn: (vars) => { expectTypeOf(vars).toEqualTypeOf<{ id: string }>() return Promise.resolve(5) }, mutationKey: ['with-vars'], }) }) it('should infer result type correctly', () => { mutationOptions({ mutationFn: () => Promise.resolve(5), mutationKey: ['key'], onMutate: () => { return { name: 'onMutateResult' } }, onSuccess: (_data, _variables, onMutateResult) => { expectTypeOf(onMutateResult).toEqualTypeOf< { name: string } | undefined >() }, }) }) it('should infer context type correctly', () => { mutationOptions({ mutationFn: (_variables, context) => { expectTypeOf(context).toEqualTypeOf() return Promise.resolve(5) }, mutationKey: ['key'], onMutate: (_variables, context) => { expectTypeOf(context).toEqualTypeOf() }, onSuccess: (_data, _variables, _onMutateResult, context) => { expectTypeOf(context).toEqualTypeOf() }, onError: (_error, _variables, _onMutateResult, context) => { expectTypeOf(context).toEqualTypeOf() }, onSettled: (_data, _error, _variables, _onMutateResult, context) => { expectTypeOf(context).toEqualTypeOf() }, }) }) it('should error if mutationFn return type mismatches TData', () => { assertType( mutationOptions({ // @ts-expect-error this is a good error, because return type is string, not number mutationFn: async () => Promise.resolve('wrong return'), }), ) }) it('should allow mutationKey to be omitted', () => { return mutationOptions({ mutationFn: () => Promise.resolve(123), onSuccess: (data) => { expectTypeOf(data).toEqualTypeOf() }, }) }) it('should infer all types when not explicitly provided', () => { expectTypeOf( mutationOptions({ mutationFn: (id: string) => Promise.resolve(id.length), mutationKey: ['key'], onSuccess: (data) => { expectTypeOf(data).toEqualTypeOf() }, }), ).toEqualTypeOf< WithRequired< CreateMutationOptions, 'mutationKey' > >() expectTypeOf( mutationOptions({ mutationFn: (id: string) => Promise.resolve(id.length), onSuccess: (data) => { expectTypeOf(data).toEqualTypeOf() }, }), ).toEqualTypeOf< Omit, 'mutationKey'> >() }) it('should infer types when used with injectMutation', () => { const mutation = injectMutation(() => mutationOptions({ mutationKey: ['key'], mutationFn: () => Promise.resolve('data'), onSuccess: (data) => { expectTypeOf(data).toEqualTypeOf() }, }), ) expectTypeOf(mutation).toEqualTypeOf< CreateMutationResult >() injectMutation( // should allow when used with injectMutation without mutationKey () => mutationOptions({ mutationFn: () => Promise.resolve('data'), onSuccess: (data) => { expectTypeOf(data).toEqualTypeOf() }, }), ) }) it('should infer types when used with injectIsMutating', () => { const isMutating = injectIsMutating( mutationOptions({ mutationKey: ['key'], mutationFn: () => Promise.resolve(5), }), ) expectTypeOf(isMutating()).toEqualTypeOf() injectIsMutating( // @ts-expect-error filters should have mutationKey mutationOptions({ mutationFn: () => Promise.resolve(5), }), ) }) it('should infer types when used with queryClient.isMutating', () => { const queryClient = new QueryClient() const isMutating = queryClient.isMutating( mutationOptions({ mutationKey: ['key'], mutationFn: () => Promise.resolve(5), }), ) expectTypeOf(isMutating).toEqualTypeOf() queryClient.isMutating( // @ts-expect-error filters should have mutationKey mutationOptions({ mutationFn: () => Promise.resolve(5), }), ) }) it('should infer types when used with injectMutationState', () => { const mutationState = injectMutationState(() => ({ filters: mutationOptions({ mutationKey: ['key'], mutationFn: () => Promise.resolve(5), }), })) expectTypeOf(mutationState()).toEqualTypeOf< Array> >() injectMutationState({ // @ts-expect-error filters should have mutationKey filters: mutationOptions({ mutationFn: () => Promise.resolve(5), }), }) }) }) ================================================ FILE: packages/angular-query-experimental/src/__tests__/mutation-options.test.ts ================================================ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { provideZonelessChangeDetection } from '@angular/core' import { TestBed } from '@angular/core/testing' import { QueryClient } from '@tanstack/query-core' import { sleep } from '@tanstack/query-test-utils' import { injectIsMutating, injectMutation, injectMutationState, mutationOptions, provideTanStackQuery, } from '..' describe('mutationOptions', () => { let queryClient: QueryClient beforeEach(() => { vi.useFakeTimers() queryClient = new QueryClient() TestBed.configureTestingModule({ providers: [ provideZonelessChangeDetection(), provideTanStackQuery(queryClient), ], }) }) afterEach(() => { vi.useRealTimers() }) it('should return the object received as a parameter without any modification (with mutationKey in mutationOptions)', () => { const object = { mutationKey: ['key'], mutationFn: () => sleep(10).then(() => 5), } as const expect(mutationOptions(object)).toStrictEqual(object) }) it('should return the object received as a parameter without any modification (without mutationKey in mutationOptions)', () => { const object = { mutationFn: () => sleep(10).then(() => 5), } as const expect(mutationOptions(object)).toStrictEqual(object) }) it('should return the number of fetching mutations when used with injectIsMutating (with mutationKey in mutationOptions)', async () => { const mutationOpts = mutationOptions({ mutationKey: ['key'], mutationFn: () => sleep(50).then(() => 'data'), }) const [mutation, isMutating] = TestBed.runInInjectionContext(() => [ injectMutation(() => mutationOpts), injectIsMutating(), ]) expect(isMutating()).toBe(0) mutation.mutate() expect(isMutating()).toBe(0) await vi.advanceTimersByTimeAsync(0) expect(isMutating()).toBe(1) await vi.advanceTimersByTimeAsync(51) expect(isMutating()).toBe(0) }) it('should return the number of fetching mutations when used with injectIsMutating (without mutationKey in mutationOptions)', async () => { const mutationOpts = mutationOptions({ mutationFn: () => sleep(50).then(() => 'data'), }) const [mutation, isMutating] = TestBed.runInInjectionContext(() => [ injectMutation(() => mutationOpts), injectIsMutating(), ]) expect(isMutating()).toBe(0) mutation.mutate() expect(isMutating()).toBe(0) await vi.advanceTimersByTimeAsync(0) expect(isMutating()).toBe(1) await vi.advanceTimersByTimeAsync(51) expect(isMutating()).toBe(0) }) it('should return the number of fetching mutations when used with injectIsMutating', async () => { const mutationOpts1 = mutationOptions({ mutationKey: ['key'], mutationFn: () => sleep(50).then(() => 'data1'), }) const mutationOpts2 = mutationOptions({ mutationFn: () => sleep(50).then(() => 'data2'), }) const [mutation1, mutation2, isMutating] = TestBed.runInInjectionContext( () => [ injectMutation(() => mutationOpts1), injectMutation(() => mutationOpts2), injectIsMutating(), ], ) expect(isMutating()).toBe(0) mutation1.mutate() mutation2.mutate() expect(isMutating()).toBe(0) await vi.advanceTimersByTimeAsync(0) expect(isMutating()).toBe(2) await vi.advanceTimersByTimeAsync(51) expect(isMutating()).toBe(0) }) it('should return the number of fetching mutations when used with injectIsMutating (filter mutationOpts1.mutationKey)', async () => { const mutationOpts1 = mutationOptions({ mutationKey: ['key'], mutationFn: () => sleep(50).then(() => 'data1'), }) const mutationOpts2 = mutationOptions({ mutationFn: () => sleep(50).then(() => 'data2'), }) const [mutation1, mutation2, isMutating] = TestBed.runInInjectionContext( () => [ injectMutation(() => mutationOpts1), injectMutation(() => mutationOpts2), injectIsMutating({ mutationKey: mutationOpts1.mutationKey }), ], ) expect(isMutating()).toBe(0) mutation1.mutate() mutation2.mutate() expect(isMutating()).toBe(0) await vi.advanceTimersByTimeAsync(0) expect(isMutating()).toBe(1) await vi.advanceTimersByTimeAsync(51) expect(isMutating()).toBe(0) }) it('should return the number of fetching mutations when used with queryClient.isMutating (with mutationKey in mutationOptions)', async () => { const mutationOpts = mutationOptions({ mutationKey: ['mutation'], mutationFn: () => sleep(500).then(() => 'data'), }) const mutation = TestBed.runInInjectionContext(() => injectMutation(() => mutationOpts), ) expect(queryClient.isMutating(mutationOpts)).toBe(0) mutation.mutate() expect(queryClient.isMutating(mutationOpts)).toBe(1) await vi.advanceTimersByTimeAsync(501) expect(queryClient.isMutating(mutationOpts)).toBe(0) }) it('should return the number of fetching mutations when used with queryClient.isMutating (without mutationKey in mutationOptions)', async () => { const mutationOpts = mutationOptions({ mutationFn: () => sleep(500).then(() => 'data'), }) const mutation = TestBed.runInInjectionContext(() => injectMutation(() => mutationOpts), ) expect(queryClient.isMutating()).toBe(0) mutation.mutate() expect(queryClient.isMutating()).toBe(1) await vi.advanceTimersByTimeAsync(501) expect(queryClient.isMutating()).toBe(0) }) it('should return the number of fetching mutations when used with queryClient.isMutating', async () => { const mutationOpts1 = mutationOptions({ mutationKey: ['mutation'], mutationFn: () => sleep(500).then(() => 'data1'), }) const mutationOpts2 = mutationOptions({ mutationFn: () => sleep(500).then(() => 'data2'), }) const [mutation1, mutation2] = TestBed.runInInjectionContext(() => [ injectMutation(() => mutationOpts1), injectMutation(() => mutationOpts2), ]) expect(queryClient.isMutating()).toBe(0) mutation1.mutate() mutation2.mutate() expect(queryClient.isMutating()).toBe(2) await vi.advanceTimersByTimeAsync(501) expect(queryClient.isMutating()).toBe(0) }) it('should return the number of fetching mutations when used with queryClient.isMutating (filter mutationOpt1.mutationKey)', async () => { const mutationOpts1 = mutationOptions({ mutationKey: ['mutation'], mutationFn: () => sleep(500).then(() => 'data1'), }) const mutationOpts2 = mutationOptions({ mutationFn: () => sleep(500).then(() => 'data2'), }) const [mutation1, mutation2] = TestBed.runInInjectionContext(() => [ injectMutation(() => mutationOpts1), injectMutation(() => mutationOpts2), ]) expect( queryClient.isMutating({ mutationKey: mutationOpts1.mutationKey }), ).toBe(0) mutation1.mutate() mutation2.mutate() expect( queryClient.isMutating({ mutationKey: mutationOpts1.mutationKey }), ).toBe(1) await vi.advanceTimersByTimeAsync(501) expect( queryClient.isMutating({ mutationKey: mutationOpts1.mutationKey }), ).toBe(0) }) it('should return the number of fetching mutations when used with injectMutationState (with mutationKey in mutationOptions)', async () => { const mutationOpts = mutationOptions({ mutationKey: ['mutation'], mutationFn: () => sleep(10).then(() => 'data'), }) const [mutation, mutationState] = TestBed.runInInjectionContext(() => [ injectMutation(() => mutationOpts), injectMutationState(() => ({ filters: { mutationKey: mutationOpts.mutationKey, status: 'success' }, })), ]) expect(mutationState().length).toBe(0) mutation.mutate() await vi.advanceTimersByTimeAsync(11) expect(mutationState().length).toBe(1) expect(mutationState()[0]?.data).toBe('data') }) it('should return the number of fetching mutations when used with injectMutationState (without mutationKey in mutationOptions)', async () => { const mutationOpts = mutationOptions({ mutationFn: () => sleep(10).then(() => 'data'), }) const [mutation, mutationState] = TestBed.runInInjectionContext(() => [ injectMutation(() => mutationOpts), injectMutationState(() => ({ filters: { status: 'success' }, })), ]) expect(mutationState().length).toBe(0) mutation.mutate() await vi.advanceTimersByTimeAsync(11) expect(mutationState().length).toBe(1) expect(mutationState()[0]?.data).toBe('data') }) it('should return the number of fetching mutations when used with injectMutationState', async () => { const mutationOpts1 = mutationOptions({ mutationKey: ['mutation'], mutationFn: () => sleep(10).then(() => 'data1'), }) const mutationOpts2 = mutationOptions({ mutationFn: () => sleep(10).then(() => 'data2'), }) const [mutation1, mutation2, mutationState] = TestBed.runInInjectionContext( () => [ injectMutation(() => mutationOpts1), injectMutation(() => mutationOpts2), injectMutationState(() => ({ filters: { status: 'success' }, })), ], ) expect(mutationState().length).toBe(0) mutation1.mutate() mutation2.mutate() await vi.advanceTimersByTimeAsync(11) expect(mutationState().length).toBe(2) expect(mutationState()[0]?.data).toBe('data1') expect(mutationState()[1]?.data).toBe('data2') }) it('should return the number of fetching mutations when used with injectMutationState (filter mutationOpt1.mutationKey)', async () => { const mutationOpts1 = mutationOptions({ mutationKey: ['mutation'], mutationFn: () => sleep(10).then(() => 'data1'), }) const mutationOpts2 = mutationOptions({ mutationFn: () => sleep(10).then(() => 'data2'), }) const [mutation1, mutation2, mutationState] = TestBed.runInInjectionContext( () => [ injectMutation(() => mutationOpts1), injectMutation(() => mutationOpts2), injectMutationState(() => ({ filters: { mutationKey: mutationOpts1.mutationKey, status: 'success', }, })), ], ) expect(mutationState().length).toBe(0) mutation1.mutate() mutation2.mutate() await vi.advanceTimersByTimeAsync(11) expect(mutationState().length).toBe(1) expect(mutationState()[0]?.data).toBe('data1') }) }) ================================================ FILE: packages/angular-query-experimental/src/__tests__/pending-tasks.test.ts ================================================ import { ApplicationRef, Component, provideZonelessChangeDetection, } from '@angular/core' import { TestBed } from '@angular/core/testing' import { HttpClient, provideHttpClient } from '@angular/common/http' import { HttpTestingController, provideHttpClientTesting, } from '@angular/common/http/testing' import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest' import { sleep } from '@tanstack/query-test-utils' import { lastValueFrom } from 'rxjs' import { QueryClient, injectMutation, injectQuery, onlineManager, provideTanStackQuery, } from '..' describe('PendingTasks Integration', () => { let queryClient: QueryClient beforeEach(() => { vi.useFakeTimers() queryClient = new QueryClient({ defaultOptions: { queries: { retry: false, }, mutations: { retry: false, }, }, }) TestBed.configureTestingModule({ providers: [ provideZonelessChangeDetection(), provideTanStackQuery(queryClient), ], }) }) afterEach(() => { onlineManager.setOnline(true) vi.useRealTimers() queryClient.clear() }) describe('Synchronous Resolution', () => { test('should handle synchronous queryFn with whenStable()', async () => { const app = TestBed.inject(ApplicationRef) const query = TestBed.runInInjectionContext(() => injectQuery(() => ({ queryKey: ['sync'], queryFn: () => 'instant-data', // Resolves synchronously })), ) // Should start as pending even with synchronous data expect(query.status()).toBe('pending') expect(query.data()).toBeUndefined() const stablePromise = app.whenStable() // Flush microtasks to allow TanStack Query's scheduled notifications to process await Promise.resolve() await vi.advanceTimersByTimeAsync(10) await stablePromise // Should work correctly even though queryFn was synchronous expect(query.status()).toBe('success') expect(query.data()).toBe('instant-data') }) test('should handle synchronous error with whenStable()', async () => { const app = TestBed.inject(ApplicationRef) const query = TestBed.runInInjectionContext(() => injectQuery(() => ({ queryKey: ['sync-error'], queryFn: () => { throw new Error('instant-error') }, // Throws synchronously })), ) const stablePromise = app.whenStable() // Flush microtasks to allow TanStack Query's scheduled notifications to process await Promise.resolve() await vi.advanceTimersByTimeAsync(10) await stablePromise expect(query.status()).toBe('error') expect(query.error()).toEqual(new Error('instant-error')) }) test('should handle synchronous mutationFn with whenStable()', async () => { const app = TestBed.inject(ApplicationRef) let mutationFnCalled = false const mutation = TestBed.runInInjectionContext(() => injectMutation(() => ({ mutationFn: async (data: string) => { mutationFnCalled = true await Promise.resolve() return `processed: ${data}` }, })), ) mutation.mutate('test') TestBed.tick() const stablePromise = app.whenStable() await Promise.resolve() await vi.advanceTimersByTimeAsync(10) await stablePromise expect(mutationFnCalled).toBe(true) expect(mutation.isSuccess()).toBe(true) expect(mutation.data()).toBe('processed: test') }) test('should handle synchronous mutation error with whenStable()', async () => { const app = TestBed.inject(ApplicationRef) const mutation = TestBed.runInInjectionContext(() => injectMutation(() => ({ mutationFn: async () => { await Promise.resolve() throw new Error('sync-mutation-error') }, })), ) mutation.mutate() TestBed.tick() const stablePromise = app.whenStable() await Promise.resolve() await vi.advanceTimersByTimeAsync(10) await stablePromise expect(mutation.isError()).toBe(true) expect(mutation.error()).toEqual(new Error('sync-mutation-error')) }) }) describe('Race Conditions', () => { test('should handle query that completes during initial subscription', async () => { const app = TestBed.inject(ApplicationRef) let resolveQuery: (value: string) => void const queryPromise = new Promise((resolve) => { resolveQuery = resolve }) const query = TestBed.runInInjectionContext(() => injectQuery(() => ({ queryKey: ['race-condition'], queryFn: () => queryPromise, })), ) // Resolve immediately to create potential race condition resolveQuery!('race-data') const stablePromise = app.whenStable() await Promise.resolve() await vi.advanceTimersByTimeAsync(10) await stablePromise expect(query.status()).toBe('success') expect(query.data()).toBe('race-data') }) test('should handle rapid refetches without task leaks', async () => { const app = TestBed.inject(ApplicationRef) let callCount = 0 const query = TestBed.runInInjectionContext(() => injectQuery(() => ({ queryKey: ['rapid-refetch'], queryFn: async () => { callCount++ await sleep(10) return `data-${callCount}` }, })), ) // Trigger multiple rapid refetches query.refetch() query.refetch() query.refetch() const stablePromise = app.whenStable() await vi.advanceTimersByTimeAsync(20) await stablePromise expect(query.status()).toBe('success') expect(query.data()).toMatch(/^data-\d+$/) }) test('should keep PendingTasks active while query retry is paused offline', async () => { const app = TestBed.inject(ApplicationRef) let attempt = 0 const query = TestBed.runInInjectionContext(() => injectQuery(() => ({ queryKey: ['paused-offline'], retry: 1, retryDelay: 50, // Longer delay to ensure we can go offline before retry queryFn: async () => { attempt++ if (attempt === 1) { throw new Error('offline-fail') } await sleep(10) return 'final-data' }, })), ) // Allow the initial attempt to start and fail await vi.advanceTimersByTimeAsync(0) await Promise.resolve() // Wait for the first attempt to complete and start retry delay await vi.advanceTimersByTimeAsync(10) await Promise.resolve() expect(query.status()).toBe('pending') expect(query.fetchStatus()).toBe('fetching') // Simulate the app going offline during retry delay onlineManager.setOnline(false) // Advance past the retry delay to trigger the pause await vi.advanceTimersByTimeAsync(50) await Promise.resolve() expect(query.fetchStatus()).toBe('paused') const stablePromise = app.whenStable() let stableResolved = false void stablePromise.then(() => { stableResolved = true }) await Promise.resolve() // PendingTasks should continue blocking stability while the fetch is paused expect(stableResolved).toBe(false) expect(query.status()).toBe('pending') // Bring the app back online so the retry can continue onlineManager.setOnline(true) // Give time for the retry to resume and complete await vi.advanceTimersByTimeAsync(20) await Promise.resolve() await stablePromise expect(stableResolved).toBe(true) expect(query.status()).toBe('success') expect(query.data()).toBe('final-data') }) }) describe('Component Destruction', () => { @Component({ template: '', }) class TestComponent { query = injectQuery(() => ({ queryKey: ['component-query'], queryFn: async () => { await sleep(100) return 'component-data' }, })) mutation = injectMutation(() => ({ mutationFn: async (data: string) => { await sleep(100) return `processed: ${data}` }, })) } test('should cleanup pending tasks when component with active query is destroyed', async () => { const app = TestBed.inject(ApplicationRef) const fixture = TestBed.createComponent(TestComponent) // Start the query expect(fixture.componentInstance.query.status()).toBe('pending') // Destroy component while query is running fixture.destroy() // Angular should become stable even though component was destroyed const stablePromise = app.whenStable() await vi.advanceTimersByTimeAsync(150) await expect(stablePromise).resolves.toEqual(undefined) }) test('should cleanup pending tasks when component with active mutation is destroyed', async () => { const app = TestBed.inject(ApplicationRef) const fixture = TestBed.createComponent(TestComponent) fixture.componentInstance.mutation.mutate('test') // Destroy component while mutation is running fixture.destroy() // Angular should become stable even though component was destroyed const stablePromise = app.whenStable() await vi.advanceTimersByTimeAsync(150) await expect(stablePromise).resolves.toEqual(undefined) }) }) describe('Concurrent Operations', () => { test('should handle multiple queries running simultaneously', async () => { const app = TestBed.inject(ApplicationRef) const query1 = TestBed.runInInjectionContext(() => injectQuery(() => ({ queryKey: ['concurrent-1'], queryFn: async () => { await sleep(30) return 'data-1' }, })), ) const query2 = TestBed.runInInjectionContext(() => injectQuery(() => ({ queryKey: ['concurrent-2'], queryFn: async () => { await sleep(50) return 'data-2' }, })), ) const query3 = TestBed.runInInjectionContext(() => injectQuery(() => ({ queryKey: ['concurrent-3'], queryFn: () => 'instant-data', // Synchronous })), ) // All queries should start expect(query1.status()).toBe('pending') expect(query2.status()).toBe('pending') expect(query3.status()).toBe('pending') const stablePromise = app.whenStable() await vi.advanceTimersByTimeAsync(60) await stablePromise // All queries should be complete expect(query1.status()).toBe('success') expect(query1.data()).toBe('data-1') expect(query2.status()).toBe('success') expect(query2.data()).toBe('data-2') expect(query3.status()).toBe('success') expect(query3.data()).toBe('instant-data') }) test('should handle multiple mutations running simultaneously', async () => { const app = TestBed.inject(ApplicationRef) const mutation1 = TestBed.runInInjectionContext(() => injectMutation(() => ({ mutationFn: async (data: string) => { await sleep(30) return `processed-1: ${data}` }, })), ) const mutation2 = TestBed.runInInjectionContext(() => injectMutation(() => ({ mutationFn: async (data: string) => { await sleep(50) return `processed-2: ${data}` }, })), ) const mutation3 = TestBed.runInInjectionContext(() => injectMutation(() => ({ mutationFn: async (data: string) => { await Promise.resolve() return `processed-3: ${data}` }, })), ) // Start all mutations mutation1.mutate('test1') mutation2.mutate('test2') mutation3.mutate('test3') TestBed.tick() const stablePromise = app.whenStable() await vi.advanceTimersByTimeAsync(60) await stablePromise // All mutations should be complete expect(mutation1.isSuccess()).toBe(true) expect(mutation1.data()).toBe('processed-1: test1') expect(mutation2.isSuccess()).toBe(true) expect(mutation2.data()).toBe('processed-2: test2') expect(mutation3.isSuccess()).toBe(true) expect(mutation3.data()).toBe('processed-3: test3') }) test('should handle mixed queries and mutations', async () => { const app = TestBed.inject(ApplicationRef) const query = TestBed.runInInjectionContext(() => injectQuery(() => ({ queryKey: ['mixed-query'], queryFn: async () => { await sleep(40) return 'query-data' }, })), ) const mutation = TestBed.runInInjectionContext(() => injectMutation(() => ({ mutationFn: async (data: string) => { await sleep(60) return `mutation: ${data}` }, })), ) // Start both operations mutation.mutate('test') const stablePromise = app.whenStable() await vi.advanceTimersByTimeAsync(70) await stablePromise // Both should be complete expect(query.status()).toBe('success') expect(query.data()).toBe('query-data') expect(mutation.isSuccess()).toBe(true) expect(mutation.data()).toBe('mutation: test') }) }) describe('HttpClient Integration', () => { beforeEach(() => { TestBed.resetTestingModule() TestBed.configureTestingModule({ providers: [ provideZonelessChangeDetection(), provideTanStackQuery(queryClient), provideHttpClient(), provideHttpClientTesting(), ], }) }) test('should handle multiple HttpClient requests with lastValueFrom', async () => { const app = TestBed.inject(ApplicationRef) const httpClient = TestBed.inject(HttpClient) const httpTestingController = TestBed.inject(HttpTestingController) const query1 = TestBed.runInInjectionContext(() => injectQuery(() => ({ queryKey: ['http-1'], queryFn: () => lastValueFrom(httpClient.get<{ id: number }>('/api/1')), })), ) const query2 = TestBed.runInInjectionContext(() => injectQuery(() => ({ queryKey: ['http-2'], queryFn: () => lastValueFrom(httpClient.get<{ id: number }>('/api/2')), })), ) // Schedule HTTP responses setTimeout(() => { const req1 = httpTestingController.expectOne('/api/1') req1.flush({ id: 1 }) const req2 = httpTestingController.expectOne('/api/2') req2.flush({ id: 2 }) }, 10) const stablePromise = app.whenStable() await vi.advanceTimersByTimeAsync(20) await stablePromise expect(query1.status()).toBe('success') expect(query1.data()).toEqual({ id: 1 }) expect(query2.status()).toBe('success') expect(query2.data()).toEqual({ id: 2 }) httpTestingController.verify() }) test('should handle HttpClient request cancellation', async () => { const app = TestBed.inject(ApplicationRef) const httpClient = TestBed.inject(HttpClient) const httpTestingController = TestBed.inject(HttpTestingController) const query = TestBed.runInInjectionContext(() => injectQuery(() => ({ queryKey: ['http-cancel'], queryFn: () => lastValueFrom(httpClient.get<{ data: string }>('/api/cancel')), })), ) // Cancel the request before it completes setTimeout(() => { const req = httpTestingController.expectOne('/api/cancel') req.error(new ProgressEvent('error'), { status: 0, statusText: 'Unknown Error', }) }, 10) const stablePromise = app.whenStable() await vi.advanceTimersByTimeAsync(20) await stablePromise expect(query.status()).toBe('error') httpTestingController.verify() }) }) describe('Edge Cases', () => { test('should handle query cancellation mid-flight', async () => { const app = TestBed.inject(ApplicationRef) const query = TestBed.runInInjectionContext(() => injectQuery(() => ({ queryKey: ['cancel-test'], queryFn: async () => { await sleep(100) return 'data' }, })), ) // Cancel the query after a short delay setTimeout(() => { queryClient.cancelQueries({ queryKey: ['cancel-test'] }) }, 20) // Advance to the cancellation point await vi.advanceTimersByTimeAsync(20) TestBed.tick() const stablePromise = app.whenStable() await vi.advanceTimersByTimeAsync(130) await stablePromise // Cancellation should restore the pre-fetch state expect(query.status()).toBe('pending') expect(query.fetchStatus()).toBe('idle') }) test('should handle query retry and pending task tracking', async () => { const app = TestBed.inject(ApplicationRef) let attemptCount = 0 const query = TestBed.runInInjectionContext(() => injectQuery(() => ({ queryKey: ['retry-test'], retry: 2, retryDelay: 10, queryFn: async () => { attemptCount++ if (attemptCount <= 2) { throw new Error(`Attempt ${attemptCount} failed`) } return 'success-data' }, })), ) const stablePromise = app.whenStable() await vi.advanceTimersByTimeAsync(50) await stablePromise expect(query.status()).toBe('success') expect(query.data()).toBe('success-data') expect(attemptCount).toBe(3) // Initial + 2 retries }) test('should handle mutation with optimistic updates', async () => { const app = TestBed.inject(ApplicationRef) const testQueryKey = ['optimistic-test'] queryClient.setQueryData(testQueryKey, 'initial-data') const mutation = TestBed.runInInjectionContext(() => injectMutation(() => ({ mutationFn: async (newData: string) => { await sleep(50) return newData }, onMutate: async (newData) => { // Optimistic update const previousData = queryClient.getQueryData(testQueryKey) queryClient.setQueryData(testQueryKey, newData) return { previousData } }, onError: (_err, _newData, context) => { // Rollback on error if (context?.previousData) { queryClient.setQueryData(testQueryKey, context.previousData) } }, })), ) mutation.mutate('optimistic-data') await Promise.resolve() // Data should be optimistically updated immediately expect(queryClient.getQueryData(testQueryKey)).toBe('optimistic-data') const stablePromise = app.whenStable() await vi.advanceTimersByTimeAsync(60) await stablePromise expect(mutation.isSuccess()).toBe(true) expect(mutation.data()).toBe('optimistic-data') expect(queryClient.getQueryData(testQueryKey)).toBe('optimistic-data') }) }) }) ================================================ FILE: packages/angular-query-experimental/src/__tests__/provide-query-client.test.ts ================================================ import { TestBed } from '@angular/core/testing' import { describe, expect, test } from 'vitest' import { InjectionToken, provideZonelessChangeDetection } from '@angular/core' import { QueryClient } from '@tanstack/query-core' import { provideQueryClient } from '../providers' describe('provideQueryClient', () => { test('should provide a QueryClient instance directly', () => { const queryClient = new QueryClient() TestBed.configureTestingModule({ providers: [ provideZonelessChangeDetection(), provideQueryClient(queryClient), ], }) const providedQueryClient = TestBed.inject(QueryClient) expect(providedQueryClient).toBe(queryClient) }) test('should provide a QueryClient instance using an InjectionToken', () => { const queryClient = new QueryClient() const CUSTOM_QUERY_CLIENT = new InjectionToken('', { factory: () => queryClient, }) TestBed.configureTestingModule({ providers: [ provideZonelessChangeDetection(), provideQueryClient(CUSTOM_QUERY_CLIENT), ], }) const providedQueryClient = TestBed.inject(QueryClient) expect(providedQueryClient).toBe(queryClient) }) }) ================================================ FILE: packages/angular-query-experimental/src/__tests__/provide-tanstack-query.test.ts ================================================ import { TestBed } from '@angular/core/testing' import { describe, expect, test } from 'vitest' import { InjectionToken, provideZonelessChangeDetection } from '@angular/core' import { QueryClient } from '@tanstack/query-core' import { provideTanStackQuery } from '../providers' describe('provideTanStackQuery', () => { test('should provide a QueryClient instance directly', () => { const queryClient = new QueryClient() TestBed.configureTestingModule({ providers: [ provideZonelessChangeDetection(), provideTanStackQuery(queryClient), ], }) const providedQueryClient = TestBed.inject(QueryClient) expect(providedQueryClient).toBe(queryClient) }) test('should provide a QueryClient instance using an InjectionToken', () => { const queryClient = new QueryClient() const CUSTOM_QUERY_CLIENT = new InjectionToken('', { factory: () => queryClient, }) TestBed.configureTestingModule({ providers: [ provideZonelessChangeDetection(), provideTanStackQuery(CUSTOM_QUERY_CLIENT), ], }) const providedQueryClient = TestBed.inject(QueryClient) expect(providedQueryClient).toBe(queryClient) }) }) ================================================ FILE: packages/angular-query-experimental/src/__tests__/query-options.test-d.ts ================================================ import { assertType, describe, expectTypeOf, test } from 'vitest' import { QueryClient, dataTagSymbol, injectQuery, queryOptions } from '..' import type { Signal } from '@angular/core' describe('queryOptions', () => { test('should not allow excess properties', () => { expectTypeOf(queryOptions).parameter(0).not.toHaveProperty('stallTime') }) test('should infer types for callbacks', () => { queryOptions({ queryKey: ['key'], queryFn: () => Promise.resolve(5), staleTime: 1000, select: (data) => { expectTypeOf(data).toEqualTypeOf() }, }) }) test('should allow undefined response in initialData', () => { const options = (id: string | null) => queryOptions({ queryKey: ['todo', id], queryFn: () => Promise.resolve({ id: '1', title: 'Do Laundry', }), initialData: () => !id ? undefined : { id, title: 'Initial Data', }, }) expectTypeOf(options(null).initialData).returns.toEqualTypeOf< { id: string; title: string } | undefined >() }) }) test('should work when passed to injectQuery', () => { const options = queryOptions({ queryKey: ['key'], queryFn: () => Promise.resolve(5), }) const { data } = injectQuery(() => options) expectTypeOf(data).toEqualTypeOf>() }) test('should work when passed to fetchQuery', () => { const options = queryOptions({ queryKey: ['key'], queryFn: () => Promise.resolve(5), }) const data = new QueryClient().fetchQuery(options) assertType>(data) }) test('should tag the queryKey with the result type of the QueryFn', () => { const { queryKey } = queryOptions({ queryKey: ['key'], queryFn: () => Promise.resolve(5), }) assertType(queryKey[dataTagSymbol]) }) test('should tag the queryKey even if no promise is returned', () => { const { queryKey } = queryOptions({ queryKey: ['key'], queryFn: () => 5, }) assertType(queryKey[dataTagSymbol]) }) test('should tag the queryKey with unknown if there is no queryFn', () => { const { queryKey } = queryOptions({ queryKey: ['key'], }) assertType(queryKey[dataTagSymbol]) }) test('should tag the queryKey with the result type of the QueryFn if select is used', () => { const { queryKey } = queryOptions({ queryKey: ['key'], queryFn: () => Promise.resolve(5), select: (data) => data.toString(), }) assertType(queryKey[dataTagSymbol]) }) test('should return the proper type when passed to getQueryData', () => { const { queryKey } = queryOptions({ queryKey: ['key'], queryFn: () => Promise.resolve(5), }) const queryClient = new QueryClient() const data = queryClient.getQueryData(queryKey) expectTypeOf(data).toEqualTypeOf() }) test('should properly type updaterFn when passed to setQueryData', () => { const { queryKey } = queryOptions({ queryKey: ['key'], queryFn: () => Promise.resolve(5), }) const queryClient = new QueryClient() const data = queryClient.setQueryData(queryKey, (prev) => { expectTypeOf(prev).toEqualTypeOf() return prev }) expectTypeOf(data).toEqualTypeOf() }) test('should properly type value when passed to setQueryData', () => { const { queryKey } = queryOptions({ queryKey: ['key'], queryFn: () => Promise.resolve(5), }) const queryClient = new QueryClient() // @ts-expect-error value should be a number queryClient.setQueryData(queryKey, '5') // @ts-expect-error value should be a number queryClient.setQueryData(queryKey, () => '5') const data = queryClient.setQueryData(queryKey, 5) expectTypeOf(data).toEqualTypeOf() }) ================================================ FILE: packages/angular-query-experimental/src/__tests__/query-options.test.ts ================================================ import { describe, expect, it } from 'vitest' import { queryOptions } from '../query-options' import type { CreateQueryOptions } from '../types' describe('queryOptions', () => { it('should return the object received as a parameter without any modification.', () => { const object: CreateQueryOptions = { queryKey: ['key'], queryFn: () => Promise.resolve(5), } as const expect(queryOptions(object)).toStrictEqual(object) }) }) ================================================ FILE: packages/angular-query-experimental/src/__tests__/signal-proxy.test.ts ================================================ import { isSignal, signal } from '@angular/core' import { describe, expect, test } from 'vitest' import { signalProxy } from '../signal-proxy' describe('signalProxy', () => { const inputSignal = signal({ fn: () => 'bar', baz: 'qux' }) const proxy = signalProxy(inputSignal) test('should have computed fields', () => { expect(proxy.baz()).toEqual('qux') expect(isSignal(proxy.baz)).toBe(true) }) test('should pass through functions as-is', () => { expect(proxy.fn()).toEqual('bar') expect(isSignal(proxy.fn)).toBe(false) }) test('supports "in" operator', () => { expect('baz' in proxy).toBe(true) expect('foo' in proxy).toBe(false) }) test('supports "Object.keys"', () => { expect(Object.keys(proxy)).toEqual(['fn', 'baz']) }) }) ================================================ FILE: packages/angular-query-experimental/src/__tests__/test-utils.ts ================================================ import { isSignal, untracked } from '@angular/core' import { SIGNAL, signalSetFn } from '@angular/core/primitives/signals' import { expect } from 'vitest' import type { InputSignal, Signal } from '@angular/core' import type { ComponentFixture } from '@angular/core/testing' /* eslint jsdoc/require-jsdoc: 0, jsdoc/require-param: 0 */ // Evaluate all signals on an object and return the result function evaluateSignals>( obj: T, ): { [K in keyof T]: ReturnType } { const result: Partial<{ [K in keyof T]: ReturnType }> = {} untracked(() => { for (const key in obj) { if ( Object.prototype.hasOwnProperty.call(obj, key) && // Only evaluate signals, not normal functions isSignal(obj[key]) ) { const func = obj[key] result[key] = func() } } }) return result as { [K in keyof T]: ReturnType } } export const expectSignals = >( obj: T, expected: Partial<{ [K in keyof T]: T[K] extends Signal ? ReturnType : never }>, ): void => { expect(evaluateSignals(obj)).toMatchObject(expected) } type ToSignalInputUpdatableMap = { [K in keyof T as T[K] extends InputSignal ? K : never]: T[K] extends InputSignal ? Value : never } function componentHasSignalInputProperty( component: object, property: TProperty, ): component is { [key in TProperty]: InputSignal } { return ( component.hasOwnProperty(property) && (component as any)[property][SIGNAL] ) } /** * Set required signal input value to component fixture * @see https://github.com/angular/angular/issues/54013 */ export function setSignalInputs>( component: T, inputs: ToSignalInputUpdatableMap, ) { for (const inputKey in inputs) { if (componentHasSignalInputProperty(component, inputKey)) { signalSetFn(component[inputKey][SIGNAL], inputs[inputKey]) } } } export function setFixtureSignalInputs>( componentFixture: ComponentFixture, inputs: ToSignalInputUpdatableMap, options: { detectChanges: boolean } = { detectChanges: true }, ) { setSignalInputs(componentFixture.componentInstance, inputs) if (options.detectChanges) { componentFixture.detectChanges() } } ================================================ FILE: packages/angular-query-experimental/src/__tests__/with-devtools.test.ts ================================================ import { afterEach, beforeEach, describe, expect, it, test, vi } from 'vitest' import { QueryClient } from '@tanstack/query-core' import { TestBed } from '@angular/core/testing' import { ENVIRONMENT_INITIALIZER, EnvironmentInjector, InjectionToken, PLATFORM_ID, createEnvironmentInjector, isDevMode, provideZonelessChangeDetection, signal, } from '@angular/core' import { provideTanStackQuery } from '../providers' import { withDevtools } from '../devtools' import type { DevtoolsButtonPosition, DevtoolsErrorType, DevtoolsPosition, } from '@tanstack/query-devtools' import type { DevtoolsOptions } from '../devtools' const mockDevtoolsInstance = { mount: vi.fn(), unmount: vi.fn(), setClient: vi.fn(), setPosition: vi.fn(), setErrorTypes: vi.fn(), setButtonPosition: vi.fn(), setInitialIsOpen: vi.fn(), } const mockTanstackQueryDevtools = vi.fn(() => mockDevtoolsInstance) vi.mock('@tanstack/query-devtools', () => ({ TanstackQueryDevtools: mockTanstackQueryDevtools, })) vi.mock('@angular/core', async () => { const actual = await vi.importActual('@angular/core') return { ...actual, isDevMode: vi.fn(), } }) const mockIsDevMode = vi.mocked(isDevMode) describe('withDevtools feature', () => { beforeEach(() => { vi.clearAllMocks() vi.useFakeTimers() }) afterEach(() => { vi.restoreAllMocks() vi.useRealTimers() TestBed.resetTestingModule() }) test.each([ { description: 'should load devtools in development mode', isDevMode: true, expectedCalled: true, }, { description: 'should not load devtools in production mode', isDevMode: false, expectedCalled: false, }, { description: `should load devtools in development mode when 'loadDevtools' is set to 'auto'`, isDevMode: true, loadDevtools: 'auto', expectedCalled: true, }, { description: `should not load devtools in production mode when 'loadDevtools' is set to 'auto'`, isDevMode: false, loadDevtools: 'auto', expectedCalled: false, }, { description: "should load devtools in development mode when 'loadDevtools' is set to true", isDevMode: true, loadDevtools: true, expectedCalled: true, }, { description: "should load devtools in production mode when 'loadDevtools' is set to true", isDevMode: false, loadDevtools: true, expectedCalled: true, }, { description: "should not load devtools in development mode when 'loadDevtools' is set to false", isDevMode: true, loadDevtools: false, expectedCalled: false, }, { description: "should not load devtools in production mode when 'loadDevtools' is set to false", isDevMode: false, loadDevtools: false, expectedCalled: false, }, ])( '$description', async ({ isDevMode: isDevModeValue, loadDevtools, expectedCalled }) => { mockIsDevMode.mockReturnValue(isDevModeValue) const providers = [ provideZonelessChangeDetection(), provideTanStackQuery( new QueryClient(), loadDevtools !== undefined ? withDevtools( () => ({ loadDevtools, }) as DevtoolsOptions, ) : withDevtools(), ), ] TestBed.configureTestingModule({ providers, }) TestBed.inject(ENVIRONMENT_INITIALIZER) await vi.advanceTimersByTimeAsync(0) TestBed.tick() await vi.dynamicImportSettled() TestBed.tick() await vi.dynamicImportSettled() if (expectedCalled) { expect(mockTanstackQueryDevtools).toHaveBeenCalled() expect(mockDevtoolsInstance.mount).toHaveBeenCalled() } else { expect(mockTanstackQueryDevtools).not.toHaveBeenCalled() expect(mockDevtoolsInstance.mount).not.toHaveBeenCalled() } }, ) it('should not continue loading devtools after injector is destroyed', async () => { TestBed.configureTestingModule({ providers: [ provideZonelessChangeDetection(), provideTanStackQuery( new QueryClient(), withDevtools(() => ({ loadDevtools: true, })), ), ], }) TestBed.inject(ENVIRONMENT_INITIALIZER) // Destroys injector TestBed.resetTestingModule() await vi.advanceTimersByTimeAsync(0) await vi.dynamicImportSettled() expect(mockTanstackQueryDevtools).not.toHaveBeenCalled() }) it('should not create devtools again when already provided', async () => { TestBed.configureTestingModule({ providers: [ provideZonelessChangeDetection(), provideTanStackQuery( new QueryClient(), withDevtools(() => ({ loadDevtools: true, })), ), ], }) TestBed.inject(ENVIRONMENT_INITIALIZER) await vi.advanceTimersByTimeAsync(0) expect(mockTanstackQueryDevtools).toHaveBeenCalledTimes(1) const injector = TestBed.inject(EnvironmentInjector) createEnvironmentInjector( [ withDevtools(() => ({ loadDevtools: true, })).ɵproviders, ], injector, ) TestBed.inject(ENVIRONMENT_INITIALIZER) await vi.advanceTimersByTimeAsync(0) expect(mockTanstackQueryDevtools).toHaveBeenCalledTimes(1) }) it('should not load devtools if platform is not browser', async () => { TestBed.configureTestingModule({ providers: [ { provide: PLATFORM_ID, useValue: 'server', }, provideZonelessChangeDetection(), provideTanStackQuery( new QueryClient(), withDevtools(() => ({ loadDevtools: true, })), ), ], }) TestBed.inject(ENVIRONMENT_INITIALIZER) await vi.runAllTimersAsync() expect(mockTanstackQueryDevtools).not.toHaveBeenCalled() }) it('should update error types', async () => { const errorTypes = signal([] as Array) TestBed.configureTestingModule({ providers: [ provideZonelessChangeDetection(), provideTanStackQuery( new QueryClient(), withDevtools(() => ({ loadDevtools: true, errorTypes: errorTypes(), })), ), ], }) TestBed.inject(ENVIRONMENT_INITIALIZER) await vi.advanceTimersByTimeAsync(0) TestBed.tick() expect(mockDevtoolsInstance.setErrorTypes).toHaveBeenCalledTimes(0) const newErrorTypes = [ { name: '', initializer: () => new Error(), }, ] errorTypes.set(newErrorTypes) TestBed.tick() expect(mockDevtoolsInstance.setErrorTypes).toHaveBeenCalledTimes(1) expect(mockDevtoolsInstance.setErrorTypes).toHaveBeenCalledWith( newErrorTypes, ) }) it('should update client', async () => { const client = signal(new QueryClient()) TestBed.configureTestingModule({ providers: [ provideZonelessChangeDetection(), provideTanStackQuery( new QueryClient(), withDevtools(() => ({ loadDevtools: true, client: client(), })), ), ], }) TestBed.inject(ENVIRONMENT_INITIALIZER) await vi.advanceTimersByTimeAsync(0) TestBed.tick() expect(mockDevtoolsInstance.setClient).toHaveBeenCalledTimes(0) const newClient = new QueryClient() client.set(newClient) TestBed.tick() expect(mockDevtoolsInstance.setClient).toHaveBeenCalledTimes(1) expect(mockDevtoolsInstance.setClient).toHaveBeenCalledWith(newClient) }) it('should update position', async () => { const position = signal('top') TestBed.configureTestingModule({ providers: [ provideZonelessChangeDetection(), provideTanStackQuery( new QueryClient(), withDevtools(() => ({ loadDevtools: true, position: position(), })), ), ], }) TestBed.inject(ENVIRONMENT_INITIALIZER) await vi.advanceTimersByTimeAsync(0) TestBed.tick() expect(mockDevtoolsInstance.setPosition).toHaveBeenCalledTimes(0) position.set('left') TestBed.tick() expect(mockDevtoolsInstance.setPosition).toHaveBeenCalledTimes(1) expect(mockDevtoolsInstance.setPosition).toHaveBeenCalledWith('left') }) it('should update button position', async () => { const buttonPosition = signal('bottom-left') TestBed.configureTestingModule({ providers: [ provideZonelessChangeDetection(), provideTanStackQuery( new QueryClient(), withDevtools(() => ({ loadDevtools: true, buttonPosition: buttonPosition(), })), ), ], }) TestBed.inject(ENVIRONMENT_INITIALIZER) await vi.advanceTimersByTimeAsync(0) TestBed.tick() expect(mockDevtoolsInstance.setButtonPosition).toHaveBeenCalledTimes(0) buttonPosition.set('bottom-right') TestBed.tick() expect(mockDevtoolsInstance.setButtonPosition).toHaveBeenCalledTimes(1) expect(mockDevtoolsInstance.setButtonPosition).toHaveBeenCalledWith( 'bottom-right', ) }) it('should update initialIsOpen', async () => { const initialIsOpen = signal(false) TestBed.configureTestingModule({ providers: [ provideZonelessChangeDetection(), provideTanStackQuery( new QueryClient(), withDevtools(() => ({ loadDevtools: true, initialIsOpen: initialIsOpen(), })), ), ], }) TestBed.inject(ENVIRONMENT_INITIALIZER) await vi.advanceTimersByTimeAsync(0) TestBed.tick() expect(mockDevtoolsInstance.setInitialIsOpen).toHaveBeenCalledTimes(0) initialIsOpen.set(true) TestBed.tick() expect(mockDevtoolsInstance.setInitialIsOpen).toHaveBeenCalledTimes(1) expect(mockDevtoolsInstance.setInitialIsOpen).toHaveBeenCalledWith(true) }) it('should destroy devtools', async () => { const loadDevtools = signal(true) TestBed.configureTestingModule({ providers: [ provideZonelessChangeDetection(), provideTanStackQuery( new QueryClient(), withDevtools(() => ({ loadDevtools: loadDevtools(), })), ), ], }) TestBed.inject(ENVIRONMENT_INITIALIZER) await vi.advanceTimersByTimeAsync(0) expect(mockDevtoolsInstance.mount).toHaveBeenCalledTimes(1) expect(mockDevtoolsInstance.unmount).toHaveBeenCalledTimes(0) loadDevtools.set(false) TestBed.tick() expect(mockDevtoolsInstance.unmount).toHaveBeenCalledTimes(1) }) it('should unmount devtools when injector is destroyed', async () => { TestBed.configureTestingModule({ providers: [ provideZonelessChangeDetection(), provideTanStackQuery( new QueryClient(), withDevtools(() => ({ loadDevtools: true, })), ), ], }) TestBed.inject(ENVIRONMENT_INITIALIZER) await vi.advanceTimersByTimeAsync(0) TestBed.tick() await vi.dynamicImportSettled() expect(mockTanstackQueryDevtools).toHaveBeenCalled() expect(mockDevtoolsInstance.mount).toHaveBeenCalledTimes(1) expect(mockDevtoolsInstance.unmount).toHaveBeenCalledTimes(0) // Destroy the injector TestBed.resetTestingModule() expect(mockDevtoolsInstance.unmount).toHaveBeenCalledTimes(1) }) it('should remount devtools when toggled from false to true', async () => { const loadDevtools = signal(false) TestBed.configureTestingModule({ providers: [ provideZonelessChangeDetection(), provideTanStackQuery( new QueryClient(), withDevtools(() => ({ loadDevtools: loadDevtools(), })), ), ], }) TestBed.inject(ENVIRONMENT_INITIALIZER) await vi.advanceTimersByTimeAsync(0) expect(mockTanstackQueryDevtools).not.toHaveBeenCalled() expect(mockDevtoolsInstance.mount).not.toHaveBeenCalled() loadDevtools.set(true) TestBed.tick() await vi.dynamicImportSettled() expect(mockTanstackQueryDevtools).toHaveBeenCalledTimes(1) expect(mockDevtoolsInstance.mount).toHaveBeenCalledTimes(1) expect(mockDevtoolsInstance.unmount).not.toHaveBeenCalled() loadDevtools.set(false) TestBed.tick() expect(mockDevtoolsInstance.unmount).toHaveBeenCalledTimes(1) expect(mockDevtoolsInstance.mount).toHaveBeenCalledTimes(1) loadDevtools.set(true) TestBed.tick() await vi.dynamicImportSettled() // Should remount (mount called twice now) expect(mockDevtoolsInstance.mount).toHaveBeenCalledTimes(2) expect(mockDevtoolsInstance.unmount).toHaveBeenCalledTimes(1) }) describe('deps parameter', () => { it('should inject dependencies and pass them to withDevtoolsFn in correct order', async () => { const mockService1 = { value: 'service1' } const mockService2 = { value: 'service2' } const mockService1Token = new InjectionToken('MockService1') const mockService2Token = new InjectionToken('MockService2') const withDevtoolsFn = vi.fn().mockReturnValue({ loadDevtools: true }) TestBed.configureTestingModule({ providers: [ provideZonelessChangeDetection(), { provide: mockService1Token, useValue: mockService1, }, { provide: mockService2Token, useValue: mockService2, }, provideTanStackQuery( new QueryClient(), withDevtools(withDevtoolsFn, { deps: [mockService1Token, mockService2Token], }), ), ], }) TestBed.inject(ENVIRONMENT_INITIALIZER) await vi.advanceTimersByTimeAsync(0) expect(withDevtoolsFn).toHaveBeenCalledWith(mockService1, mockService2) }) it('should work with empty deps array', async () => { const withDevtoolsFn = vi.fn().mockReturnValue({ loadDevtools: true }) TestBed.configureTestingModule({ providers: [ provideZonelessChangeDetection(), provideTanStackQuery( new QueryClient(), withDevtools(withDevtoolsFn, { deps: [], }), ), ], }) TestBed.inject(ENVIRONMENT_INITIALIZER) await vi.advanceTimersByTimeAsync(0) expect(withDevtoolsFn).toHaveBeenCalledWith() }) it('should reactively update when injected services change', async () => { class ReactiveService { enabled = signal(false) position = signal('bottom') } const withDevtoolsFn = (service: ReactiveService) => ({ loadDevtools: service.enabled(), position: service.position(), }) TestBed.configureTestingModule({ providers: [ provideZonelessChangeDetection(), ReactiveService, provideTanStackQuery( new QueryClient(), withDevtools(withDevtoolsFn, { deps: [ReactiveService], }), ), ], }) TestBed.inject(ENVIRONMENT_INITIALIZER) await vi.advanceTimersByTimeAsync(0) const service = TestBed.inject(ReactiveService) expect(mockTanstackQueryDevtools).not.toHaveBeenCalled() service.enabled.set(true) TestBed.tick() await vi.dynamicImportSettled() expect(mockTanstackQueryDevtools).toHaveBeenCalledTimes(1) expect(mockTanstackQueryDevtools).toHaveBeenCalledWith( expect.objectContaining({ position: 'bottom', }), ) service.position.set('top') TestBed.tick() expect(mockDevtoolsInstance.setPosition).toHaveBeenCalledWith('top') }) }) }) ================================================ FILE: packages/angular-query-experimental/src/devtools/index.ts ================================================ export type { DevtoolsOptions, WithDevtools, WithDevtoolsFn, WithDevtoolsOptions, } from './types' export { withDevtools } from './with-devtools' ================================================ FILE: packages/angular-query-experimental/src/devtools/stub.ts ================================================ import type { WithDevtools } from './types' // Stub which replaces `withDevtools` in production builds export const withDevtools: WithDevtools = () => ({ ɵkind: 'Devtools', ɵproviders: [], }) ================================================ FILE: packages/angular-query-experimental/src/devtools/types.ts ================================================ import type { QueryClient } from '@tanstack/query-core' import type { DevtoolsButtonPosition, DevtoolsErrorType, DevtoolsPosition, } from '@tanstack/query-devtools' import type { DeveloperToolsFeature } from '../providers' /** * Options for configuring withDevtools. */ export interface WithDevtoolsOptions { /** * An array of dependencies to be injected and passed to the `withDevtoolsFn` function. * * **Example** * ```ts * export const appConfig: ApplicationConfig = { * providers: [ * provideTanStackQuery( * new QueryClient(), * withDevtools( * (devToolsOptionsManager: DevtoolsOptionsManager) => ({ * loadDevtools: devToolsOptionsManager.loadDevtools(), * }), * { * deps: [DevtoolsOptionsManager], * }, * ), * ), * ], * } * ``` */ deps?: Array } /** * Options for configuring the TanStack Query devtools. */ export interface DevtoolsOptions { /** * Set this true if you want the devtools to default to being open */ initialIsOpen?: boolean /** * The position of the TanStack logo to open and close the devtools panel. * `top-left` | `top-right` | `bottom-left` | `bottom-right` | `relative` * Defaults to `bottom-right`. */ buttonPosition?: DevtoolsButtonPosition /** * The position of the TanStack Query devtools panel. * `top` | `bottom` | `left` | `right` * Defaults to `bottom`. */ position?: DevtoolsPosition /** * Custom instance of QueryClient */ client?: QueryClient /** * Use this so you can define custom errors that can be shown in the devtools. */ errorTypes?: Array /** * Use this to pass a nonce to the style tag that is added to the document head. This is useful if you are using a Content Security Policy (CSP) nonce to allow inline styles. */ styleNonce?: string /** * Use this so you can attach the devtool's styles to a specific element in the DOM. */ shadowDOMTarget?: ShadowRoot /** * Set this to true to hide disabled queries from the devtools panel. */ hideDisabledQueries?: boolean /** * Whether the developer tools should load. * - `auto`- (Default) Lazily loads devtools when in development mode. Skips loading in production mode. * - `true`- Always load the devtools, regardless of the environment. * - `false`- Never load the devtools, regardless of the environment. * * You can use `true` and `false` to override loading developer tools from an environment file. * For example, a test environment might run in production mode but you may want to load developer tools. * * Additionally, you can use a signal in the callback to dynamically load the devtools based on a condition. For example, * a signal created from a RxJS observable that listens for a keyboard shortcut. * * **Example** * ```ts * withDevtools(() => ({ * initialIsOpen: true, * loadDevtools: inject(ExampleService).loadDevtools() * })) * ``` */ loadDevtools?: 'auto' | boolean } export type WithDevtoolsFn = (...deps: Array) => DevtoolsOptions export type WithDevtools = ( withDevtoolsFn?: WithDevtoolsFn, options?: WithDevtoolsOptions, ) => DeveloperToolsFeature ================================================ FILE: packages/angular-query-experimental/src/devtools/with-devtools.ts ================================================ import { isPlatformBrowser } from '@angular/common' import { DestroyRef, ENVIRONMENT_INITIALIZER, InjectionToken, Injector, PLATFORM_ID, computed, effect, inject, isDevMode, } from '@angular/core' import { QueryClient, noop, onlineManager } from '@tanstack/query-core' import { queryFeature } from '../providers' import type { Signal } from '@angular/core' import type { DevtoolsOptions, WithDevtools, WithDevtoolsFn, WithDevtoolsOptions, } from './types' import type { TanstackQueryDevtools } from '@tanstack/query-devtools' /** * Internal token used to prevent double providing of devtools in child injectors */ const DEVTOOLS_PROVIDED = new InjectionToken('', { factory: () => ({ isProvided: false, }), }) /** * Internal token for providing devtools options */ const DEVTOOLS_OPTIONS_SIGNAL = new InjectionToken>('') /** * Enables developer tools in Angular development builds. * * **Example** * * ```ts * export const appConfig: ApplicationConfig = { * providers: [ * provideTanStackQuery(new QueryClient(), withDevtools()) * ] * } * ``` * The devtools will be rendered in ``. * * If you need more control over when devtools are loaded, you can use the `loadDevtools` option. * * If you need more control over where devtools are rendered, consider `injectDevtoolsPanel`. This allows rendering devtools inside your own devtools for example. * @param withDevtoolsFn - A function that returns `DevtoolsOptions`. * @param options - Additional options for configuring `withDevtools`. * @returns A set of providers for use with `provideTanStackQuery`. * @see {@link provideTanStackQuery} * @see {@link DevtoolsOptions} */ export const withDevtools: WithDevtools = ( withDevtoolsFn?: WithDevtoolsFn, options: WithDevtoolsOptions = {}, ) => queryFeature('Devtools', [ { provide: DEVTOOLS_OPTIONS_SIGNAL, useFactory: (...deps: Array) => computed(() => withDevtoolsFn?.(...deps) ?? {}), deps: options.deps || [], }, { // Do not use provideEnvironmentInitializer while Angular < v19 is supported provide: ENVIRONMENT_INITIALIZER, multi: true, useFactory: () => { const devtoolsProvided = inject(DEVTOOLS_PROVIDED) if ( !isPlatformBrowser(inject(PLATFORM_ID)) || devtoolsProvided.isProvided ) return noop devtoolsProvided.isProvided = true let injectorIsDestroyed = false inject(DestroyRef).onDestroy(() => (injectorIsDestroyed = true)) return () => { const injectedClient = inject(QueryClient, { optional: true, }) const destroyRef = inject(DestroyRef) const devtoolsOptions = inject(DEVTOOLS_OPTIONS_SIGNAL) const injector = inject(Injector) let devtools: TanstackQueryDevtools | null = null let el: HTMLElement | null = null const shouldLoadToolsSignal = computed(() => { const { loadDevtools } = devtoolsOptions() return typeof loadDevtools === 'boolean' ? loadDevtools : isDevMode() }) const getResolvedQueryClient = () => { const client = devtoolsOptions().client ?? injectedClient if (!client) { throw new Error('No QueryClient found') } return client } const destroyDevtools = () => { devtools?.unmount() el?.remove() devtools = null } effect( () => { const shouldLoadTools = shouldLoadToolsSignal() const { client, position, errorTypes, buttonPosition, initialIsOpen, } = devtoolsOptions() if (!shouldLoadTools) { // Destroy or do nothing devtools && destroyDevtools() return } if (devtools) { // Update existing devtools config client && devtools.setClient(client) position && devtools.setPosition(position) errorTypes && devtools.setErrorTypes(errorTypes) buttonPosition && devtools.setButtonPosition(buttonPosition) typeof initialIsOpen === 'boolean' && devtools.setInitialIsOpen(initialIsOpen) return } // Create devtools import('@tanstack/query-devtools') .then((queryDevtools) => { // As this code runs async, the injector could have been destroyed if (injectorIsDestroyed) return devtools = new queryDevtools.TanstackQueryDevtools({ ...devtoolsOptions(), client: getResolvedQueryClient(), queryFlavor: 'Angular Query', version: '5', onlineManager, }) el = document.body.appendChild(document.createElement('div')) el.classList.add('tsqd-parent-container') devtools.mount(el) destroyRef.onDestroy(destroyDevtools) }) .catch((error) => { console.error( 'Install @tanstack/query-devtools or reinstall without --omit=optional.', error, ) }) }, { injector }, ) } }, }, ]) ================================================ FILE: packages/angular-query-experimental/src/devtools/production/index.ts ================================================ export * from '..' ================================================ FILE: packages/angular-query-experimental/src/devtools-panel/index.ts ================================================ export type { InjectDevtoolsPanel, DevtoolsPanelOptions, InjectDevtoolsPanelOptions, DevtoolsPanelRef, } from './types' export { injectDevtoolsPanel } from './inject-devtools-panel' ================================================ FILE: packages/angular-query-experimental/src/devtools-panel/inject-devtools-panel.ts ================================================ import { DestroyRef, Injector, PLATFORM_ID, assertInInjectionContext, computed, effect, inject, runInInjectionContext, untracked, } from '@angular/core' import { QueryClient, onlineManager } from '@tanstack/query-core' import { isPlatformBrowser } from '@angular/common' import type { TanstackQueryDevtoolsPanel } from '@tanstack/query-devtools' import type { DevtoolsPanelOptions, InjectDevtoolsPanel, InjectDevtoolsPanelOptions, } from './types' /** * Inject a TanStack Query devtools panel and render it in the DOM. * * Devtools panel allows programmatic control over the devtools, for example if you want to render * the devtools as part of your own devtools. * * Consider `withDevtools` instead if you don't need this. * @param injectDevtoolsPanelFn - A function that returns devtools panel options. * @param options - Additional configuration * @returns DevtoolsPanelRef * @see https://tanstack.com/query/v5/docs/framework/angular/devtools */ export const injectDevtoolsPanel: InjectDevtoolsPanel = ( injectDevtoolsPanelFn: () => DevtoolsPanelOptions, options?: InjectDevtoolsPanelOptions, ) => { !options?.injector && assertInInjectionContext(injectDevtoolsPanel) const currentInjector = options?.injector ?? inject(Injector) return runInInjectionContext(currentInjector, () => { const destroyRef = inject(DestroyRef) const isBrowser = isPlatformBrowser(inject(PLATFORM_ID)) const injectedClient = inject(QueryClient, { optional: true }) const queryOptions = computed(injectDevtoolsPanelFn) let devtools: TanstackQueryDevtoolsPanel | null = null const destroy = () => { devtools?.unmount() devtools = null } if (!isBrowser) return { destroy, } effect(() => { const { client = injectedClient, errorTypes = [], styleNonce, shadowDOMTarget, onClose, hostElement, } = queryOptions() untracked(() => { if (!client) throw new Error('No QueryClient found') if (!devtools && hostElement) { import('@tanstack/query-devtools') .then((queryDevtools) => { devtools = new queryDevtools.TanstackQueryDevtoolsPanel({ client, queryFlavor: 'Angular Query', version: '5', buttonPosition: 'bottom-left', position: 'bottom', initialIsOpen: true, errorTypes, styleNonce, shadowDOMTarget, onClose, onlineManager, }) devtools.mount(hostElement.nativeElement) }) .catch((error) => { console.error( 'Install @tanstack/query-devtools or reinstall without --omit=optional.', error, ) }) } else if (devtools && hostElement) { devtools.setClient(client) devtools.setErrorTypes(errorTypes) onClose && devtools.setOnClose(onClose) } else if (devtools && !hostElement) { destroy() } }) }) destroyRef.onDestroy(destroy) return { destroy, } }) } ================================================ FILE: packages/angular-query-experimental/src/devtools-panel/stub.ts ================================================ import { noop } from '@tanstack/query-core' import type { InjectDevtoolsPanel } from './types' // Stub which replaces `injectDevtoolsPanel` in production builds export const injectDevtoolsPanel: InjectDevtoolsPanel = () => ({ destroy: noop, }) ================================================ FILE: packages/angular-query-experimental/src/devtools-panel/types.ts ================================================ import type { DevtoolsErrorType } from '@tanstack/query-devtools' import type { ElementRef, Injector } from '@angular/core' import type { QueryClient } from '@tanstack/query-core' export interface InjectDevtoolsPanelOptions { /** * The `Injector` in which to create the devtools panel. * * If this is not provided, the current injection context will be used instead (via `inject`). */ injector?: Injector } /** * A devtools panel, which can be manually destroyed. */ export interface DevtoolsPanelRef { /** * Destroy the panel, removing it from the DOM and stops listening to signal changes. */ destroy: () => void } export interface DevtoolsPanelOptions { /** * Custom instance of QueryClient */ client?: QueryClient /** * Use this so you can define custom errors that can be shown in the devtools. */ errorTypes?: Array /** * Use this to pass a nonce to the style tag that is added to the document head. This is useful if you are using a Content Security Policy (CSP) nonce to allow inline styles. */ styleNonce?: string /** * Use this so you can attach the devtool's styles to specific element in the DOM. */ shadowDOMTarget?: ShadowRoot /** * Callback function that is called when the devtools panel is closed */ onClose?: () => unknown /** * Element where to render the devtools panel. When set to undefined or null, the devtools panel will not be created, or destroyed if existing. * If changed from undefined to a ElementRef, the devtools panel will be created. */ hostElement?: ElementRef } export type InjectDevtoolsPanel = ( injectDevtoolsPanelFn: () => DevtoolsPanelOptions, options?: InjectDevtoolsPanelOptions, ) => DevtoolsPanelRef ================================================ FILE: packages/angular-query-experimental/src/devtools-panel/production/index.ts ================================================ export * from '..' ================================================ FILE: packages/angular-query-experimental/src/inject-queries-experimental/index.ts ================================================ export * from '../inject-queries' ================================================ FILE: packages/angular-query-persist-client/eslint.config.js ================================================ // @ts-check import pluginJsdoc from 'eslint-plugin-jsdoc' import rootConfig from './root.eslint.config.js' export default [ ...rootConfig, pluginJsdoc.configs['flat/recommended-typescript'], { rules: { 'cspell/spellchecker': [ 'warn', { cspell: { ignoreRegExpList: ['\\ɵ.+'], }, }, ], 'jsdoc/require-hyphen-before-param-description': 1, 'jsdoc/sort-tags': 1, 'jsdoc/require-throws': 1, 'jsdoc/check-tag-names': [ 'warn', { // Not compatible with Api Extractor @public typed: false, }, ], }, }, ] ================================================ FILE: packages/angular-query-persist-client/package.json ================================================ { "name": "@tanstack/angular-query-persist-client", "private": true, "version": "5.62.7", "description": "Angular bindings to work with persisters in TanStack/angular-query", "author": "Omer Gronich", "license": "MIT", "repository": { "type": "git", "url": "https://github.com/TanStack/query.git", "directory": "packages/angular-query-persist-client" }, "homepage": "https://tanstack.com/query", "funding": { "type": "github", "url": "https://github.com/sponsors/tannerlinsley" }, "scripts": { "clean": "premove ./build ./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": "publint --strict && attw --pack", "build": "pnpm build:tsup", "build:tsup": "tsup --tsconfig tsconfig.prod.json" }, "type": "module", "types": "build/index.d.ts", "module": "build/index.mjs", "exports": { ".": { "types": "./build/index.d.ts", "default": "./build/index.mjs" }, "./package.json": { "default": "./package.json" } }, "sideEffects": false, "files": [ "build", "src", "!src/__tests__" ], "dependencies": { "@tanstack/query-persist-client-core": "workspace:*" }, "devDependencies": { "@angular/animations": "^20.0.0", "@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": "workspace:*", "@tanstack/query-test-utils": "workspace:*", "@testing-library/angular": "^18.0.0", "@testing-library/dom": "^10.4.0", "eslint-plugin-jsdoc": "^50.5.0", "npm-run-all2": "^5.0.0" }, "peerDependencies": { "@angular/common": ">=16.0.0", "@angular/core": ">=16.0.0", "@tanstack/angular-query-experimental": "workspace:^" } } ================================================ FILE: packages/angular-query-persist-client/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-persist-client/tsconfig.json ================================================ { "extends": "../../tsconfig.json", "compilerOptions": { "outDir": "./dist-ts", "rootDir": ".", "noFallthroughCasesInSwitch": true, "useDefineForClassFields": false, "target": "ES2022" }, "include": ["src", "test-setup.ts", "*.config.*", "package.json"], "references": [ { "path": "../angular-query-experimental" }, { "path": "../query-persist-client-core" } ] } ================================================ FILE: packages/angular-query-persist-client/tsconfig.prod.json ================================================ { "extends": "./tsconfig.json", "compilerOptions": { "incremental": false, "composite": false, "rootDir": "../../" } } ================================================ FILE: packages/angular-query-persist-client/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-persist-client/vite.config.ts ================================================ import { defineConfig } from 'vitest/config' import packageJson from './package.json' export default 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, }, }) ================================================ FILE: packages/angular-query-persist-client/.attw.json ================================================ { "ignoreRules": ["cjs-resolves-to-esm"] } ================================================ SYMLINK: packages/angular-query-persist-client/root.eslint.config.js -> eslint.config.js ================================================ ================================================ FILE: packages/angular-query-persist-client/src/index.ts ================================================ // Re-export core export * from '@tanstack/query-persist-client-core' export * from './with-persist-query-client' ================================================ FILE: packages/angular-query-persist-client/src/with-persist-query-client.ts ================================================ import { QueryClient, provideIsRestoring, queryFeature, } from '@tanstack/angular-query-experimental' import { DestroyRef, ENVIRONMENT_INITIALIZER, PLATFORM_ID, inject, signal, } from '@angular/core' import { isPlatformBrowser } from '@angular/common' import { persistQueryClientRestore, persistQueryClientSubscribe, } from '@tanstack/query-persist-client-core' import type { PersistQueryClientOptions as PersistQueryClientOptionsCore } from '@tanstack/query-persist-client-core' import type { PersistQueryClientFeature } from '@tanstack/angular-query-experimental' type PersistQueryClientOptions = { persistOptions: Omit onSuccess?: () => Promise | unknown onError?: () => Promise | unknown } /** * Enables persistence. * * **Example** * * ```ts * const localStoragePersister = createAsyncStoragePersister({ * storage: window.localStorage, * }) * * export const appConfig: ApplicationConfig = { * providers: [ * provideTanStackQuery( * new QueryClient(), * withPersistQueryClient({ * persistOptions: { * persister: localStoragePersister, * }, * onSuccess: () => console.log('Restoration completed successfully.'), * }) * ), * ], * }; * ``` * @param persistQueryClientOptions - persistence options and optional onSuccess and onError callbacks which get called when the restoration process is complete. * @returns A set of providers for use with `provideTanStackQuery`. * @public */ export function withPersistQueryClient( persistQueryClientOptions: PersistQueryClientOptions, ): PersistQueryClientFeature { const isRestoring = signal(true) const providers = [ provideIsRestoring(isRestoring.asReadonly()), { // Do not use provideEnvironmentInitializer while Angular < v19 is supported provide: ENVIRONMENT_INITIALIZER, multi: true, useValue: () => { if (!isPlatformBrowser(inject(PLATFORM_ID))) return const destroyRef = inject(DestroyRef) const queryClient = inject(QueryClient) const { onSuccess, onError, persistOptions } = persistQueryClientOptions const options = { queryClient, ...persistOptions } persistQueryClientRestore(options) .then(() => { onSuccess?.() }) .catch(() => { onError?.() }) .finally(() => { isRestoring.set(false) const cleanup = persistQueryClientSubscribe(options) destroyRef.onDestroy(cleanup) }) }, }, ] return queryFeature('PersistQueryClient', providers) } ================================================ FILE: packages/angular-query-persist-client/src/__tests__/with-persist-query-client.test.ts ================================================ import { describe, expect, test, vi } from 'vitest' import { QueryClient, injectQuery, provideTanStackQuery, } from '@tanstack/angular-query-experimental' import { persistQueryClientSave } from '@tanstack/query-persist-client-core' import { Component, effect, provideZonelessChangeDetection, } from '@angular/core' import { render, screen, waitFor } from '@testing-library/angular' import { queryKey, sleep } from '@tanstack/query-test-utils' import { withPersistQueryClient } from '../with-persist-query-client' import type { PersistedClient, Persister, } from '@tanstack/query-persist-client-core' const createMockPersister = (): Persister => { let storedState: PersistedClient | undefined return { persistClient(persistClient: PersistedClient) { storedState = persistClient }, async restoreClient() { await sleep(10) return storedState }, removeClient() { storedState = undefined }, } } const createMockErrorPersister = ( removeClient: Persister['removeClient'], ): [Error, Persister] => { const error = new Error('restore failed') return [ error, { async persistClient() { // noop }, async restoreClient() { await sleep(10) throw error }, removeClient, }, ] } describe('withPersistQueryClient', () => { test('restores cache from persister', async () => { const key = queryKey() const states: Array<{ status: string fetchStatus: string data: string | undefined }> = [] const queryClient = new QueryClient() await queryClient.prefetchQuery({ queryKey: key, queryFn: () => Promise.resolve('hydrated'), }) const persister = createMockPersister() await persistQueryClientSave({ queryClient, persister }) queryClient.clear() @Component({ template: `

{{ state.data() }}

fetchStatus: {{ state.fetchStatus() }}

`, }) class Page { state = injectQuery(() => ({ queryKey: key, queryFn: async () => { await sleep(10) return 'fetched' }, })) _ = effect(() => { states.push({ status: this.state.status(), fetchStatus: this.state.fetchStatus(), data: this.state.data(), }) }) } render(Page, { providers: [ provideZonelessChangeDetection(), provideTanStackQuery( queryClient, withPersistQueryClient({ persistOptions: { persister } }), ), ], }) await waitFor(() => screen.getByText('fetchStatus: idle')) await waitFor(() => screen.getByText('hydrated')) await waitFor(() => screen.getByText('fetched')) expect(states).toHaveLength(3) expect(states[0]).toMatchObject({ status: 'pending', fetchStatus: 'idle', data: undefined, }) expect(states[1]).toMatchObject({ status: 'success', fetchStatus: 'fetching', data: 'hydrated', }) expect(states[2]).toMatchObject({ status: 'success', fetchStatus: 'idle', data: 'fetched', }) }) test.todo( '(Once injectQueries is functional) verify that injectQueries transitions to an idle state', ) test('should show initialData while restoring', async () => { const key = queryKey() const states: Array<{ status: string fetchStatus: string data: string | undefined }> = [] const queryClient = new QueryClient() await queryClient.prefetchQuery({ queryKey: key, queryFn: () => Promise.resolve('hydrated'), }) const persister = createMockPersister() await persistQueryClientSave({ queryClient, persister }) queryClient.clear() @Component({ template: `

{{ state.data() }}

fetchStatus: {{ state.fetchStatus() }}

`, }) class Page { state = injectQuery(() => ({ queryKey: key, queryFn: async () => { await sleep(10) return 'fetched' }, initialData: 'initial', // make sure that initial data is older than the hydration data // otherwise initialData would be newer and takes precedence initialDataUpdatedAt: 1, })) _ = effect(() => { states.push({ status: this.state.status(), fetchStatus: this.state.fetchStatus(), data: this.state.data(), }) }) } render(Page, { providers: [ provideZonelessChangeDetection(), provideTanStackQuery( queryClient, withPersistQueryClient({ persistOptions: { persister } }), ), ], }) await waitFor(() => screen.getByText('fetched')) expect(states).toHaveLength(3) expect(states[0]).toMatchObject({ status: 'success', fetchStatus: 'idle', data: 'initial', }) expect(states[1]).toMatchObject({ status: 'success', fetchStatus: 'fetching', data: 'hydrated', }) expect(states[2]).toMatchObject({ status: 'success', fetchStatus: 'idle', data: 'fetched', }) }) test('should not refetch after restoring when data is fresh', async () => { const key = queryKey() const states: Array<{ status: string fetchStatus: string data: string | undefined }> = [] const queryClient = new QueryClient() await queryClient.prefetchQuery({ queryKey: key, queryFn: () => Promise.resolve('hydrated'), }) const persister = createMockPersister() await persistQueryClientSave({ queryClient, persister }) queryClient.clear() let fetched = false @Component({ template: `

data: {{ state.data() ?? 'null' }}

fetchStatus: {{ state.fetchStatus() }}

`, }) class Page { state = injectQuery(() => ({ queryKey: key, queryFn: async () => { fetched = true await sleep(10) return 'fetched' }, staleTime: Infinity, })) _ = effect(() => { states.push({ status: this.state.status(), fetchStatus: this.state.fetchStatus(), data: this.state.data(), }) }) } render(Page, { providers: [ provideZonelessChangeDetection(), provideTanStackQuery( queryClient, withPersistQueryClient({ persistOptions: { persister } }), ), ], }) await waitFor(() => screen.getByText('data: null')) await waitFor(() => screen.getByText('data: hydrated')) expect(states).toHaveLength(2) expect(fetched).toBe(false) expect(states[0]).toMatchObject({ status: 'pending', fetchStatus: 'idle', data: undefined, }) expect(states[1]).toMatchObject({ status: 'success', fetchStatus: 'idle', data: 'hydrated', }) }) test('should call onSuccess after successful restoring', async () => { const key = queryKey() const queryClient = new QueryClient() await queryClient.prefetchQuery({ queryKey: key, queryFn: () => Promise.resolve('hydrated'), }) const persister = createMockPersister() await persistQueryClientSave({ queryClient, persister }) queryClient.clear() @Component({ template: `

{{ state.data() }}

fetchStatus: {{ state.fetchStatus() }}

`, }) class Page { state = injectQuery(() => ({ queryKey: key, queryFn: async () => { await sleep(10) return 'fetched' }, })) } const onSuccess = vi.fn() render(Page, { providers: [ provideZonelessChangeDetection(), provideTanStackQuery( queryClient, withPersistQueryClient({ persistOptions: { persister }, onSuccess, }), ), ], }) expect(onSuccess).toHaveBeenCalledTimes(0) await waitFor(() => screen.getByText('fetched')) expect(onSuccess).toHaveBeenCalledTimes(1) }) test('should remove cache after non-successful restoring', async () => { const key = queryKey() const onErrorMock = vi .spyOn(console, 'error') .mockImplementation(() => undefined) const queryClient = new QueryClient() const removeClient = vi.fn() const [error, persister] = createMockErrorPersister(removeClient) const onSuccess = vi.fn() const onError = vi.fn() @Component({ template: `

{{ state.data() }}

fetchStatus: {{ state.fetchStatus() }}

`, }) class Page { state = injectQuery(() => ({ queryKey: key, queryFn: async () => { await sleep(10) return 'fetched' }, })) } render(Page, { providers: [ provideZonelessChangeDetection(), provideTanStackQuery( queryClient, withPersistQueryClient({ persistOptions: { persister }, onSuccess, onError, }), ), ], }) await waitFor(() => screen.getByText('fetched')) expect(removeClient).toHaveBeenCalledTimes(1) expect(onSuccess).toHaveBeenCalledTimes(0) expect(onError).toHaveBeenCalledTimes(1) expect(onErrorMock).toHaveBeenCalledTimes(1) expect(onErrorMock).toHaveBeenNthCalledWith(1, error) onErrorMock.mockRestore() }) }) ================================================ FILE: packages/eslint-plugin-query/eslint.config.js ================================================ // @ts-check import vitest from '@vitest/eslint-plugin' import rootConfig from './root.eslint.config.js' export default [ ...rootConfig, { plugins: { vitest }, rules: { ...vitest.configs.recommended.rules, 'vitest/expect-expect': [ 'warn', { assertFunctionNames: ['expect', 'expectArrayEqualIgnoreOrder'], }, ], }, }, ] ================================================ FILE: packages/eslint-plugin-query/package.json ================================================ { "name": "@tanstack/eslint-plugin-query", "version": "5.89.0", "description": "ESLint plugin for TanStack Query", "author": "Eliya Cohen", "license": "MIT", "repository": { "type": "git", "url": "git+https://github.com/TanStack/query.git", "directory": "packages/eslint-plugin-query" }, "homepage": "https://tanstack.com/query", "funding": { "type": "github", "url": "https://github.com/sponsors/tannerlinsley" }, "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": "publint --strict && attw --pack", "build": "tsup --tsconfig tsconfig.prod.json" }, "type": "module", "types": "build/legacy/index.d.ts", "main": "build/legacy/index.cjs", "module": "build/legacy/index.js", "react-native": "src/index.ts", "exports": { ".": { "@tanstack/custom-condition": "./src/index.ts", "import": { "types": "./build/modern/index.d.ts", "default": "./build/modern/index.js" }, "require": { "types": "./build/modern/index.d.cts", "default": "./build/modern/index.cjs" } }, "./package.json": "./package.json" }, "sideEffects": false, "files": [ "build", "src", "!src/__tests__" ], "dependencies": { "@typescript-eslint/utils": "^8.44.0" }, "devDependencies": { "@typescript-eslint/parser": "^8.44.0", "@typescript-eslint/rule-tester": "^8.44.0", "combinate": "^1.1.11", "eslint": "^9.36.0", "npm-run-all2": "^5.0.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0" } } ================================================ FILE: packages/eslint-plugin-query/root.tsup.config.js ================================================ // @ts-check /** * @param {Object} opts - Options for building configurations. * @param {string[]} opts.entry - The entry array. * @returns {import('tsup').Options} */ export function modernConfig(opts) { return { entry: opts.entry, format: ['cjs', 'esm'], target: ['chrome91', 'firefox90', 'edge91', 'safari15', 'ios15', 'opera77'], outDir: 'build/modern', dts: true, sourcemap: true, clean: true, external: ['typescript'], footer: ({ format }) => { if (format === 'cjs') { // workaround for CJS default export // @see https://github.com/evanw/esbuild/issues/1182#issuecomment-1011414271 return { js: `module.exports = module.exports.default` } } return }, } } /** * @param {Object} opts - Options for building configurations. * @param {string[]} opts.entry - The entry array. * @returns {import('tsup').Options} */ export function legacyConfig(opts) { return { entry: opts.entry, format: ['cjs', 'esm'], target: ['es2020', 'node16'], outDir: 'build/legacy', dts: true, sourcemap: true, clean: true, external: ['typescript'], } } ================================================ FILE: packages/eslint-plugin-query/tsconfig.json ================================================ { "extends": "../../tsconfig.json", "compilerOptions": { "outDir": "./dist-ts", "rootDir": "." }, "include": ["src", "*.config.*", "package.json"] } ================================================ FILE: packages/eslint-plugin-query/tsconfig.prod.json ================================================ { "extends": "./tsconfig.json", "compilerOptions": { "incremental": false, "composite": false, "rootDir": "../../" } } ================================================ FILE: packages/eslint-plugin-query/tsup.config.ts ================================================ import { defineConfig } from 'tsup' import { legacyConfig, modernConfig } from './root.tsup.config.js' export default defineConfig([ modernConfig({ entry: ['src/*.ts'] }), legacyConfig({ entry: ['src/*.ts'] }), ]) ================================================ FILE: packages/eslint-plugin-query/vite.config.ts ================================================ import { defineConfig, mergeConfig } from 'vitest/config' import { tanstackViteConfig } from '@tanstack/config/vite' import packageJson from './package.json' 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, globals: true, coverage: { enabled: true, provider: 'istanbul', include: ['src/**/*'] }, typecheck: { enabled: true }, restoreMocks: true, }, }) export default mergeConfig( config, tanstackViteConfig({ entry: './src/index.ts', srcDir: './src', exclude: ['./src/__tests__'], }), ) ================================================ FILE: packages/eslint-plugin-query/.attw.json ================================================ { "ignoreRules": ["false-export-default"] } ================================================ SYMLINK: packages/eslint-plugin-query/root.eslint.config.js -> eslint.config.js ================================================ ================================================ FILE: packages/eslint-plugin-query/src/index.ts ================================================ import { rules } from './rules' import type { ESLint, Linter } from 'eslint' import type { RuleModule } from '@typescript-eslint/utils/ts-eslint' type RuleKey = keyof typeof rules export interface Plugin extends Omit { rules: Record> configs: { recommended: ESLint.ConfigData 'flat/recommended': Array } } export const plugin: Plugin = { meta: { name: '@tanstack/eslint-plugin-query', }, configs: {} as Plugin['configs'], rules, } // Assign configs here so we can reference `plugin` Object.assign(plugin.configs, { recommended: { plugins: ['@tanstack/query'], rules: { '@tanstack/query/exhaustive-deps': 'error', '@tanstack/query/no-rest-destructuring': 'warn', '@tanstack/query/stable-query-client': 'error', '@tanstack/query/no-unstable-deps': 'error', '@tanstack/query/infinite-query-property-order': 'error', '@tanstack/query/no-void-query-fn': 'error', '@tanstack/query/mutation-property-order': 'error', }, }, 'flat/recommended': [ { name: 'tanstack/query/flat/recommended', plugins: { '@tanstack/query': plugin, }, rules: { '@tanstack/query/exhaustive-deps': 'error', '@tanstack/query/no-rest-destructuring': 'warn', '@tanstack/query/stable-query-client': 'error', '@tanstack/query/no-unstable-deps': 'error', '@tanstack/query/infinite-query-property-order': 'error', '@tanstack/query/no-void-query-fn': 'error', '@tanstack/query/mutation-property-order': 'error', }, }, ], }) export default plugin ================================================ FILE: packages/eslint-plugin-query/src/rules.ts ================================================ import * as exhaustiveDeps from './rules/exhaustive-deps/exhaustive-deps.rule' import * as stableQueryClient from './rules/stable-query-client/stable-query-client.rule' import * as noRestDestructuring from './rules/no-rest-destructuring/no-rest-destructuring.rule' import * as noUnstableDeps from './rules/no-unstable-deps/no-unstable-deps.rule' import * as infiniteQueryPropertyOrder from './rules/infinite-query-property-order/infinite-query-property-order.rule' import * as noVoidQueryFn from './rules/no-void-query-fn/no-void-query-fn.rule' import * as mutationPropertyOrder from './rules/mutation-property-order/mutation-property-order.rule' import type { ESLintUtils } from '@typescript-eslint/utils' import type { ExtraRuleDocs } from './types' export const rules: Record< string, ESLintUtils.RuleModule< string, ReadonlyArray, ExtraRuleDocs, ESLintUtils.RuleListener > > = { [exhaustiveDeps.name]: exhaustiveDeps.rule, [stableQueryClient.name]: stableQueryClient.rule, [noRestDestructuring.name]: noRestDestructuring.rule, [noUnstableDeps.name]: noUnstableDeps.rule, [infiniteQueryPropertyOrder.name]: infiniteQueryPropertyOrder.rule, [noVoidQueryFn.name]: noVoidQueryFn.rule, [mutationPropertyOrder.name]: mutationPropertyOrder.rule, } ================================================ FILE: packages/eslint-plugin-query/src/types.ts ================================================ export type ExtraRuleDocs = { recommended: 'strict' | 'error' | 'warn' } ================================================ FILE: packages/eslint-plugin-query/src/__tests__/exhaustive-deps.test.ts ================================================ import { RuleTester } from '@typescript-eslint/rule-tester' import { rule } from '../rules/exhaustive-deps/exhaustive-deps.rule' import { normalizeIndent } from './test-utils' const ruleTester = new RuleTester() ruleTester.run('exhaustive-deps', rule, { valid: [ { name: 'should pass when deps are passed in array (react)', code: 'useQuery({ queryKey: ["todos"], queryFn: fetchTodos });', }, { name: 'should pass when deps are passed in array (solid)', code: 'createQuery({ queryKey: ["todos"], queryFn: fetchTodos });', }, { name: 'should pass when deps are passed in array', code: 'useQuery({ queryKey: ["entity", id], queryFn: () => api.getEntity(id) });', }, { name: 'should pass when deps are passed in template literal', code: 'useQuery({ queryKey: [`entity/${id}`], queryFn: () => api.getEntity(id) });', }, { name: 'should not pass fetch', code: 'useQuery({ queryKey: ["entity", id], queryFn: () => fetch(id) });', }, { name: 'should not pass axios.get', code: 'useQuery({ queryKey: ["entity", id], queryFn: () => axios.get(id) });', }, { name: 'should not pass api.entity.get', code: 'useQuery({ queryKey: ["entity", id], queryFn: () => api.entity.get(id) });', }, { name: 'should not pass api when is being used for calling a function', code: ` import useApi from './useApi' const useFoo = () => { const api = useApi(); return useQuery({ queryKey: ['foo'], queryFn: () => api.fetchFoo(), }) } `, }, { name: 'should pass props.src', code: ` function MyComponent(props) { useQuery({ queryKey: ["entity", props.src], queryFn: () => api.entity.get(props.src) }); } `, }, { name: 'identify !!props.id (unary expression)', code: ` function MyComponent(props) { useQuery({ queryKey: ["entity", !!props.id], queryFn: () => api.entity.get(props.id) }); } `, }, { name: 'identify props?.id (chain expression)', code: ` function MyComponent(props) { useQuery({ queryKey: ["entity", props?.id], queryFn: () => api.entity.get(props?.id) }); } `, }, { name: 'identify props!.id (ts non null expression)', code: ` function MyComponent(props) { useQuery({ queryKey: ["entity", props!.id], queryFn: () => api.entity.get(props!.id) }); } `, }, { name: 'should ignore keys from callback', code: ` function MyComponent(props) { useQuery({ queryKey: ["foo", dep1], queryFn: ({ queryKey: [, dep] }) => fetch(dep), }); } `, }, { name: 'should ignore type identifiers', code: ` type Result = {}; function MyComponent(props) { useQuery({ queryKey: ["foo", dep], queryFn: () => api.get(dep), }); } `, }, { name: 'should add "...args" to deps', code: ` function foo(...args) {} function useData(arg, ...args) { return useQuery({ queryKey: ['foo', arg, ...args], queryFn: async () => foo([arg, ...args]) }); } `, }, { name: 'should not add class to deps', code: ` class Foo {} useQuery({ queryKey: ['foo'], queryFn: async () => new Foo() }); `, }, { name: 'should not add `undefined` to deps', code: ` useQuery({ queryKey: [], queryFn: async () => { if (undefined) { return null; } return 1 }, }); `, }, { name: 'should not fail when queryKey is a queryKeyFactory while having a dep as first arg', code: normalizeIndent` const fooQueryKeyFactory = { foo: () => ['foo'] as const, num: (num: number) => [...fooQueryKeyFactory.foo(), num] as const, } const useFoo = (num: number) => useQuery({ queryKey: fooQueryKeyFactory.foo(num), queryFn: () => Promise.resolve(num), }) `, }, { name: 'should not fail when queryKey is a queryKeyFactory while having a dep in object', code: normalizeIndent` const fooQueryKeyFactory = { foo: () => ['foo'] as const, num: (num: number) => [...fooQueryKeyFactory.foo(), num] as const, } const useFoo = (num: number) => useQuery({ queryKey: fooQueryKeyFactory.foo({ x: num }), queryFn: () => Promise.resolve(num), }) `, }, { name: 'should not fail when queryKey is a queryKeyFactory while having a dep in object 2', code: normalizeIndent` const fooQueryKeyFactory = { foo: () => ['foo'] as const, num: (num: number) => [...fooQueryKeyFactory.foo(), num] as const, } const useFoo = (num: number) => useQuery({ queryKey: fooQueryKeyFactory.foo({ num }), queryFn: () => Promise.resolve(num), }) `, }, { name: 'should not fail when queryKey is a queryKeyFactory while having a dep in array', code: normalizeIndent` const fooQueryKeyFactory = { foo: () => ['foo'] as const, num: (num: number) => [...fooQueryKeyFactory.foo(), num] as const, } const useFoo = (num: number) => useQuery({ queryKey: fooQueryKeyFactory.foo([num]), queryFn: () => Promise.resolve(num), }) `, }, { name: 'should not fail when queryKey is a queryKeyFactory while having a dep in second arg', code: normalizeIndent` const fooQueryKeyFactory = { foo: () => ['foo'] as const, num: (num: number) => [...fooQueryKeyFactory.foo(), num] as const, } const useFoo = (num: number) => useQuery({ queryKey: fooQueryKeyFactory.foo(1, num), queryFn: () => Promise.resolve(num), }) `, }, { name: 'should not fail when queryKey is a queryKeyFactory while having a dep is object prop', code: normalizeIndent` const fooQueryKeyFactory = { foo: () => ['foo'] as const, num: (num: number) => [...fooQueryKeyFactory.foo(), num] as const, } const useFoo = (obj: { num: number }) => useQuery({ queryKey: fooQueryKeyFactory.foo(obj.num), queryFn: () => Promise.resolve(obj.num), }) `, }, { name: 'should not treat new Error as missing dependency', code: normalizeIndent` useQuery({ queryKey: ['foo'], queryFn: () => Promise.reject(new Error('1')), }) `, }, { name: 'should see id when there is a const assertion', code: normalizeIndent` const useX = (id: number) => { return useQuery({ queryKey: ['foo', id] as const, queryFn: async () => id, }) } `, }, { name: 'should not fail if queryKey is having the whole object while queryFn uses some props of it', code: normalizeIndent` const state = { foo: 'foo', bar: 'bar' } useQuery({ queryKey: ['state', state], queryFn: () => Promise.resolve({ foo: state.foo, bar: state.bar }) }) `, }, { name: 'should not fail if queryKey does not include an internal dependency', code: normalizeIndent` useQuery({ queryKey: ["api"], queryFn: async () => { const response = Promise.resolve([]); const data = await response.json(); return data[0].name; }, }); `, }, { name: 'should ignore constants defined out of scope (react component, function declaration)', code: ` const CONST_VAL = 1 function MyComponent() { useQuery({ queryKey: ["foo"], queryFn: () => CONST_VAL }); } `, }, { name: 'should ignore constants defined out of scope (react component, function expression)', code: ` const CONST_VAL = 1 const MyComponent = () => { useQuery({ queryKey: ["foo"], queryFn: () => CONST_VAL }); } `, }, { name: 'should ignore constants defined out of scope (react component, anonymous function)', code: ` const CONST_VAL = 1 const MyComponent = function () { useQuery({ queryKey: ["foo"], queryFn: () => CONST_VAL }); } `, }, { name: 'should ignore constants defined out of scope (non react component/hook function)', code: ` const CONST_VAL = 1 function fn() { return { queryKey: ["foo"], queryFn: () => CONST_VAL } } `, }, { name: 'should ignore constants defined out of scope (react hook, function declaration)', code: ` const CONST_VAL = 1 function useHook() { useQuery({ queryKey: ["foo"], queryFn: () => CONST_VAL }); } `, }, { name: 'should ignore constants defined out of scope (react hook, function expression)', code: ` const CONST_VAL = 1 const useHook = () => { useQuery({ queryKey: ["foo"], queryFn: () => CONST_VAL }); } `, }, { name: 'should ignore constants defined out of scope (react hook, anonymous function)', code: ` const CONST_VAL = 1 const useHook = function () { useQuery({ queryKey: ["foo"], queryFn: () => CONST_VAL }); } `, }, { name: 'query key with nullish coalescing operator', code: ` const factory = (id: number) => ['foo', id]; function Component({ id }) { useQuery({ queryKey: factory(id ?? -1), queryFn: () => Promise.resolve({ id }) }); } `, }, { name: 'instanceof value should not be in query key', code: ` class SomeClass {} function Component({ value }) { useQuery({ queryKey: ['foo'], queryFn: () => { return value instanceof SomeClass; } }); } `, }, { name: 'queryFn as a ternary expression with dep and a skipToken', code: normalizeIndent` import { useQuery, skipToken } from "@tanstack/react-query"; const fetch = true function Component({ id }) { useQuery({ queryKey: [id], queryFn: fetch ? () => Promise.resolve(id) : skipToken }) } `, }, { name: 'should not fail when queryFn uses nullish coalescing operator', code: normalizeIndent` useQuery({ queryKey: ["foo", options], queryFn: () => options?.params ?? options }); `, }, { name: 'should not fail when queryKey uses arrow function to produce a key', code: normalizeIndent` const obj = reactive<{ boo?: string }>({}); const query = useQuery({ queryKey: ['foo', () => obj.boo], queryFn: () => fetch(\`/mock/getSomething/\${obj.boo}\`), enable: () => !!obj.boo, }); `, }, { name: 'should not fail when queryKey uses arrow function to produce a key as the body return', code: normalizeIndent` const obj = reactive<{ boo?: string }>({}); const query = useQuery({ queryKey: ['foo', () => { return obj.boo }], queryFn: () => fetch(\`/mock/getSomething/\${obj.boo}\`), enable: () => !!obj.boo, }); `, }, { name: 'should not fail when queryKey uses function expression to produce a key as the body return', code: normalizeIndent` const obj = reactive<{ boo?: string }>({}); const query = useQuery({ queryKey: ['foo', function() { return obj.boo }], queryFn: () => fetch(\`/mock/getSomething/\${obj.boo}\`), enable: () => !!obj.boo, }); `, }, { name: 'should not fail when queryFn inside queryOptions contains a reference to an external variable', code: normalizeIndent` const EXTERNAL = 1; export const queries = { foo: queryOptions({ queryKey: ['foo'], queryFn: () => Promise.resolve(EXTERNAL), }), }; `, }, { name: 'should pass with optional chaining as key', code: ` function useTest(data?: any) { return useQuery({ queryKey: ['query-name', data?.address], queryFn: async () => sendQuery(data.address), enabled: !!data?.address, }) } `, }, { name: 'should pass with optional chaining as key and non-null assertion in queryFn', code: ` function useTest(data?: any) { return useQuery({ queryKey: ['query-name', data?.address], queryFn: async () => sendQuery(data!.address), enabled: !!data?.address, }) } `, }, { name: 'should pass with optional chaining as key and non-null assertion at the end of the variable in queryFn', code: ` function useTest(data?: any) { return useQuery({ queryKey: ['query-name', data?.address], queryFn: async () => sendQuery(data!.address!), enabled: !!data?.address, }) } `, }, ], invalid: [ { name: 'should fail when deps are missing in query factory', code: normalizeIndent` const todoQueries = { list: () => ({ queryKey: ['entity'], queryFn: fetchEntities }), detail: (id) => ({ queryKey: ['entity'], queryFn: () => fetchEntity(id) }) } `, errors: [ { messageId: 'missingDeps', data: { deps: 'id' }, suggestions: [ { messageId: 'fixTo', data: { result: "['entity', id]" }, output: normalizeIndent` const todoQueries = { list: () => ({ queryKey: ['entity'], queryFn: fetchEntities }), detail: (id) => ({ queryKey: ['entity', id], queryFn: () => fetchEntity(id) }) } `, }, ], }, ], }, { name: 'should fail when no deps are passed (react)', code: normalizeIndent` function Component() { const id = 1; useQuery({ queryKey: ["entity"], queryFn: () => api.getEntity(id) }); } `, errors: [ { messageId: 'missingDeps', data: { deps: 'id' }, suggestions: [ { messageId: 'fixTo', data: { result: '["entity", id]' }, output: normalizeIndent` function Component() { const id = 1; useQuery({ queryKey: ["entity", id], queryFn: () => api.getEntity(id) }); } `, }, ], }, ], }, { name: 'should fail when no deps are passed (solid)', code: normalizeIndent` function Component() { const id = 1; createQuery({ queryKey: ["entity"], queryFn: () => api.getEntity(id) }); } `, errors: [ { messageId: 'missingDeps', data: { deps: 'id' }, suggestions: [ { messageId: 'fixTo', data: { result: '["entity", id]' }, output: normalizeIndent` function Component() { const id = 1; createQuery({ queryKey: ["entity", id], queryFn: () => api.getEntity(id) }); } `, }, ], }, ], }, { name: 'should fail when deps are passed incorrectly', code: normalizeIndent` function Component() { const id = 1; useQuery({ queryKey: ["entity/\${id}"], queryFn: () => api.getEntity(id) }); } `, errors: [ { messageId: 'missingDeps', data: { deps: 'id' }, suggestions: [ { messageId: 'fixTo', data: { result: '["entity/${id}", id]' }, output: normalizeIndent` function Component() { const id = 1; useQuery({ queryKey: ["entity/\${id}", id], queryFn: () => api.getEntity(id) }); } `, }, ], }, ], }, { name: 'should pass missing dep while key has a template literal', code: normalizeIndent` function Component() { const a = 1; const b = 2; useQuery({ queryKey: [\`entity/\${a}\`], queryFn: () => api.getEntity(a, b) }); } `, errors: [ { messageId: 'missingDeps', data: { deps: 'b' }, suggestions: [ { messageId: 'fixTo', data: { result: '[`entity/${a}`, b]' }, output: normalizeIndent` function Component() { const a = 1; const b = 2; useQuery({ queryKey: [\`entity/\${a}\`, b], queryFn: () => api.getEntity(a, b) }); } `, }, ], }, ], }, { name: 'should fail when dep exists inside setter and missing in queryKey', code: normalizeIndent` function Component() { const [id] = React.useState(1); useQuery({ queryKey: ["entity"], queryFn: () => { const { data } = axios.get(\`.../\${id}\`); return data; } }); } `, errors: [ { messageId: 'missingDeps', data: { deps: 'id' }, suggestions: [ { messageId: 'fixTo', data: { result: '["entity", id]' }, output: normalizeIndent` function Component() { const [id] = React.useState(1); useQuery({ queryKey: ["entity", id], queryFn: () => { const { data } = axios.get(\`.../\${id}\`); return data; } }); } `, }, ], }, ], }, { name: 'should fail when dep does not exist while having a complex queryKey', code: normalizeIndent` const todoQueries = { key: (a, b, c, d, e) => ({ queryKey: ["entity", a, [b], { c }, 1, true], queryFn: () => api.getEntity(a, b, c, d, e) }) } `, errors: [ { messageId: 'missingDeps', data: { deps: 'd, e' }, suggestions: [ { messageId: 'fixTo', data: { result: '["entity", a, [b], { c }, 1, true, d, e]' }, output: normalizeIndent` const todoQueries = { key: (a, b, c, d, e) => ({ queryKey: ["entity", a, [b], { c }, 1, true, d, e], queryFn: () => api.getEntity(a, b, c, d, e) }) } `, }, ], }, ], }, { name: 'should fail when dep does not exist while having a complex queryKey #2', code: normalizeIndent` const todoQueries = { key: (dep1, dep2, dep3, dep4, dep5, dep6, dep7, dep8) => ({ queryKey: ['foo', {dep1, dep2: dep2, bar: dep3, baz: [dep4, dep5]}, [dep6, dep7]], queryFn: () => api.getEntity(dep1, dep2, dep3, dep4, dep5, dep6, dep7, dep8), }), }; `, errors: [ { messageId: 'missingDeps', data: { deps: 'dep8' }, suggestions: [ { messageId: 'fixTo', data: { result: "['foo', {dep1, dep2: dep2, bar: dep3, baz: [dep4, dep5]}, [dep6, dep7], dep8]", }, output: normalizeIndent` const todoQueries = { key: (dep1, dep2, dep3, dep4, dep5, dep6, dep7, dep8) => ({ queryKey: ['foo', {dep1, dep2: dep2, bar: dep3, baz: [dep4, dep5]}, [dep6, dep7], dep8], queryFn: () => api.getEntity(dep1, dep2, dep3, dep4, dep5, dep6, dep7, dep8), }), }; `, }, ], }, ], }, { name: 'should fail when two deps that depend on each other are missing', code: normalizeIndent` function Component({ map, key }) { useQuery({ queryKey: ["key"], queryFn: () => api.get(map[key]) }); } `, errors: [ { messageId: 'missingDeps', data: { deps: 'map[key]' }, suggestions: [ { messageId: 'fixTo', data: { result: '["key", map[key]]', }, output: normalizeIndent` function Component({ map, key }) { useQuery({ queryKey: ["key", map[key]], queryFn: () => api.get(map[key]) }); } `, }, ], }, ], }, { name: 'should fail when a queryKey is a reference of an array expression with a missing dep', code: normalizeIndent` function Component() { const x = 5; const queryKey = ['foo'] useQuery({ queryKey, queryFn: () => x }) } `, errors: [ { messageId: 'missingDeps', data: { deps: 'x' }, suggestions: [ { messageId: 'fixTo', data: { result: "['foo', x]", }, output: normalizeIndent` function Component() { const x = 5; const queryKey = ['foo', x] useQuery({ queryKey, queryFn: () => x }) } `, }, ], }, ], }, { name: 'should fail when queryKey is a queryKeyFactory while having missing dep', code: normalizeIndent` const fooQueryKeyFactory = { foo: () => ['foo'] as const } const useFoo = (num: number) => useQuery({ queryKey: fooQueryKeyFactory.foo(), queryFn: () => Promise.resolve(num), }) `, errors: [ { messageId: 'missingDeps', data: { deps: 'num' }, }, ], }, { name: 'should fail if queryFn is using multiple object props when only one of them is in the queryKey', code: normalizeIndent` function Component() { const state = { foo: 'foo', bar: 'bar' } useQuery({ queryKey: ['state', state.foo], queryFn: () => Promise.resolve({ foo: state.foo, bar: state.bar }) }) } `, errors: [ { suggestions: [ { messageId: 'fixTo', output: normalizeIndent` function Component() { const state = { foo: 'foo', bar: 'bar' } useQuery({ queryKey: ['state', state.foo, state.bar], queryFn: () => Promise.resolve({ foo: state.foo, bar: state.bar }) }) } `, }, ], messageId: 'missingDeps', data: { deps: 'state.bar' }, }, ], }, { name: 'should fail if queryFn is invalid while using FunctionExpression syntax', code: normalizeIndent` function Component() { const id = 1; useQuery({ queryKey: [], queryFn() { Promise.resolve(id) } }); } `, errors: [ { suggestions: [ { messageId: 'fixTo', output: normalizeIndent` function Component() { const id = 1; useQuery({ queryKey: [id], queryFn() { Promise.resolve(id) } }); } `, }, ], messageId: 'missingDeps', data: { deps: 'id' }, }, ], }, { name: 'should fail if queryFn is a ternary expression with missing dep and a skipToken', code: normalizeIndent` import { useQuery, skipToken } from "@tanstack/react-query"; const fetch = true function Component({ id }) { useQuery({ queryKey: [], queryFn: fetch ? () => Promise.resolve(id) : skipToken }) } `, errors: [ { suggestions: [ { messageId: 'fixTo', output: normalizeIndent` import { useQuery, skipToken } from "@tanstack/react-query"; const fetch = true function Component({ id }) { useQuery({ queryKey: [id], queryFn: fetch ? () => Promise.resolve(id) : skipToken }) } `, }, ], messageId: 'missingDeps', data: { deps: 'id' }, }, ], }, ], }) ================================================ FILE: packages/eslint-plugin-query/src/__tests__/infinite-query-property-order.rule.test.ts ================================================ import { RuleTester } from '@typescript-eslint/rule-tester' import combinate from 'combinate' import { checkedProperties, infiniteQueryFunctions, } from '../rules/infinite-query-property-order/constants' import { name, rule, } from '../rules/infinite-query-property-order/infinite-query-property-order.rule' import { generateInterleavedCombinations, generatePartialCombinations, generatePermutations, normalizeIndent, } from './test-utils' import type { InfiniteQueryFunctions } from '../rules/infinite-query-property-order/constants' const ruleTester = new RuleTester() type CheckedProperties = (typeof checkedProperties)[number] const orderIndependentProps = [ 'queryKey', '...objectExpressionSpread', '...callExpressionSpread', '...memberCallExpressionSpread', ] as const type OrderIndependentProps = (typeof orderIndependentProps)[number] interface TestCase { infiniteQueryFunction: InfiniteQueryFunctions properties: Array } const validTestMatrix = combinate({ infiniteQueryFunction: [...infiniteQueryFunctions], properties: generatePartialCombinations(checkedProperties, 2), }) export function generateInvalidPermutations( arr: ReadonlyArray, ): Array<{ invalid: Array valid: Array }> { const combinations = generatePartialCombinations(arr, 2) const allPermutations: Array<{ invalid: Array valid: Array }> = [] for (const combination of combinations) { const permutations = generatePermutations(combination) // skip the first permutation as it matches the original combination const invalidPermutations = permutations.slice(1) if ( combination.includes('getNextPageParam') && combination.includes('getPreviousPageParam') ) { if ( combination.indexOf('getNextPageParam') < combination.indexOf('getPreviousPageParam') ) { // since we ignore the relative order of 'getPreviousPageParam' and 'getNextPageParam', we skip this combination (but keep the other one where `getPreviousPageParam` is before `getNextPageParam`) continue } } allPermutations.push( ...invalidPermutations .map((p) => { // ignore the relative order of 'getPreviousPageParam' and 'getNextPageParam' const correctedValid = [...combination].sort((a, b) => { if ( (a === 'getNextPageParam' && b === 'getPreviousPageParam') || (a === 'getPreviousPageParam' && b === 'getNextPageParam') ) { return p.indexOf(a) - p.indexOf(b) } return checkedProperties.indexOf(a) - checkedProperties.indexOf(b) }) return { invalid: p, valid: correctedValid } }) .filter( ({ invalid }) => // if `getPreviousPageParam` and `getNextPageParam` are next to each other and `queryFn` is not present, we skip this invalid permutation Math.abs( invalid.indexOf('getNextPageParam') - invalid.indexOf('getPreviousPageParam'), ) !== 1, ), ) } return allPermutations } const invalidPermutations = generateInvalidPermutations(checkedProperties) type Interleaved = CheckedProperties | OrderIndependentProps const interleavedInvalidPermutations: Array<{ invalid: Array valid: Array }> = [] for (const invalidPermutation of invalidPermutations) { const invalid = generateInterleavedCombinations( invalidPermutation.invalid, orderIndependentProps, ) const valid = generateInterleavedCombinations( invalidPermutation.valid, orderIndependentProps, ) for (let i = 0; i < invalid.length; i++) { interleavedInvalidPermutations.push({ invalid: invalid[i]!, valid: valid[i]!, }) } } const invalidTestMatrix = combinate({ infiniteQueryFunction: [...infiniteQueryFunctions], properties: interleavedInvalidPermutations, }) const callExpressionSpread = normalizeIndent` ...communitiesQuery({ filters: { ...fieldValues, placementFormats: [], }, })` function getCode({ infiniteQueryFunction: infiniteQueryFunction, properties, }: TestCase) { function getPropertyCode( property: CheckedProperties | OrderIndependentProps, ) { switch (property) { case '...objectExpressionSpread': return `...objectExpressionSpread` case '...callExpressionSpread': return callExpressionSpread case '...memberCallExpressionSpread': return '...myOptions.infiniteQueryOptions()' case 'queryKey': return `queryKey: ['projects']` case 'queryFn': return 'queryFn: async ({ pageParam }) => { \n await fetch(`/api/projects?cursor=${pageParam}`) \n return await response.json() \n }' case 'getPreviousPageParam': return 'getPreviousPageParam: (firstPage) => firstPage.previousId ?? undefined' case 'getNextPageParam': return 'getNextPageParam: (lastPage) => lastPage.nextId ?? undefined' } } return ` import { ${infiniteQueryFunction} } from '@tanstack/react-query' ${infiniteQueryFunction}({ ${properties.map(getPropertyCode).join(',\n ')} }) ` } const validTestCases = validTestMatrix.map( ({ infiniteQueryFunction, properties }) => ({ name: `should pass when order is correct for ${infiniteQueryFunction} with order: ${properties.join(', ')}`, code: getCode({ infiniteQueryFunction, properties }), }), ) const invalidTestCases = invalidTestMatrix.map( ({ infiniteQueryFunction, properties }) => ({ name: `incorrect property order is detected for ${infiniteQueryFunction} with invalid order: ${properties.invalid.join(', ')}, valid order: ${properties.valid.join(', ')}`, code: getCode({ infiniteQueryFunction: infiniteQueryFunction, properties: properties.invalid, }), errors: [{ messageId: 'invalidOrder' }], output: getCode({ infiniteQueryFunction: infiniteQueryFunction, properties: properties.valid, }), }), ) ruleTester.run(name, rule, { valid: validTestCases, invalid: invalidTestCases, }) // regression tests const regressionTestCases = { valid: [ { name: 'should pass with call expression spread', code: normalizeIndent` import { useInfiniteQuery } from '@tanstack/react-query' const { data, isFetching, isLoading, hasNextPage, fetchNextPage } = useInfiniteQuery({ ...communitiesQuery({ filters: { ...fieldValues, placementFormats: [], }, }), refetchOnMount: false, })`, }, ], invalid: [], } ruleTester.run(name, rule, regressionTestCases) ================================================ FILE: packages/eslint-plugin-query/src/__tests__/mutation-property-order.rule.test.ts ================================================ import { RuleTester } from '@typescript-eslint/rule-tester' import combinate from 'combinate' import { checkedProperties, mutationFunctions, } from '../rules/mutation-property-order/constants' import { name, rule, } from '../rules/mutation-property-order/mutation-property-order.rule' import { generateInterleavedCombinations, generatePartialCombinations, generatePermutations, normalizeIndent, } from './test-utils' import type { MutationFunctions } from '../rules/mutation-property-order/constants' const ruleTester = new RuleTester() type CheckedProperties = (typeof checkedProperties)[number] const orderIndependentProps = [ 'gcTime', '...objectExpressionSpread', '...callExpressionSpread', '...memberCallExpressionSpread', ] as const type OrderIndependentProps = (typeof orderIndependentProps)[number] interface TestCase { mutationFunction: MutationFunctions properties: Array } const validTestMatrix = combinate({ mutationFunction: [...mutationFunctions], properties: generatePartialCombinations(checkedProperties, 2), }) export function generateInvalidPermutations( arr: ReadonlyArray, ): Array<{ invalid: Array valid: Array }> { const combinations = generatePartialCombinations(arr, 2) const allPermutations: Array<{ invalid: Array valid: Array }> = [] for (const combination of combinations) { const permutations = generatePermutations(combination) // skip the first permutation as it matches the original combination const invalidPermutations = permutations.slice(1) if (combination.includes('onError') && combination.includes('onSettled')) { if (combination.indexOf('onError') < combination.indexOf('onSettled')) { // since we ignore the relative order of 'onError' and 'onSettled', we skip this combination (but keep the other one where `onSettled` is before `onError`) continue } } allPermutations.push( ...invalidPermutations .map((p) => { // ignore the relative order of 'onError' and 'onSettled' const correctedValid = [...combination].sort((a, b) => { if ( (a === 'onSettled' && b === 'onError') || (a === 'onError' && b === 'onSettled') ) { return p.indexOf(a) - p.indexOf(b) } return checkedProperties.indexOf(a) - checkedProperties.indexOf(b) }) return { invalid: p, valid: correctedValid } }) .filter( ({ invalid }) => // if `onError` and `onSettled` are next to each other and `onMutate` is not present, we skip this invalid permutation Math.abs( invalid.indexOf('onSettled') - invalid.indexOf('onError'), ) !== 1, ), ) } return allPermutations } const invalidPermutations = generateInvalidPermutations(checkedProperties) type Interleaved = CheckedProperties | OrderIndependentProps const interleavedInvalidPermutations: Array<{ invalid: Array valid: Array }> = [] for (const invalidPermutation of invalidPermutations) { const invalid = generateInterleavedCombinations( invalidPermutation.invalid, orderIndependentProps, ) const valid = generateInterleavedCombinations( invalidPermutation.valid, orderIndependentProps, ) for (let i = 0; i < invalid.length; i++) { interleavedInvalidPermutations.push({ invalid: invalid[i]!, valid: valid[i]!, }) } } const invalidTestMatrix = combinate({ mutationFunction: [...mutationFunctions], properties: interleavedInvalidPermutations, }) const callExpressionSpread = normalizeIndent` ...mutationOptions({ onSuccess: () => {}, retry: 3, })` function getCode({ mutationFunction: mutationFunction, properties }: TestCase) { function getPropertyCode( property: CheckedProperties | OrderIndependentProps, ) { switch (property) { case '...objectExpressionSpread': return `...objectExpressionSpread` case '...callExpressionSpread': return callExpressionSpread case '...memberCallExpressionSpread': return '...myOptions.mutationOptions()' case 'gcTime': return 'gcTime: 5 * 60 * 1000' case 'onMutate': return 'onMutate: (data) => {\n return { foo: data }\n}' case 'onError': return 'onError: (error, variables, onMutateResult) => {\n console.log("error:", error, "onMutateResult:", scope)\n}' case 'onSettled': return 'onSettled: (data, error, variables, onMutateResult) => {\n console.log("settled", onMutateResult)\n}' } } return ` import { ${mutationFunction} } from '@tanstack/react-query' ${mutationFunction}({ ${properties.map(getPropertyCode).join(',\n ')} }) ` } const validTestCases = validTestMatrix.map( ({ mutationFunction, properties }) => ({ name: `should pass when order is correct for ${mutationFunction} with order: ${properties.join(', ')}`, code: getCode({ mutationFunction: mutationFunction, properties, }), }), ) const invalidTestCases = invalidTestMatrix.map( ({ mutationFunction, properties }) => ({ name: `incorrect property order id detected for ${mutationFunction} with invalid order: ${properties.invalid.join(', ')}, valid order ${properties.valid.join(', ')}`, code: getCode({ mutationFunction: mutationFunction, properties: properties.invalid, }), errors: [{ messageId: 'invalidOrder' }], output: getCode({ mutationFunction: mutationFunction, properties: properties.valid, }), }), ) ruleTester.run(name, rule, { valid: validTestCases, invalid: invalidTestCases, }) const regressionTestCases = { valid: [ { name: 'should pass with call expression spread in useMutation', code: normalizeIndent` import { useMutation } from '@tanstack/react-query' const { mutate } = useMutation({ ...mutationOptions({ retry: 3, onSuccess: () => console.log('success'), }), onMutate: (data) => { return { foo: data } }, onError: (error, variables, onMutateResult) => { console.log(error, onMutateResult) }, onSettled: (data, error, variables, onMutateResult) => { console.log('settled', onMutateResult) }, }) `, }, ], invalid: [], } ruleTester.run(name, rule, regressionTestCases) ================================================ FILE: packages/eslint-plugin-query/src/__tests__/no-rest-destructuring.test.ts ================================================ import { RuleTester } from '@typescript-eslint/rule-tester' import { rule } from '../rules/no-rest-destructuring/no-rest-destructuring.rule' import { normalizeIndent } from './test-utils' const ruleTester = new RuleTester() ruleTester.run('no-rest-destructuring', rule, { valid: [ { name: 'useQuery is not captured', code: normalizeIndent` import { useQuery } from '@tanstack/react-query' function Component() { useQuery() return } `, }, { name: 'useQuery is not destructured', code: normalizeIndent` import { useQuery } from '@tanstack/react-query' function Component() { const query = useQuery() return } `, }, { name: 'useQuery is destructured without rest', code: normalizeIndent` import { useQuery } from '@tanstack/react-query' function Component() { const { data, isLoading, isError } = useQuery() return } `, }, { name: 'useInfiniteQuery is not captured', code: normalizeIndent` import { useInfiniteQuery } from '@tanstack/react-query' function Component() { useInfiniteQuery() return } `, }, { name: 'useInfiniteQuery is not destructured', code: normalizeIndent` import { useInfiniteQuery } from '@tanstack/react-query' function Component() { const query = useInfiniteQuery() return } `, }, { name: 'useInfiniteQuery is destructured without rest', code: normalizeIndent` import { useInfiniteQuery } from '@tanstack/react-query' function Component() { const { data, isLoading, isError } = useInfiniteQuery() return } `, }, { name: 'useQueries is not captured', code: normalizeIndent` import { useQueries } from '@tanstack/react-query' function Component() { useQueries([]) return } `, }, { name: 'useQueries is not destructured', code: normalizeIndent` import { useQueries } from '@tanstack/react-query' function Component() { const queries = useQueries([]) return } `, }, { name: 'useQueries array has no rest destructured element', code: normalizeIndent` import { useQueries } from '@tanstack/react-query' function Component() { const [query1, { data, isLoading },, ...others] = useQueries([ { queryKey: ['key1'], queryFn: () => {} }, { queryKey: ['key2'], queryFn: () => {} }, { queryKey: ['key3'], queryFn: () => {} }, { queryKey: ['key4'], queryFn: () => {} }, { queryKey: ['key5'], queryFn: () => {} }, ]) return } `, }, { name: 'useQuery is destructured with rest but not from tanstack query', code: normalizeIndent` import { useQuery } from 'other-package' function Component() { const { data, ...rest } = useQuery() return } `, }, { name: 'useInfiniteQuery is destructured with rest but not from tanstack query', code: normalizeIndent` import { useInfiniteQuery } from 'other-package' function Component() { const { data, ...rest } = useInfiniteQuery() return } `, }, { name: 'useQueries array has rest destructured element but not from tanstack query', code: normalizeIndent` import { useQueries } from 'other-package' function Component() { const [query1, { data, ...rest }] = useQueries([ { queryKey: ['key1'], queryFn: () => {} }, { queryKey: ['key2'], queryFn: () => {} }, ]) return } `, }, { name: 'useSuspenseQuery is not captured', code: normalizeIndent` import { useSuspenseQuery } from '@tanstack/react-query' function Component() { useSuspenseQuery() return } `, }, { name: 'useSuspenseQuery is not destructured', code: normalizeIndent` import { useSuspenseQuery } from '@tanstack/react-query' function Component() { const query = useSuspenseQuery() return } `, }, { name: 'useSuspenseQuery is destructured without rest', code: normalizeIndent` import { useSuspenseQuery } from '@tanstack/react-query' function Component() { const { data, isLoading, isError } = useSuspenseQuery() return } `, }, { name: 'useSuspenseInfiniteQuery is not captured', code: normalizeIndent` import { useSuspenseInfiniteQuery } from '@tanstack/react-query' function Component() { useSuspenseInfiniteQuery() return } `, }, { name: 'useSuspenseInfiniteQuery is not destructured', code: normalizeIndent` import { useSuspenseInfiniteQuery } from '@tanstack/react-query' function Component() { const query = useSuspenseInfiniteQuery() return } `, }, { name: 'useSuspenseInfiniteQuery is destructured without rest', code: normalizeIndent` import { useSuspenseInfiniteQuery } from '@tanstack/react-query' function Component() { const { data, isLoading, isError } = useSuspenseInfiniteQuery() return } `, }, { name: 'useSuspenseQueries is not captured', code: normalizeIndent` import { useSuspenseQueries } from '@tanstack/react-query' function Component() { useSuspenseQueries([]) return } `, }, { name: 'useSuspenseQueries is not destructured', code: normalizeIndent` import { useSuspenseQueries } from '@tanstack/react-query' function Component() { const queries = useSuspenseQueries([]) return } `, }, { name: 'useSuspenseQueries array has no rest destructured element', code: normalizeIndent` import { useSuspenseQueries } from '@tanstack/react-query' function Component() { const [query1, { data, isLoading }] = useSuspenseQueries([ { queryKey: ['key1'], queryFn: () => {} }, { queryKey: ['key2'], queryFn: () => {} }, ]) return } `, }, { name: 'useSuspenseQuery is destructured with rest but not from tanstack query', code: normalizeIndent` import { useSuspenseQuery } from 'other-package' function Component() { const { data, ...rest } = useSuspenseQuery() return } `, }, { name: 'useSuspenseInfiniteQuery is destructured with rest but not from tanstack query', code: normalizeIndent` import { useSuspenseInfiniteQuery } from 'other-package' function Component() { const { data, ...rest } = useSuspenseInfiniteQuery() return } `, }, { name: 'useSuspenseQueries array has rest destructured element but not from tanstack query', code: normalizeIndent` import { useSuspenseQueries } from 'other-package' function Component() { const [query1, { data, ...rest }] = useSuspenseQueries([ { queryKey: ['key1'], queryFn: () => {} }, { queryKey: ['key2'], queryFn: () => {} }, ]) return } `, }, ], invalid: [ { name: 'useQuery is destructured with rest', code: normalizeIndent` import { useQuery } from '@tanstack/react-query' function Component() { const { data, ...rest } = useQuery() return } `, errors: [{ messageId: 'objectRestDestructure' }], }, { name: 'useInfiniteQuery is destructured with rest', code: normalizeIndent` import { useInfiniteQuery } from '@tanstack/react-query' function Component() { const { data, ...rest } = useInfiniteQuery() return } `, errors: [{ messageId: 'objectRestDestructure' }], }, { name: 'useQueries array has rest destructured element', code: normalizeIndent` import { useQueries } from '@tanstack/react-query' function Component() { const [query1, { data, ...rest }] = useQueries([ { queryKey: ['key1'], queryFn: () => {} }, { queryKey: ['key2'], queryFn: () => {} }, ]) return } `, errors: [{ messageId: 'objectRestDestructure' }], }, { name: 'useSuspenseQuery is destructured with rest', code: normalizeIndent` import { useSuspenseQuery } from '@tanstack/react-query' function Component() { const { data, ...rest } = useSuspenseQuery() return } `, errors: [{ messageId: 'objectRestDestructure' }], }, { name: 'useSuspenseInfiniteQuery is destructured with rest', code: normalizeIndent` import { useSuspenseInfiniteQuery } from '@tanstack/react-query' function Component() { const { data, ...rest } = useSuspenseInfiniteQuery() return } `, errors: [{ messageId: 'objectRestDestructure' }], }, { name: 'useSuspenseQueries is destructured with rest', code: normalizeIndent` import { useSuspenseQueries } from '@tanstack/react-query' function Component() { const [query1, { data, ...rest }] = useSuspenseQueries([ { queryKey: ['key1'], queryFn: () => {} }, { queryKey: ['key2'], queryFn: () => {} }, ]) return } `, errors: [{ messageId: 'objectRestDestructure' }], }, { name: 'useQuery result is spread in return statement', code: normalizeIndent` import { useQuery } from '@tanstack/react-query' function Component() { const query = useQuery() return { ...query, data: query.data[0] } } `, errors: [{ messageId: 'objectRestDestructure' }], }, { name: 'useQuery result is spread in object expression', code: normalizeIndent` import { useQuery } from '@tanstack/react-query' function Component() { const query = useQuery() const result = { ...query, data: query.data[0] } return result } `, errors: [{ messageId: 'objectRestDestructure' }], }, ], }) ================================================ FILE: packages/eslint-plugin-query/src/__tests__/no-unstable-deps.test.ts ================================================ import { RuleTester } from '@typescript-eslint/rule-tester' import { reactHookNames, rule, useQueryHookNames, } from '../rules/no-unstable-deps/no-unstable-deps.rule' const ruleTester = new RuleTester() interface TestCase { reactHookImport: string reactHookInvocation: string reactHookAlias: string } const baseTestCases = { valid: ({ reactHookImport, reactHookInvocation, reactHookAlias }: TestCase) => [ { name: `should pass when destructured mutate is passed to ${reactHookAlias} as dependency`, code: ` ${reactHookImport} import { useMutation } from "@tanstack/react-query"; function Component() { const { mutate } = useMutation({ mutationFn: (value: string) => value }); const callback = ${reactHookInvocation}(() => { mutate('hello') }, [mutate]); return; } `, }, ].concat( useQueryHookNames.map((queryHook) => ({ name: `should pass result of ${queryHook} is passed to ${reactHookInvocation} as dependency`, code: ` ${reactHookImport} import { ${queryHook} } from "@tanstack/react-query"; function Component() { const { refetch } = ${queryHook}({ queryFn: (value: string) => value }); const callback = ${reactHookInvocation}(() => { query.refetch() }, [refetch]); return; } `, })), ), invalid: ({ reactHookImport, reactHookInvocation, reactHookAlias, }: TestCase) => [ { name: `result of useMutation is passed to ${reactHookInvocation} as dependency `, code: ` ${reactHookImport} import { useMutation } from "@tanstack/react-query"; function Component() { const mutation = useMutation({ mutationFn: (value: string) => value }); const callback = ${reactHookInvocation}(() => { mutation.mutate('hello') }, [mutation]); return; } `, errors: [ { messageId: 'noUnstableDeps', data: { reactHook: reactHookAlias, queryHook: 'useMutation' }, }, ], }, ].concat( useQueryHookNames.map((queryHook) => ({ name: `result of ${queryHook} is passed to ${reactHookInvocation} as dependency`, code: ` ${reactHookImport} import { ${queryHook} } from "@tanstack/react-query"; function Component() { const query = ${queryHook}({ queryFn: (value: string) => value }); const callback = ${reactHookInvocation}(() => { query.refetch() }, [query]); return; } `, errors: [ { messageId: 'noUnstableDeps', data: { reactHook: reactHookAlias, queryHook }, }, ], })), ), } const testCases = (reactHookName: string) => [ { reactHookImport: 'import * as React from "React";', reactHookInvocation: `React.${reactHookName}`, reactHookAlias: reactHookName, }, { reactHookImport: `import { ${reactHookName} } from "React";`, reactHookInvocation: reactHookName, reactHookAlias: reactHookName, }, { reactHookImport: `import { ${reactHookName} as useAlias } from "React";`, reactHookInvocation: 'useAlias', reactHookAlias: 'useAlias', }, ] reactHookNames.forEach((reactHookName) => { testCases(reactHookName).forEach( ({ reactHookInvocation, reactHookAlias, reactHookImport }) => { ruleTester.run('no-unstable-deps', rule, { valid: baseTestCases.valid({ reactHookImport, reactHookInvocation, reactHookAlias, }), invalid: baseTestCases.invalid({ reactHookImport, reactHookInvocation, reactHookAlias, }), }) }, ) }) ================================================ FILE: packages/eslint-plugin-query/src/__tests__/no-void-query-fn.test.ts ================================================ import path from 'node:path' import { RuleTester } from '@typescript-eslint/rule-tester' import { afterAll, describe, it, test } from 'vitest' import { rule } from '../rules/no-void-query-fn/no-void-query-fn.rule' import { normalizeIndent } from './test-utils' RuleTester.afterAll = afterAll RuleTester.describe = describe RuleTester.it = it const ruleTester = new RuleTester({ languageOptions: { parser: await import('@typescript-eslint/parser'), parserOptions: { project: true, tsconfigRootDir: path.resolve(__dirname, './ts-fixture'), }, }, }) test('should run rule tests', () => { ruleTester.run('no-void-query-fn', rule, { valid: [ { name: 'queryFn returns a value', code: normalizeIndent` import { useQuery } from '@tanstack/react-query' function Component() { const query = useQuery({ queryKey: ['test'], queryFn: () => ({ data: 'test' }), }) return null } `, }, { name: 'queryFn returns a Promise', code: normalizeIndent` import { useQuery } from '@tanstack/react-query' function Component() { const query = useQuery({ queryKey: ['test'], queryFn: async () => ({ data: 'test' }), }) return null } `, }, { name: 'queryFn returns Promise.resolve', code: normalizeIndent` import { useQuery } from '@tanstack/react-query' function Component() { const query = useQuery({ queryKey: ['test'], queryFn: () => Promise.resolve({ data: 'test' }), }) return null } `, }, { name: 'queryFn with explicit Promise type', code: normalizeIndent` import { useQuery } from '@tanstack/react-query' interface Data { value: string } function Component() { const query = useQuery({ queryKey: ['test'], queryFn: async (): Promise => { return { value: 'test' } }, }) return null } `, }, { name: 'queryFn with generic Promise type', code: normalizeIndent` import { useQuery } from '@tanstack/react-query' interface Response { data: T } function Component() { const query = useQuery({ queryKey: ['test'], queryFn: async (): Promise> => { return { data: 'test' } }, }) return null } `, }, { name: 'queryFn with external async function', code: normalizeIndent` import { useQuery } from '@tanstack/react-query' async function fetchData(): Promise<{ data: string }> { return { data: 'test' } } function Component() { const query = useQuery({ queryKey: ['test'], queryFn: fetchData, }) return null } `, }, { name: 'queryFn returns null', code: normalizeIndent` import { useQuery } from '@tanstack/react-query' function Component() { const query = useQuery({ queryKey: ['test'], queryFn: () => null, }) return null } `, }, { name: 'queryFn returns 0', code: normalizeIndent` import { useQuery } from '@tanstack/react-query' function Component() { const query = useQuery({ queryKey: ['test'], queryFn: () => 0, }) return null } `, }, { name: 'queryFn returns false', code: normalizeIndent` import { useQuery } from '@tanstack/react-query' function Component() { const query = useQuery({ queryKey: ['test'], queryFn: () => false, }) return null } `, }, ], invalid: [ { name: 'queryFn returns void', code: normalizeIndent` import { useQuery } from '@tanstack/react-query' function Component() { const query = useQuery({ queryKey: ['test'], queryFn: () => { console.log('test') }, }) return null } `, errors: [{ messageId: 'noVoidReturn' }], }, { name: 'queryFn returns undefined', code: normalizeIndent` import { useQuery } from '@tanstack/react-query' function Component() { const query = useQuery({ queryKey: ['test'], queryFn: () => undefined, }) return null } `, errors: [{ messageId: 'noVoidReturn' }], }, { name: 'async queryFn returns void', code: normalizeIndent` import { useQuery } from '@tanstack/react-query' function Component() { const query = useQuery({ queryKey: ['test'], queryFn: async () => { await someOperation() }, }) return null } `, errors: [{ messageId: 'noVoidReturn' }], }, { name: 'queryFn with explicit void Promise', code: normalizeIndent` import { useQuery } from '@tanstack/react-query' function Component() { const query = useQuery({ queryKey: ['test'], queryFn: async (): Promise => { await someOperation() }, }) return null } `, errors: [{ messageId: 'noVoidReturn' }], }, { name: 'queryFn with Promise.resolve(undefined)', code: normalizeIndent` import { useQuery } from '@tanstack/react-query' function Component() { const query = useQuery({ queryKey: ['test'], queryFn: () => Promise.resolve(undefined), }) return null } `, errors: [{ messageId: 'noVoidReturn' }], }, { name: 'queryFn with external void async function', code: normalizeIndent` import { useQuery } from '@tanstack/react-query' async function voidOperation(): Promise { await someOperation() } function Component() { const query = useQuery({ queryKey: ['test'], queryFn: voidOperation, }) return null } `, errors: [{ messageId: 'noVoidReturn' }], }, { name: 'queryFn with conditional return (one branch missing)', code: normalizeIndent` import { useQuery } from '@tanstack/react-query' function Component() { const query = useQuery({ queryKey: ['test'], queryFn: () => { if (Math.random() > 0.5) { return { data: 'test' } } // Missing return in the else case }, }) return null } `, errors: [{ messageId: 'noVoidReturn' }], }, { name: 'queryFn with ternary operator returning undefined', code: normalizeIndent` import { useQuery } from '@tanstack/react-query' function Component() { const query = useQuery({ queryKey: ['test'], queryFn: () => Math.random() > 0.5 ? { data: 'test' } : undefined, }) return null } `, errors: [{ messageId: 'noVoidReturn' }], }, { name: 'async queryFn with try/catch missing return in catch', code: normalizeIndent` import { useQuery } from '@tanstack/react-query' function Component() { const query = useQuery({ queryKey: ['test'], queryFn: async () => { try { return { data: 'test' } } catch (error) { console.error(error) // No return here results in an implicit undefined } }, }) return null } `, errors: [{ messageId: 'noVoidReturn' }], }, ], }) }, 10_000) ================================================ FILE: packages/eslint-plugin-query/src/__tests__/sort-data-by-order.utils.test.ts ================================================ import { describe, expect, test } from 'vitest' import { sortDataByOrder } from '../utils/sort-data-by-order' describe('create-route-property-order utils', () => { describe('sortDataByOrder', () => { const testCases = [ { data: [{ key: 'a' }, { key: 'c' }, { key: 'b' }], orderArray: [ [['a'], ['b']], [['b'], ['c']], ], key: 'key', expected: [{ key: 'a' }, { key: 'b' }, { key: 'c' }], }, { data: [{ key: 'b' }, { key: 'a' }, { key: 'c' }], orderArray: [ [['a'], ['b']], [['b'], ['c']], ], key: 'key', expected: [{ key: 'a' }, { key: 'b' }, { key: 'c' }], }, { data: [{ key: 'a' }, { key: 'b' }, { key: 'c' }], orderArray: [ [['a'], ['b']], [['b'], ['c']], ], key: 'key', expected: null, }, { data: [{ key: 'a' }, { key: 'b' }, { key: 'c' }, { key: 'd' }], orderArray: [ [['a'], ['b']], [['b'], ['c']], ], key: 'key', expected: null, }, { data: [{ key: 'a' }, { key: 'b' }, { key: 'd' }, { key: 'c' }], orderArray: [ [['a'], ['b']], [['b'], ['c']], ], key: 'key', expected: null, }, { data: [{ key: 'd' }, { key: 'a' }, { key: 'b' }, { key: 'c' }], orderArray: [ [['a'], ['b']], [['b'], ['c']], ], key: 'key', expected: null, }, { data: [{ key: 'd' }, { key: 'b' }, { key: 'a' }, { key: 'c' }], orderArray: [ [['a'], ['b']], [['b'], ['c']], ], key: 'key', expected: [{ key: 'd' }, { key: 'a' }, { key: 'b' }, { key: 'c' }], }, ] as const test.each(testCases)( '$data $orderArray $key $expected', ({ data, orderArray, key, expected }) => { const sortedData = sortDataByOrder(data, orderArray, key) expect(sortedData).toEqual(expected) }, ) }) }) ================================================ FILE: packages/eslint-plugin-query/src/__tests__/stable-query-client.test.ts ================================================ import { RuleTester } from '@typescript-eslint/rule-tester' import { rule } from '../rules/stable-query-client/stable-query-client.rule' import { normalizeIndent } from './test-utils' const ruleTester = new RuleTester() ruleTester.run('stable-query-client', rule, { valid: [ { name: 'QueryClient is stable when wrapped in React.useState', code: normalizeIndent` import { QueryClient } from "@tanstack/react-query"; function Component() { const [queryClient] = React.useState(() => new QueryClient()); return; } `, }, { name: 'QueryClient is stable when wrapped in useState', code: normalizeIndent` import { QueryClient } from "@tanstack/react-query"; function Component() { const [queryClient] = useState(() => new QueryClient()); return; } `, }, { name: 'QueryClient is stable when wrapped in React.useMemo', code: normalizeIndent` import { QueryClient } from "@tanstack/react-query"; function Component() { const [queryClient] = React.useMemo(() => new QueryClient(), []); return; } `, }, { name: 'QueryClient is stable when wrapped in useAnything', code: normalizeIndent` import { QueryClient } from "@tanstack/react-query"; function Component() { const [queryClient] = useAnything(() => new QueryClient()); return; } `, }, { name: 'QueryClient is imported from a non-tanstack package', code: normalizeIndent` import { QueryClient } from "other-library"; function Component() { const queryClient = new QueryClient(); return; } `, }, { name: 'QueryClient is not imported from @tanstack/react-query', code: normalizeIndent` import { QueryClient } from "@tanstack/solid-query"; function Component() { const queryClient = new QueryClient(); return; } `, }, { name: 'QueryClient is invoked outside of a function', code: normalizeIndent` import { QueryClient } from "@tanstack/solid-query"; const queryClient = new QueryClient(); function Component() { return; } `, }, { name: 'QueryClient is invoked in a non-component function', code: normalizeIndent` import { QueryClient } from "@tanstack/solid-query"; function someFn() { const queryClient = new QueryClient(); return; } `, }, { name: 'QueryClient is invoked in an async (react server) component', code: normalizeIndent` import { QueryClient } from "@tanstack/solid-query"; async function AsyncComponent() { const queryClient = new QueryClient(); return; } `, }, ], invalid: [ { name: 'QueryClient is not stable when it is not wrapped in React.useState in component', code: normalizeIndent` import { QueryClient } from "@tanstack/react-query"; function Component() { const queryClient = new QueryClient(); return; } `, output: normalizeIndent` import { QueryClient } from "@tanstack/react-query"; function Component() { const [queryClient] = React.useState(() => new QueryClient()); return; } `, errors: [{ messageId: 'unstable' }], }, { name: 'QueryClient is not stable when it is not wrapped in React.useState in custom hook', code: normalizeIndent` import { QueryClient } from "@tanstack/react-query"; function useHook() { const queryClient = new QueryClient(); return; } `, output: normalizeIndent` import { QueryClient } from "@tanstack/react-query"; function useHook() { const [queryClient] = React.useState(() => new QueryClient()); return; } `, errors: [{ messageId: 'unstable' }], }, { name: 'preserve QueryClient options', code: normalizeIndent` import { QueryClient } from "@tanstack/react-query"; function Component() { const queryClient = new QueryClient({ defaultOptions: { /* */ } }); return; } `, output: normalizeIndent` import { QueryClient } from "@tanstack/react-query"; function Component() { const [queryClient] = React.useState(() => new QueryClient({ defaultOptions: { /* */ } })); return; } `, errors: [{ messageId: 'unstable' }], }, { name: 'preserve QueryClient variable declarator name', code: normalizeIndent` import { QueryClient } from "@tanstack/react-query"; function Component() { const customName = new QueryClient(); return; } `, output: normalizeIndent` import { QueryClient } from "@tanstack/react-query"; function Component() { const [customName] = React.useState(() => new QueryClient()); return; } `, errors: [{ messageId: 'unstable' }], }, ], }) ================================================ FILE: packages/eslint-plugin-query/src/__tests__/test-utils.test.ts ================================================ import { describe, expect, test } from 'vitest' import { expectArrayEqualIgnoreOrder, generateInterleavedCombinations, generatePartialCombinations, generatePermutations, } from './test-utils' describe('test-utils', () => { describe('generatePermutations', () => { const testCases = [ { input: ['a', 'b', 'c'], expected: [ ['a', 'b', 'c'], ['a', 'c', 'b'], ['b', 'a', 'c'], ['b', 'c', 'a'], ['c', 'a', 'b'], ['c', 'b', 'a'], ], }, { input: ['a', 'b'], expected: [ ['a', 'b'], ['b', 'a'], ], }, { input: ['a'], expected: [['a']], }, ] test.each(testCases)('$input $expected', ({ input, expected }) => { const permutations = generatePermutations(input) expect(permutations).toEqual(expected) }) }) describe('generatePartialCombinations', () => { const testCases = [ { input: ['a', 'b', 'c'], minLength: 2, expected: [ ['a', 'b'], ['a', 'c'], ['b', 'c'], ['a', 'b', 'c'], ], }, { input: ['a', 'b'], expected: [['a', 'b']], minLength: 2, }, { input: ['a'], expected: [], minLength: 2, }, { input: ['a'], expected: [['a']], minLength: 1, }, { input: ['a'], expected: [[], ['a']], minLength: 0, }, ] test.each(testCases)( '$input $minLength $expected', ({ input, minLength, expected }) => { const combinations = generatePartialCombinations(input, minLength) expectArrayEqualIgnoreOrder(combinations, expected) }, ) }) describe('generateInterleavedCombinations', () => { const testCases = [ { data: ['a', 'b'], additional: ['x'], expected: [ ['a', 'b'], ['x', 'a', 'b'], ['a', 'x', 'b'], ['a', 'b', 'x'], ], }, ] test.each(testCases)( '$input $expected', ({ data, additional, expected }) => { const combinations = generateInterleavedCombinations(data, additional) expectArrayEqualIgnoreOrder(combinations, expected) }, ) }) }) ================================================ FILE: packages/eslint-plugin-query/src/__tests__/test-utils.ts ================================================ import { expect } from 'vitest' export function normalizeIndent(template: TemplateStringsArray) { const codeLines = template[0]?.split('\n') ?? [''] const leftPadding = codeLines[1]?.match(/\s+/)?.[0] ?? '' return codeLines.map((line) => line.slice(leftPadding.length)).join('\n') } export function generatePermutations(arr: Array): Array> { if (arr.length <= 1) { return [arr] } const result: Array> = [] for (let i = 0; i < arr.length; i++) { const rest = arr.slice(0, i).concat(arr.slice(i + 1)) const restPermutations = generatePermutations(rest) for (const perm of restPermutations) { result.push([arr[i]!, ...perm]) } } return result } export function generatePartialCombinations( arr: ReadonlyArray, minLength: number, ): Array> { const result: Array> = [] function backtrack(start: number, current: Array) { if (current.length > minLength - 1) { result.push([...current]) } for (let i = start; i < arr.length; i++) { current.push(arr[i]!) backtrack(i + 1, current) current.pop() } } backtrack(0, []) return result } export function expectArrayEqualIgnoreOrder(a: Array, b: Array) { expect([...a].sort()).toEqual([...b].sort()) } export function generateInterleavedCombinations< TData, TAdditional, TResult extends TData | TAdditional, >( data: Array | ReadonlyArray, additional: Array | ReadonlyArray, ): Array> { const result: Array> = [] function getSubsets(array: Array): Array> { return array.reduce( (subsets, value) => { return subsets.concat(subsets.map((set) => [...set, value])) }, [[]] as Array>, ) } function insertAtPositions( baseData: Array, subset: Array, ): Array> { const combinations: Array> = [] const recurse = ( currentData: Array, currentSubset: Array, start: number, ): void => { if (currentSubset.length === 0) { combinations.push([...currentData]) return } for (let i = start; i <= currentData.length; i++) { const newData = [ ...currentData.slice(0, i), currentSubset[0]!, ...currentData.slice(i), ] recurse(newData, currentSubset.slice(1), i + 1) } } recurse(baseData, subset, 0) return combinations } const subsets = getSubsets(additional as Array) subsets.forEach((subset) => { result.push( ...insertAtPositions(data as Array, subset as Array), ) }) return result } ================================================ FILE: packages/eslint-plugin-query/src/__tests__/ts-fixture/file.ts ================================================ // File needs to exists ================================================ FILE: packages/eslint-plugin-query/src/__tests__/ts-fixture/tsconfig.json ================================================ { "compilerOptions": { "target": "ESNext", "module": "ESNext", "moduleResolution": "Bundler", "strict": true, "skipLibCheck": true }, "include": ["**/*"] } ================================================ FILE: packages/eslint-plugin-query/src/rules/exhaustive-deps/exhaustive-deps.rule.ts ================================================ import { AST_NODE_TYPES, ESLintUtils } from '@typescript-eslint/utils' import { ASTUtils } from '../../utils/ast-utils' import { getDocsUrl } from '../../utils/get-docs-url' import { uniqueBy } from '../../utils/unique-by' import { detectTanstackQueryImports } from '../../utils/detect-react-query-imports' import { ExhaustiveDepsUtils } from './exhaustive-deps.utils' import type { TSESLint, TSESTree } from '@typescript-eslint/utils' import type { ExtraRuleDocs } from '../../types' const QUERY_KEY = 'queryKey' const QUERY_FN = 'queryFn' export const name = 'exhaustive-deps' const createRule = ESLintUtils.RuleCreator(getDocsUrl) export const rule = createRule({ name, meta: { type: 'problem', docs: { description: 'Exhaustive deps rule for useQuery', recommended: 'error', }, messages: { missingDeps: `The following dependencies are missing in your queryKey: {{deps}}`, fixTo: 'Fix to {{result}}', }, hasSuggestions: true, fixable: 'code', schema: [], }, defaultOptions: [], create: detectTanstackQueryImports((context) => { return { Property: (node) => { if ( !ASTUtils.isObjectExpression(node.parent) || !ASTUtils.isIdentifierWithName(node.key, QUERY_KEY) ) { return } const scopeManager = context.sourceCode.scopeManager const queryKey = ASTUtils.findPropertyWithIdentifierKey( node.parent.properties, QUERY_KEY, ) const queryFn = ASTUtils.findPropertyWithIdentifierKey( node.parent.properties, QUERY_FN, ) if ( scopeManager === null || queryKey === undefined || queryFn === undefined || !ASTUtils.isNodeOfOneOf(queryFn.value, [ AST_NODE_TYPES.ArrowFunctionExpression, AST_NODE_TYPES.FunctionExpression, AST_NODE_TYPES.ConditionalExpression, ]) ) { return } let queryKeyNode = queryKey.value if ( queryKeyNode.type === AST_NODE_TYPES.TSAsExpression && queryKeyNode.expression.type === AST_NODE_TYPES.ArrayExpression ) { queryKeyNode = queryKeyNode.expression } if (queryKeyNode.type === AST_NODE_TYPES.Identifier) { const expression = ASTUtils.getReferencedExpressionByIdentifier({ context, node: queryKeyNode, }) if (expression?.type === AST_NODE_TYPES.ArrayExpression) { queryKeyNode = expression } } const externalRefs = ASTUtils.getExternalRefs({ scopeManager, sourceCode: context.sourceCode, node: getQueryFnRelevantNode(queryFn), }) const relevantRefs = externalRefs.filter((reference) => ExhaustiveDepsUtils.isRelevantReference({ sourceCode: context.sourceCode, reference, scopeManager, node: getQueryFnRelevantNode(queryFn), }), ) const existingKeys = ASTUtils.getNestedIdentifiers(queryKeyNode).map( (identifier) => ASTUtils.mapKeyNodeToBaseText(identifier, context.sourceCode), ) const missingRefs = relevantRefs .map((ref) => ({ ref: ref, text: ASTUtils.mapKeyNodeToBaseText( ref.identifier, context.sourceCode, ), })) .filter(({ ref, text }) => { return ( !ref.isTypeReference && !ASTUtils.isAncestorIsCallee(ref.identifier) && !existingKeys.some((existingKey) => existingKey === text) && !existingKeys.includes(text.split(/[?.]/)[0] ?? '') ) }) .map(({ ref, text }) => ({ identifier: ref.identifier, text: text, })) const uniqueMissingRefs = uniqueBy(missingRefs, (x) => x.text) if (uniqueMissingRefs.length > 0) { const missingAsText = uniqueMissingRefs .map((ref) => ASTUtils.mapKeyNodeToText(ref.identifier, context.sourceCode), ) .join(', ') const queryKeyValue = context.sourceCode.getText(queryKeyNode) const existingWithMissing = queryKeyValue === '[]' ? `[${missingAsText}]` : queryKeyValue.replace(/\]$/, `, ${missingAsText}]`) const suggestions: TSESLint.ReportSuggestionArray = [] if (queryKeyNode.type === AST_NODE_TYPES.ArrayExpression) { suggestions.push({ messageId: 'fixTo', data: { result: existingWithMissing }, fix(fixer) { return fixer.replaceText(queryKeyNode, existingWithMissing) }, }) } context.report({ node: node, messageId: 'missingDeps', data: { deps: uniqueMissingRefs.map((ref) => ref.text).join(', '), }, suggest: suggestions, }) } }, } }), }) function getQueryFnRelevantNode(queryFn: TSESTree.Property) { if (queryFn.value.type !== AST_NODE_TYPES.ConditionalExpression) { return queryFn.value } if ( queryFn.value.consequent.type === AST_NODE_TYPES.Identifier && queryFn.value.consequent.name === 'skipToken' ) { return queryFn.value.alternate } return queryFn.value.consequent } ================================================ FILE: packages/eslint-plugin-query/src/rules/exhaustive-deps/exhaustive-deps.utils.ts ================================================ import { AST_NODE_TYPES } from '@typescript-eslint/utils' import { ASTUtils } from '../../utils/ast-utils' import type { TSESLint, TSESTree } from '@typescript-eslint/utils' export const ExhaustiveDepsUtils = { isRelevantReference(params: { sourceCode: Readonly reference: TSESLint.Scope.Reference scopeManager: TSESLint.Scope.ScopeManager node: TSESTree.Node }) { const { sourceCode, reference, scopeManager, node } = params const component = ASTUtils.getFunctionAncestor(sourceCode, node) if (component === undefined) { return false } if ( !ASTUtils.isDeclaredInNode({ scopeManager, reference, functionNode: component, }) ) { return false } return ( reference.identifier.name !== 'undefined' && reference.identifier.parent.type !== AST_NODE_TYPES.NewExpression && !ExhaustiveDepsUtils.isInstanceOfKind(reference.identifier.parent) ) }, isInstanceOfKind(node: TSESTree.Node) { return ( node.type === AST_NODE_TYPES.BinaryExpression && node.operator === 'instanceof' ) }, } ================================================ FILE: packages/eslint-plugin-query/src/rules/infinite-query-property-order/constants.ts ================================================ export const infiniteQueryFunctions = [ 'infiniteQueryOptions', 'useInfiniteQuery', 'useSuspenseInfiniteQuery', ] as const export type InfiniteQueryFunctions = (typeof infiniteQueryFunctions)[number] export const checkedProperties = [ 'queryFn', 'getPreviousPageParam', 'getNextPageParam', ] as const export type InfiniteQueryProperties = (typeof checkedProperties)[number] export const sortRules = [ [['queryFn'], ['getPreviousPageParam', 'getNextPageParam']], ] as const ================================================ FILE: packages/eslint-plugin-query/src/rules/infinite-query-property-order/infinite-query-property-order.rule.ts ================================================ import { createPropertyOrderRule } from '../../utils/create-property-order-rule' import { infiniteQueryFunctions, sortRules } from './constants' import type { InfiniteQueryFunctions, InfiniteQueryProperties, } from './constants' export const name = 'infinite-query-property-order' export const rule = createPropertyOrderRule< InfiniteQueryFunctions, InfiniteQueryProperties >( { name, meta: { type: 'problem', docs: { description: 'Ensure correct order of inference sensitive properties for infinite queries', recommended: 'error', }, messages: { invalidOrder: 'Invalid order of properties for `{{function}}`.', }, schema: [], hasSuggestions: true, fixable: 'code', }, defaultOptions: [], }, infiniteQueryFunctions, sortRules, ) ================================================ FILE: packages/eslint-plugin-query/src/rules/mutation-property-order/constants.ts ================================================ export const mutationFunctions = ['useMutation'] as const export type MutationFunctions = (typeof mutationFunctions)[number] export const checkedProperties = ['onMutate', 'onError', 'onSettled'] as const export type MutationProperties = (typeof checkedProperties)[number] export const sortRules = [[['onMutate'], ['onError', 'onSettled']]] as const ================================================ FILE: packages/eslint-plugin-query/src/rules/mutation-property-order/mutation-property-order.rule.ts ================================================ import { createPropertyOrderRule } from '../../utils/create-property-order-rule' import { mutationFunctions, sortRules } from './constants' import type { MutationFunctions, MutationProperties } from './constants' export const name = 'mutation-property-order' export const rule = createPropertyOrderRule< MutationFunctions, MutationProperties >( { name, meta: { type: 'problem', docs: { description: 'Ensure correct order of inference-sensitive properties in useMutation()', recommended: 'error', }, messages: { invalidOrder: 'Invalid order of properties for `{{function}}`.', }, schema: [], hasSuggestions: true, fixable: 'code', }, defaultOptions: [], }, mutationFunctions, sortRules, ) ================================================ FILE: packages/eslint-plugin-query/src/rules/no-rest-destructuring/no-rest-destructuring.rule.ts ================================================ import { AST_NODE_TYPES, ESLintUtils } from '@typescript-eslint/utils' import { getDocsUrl } from '../../utils/get-docs-url' import { ASTUtils } from '../../utils/ast-utils' import { detectTanstackQueryImports } from '../../utils/detect-react-query-imports' import { NoRestDestructuringUtils } from './no-rest-destructuring.utils' import type { ExtraRuleDocs } from '../../types' export const name = 'no-rest-destructuring' const queryHooks = [ 'useQuery', 'useQueries', 'useInfiniteQuery', 'useSuspenseQuery', 'useSuspenseQueries', 'useSuspenseInfiniteQuery', ] const createRule = ESLintUtils.RuleCreator(getDocsUrl) export const rule = createRule({ name, meta: { type: 'problem', docs: { description: 'Disallows rest destructuring in queries', recommended: 'warn', }, messages: { objectRestDestructure: `Object rest destructuring on a query will observe all changes to the query, leading to excessive re-renders.`, }, schema: [], }, defaultOptions: [], create: detectTanstackQueryImports((context, _, helpers) => { const queryResultVariables = new Set() return { CallExpression: (node) => { if ( !ASTUtils.isIdentifierWithOneOfNames(node.callee, queryHooks) || node.parent.type !== AST_NODE_TYPES.VariableDeclarator || !helpers.isTanstackQueryImport(node.callee) ) { return } const returnValue = node.parent.id if ( node.callee.name !== 'useQueries' && node.callee.name !== 'useSuspenseQueries' ) { if (NoRestDestructuringUtils.isObjectRestDestructuring(returnValue)) { return context.report({ node: node.parent, messageId: 'objectRestDestructure', }) } if (returnValue.type === AST_NODE_TYPES.Identifier) { queryResultVariables.add(returnValue.name) } return } if (returnValue.type !== AST_NODE_TYPES.ArrayPattern) { if (returnValue.type === AST_NODE_TYPES.Identifier) { queryResultVariables.add(returnValue.name) } return } returnValue.elements.forEach((queryResult) => { if (queryResult === null) { return } if (NoRestDestructuringUtils.isObjectRestDestructuring(queryResult)) { context.report({ node: queryResult, messageId: 'objectRestDestructure', }) } }) }, VariableDeclarator: (node) => { if ( node.init?.type === AST_NODE_TYPES.Identifier && queryResultVariables.has(node.init.name) && NoRestDestructuringUtils.isObjectRestDestructuring(node.id) ) { context.report({ node, messageId: 'objectRestDestructure', }) } }, SpreadElement: (node) => { if ( node.argument.type === AST_NODE_TYPES.Identifier && queryResultVariables.has(node.argument.name) ) { context.report({ node, messageId: 'objectRestDestructure', }) } }, } }), }) ================================================ FILE: packages/eslint-plugin-query/src/rules/no-rest-destructuring/no-rest-destructuring.utils.ts ================================================ import { AST_NODE_TYPES } from '@typescript-eslint/utils' import type { TSESTree } from '@typescript-eslint/utils' export const NoRestDestructuringUtils = { isObjectRestDestructuring(node: TSESTree.Node): boolean { if (node.type !== AST_NODE_TYPES.ObjectPattern) { return false } return node.properties.some((p) => p.type === AST_NODE_TYPES.RestElement) }, } ================================================ FILE: packages/eslint-plugin-query/src/rules/no-unstable-deps/no-unstable-deps.rule.ts ================================================ import { AST_NODE_TYPES, ESLintUtils } from '@typescript-eslint/utils' import { getDocsUrl } from '../../utils/get-docs-url' import { detectTanstackQueryImports } from '../../utils/detect-react-query-imports' import type { TSESTree } from '@typescript-eslint/utils' import type { ExtraRuleDocs } from '../../types' export const name = 'no-unstable-deps' export const reactHookNames = ['useEffect', 'useCallback', 'useMemo'] export const useQueryHookNames = [ 'useQuery', 'useSuspenseQuery', 'useQueries', 'useSuspenseQueries', 'useInfiniteQuery', 'useSuspenseInfiniteQuery', ] const allHookNames = ['useMutation', ...useQueryHookNames] const createRule = ESLintUtils.RuleCreator(getDocsUrl) export const rule = createRule({ name, meta: { type: 'problem', docs: { description: 'Disallow putting the result of query hooks directly in a React hook dependency array', recommended: 'error', }, messages: { noUnstableDeps: `The result of {{queryHook}} is not referentially stable, so don't pass it directly into the dependencies array of {{reactHook}}. Instead, destructure the return value of {{queryHook}} and pass the destructured values into the dependency array of {{reactHook}}.`, }, schema: [], }, defaultOptions: [], create: detectTanstackQueryImports((context) => { const trackedVariables: Record = {} const hookAliasMap: Record = {} function getReactHook(node: TSESTree.CallExpression): string | undefined { if (node.callee.type === 'Identifier') { const calleeName = node.callee.name // Check if the identifier is a known React hook or an alias if (reactHookNames.includes(calleeName) || calleeName in hookAliasMap) { return calleeName } } else if ( node.callee.type === 'MemberExpression' && node.callee.object.type === 'Identifier' && node.callee.object.name === 'React' && node.callee.property.type === 'Identifier' && reactHookNames.includes(node.callee.property.name) ) { // Member expression case: `React.useCallback` return node.callee.property.name } return undefined } function collectVariableNames( pattern: TSESTree.BindingName, queryHook: string, ) { if (pattern.type === AST_NODE_TYPES.Identifier) { trackedVariables[pattern.name] = queryHook } } return { ImportDeclaration(node: TSESTree.ImportDeclaration) { if ( node.specifiers.length > 0 && node.importKind === 'value' && node.source.value === 'React' ) { node.specifiers.forEach((specifier) => { if ( specifier.type === AST_NODE_TYPES.ImportSpecifier && specifier.imported.type === AST_NODE_TYPES.Identifier && reactHookNames.includes(specifier.imported.name) ) { // Track alias or direct import hookAliasMap[specifier.local.name] = specifier.imported.name } }) } }, VariableDeclarator(node) { if ( node.init !== null && node.init.type === AST_NODE_TYPES.CallExpression && node.init.callee.type === AST_NODE_TYPES.Identifier && allHookNames.includes(node.init.callee.name) ) { collectVariableNames(node.id, node.init.callee.name) } }, CallExpression: (node) => { const reactHook = getReactHook(node) if ( reactHook !== undefined && node.arguments.length > 1 && node.arguments[1]?.type === AST_NODE_TYPES.ArrayExpression ) { const depsArray = node.arguments[1].elements depsArray.forEach((dep) => { if ( dep !== null && dep.type === AST_NODE_TYPES.Identifier && trackedVariables[dep.name] !== undefined ) { const queryHook = trackedVariables[dep.name] context.report({ node: dep, messageId: 'noUnstableDeps', data: { queryHook, reactHook, }, }) } }) } }, } }), }) ================================================ FILE: packages/eslint-plugin-query/src/rules/no-void-query-fn/no-void-query-fn.rule.ts ================================================ import { ESLintUtils } from '@typescript-eslint/utils' import ts from 'typescript' import { ASTUtils } from '../../utils/ast-utils' import { detectTanstackQueryImports } from '../../utils/detect-react-query-imports' import { getDocsUrl } from '../../utils/get-docs-url' import type { ExtraRuleDocs } from '../../types' export const name = 'no-void-query-fn' const createRule = ESLintUtils.RuleCreator(getDocsUrl) export const rule = createRule({ name, meta: { type: 'problem', docs: { description: 'Ensures queryFn returns a non-undefined value', recommended: 'error', }, messages: { noVoidReturn: 'queryFn must return a non-undefined value', }, schema: [], }, defaultOptions: [], create: detectTanstackQueryImports((context) => { return { Property(node) { if ( !ASTUtils.isObjectExpression(node.parent) || !ASTUtils.isIdentifierWithName(node.key, 'queryFn') ) { return } const parserServices = context.sourceCode.parserServices if ( !parserServices || !parserServices.esTreeNodeToTSNodeMap || !parserServices.program ) { return } const checker = parserServices.program.getTypeChecker() const tsNode = parserServices.esTreeNodeToTSNodeMap.get(node.value) const type = checker.getTypeAtLocation(tsNode) // Get the return type of the function if (type.getCallSignatures().length > 0) { const returnType = type.getCallSignatures()[0]?.getReturnType() if (!returnType) { return } // Check if return type is void or undefined if (isIllegalReturn(checker, returnType)) { context.report({ node: node.value, messageId: 'noVoidReturn', }) } } }, } }), }) function isIllegalReturn(checker: ts.TypeChecker, type: ts.Type): boolean { const awaited = checker.getAwaitedType(type) if (!awaited) return false if (awaited.isUnion()) { return awaited.types.some((t) => isIllegalReturn(checker, t)) } return awaited.flags & (ts.TypeFlags.Void | ts.TypeFlags.Undefined) ? true : false } ================================================ FILE: packages/eslint-plugin-query/src/rules/stable-query-client/stable-query-client.rule.ts ================================================ import { AST_NODE_TYPES, ESLintUtils } from '@typescript-eslint/utils' import { ASTUtils } from '../../utils/ast-utils' import { getDocsUrl } from '../../utils/get-docs-url' import { detectTanstackQueryImports } from '../../utils/detect-react-query-imports' import type { TSESLint } from '@typescript-eslint/utils' import type { ExtraRuleDocs } from '../../types' export const name = 'stable-query-client' const createRule = ESLintUtils.RuleCreator(getDocsUrl) export const rule = createRule({ name, meta: { type: 'problem', docs: { description: 'Makes sure that QueryClient is stable', recommended: 'error', }, messages: { unstable: [ 'QueryClient is not stable. It should be either extracted from the component or wrapped in React.useState.', 'See https://tkdodo.eu/blog/react-query-fa-qs#2-the-queryclient-is-not-stable', ].join('\n'), fixTo: 'Fix to {{result}}', }, hasSuggestions: true, fixable: 'code', schema: [], }, defaultOptions: [], create: detectTanstackQueryImports((context, _, helpers) => { return { NewExpression: (node) => { if ( node.callee.type !== AST_NODE_TYPES.Identifier || node.callee.name !== 'QueryClient' || node.parent.type !== AST_NODE_TYPES.VariableDeclarator || !helpers.isSpecificTanstackQueryImport( node.callee, '@tanstack/react-query', ) ) { return } const fnAncestor = ASTUtils.getFunctionAncestor( context.sourceCode, node, ) const isReactServerComponent = fnAncestor?.async === true if ( !ASTUtils.isValidReactComponentOrHookName(fnAncestor?.id) || isReactServerComponent ) { return } context.report({ node: node.parent, messageId: 'unstable', fix: (() => { const { parent } = node if (parent.id.type !== AST_NODE_TYPES.Identifier) { return } // we need the fallbacks for backwards compat with eslint < 8.37.0 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition const sourceCode = context.sourceCode ?? context.getSourceCode() const nodeText = sourceCode.getText(node) const variableName = parent.id.name return (fixer: TSESLint.RuleFixer) => { return fixer.replaceTextRange( [parent.range[0], parent.range[1]], `[${variableName}] = React.useState(() => ${nodeText})`, ) } })(), }) }, } }), }) ================================================ FILE: packages/eslint-plugin-query/src/utils/ast-utils.ts ================================================ import { AST_NODE_TYPES } from '@typescript-eslint/utils' import { uniqueBy } from './unique-by' import type { TSESLint, TSESTree } from '@typescript-eslint/utils' export const ASTUtils = { isNodeOfOneOf( node: TSESTree.Node, types: ReadonlyArray, ): node is TSESTree.Node & { type: T } { return types.includes(node.type as T) }, isIdentifier(node: TSESTree.Node): node is TSESTree.Identifier { return node.type === AST_NODE_TYPES.Identifier }, isIdentifierWithName( node: TSESTree.Node, name: string, ): node is TSESTree.Identifier { return ASTUtils.isIdentifier(node) && node.name === name }, isIdentifierWithOneOfNames>( node: TSESTree.Node, name: T, ): node is TSESTree.Identifier & { name: T[number] } { return ASTUtils.isIdentifier(node) && name.includes(node.name) }, isProperty(node: TSESTree.Node): node is TSESTree.Property { return node.type === AST_NODE_TYPES.Property }, isObjectExpression(node: TSESTree.Node): node is TSESTree.ObjectExpression { return node.type === AST_NODE_TYPES.ObjectExpression }, isPropertyWithIdentifierKey( node: TSESTree.Node, key: string, ): node is TSESTree.Property { return ( ASTUtils.isProperty(node) && ASTUtils.isIdentifierWithName(node.key, key) ) }, findPropertyWithIdentifierKey( properties: Array, key: string, ): TSESTree.Property | undefined { // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion return properties.find((x) => ASTUtils.isPropertyWithIdentifierKey(x, key), ) as TSESTree.Property | undefined }, getNestedIdentifiers(node: TSESTree.Node): Array { const identifiers: Array = [] if (ASTUtils.isIdentifier(node)) { identifiers.push(node) } if ('arguments' in node) { node.arguments.forEach((x) => { identifiers.push(...ASTUtils.getNestedIdentifiers(x)) }) } if ('elements' in node) { node.elements.forEach((x) => { if (x !== null) { identifiers.push(...ASTUtils.getNestedIdentifiers(x)) } }) } if ('properties' in node) { node.properties.forEach((x) => { identifiers.push(...ASTUtils.getNestedIdentifiers(x)) }) } if ('expressions' in node) { node.expressions.forEach((x) => { identifiers.push(...ASTUtils.getNestedIdentifiers(x)) }) } if ('left' in node) { identifiers.push(...ASTUtils.getNestedIdentifiers(node.left)) } if ('right' in node) { identifiers.push(...ASTUtils.getNestedIdentifiers(node.right)) } if (node.type === AST_NODE_TYPES.Property) { identifiers.push(...ASTUtils.getNestedIdentifiers(node.value)) } if (node.type === AST_NODE_TYPES.SpreadElement) { identifiers.push(...ASTUtils.getNestedIdentifiers(node.argument)) } if (node.type === AST_NODE_TYPES.MemberExpression) { identifiers.push(...ASTUtils.getNestedIdentifiers(node.object)) } if (node.type === AST_NODE_TYPES.UnaryExpression) { identifiers.push(...ASTUtils.getNestedIdentifiers(node.argument)) } if (node.type === AST_NODE_TYPES.ChainExpression) { identifiers.push(...ASTUtils.getNestedIdentifiers(node.expression)) } if (node.type === AST_NODE_TYPES.TSNonNullExpression) { identifiers.push(...ASTUtils.getNestedIdentifiers(node.expression)) } if (node.type === AST_NODE_TYPES.ArrowFunctionExpression) { identifiers.push(...ASTUtils.getNestedIdentifiers(node.body)) } if (node.type === AST_NODE_TYPES.FunctionExpression) { identifiers.push(...ASTUtils.getNestedIdentifiers(node.body)) } if (node.type === AST_NODE_TYPES.BlockStatement) { identifiers.push( ...node.body.map((body) => ASTUtils.getNestedIdentifiers(body)).flat(), ) } if (node.type === AST_NODE_TYPES.ReturnStatement && node.argument) { identifiers.push(...ASTUtils.getNestedIdentifiers(node.argument)) } return identifiers }, isAncestorIsCallee(identifier: TSESTree.Node) { let previousNode = identifier let currentNode = identifier.parent while (currentNode !== undefined) { if ( currentNode.type === AST_NODE_TYPES.CallExpression && currentNode.callee === previousNode ) { return true } if (currentNode.type !== AST_NODE_TYPES.MemberExpression) { return false } previousNode = currentNode currentNode = currentNode.parent } return false }, traverseUpOnly( identifier: TSESTree.Node, allowedNodeTypes: Array, ): TSESTree.Node { const parent = identifier.parent if (parent !== undefined && allowedNodeTypes.includes(parent.type)) { return ASTUtils.traverseUpOnly(parent, allowedNodeTypes) } return identifier }, isDeclaredInNode(params: { functionNode: TSESTree.Node reference: TSESLint.Scope.Reference scopeManager: TSESLint.Scope.ScopeManager }) { const { functionNode, reference, scopeManager } = params const scope = scopeManager.acquire(functionNode) if (scope === null) { return false } return scope.set.has(reference.identifier.name) }, getExternalRefs(params: { scopeManager: TSESLint.Scope.ScopeManager sourceCode: Readonly node: TSESTree.Node }): Array { const { scopeManager, sourceCode, node } = params const scope = scopeManager.acquire(node) if (scope === null) { return [] } const references = scope.references .filter((x) => x.isRead() && !scope.set.has(x.identifier.name)) .map((x) => { const referenceNode = ASTUtils.traverseUpOnly(x.identifier, [ AST_NODE_TYPES.MemberExpression, AST_NODE_TYPES.Identifier, ]) return { variable: x, node: referenceNode, text: sourceCode.getText(referenceNode), } }) const localRefIds = new Set( [...scope.set.values()].map((x) => sourceCode.getText(x.identifiers[0])), ) const externalRefs = references.filter( (x) => x.variable.resolved === null || !localRefIds.has(x.text), ) return uniqueBy(externalRefs, (x) => x.text).map((x) => x.variable) }, mapKeyNodeToText( node: TSESTree.Node, sourceCode: Readonly, ) { return sourceCode.getText( ASTUtils.traverseUpOnly(node, [ AST_NODE_TYPES.MemberExpression, AST_NODE_TYPES.TSNonNullExpression, AST_NODE_TYPES.Identifier, ]), ) }, mapKeyNodeToBaseText( node: TSESTree.Node, sourceCode: Readonly, ) { return ASTUtils.mapKeyNodeToText(node, sourceCode).replace( /(?:\?(\.)|!)/g, '$1', ) }, isValidReactComponentOrHookName( identifier: TSESTree.Identifier | null | undefined, ) { return ( identifier !== null && identifier !== undefined && /^(use|[A-Z])/.test(identifier.name) ) }, getFunctionAncestor( sourceCode: Readonly, node: TSESTree.Node, ) { for (const ancestor of sourceCode.getAncestors(node)) { if ( ASTUtils.isNodeOfOneOf(ancestor, [ AST_NODE_TYPES.FunctionDeclaration, AST_NODE_TYPES.FunctionExpression, AST_NODE_TYPES.ArrowFunctionExpression, ]) ) { return ancestor } if ( ancestor.parent?.type === AST_NODE_TYPES.VariableDeclarator && ancestor.parent.id.type === AST_NODE_TYPES.Identifier && ASTUtils.isNodeOfOneOf(ancestor, [ AST_NODE_TYPES.FunctionDeclaration, AST_NODE_TYPES.FunctionExpression, AST_NODE_TYPES.ArrowFunctionExpression, ]) ) { return ancestor } } return undefined }, getReferencedExpressionByIdentifier(params: { node: TSESTree.Node context: Readonly>> }) { const { node, context } = params // we need the fallbacks for backwards compat with eslint < 8.37.0 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition const sourceCode = context.sourceCode ?? context.getSourceCode() // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition const scope = context.sourceCode.getScope(node) ? sourceCode.getScope(node) : context.getScope() const resolvedNode = scope.references.find((ref) => ref.identifier === node) ?.resolved?.defs[0]?.node if (resolvedNode?.type !== AST_NODE_TYPES.VariableDeclarator) { return null } return resolvedNode.init }, getClosestVariableDeclarator(node: TSESTree.Node) { let currentNode: TSESTree.Node | undefined = node while (currentNode.type !== AST_NODE_TYPES.Program) { if (currentNode.type === AST_NODE_TYPES.VariableDeclarator) { return currentNode } currentNode = currentNode.parent } return undefined }, getNestedReturnStatements( node: TSESTree.Node, ): Array { const returnStatements: Array = [] if (node.type === AST_NODE_TYPES.ReturnStatement) { returnStatements.push(node) } if ('body' in node && node.body !== undefined && node.body !== null) { Array.isArray(node.body) ? node.body.forEach((x) => { returnStatements.push(...ASTUtils.getNestedReturnStatements(x)) }) : returnStatements.push( ...ASTUtils.getNestedReturnStatements(node.body), ) } if ('consequent' in node) { Array.isArray(node.consequent) ? node.consequent.forEach((x) => { returnStatements.push(...ASTUtils.getNestedReturnStatements(x)) }) : returnStatements.push( ...ASTUtils.getNestedReturnStatements(node.consequent), ) } if ('alternate' in node && node.alternate !== null) { Array.isArray(node.alternate) ? node.alternate.forEach((x) => { returnStatements.push(...ASTUtils.getNestedReturnStatements(x)) }) : returnStatements.push( ...ASTUtils.getNestedReturnStatements(node.alternate), ) } if ('cases' in node) { node.cases.forEach((x) => { returnStatements.push(...ASTUtils.getNestedReturnStatements(x)) }) } if ('block' in node) { returnStatements.push(...ASTUtils.getNestedReturnStatements(node.block)) } if ('handler' in node && node.handler !== null) { returnStatements.push(...ASTUtils.getNestedReturnStatements(node.handler)) } if ('finalizer' in node && node.finalizer !== null) { returnStatements.push( ...ASTUtils.getNestedReturnStatements(node.finalizer), ) } if ( 'expression' in node && node.expression !== true && node.expression !== false ) { returnStatements.push( ...ASTUtils.getNestedReturnStatements(node.expression), ) } if ('test' in node && node.test !== null) { returnStatements.push(...ASTUtils.getNestedReturnStatements(node.test)) } return returnStatements }, } ================================================ FILE: packages/eslint-plugin-query/src/utils/create-property-order-rule.ts ================================================ import { AST_NODE_TYPES, ESLintUtils } from '@typescript-eslint/utils' import { getDocsUrl } from './get-docs-url' import { detectTanstackQueryImports } from './detect-react-query-imports' import { sortDataByOrder } from './sort-data-by-order' import type { ExtraRuleDocs } from '../types' const createRule = ESLintUtils.RuleCreator(getDocsUrl) export function createPropertyOrderRule< TFunc extends string, TProp extends string, >( options: Omit[0], 'create'>, targetFunctions: ReadonlyArray | Array, orderRules: ReadonlyArray< Readonly<[ReadonlyArray, ReadonlyArray]> >, ) { const targetFunctionSet = new Set(targetFunctions) function isTargetFunction(node: any): node is TFunc { return targetFunctionSet.has(node) } return createRule({ ...options, create: detectTanstackQueryImports((context) => { return { CallExpression(node) { if (node.callee.type !== AST_NODE_TYPES.Identifier) { return } const functions = node.callee.name if (!isTargetFunction(functions)) { return } const argument = node.arguments[0] if (argument === undefined || argument.type !== 'ObjectExpression') { return } const allProperties = argument.properties // no need to sort if there is at max 1 property if (allProperties.length < 2) { return } const properties = allProperties.flatMap((p, index) => { if ( p.type === AST_NODE_TYPES.Property && p.key.type === AST_NODE_TYPES.Identifier ) { return { name: p.key.name, property: p } } else return { name: `_property_${index}`, property: p } }) const sortedProperties = sortDataByOrder( properties, orderRules, 'name', ) if (sortedProperties === null) { return } context.report({ node: argument, data: { function: node.callee.name }, messageId: 'invalidOrder', fix(fixer) { const sourceCode = context.sourceCode const reorderedText = sortedProperties.reduce( (sourceText, specifier, index) => { let textBetweenProperties = '' if (index < allProperties.length - 1) { textBetweenProperties = sourceCode .getText() .slice( allProperties[index]!.range[1], allProperties[index + 1]!.range[0], ) } return ( sourceText + sourceCode.getText(specifier.property) + textBetweenProperties ) }, '', ) return fixer.replaceTextRange( [allProperties[0]!.range[0], allProperties.at(-1)!.range[1]], reorderedText, ) }, }) }, } }), }) } ================================================ FILE: packages/eslint-plugin-query/src/utils/detect-react-query-imports.ts ================================================ import { TSESTree } from '@typescript-eslint/utils' import type { ESLintUtils, TSESLint } from '@typescript-eslint/utils' type Create = Parameters< ReturnType >[0]['create'] type Context = Parameters[0] type Options = Parameters[1] type Helpers = { isSpecificTanstackQueryImport: ( node: TSESTree.Identifier, source: string, ) => boolean isTanstackQueryImport: (node: TSESTree.Identifier) => boolean } type EnhancedCreate = ( context: Context, options: Options, helpers: Helpers, ) => ReturnType export function detectTanstackQueryImports(create: EnhancedCreate): Create { return (context, optionsWithDefault) => { const tanstackQueryImportSpecifiers: Array = [] const helpers: Helpers = { isSpecificTanstackQueryImport(node, source) { return !!tanstackQueryImportSpecifiers.find((specifier) => { if ( specifier.type === TSESTree.AST_NODE_TYPES.ImportSpecifier && specifier.parent.type === TSESTree.AST_NODE_TYPES.ImportDeclaration && specifier.parent.source.value === source ) { return node.name === specifier.local.name } return false }) }, isTanstackQueryImport(node) { return !!tanstackQueryImportSpecifiers.find((specifier) => { if (specifier.type === TSESTree.AST_NODE_TYPES.ImportSpecifier) { return node.name === specifier.local.name } return false }) }, } const detectionInstructions: TSESLint.RuleListener = { ImportDeclaration(node) { if ( node.specifiers.length > 0 && // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition (node.importKind === 'value' || node.importKind === undefined) && node.source.value.startsWith('@tanstack/') && node.source.value.endsWith('-query') ) { tanstackQueryImportSpecifiers.push(...node.specifiers) } }, } // Call original rule definition const ruleInstructions = create(context, optionsWithDefault, helpers) const enhancedRuleInstructions: TSESLint.RuleListener = {} const allKeys = new Set( Object.keys(detectionInstructions).concat(Object.keys(ruleInstructions)), ) // Iterate over ALL instructions keys so we can override original rule instructions // to prevent their execution if conditions to report errors are not met. allKeys.forEach((instruction) => { enhancedRuleInstructions[instruction] = (node) => { if (instruction in detectionInstructions) { detectionInstructions[instruction]?.(node) } const ruleInstruction = ruleInstructions[instruction] // TODO: canReportErrors() if (ruleInstruction) { return ruleInstruction(node) } return undefined } }) return enhancedRuleInstructions } } ================================================ FILE: packages/eslint-plugin-query/src/utils/get-docs-url.ts ================================================ export const getDocsUrl = (ruleName: string): string => `https://tanstack.com/query/latest/docs/eslint/${ruleName}` ================================================ FILE: packages/eslint-plugin-query/src/utils/sort-data-by-order.ts ================================================ export function sortDataByOrder( data: Array | ReadonlyArray, orderRules: ReadonlyArray< Readonly<[ReadonlyArray, ReadonlyArray]> >, key: TKey, ): Array | null { const getSubsetIndex = ( item: T[TKey], subsets: ReadonlyArray | Array>, ): number | null => { for (let i = 0; i < subsets.length; i++) { if (subsets[i]?.includes(item)) { return i } } return null } const orderSets = orderRules.reduce( (sets, [A, B]) => [...sets, A, B], [] as Array | Array>, ) const inOrderArray = data.filter( (item) => getSubsetIndex(item[key], orderSets) !== null, ) let wasResorted = false as boolean // Sort by the relative order defined by the rules const sortedArray = inOrderArray.sort((a, b) => { const aKey = a[key], bKey = b[key] const aSubsetIndex = getSubsetIndex(aKey, orderSets) const bSubsetIndex = getSubsetIndex(bKey, orderSets) // If both items belong to different subsets, sort by their subset order if ( aSubsetIndex !== null && bSubsetIndex !== null && aSubsetIndex !== bSubsetIndex ) { return aSubsetIndex - bSubsetIndex } // If both items belong to the same subset or neither is in the subset, keep their relative order return 0 }) const inOrderIterator = sortedArray.values() const result = data.map((item) => { if (getSubsetIndex(item[key], orderSets) !== null) { const sortedItem = inOrderIterator.next().value! if (sortedItem[key] !== item[key]) { wasResorted = true } return sortedItem } return item }) if (!wasResorted) { return null } return result } ================================================ FILE: packages/eslint-plugin-query/src/utils/unique-by.ts ================================================ export function uniqueBy(arr: Array, fn: (x: T) => unknown): Array { return arr.filter((x, i, a) => a.findIndex((y) => fn(x) === fn(y)) === i) } ================================================ FILE: packages/query-async-storage-persister/eslint.config.js ================================================ // @ts-check import rootConfig from './root.eslint.config.js' export default [...rootConfig] ================================================ FILE: packages/query-async-storage-persister/package.json ================================================ { "name": "@tanstack/query-async-storage-persister", "version": "5.89.0", "description": "A persister for asynchronous storages, to be used with TanStack/Query", "author": "tannerlinsley", "license": "MIT", "repository": { "type": "git", "url": "https://github.com/TanStack/query.git", "directory": "packages/query-async-storage-persister" }, "homepage": "https://tanstack.com/query", "funding": { "type": "github", "url": "https://github.com/sponsors/tannerlinsley" }, "scripts": { "clean": "premove ./build ./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": "publint --strict && attw --pack", "build": "tsup --tsconfig tsconfig.prod.json" }, "type": "module", "types": "build/legacy/index.d.ts", "main": "build/legacy/index.cjs", "module": "build/legacy/index.js", "react-native": "src/index.ts", "exports": { ".": { "@tanstack/custom-condition": "./src/index.ts", "import": { "types": "./build/modern/index.d.ts", "default": "./build/modern/index.js" }, "require": { "types": "./build/modern/index.d.cts", "default": "./build/modern/index.cjs" } }, "./package.json": "./package.json" }, "sideEffects": false, "files": [ "build", "src", "!src/__tests__" ], "dependencies": { "@tanstack/query-core": "workspace:*", "@tanstack/query-persist-client-core": "workspace:*" }, "devDependencies": { "@tanstack/query-test-utils": "workspace:*", "npm-run-all2": "^5.0.0" } } ================================================ FILE: packages/query-async-storage-persister/tsconfig.json ================================================ { "extends": "../../tsconfig.json", "compilerOptions": { "outDir": "./dist-ts", "rootDir": "." }, "include": ["src", "*.config.*", "package.json"], "references": [{ "path": "../query-persist-client-core" }] } ================================================ FILE: packages/query-async-storage-persister/tsconfig.prod.json ================================================ { "extends": "./tsconfig.json", "compilerOptions": { "incremental": false, "composite": false, "rootDir": "../../" } } ================================================ FILE: packages/query-async-storage-persister/tsup.config.ts ================================================ import { defineConfig } from 'tsup' import { legacyConfig, modernConfig } from './root.tsup.config.js' export default defineConfig([ modernConfig({ entry: ['src/*.ts'] }), legacyConfig({ entry: ['src/*.ts'] }), ]) ================================================ FILE: packages/query-async-storage-persister/vite.config.ts ================================================ import { defineConfig } from 'vitest/config' import packageJson from './package.json' export default 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, coverage: { enabled: true, provider: 'istanbul', include: ['src/**/*'] }, typecheck: { enabled: true }, restoreMocks: true, }, }) ================================================ SYMLINK: packages/query-async-storage-persister/root.eslint.config.js -> eslint.config.js ================================================ ================================================ SYMLINK: packages/query-async-storage-persister/root.tsup.config.js -> getTsupConfig.js ================================================ ================================================ FILE: packages/query-async-storage-persister/src/asyncThrottle.ts ================================================ import { timeoutManager } from '@tanstack/query-core' import { noop } from './utils' interface AsyncThrottleOptions { interval?: number onError?: (error: unknown) => void } export function asyncThrottle>( func: (...args: TArgs) => Promise, { interval = 1000, onError = noop }: AsyncThrottleOptions = {}, ) { if (typeof func !== 'function') throw new Error('argument is not function.') let nextExecutionTime = 0 let lastArgs = null let isExecuting = false let isScheduled = false return async (...args: TArgs) => { lastArgs = args if (isScheduled) return isScheduled = true while (isExecuting) { await new Promise((done) => timeoutManager.setTimeout(done, interval)) } while (Date.now() < nextExecutionTime) { await new Promise((done) => timeoutManager.setTimeout(done, nextExecutionTime - Date.now()), ) } isScheduled = false isExecuting = true try { await func(...lastArgs) } catch (error) { try { onError(error) } catch {} } nextExecutionTime = Date.now() + interval isExecuting = false } } ================================================ FILE: packages/query-async-storage-persister/src/index.ts ================================================ import { asyncThrottle } from './asyncThrottle' import { noop } from './utils' import type { AsyncStorage, MaybePromise, PersistedClient, Persister, Promisable, } from '@tanstack/query-persist-client-core' export type AsyncPersistRetryer = (props: { persistedClient: PersistedClient error: Error errorCount: number }) => Promisable interface CreateAsyncStoragePersisterOptions { /** The storage client used for setting and retrieving items from cache. * For SSR pass in `undefined`. Note that window.localStorage can be * `null` in Android WebViews depending on how they are configured. */ storage: AsyncStorage | undefined | null /** The key to use when storing the cache */ key?: string /** To avoid spamming, * pass a time in ms to throttle saving the cache to disk */ throttleTime?: number /** * How to serialize the data to storage. * @default `JSON.stringify` */ serialize?: (client: PersistedClient) => MaybePromise /** * How to deserialize the data from storage. * @default `JSON.parse` */ deserialize?: (cachedString: string) => MaybePromise retry?: AsyncPersistRetryer } export const createAsyncStoragePersister = ({ storage, key = `REACT_QUERY_OFFLINE_CACHE`, throttleTime = 1000, serialize = JSON.stringify, deserialize = JSON.parse, retry, }: CreateAsyncStoragePersisterOptions): Persister => { if (storage) { const trySave = async ( persistedClient: PersistedClient, ): Promise => { try { const serialized = await serialize(persistedClient) await storage.setItem(key, serialized) return } catch (error) { return error as Error } } return { persistClient: asyncThrottle( async (persistedClient) => { let client: PersistedClient | undefined = persistedClient let error = await trySave(client) let errorCount = 0 while (error && client) { errorCount++ client = await retry?.({ persistedClient: client, error, errorCount, }) if (client) { error = await trySave(client) } } }, { interval: throttleTime }, ), restoreClient: async () => { const cacheString = await storage.getItem(key) if (!cacheString) { return } return await deserialize(cacheString) }, removeClient: () => storage.removeItem(key), } } return { persistClient: noop, restoreClient: () => Promise.resolve(undefined), removeClient: noop, } } ================================================ FILE: packages/query-async-storage-persister/src/utils.ts ================================================ export function noop(): void export function noop(): undefined export function noop() {} ================================================ FILE: packages/query-async-storage-persister/src/__tests__/asyncThrottle.test.ts ================================================ import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest' import { sleep } from '@tanstack/query-test-utils' import { asyncThrottle } from '../asyncThrottle' describe('asyncThrottle', () => { beforeEach(() => { vi.useFakeTimers() }) afterEach(() => { vi.useRealTimers() }) test('basic', async () => { const interval = 10 const execTimeStamps: Array = [] const mockFunc = vi.fn( async (id: number, complete?: (value?: unknown) => void) => { await sleep(1) execTimeStamps.push(Date.now()) if (complete) { complete(id) } }, ) const testFunc = asyncThrottle(mockFunc, { interval }) testFunc(1) await vi.advanceTimersByTimeAsync(1) testFunc(2) await vi.advanceTimersByTimeAsync(1) new Promise((resolve) => testFunc(3, resolve)) await vi.advanceTimersToNextTimerAsync() await vi.advanceTimersByTimeAsync(interval) expect(mockFunc).toBeCalledTimes(2) expect(mockFunc.mock.calls[1]?.[0]).toBe(3) expect(execTimeStamps.length).toBe(2) expect(execTimeStamps[1]! - execTimeStamps[0]!).toBeGreaterThanOrEqual( interval, ) }) test('Bug #3331 case 1: Special timing', async () => { const interval = 1000 const execTimeStamps: Array = [] const mockFunc = vi.fn( async (id: number, complete?: (value?: unknown) => void) => { await sleep(30) execTimeStamps.push(Date.now()) if (complete) { complete(id) } }, ) const testFunc = asyncThrottle(mockFunc, { interval }) testFunc(1) testFunc(2) await vi.advanceTimersByTimeAsync(35) testFunc(3) await vi.advanceTimersByTimeAsync(35) new Promise((resolve) => testFunc(4, resolve)) await vi.advanceTimersToNextTimerAsync() await vi.advanceTimersByTimeAsync(interval) expect(mockFunc).toBeCalledTimes(2) expect(mockFunc.mock.calls[1]?.[0]).toBe(4) expect(execTimeStamps.length).toBe(2) expect(execTimeStamps[1]! - execTimeStamps[0]!).toBeGreaterThanOrEqual( interval, ) }) test('Bug #3331 case 2: "func" execution time is greater than the interval.', async () => { const interval = 1000 const execTimeStamps: Array = [] const mockFunc = vi.fn( async (id: number, complete?: (value?: unknown) => void) => { await sleep(interval + 10) execTimeStamps.push(Date.now()) if (complete) { complete(id) } }, ) const testFunc = asyncThrottle(mockFunc, { interval }) testFunc(1) testFunc(2) new Promise((resolve) => testFunc(3, resolve)) await vi.advanceTimersToNextTimerAsync() await vi.advanceTimersByTimeAsync(interval + 10) await vi.advanceTimersByTimeAsync(interval + 10) expect(mockFunc).toBeCalledTimes(2) expect(mockFunc.mock.calls[1]?.[0]).toBe(3) expect(execTimeStamps.length).toBe(2) expect(execTimeStamps[1]! - execTimeStamps[0]!).toBeGreaterThanOrEqual( interval, ) }) test('"func" throw error not break next invoke', async () => { const interval = 10 const mockFunc = vi.fn( async (id: number, complete?: (value?: unknown) => void) => { if (id === 1) throw new Error('error') await sleep(1) if (complete) { complete(id) } }, ) const testFunc = asyncThrottle(mockFunc, { interval }) testFunc(1) await vi.advanceTimersByTimeAsync(1) new Promise((resolve) => testFunc(2, resolve)) await vi.advanceTimersByTimeAsync(interval) expect(mockFunc).toBeCalledTimes(2) expect(mockFunc.mock.calls[1]?.[0]).toBe(2) }) test('"onError" should be called when "func" throw error', () => { const err = new Error('error') const handleError = (e: unknown) => { expect(e).toBe(err) } const testFunc = asyncThrottle( () => { throw err }, { onError: handleError }, ) testFunc() }) test('should throw error when "func" is not a function', () => { expect(() => asyncThrottle(1 as any)).toThrowError() }) }) ================================================ FILE: packages/query-broadcast-client-experimental/eslint.config.js ================================================ // @ts-check import rootConfig from './root.eslint.config.js' export default [...rootConfig] ================================================ FILE: packages/query-broadcast-client-experimental/package.json ================================================ { "name": "@tanstack/query-broadcast-client-experimental", "version": "5.89.0", "description": "An experimental plugin to for broadcasting the state of your queryClient between browser tabs/windows", "author": "tannerlinsley", "license": "MIT", "repository": { "type": "git", "url": "https://github.com/TanStack/query.git", "directory": "packages/query-broadcast-client-experimental" }, "homepage": "https://tanstack.com/query", "funding": { "type": "github", "url": "https://github.com/sponsors/tannerlinsley" }, "scripts": { "clean": "premove ./build ./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": "publint --strict && attw --pack", "build": "tsup --tsconfig tsconfig.prod.json" }, "type": "module", "types": "build/legacy/index.d.ts", "main": "build/legacy/index.cjs", "module": "build/legacy/index.js", "exports": { ".": { "@tanstack/custom-condition": "./src/index.ts", "import": { "types": "./build/modern/index.d.ts", "default": "./build/modern/index.js" }, "require": { "types": "./build/modern/index.d.cts", "default": "./build/modern/index.cjs" } }, "./package.json": "./package.json" }, "sideEffects": false, "files": [ "build", "src", "!src/__tests__" ], "dependencies": { "@tanstack/query-core": "workspace:*", "broadcast-channel": "^7.0.0" }, "devDependencies": { "@testing-library/react": "^16.1.0", "@vitejs/plugin-react": "^4.3.4", "npm-run-all2": "^5.0.0" } } ================================================ FILE: packages/query-broadcast-client-experimental/test-setup.ts ================================================ import '@testing-library/jest-dom/vitest' import { act, cleanup as cleanupRTL } from '@testing-library/react' import { afterEach } from 'vitest' import { notifyManager } from '@tanstack/query-core' // https://testing-library.com/docs/react-testing-library/api#cleanup afterEach(() => { cleanupRTL() }) // Wrap notifications with act to make sure React knows about React Query updates notifyManager.setNotifyFunction((fn) => { act(fn) }) ================================================ FILE: packages/query-broadcast-client-experimental/tsconfig.json ================================================ { "extends": "../../tsconfig.json", "compilerOptions": { "outDir": "./dist-ts", "rootDir": "." }, "include": ["src", "test-setup.ts", "*.config.*", "package.json"], "references": [{ "path": "../query-core" }] } ================================================ FILE: packages/query-broadcast-client-experimental/tsconfig.prod.json ================================================ { "extends": "./tsconfig.json", "compilerOptions": { "incremental": false, "composite": false, "rootDir": "../../" } } ================================================ FILE: packages/query-broadcast-client-experimental/tsup.config.ts ================================================ import { defineConfig } from 'tsup' import { legacyConfig, modernConfig } from './root.tsup.config.js' export default defineConfig([ modernConfig({ entry: ['src/*.ts'] }), legacyConfig({ entry: ['src/*.ts'] }), ]) ================================================ FILE: packages/query-broadcast-client-experimental/vite.config.ts ================================================ import { defineConfig } from 'vitest/config' import react from '@vitejs/plugin-react' import packageJson from './package.json' export default defineConfig({ plugins: [react()], // 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 }, restoreMocks: true, retry: process.env.CI ? 3 : 0, }, }) ================================================ SYMLINK: packages/query-broadcast-client-experimental/root.eslint.config.js -> eslint.config.js ================================================ ================================================ SYMLINK: packages/query-broadcast-client-experimental/root.tsup.config.js -> getTsupConfig.js ================================================ ================================================ FILE: packages/query-broadcast-client-experimental/src/index.ts ================================================ import { BroadcastChannel } from 'broadcast-channel' import type { BroadcastChannelOptions } from 'broadcast-channel' import type { QueryClient } from '@tanstack/query-core' interface BroadcastQueryClientOptions { queryClient: QueryClient broadcastChannel?: string options?: BroadcastChannelOptions } export function broadcastQueryClient({ queryClient, broadcastChannel = 'tanstack-query', options, }: BroadcastQueryClientOptions): () => void { let transaction = false const tx = (cb: () => void) => { transaction = true cb() transaction = false } const channel = new BroadcastChannel(broadcastChannel, { webWorkerSupport: false, ...options, }) const queryCache = queryClient.getQueryCache() const unsubscribe = queryClient.getQueryCache().subscribe((queryEvent) => { if (transaction) { return } const { query: { queryHash, queryKey, state, observers }, } = queryEvent if (queryEvent.type === 'updated' && queryEvent.action.type === 'success') { channel.postMessage({ type: 'updated', queryHash, queryKey, state, }) } if (queryEvent.type === 'removed' && observers.length > 0) { channel.postMessage({ type: 'removed', queryHash, queryKey, }) } if (queryEvent.type === 'added') { channel.postMessage({ type: 'added', queryHash, queryKey, }) } }) channel.onmessage = (action) => { if (!action?.type) { return } tx(() => { const { type, queryHash, queryKey, state } = action const query = queryCache.get(queryHash) if (type === 'updated') { if (query) { query.setState(state) return } queryCache.build( queryClient, { queryKey, queryHash, }, state, ) } else if (type === 'removed') { if (query) { queryCache.remove(query) } } else if (type === 'added') { if (query) { query.setState(state) return } queryCache.build( queryClient, { queryKey, queryHash, }, state, ) } }) } return () => { unsubscribe() channel.close() } } ================================================ FILE: packages/query-broadcast-client-experimental/src/__tests__/index.test.ts ================================================ import { QueryClient } from '@tanstack/query-core' import { beforeEach, describe, expect, it } from 'vitest' import { broadcastQueryClient } from '..' import type { QueryCache } from '@tanstack/query-core' describe('broadcastQueryClient', () => { let queryClient: QueryClient let queryCache: QueryCache beforeEach(() => { queryClient = new QueryClient() queryCache = queryClient.getQueryCache() }) it('should subscribe to the query cache', () => { broadcastQueryClient({ queryClient, broadcastChannel: 'test_channel', }) expect(queryCache.hasListeners()).toBe(true) }) it('should not have any listeners after cleanup', () => { const unsubscribe = broadcastQueryClient({ queryClient, broadcastChannel: 'test_channel', }) unsubscribe() expect(queryCache.hasListeners()).toBe(false) }) }) ================================================ FILE: packages/query-codemods/eslint.config.js ================================================ // @ts-check import rootConfig from './root.eslint.config.js' export default [ ...rootConfig, { rules: { 'cspell/spellchecker': 'off', '@typescript-eslint/no-unnecessary-condition': 'off', 'import/no-duplicates': 'off', 'import/no-unresolved': 'off', 'import/order': 'off', 'no-shadow': 'off', 'sort-imports': 'off', }, }, ] ================================================ FILE: packages/query-codemods/package.json ================================================ { "name": "@tanstack/query-codemods", "private": true, "description": "Collection of codemods to make the migration easier.", "author": "Balázs Máté Petró", "license": "MIT", "repository": { "type": "git", "url": "https://github.com/TanStack/query.git", "directory": "packages/query-codemods" }, "homepage": "https://tanstack.com/query", "funding": { "type": "github", "url": "https://github.com/sponsors/tannerlinsley" }, "scripts": { "clean": "premove ./coverage ./dist-ts", "test:eslint": "eslint --concurrency=auto ./src", "test:lib": "vitest", "test:lib:dev": "pnpm run test:lib --watch" }, "type": "module", "exports": { "./package.json": "./package.json" }, "sideEffects": false, "files": [ "src", "!src/jest.config.js", "!src/**/__testfixtures__", "!src/**/__tests__" ], "devDependencies": { "@types/jscodeshift": "17.3.0", "jscodeshift": "17.3.0" } } ================================================ FILE: packages/query-codemods/tsconfig.json ================================================ { "extends": "../../tsconfig.json", "compilerOptions": { "outDir": "./dist-ts", "rootDir": "." }, "include": ["src", "*.config.*", "package.json"] } ================================================ FILE: packages/query-codemods/vite.config.ts ================================================ import { defineConfig } from 'vitest/config' import packageJson from './package.json' export default 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, globals: true, coverage: { enabled: true, provider: 'istanbul', include: ['src/**/*'] }, typecheck: { enabled: true }, restoreMocks: true, }, }) ================================================ SYMLINK: packages/query-codemods/root.eslint.config.js -> eslint.config.js ================================================ ================================================ FILE: packages/query-codemods/src/utils/index.cjs ================================================ module.exports = ({ root, jscodeshift }) => { const findImportIdentifierOf = (importSpecifiers, identifier) => { const specifier = importSpecifiers .filter((node) => node.value.imported.name === identifier) .paths() if (specifier.length > 0) { return specifier[0].value.local } return jscodeshift.identifier(identifier) } const findImportSpecifiers = (packageName) => root .find(jscodeshift.ImportDeclaration, { source: { value: packageName, }, }) .find(jscodeshift.ImportSpecifier, {}) const locateImports = ( identifiers, packageName = '@tanstack/react-query', ) => { const findNamespaceImportIdentifier = () => { const specifier = root .find(jscodeshift.ImportDeclaration, { source: { value: packageName, }, }) .find(jscodeshift.ImportNamespaceSpecifier) .paths() return specifier.length > 0 ? specifier[0].value.local : null } /** * First, we search for the namespace import identifier because if we have any, we assume the consumer uses * namespace imports. In this case, we won't search for named imports at all. */ const namespaceImportIdentifier = findNamespaceImportIdentifier() if (namespaceImportIdentifier) { const identifierMap = {} for (const identifier of identifiers) { identifierMap[identifier] = jscodeshift.identifier(identifier) } return { namespace: namespaceImportIdentifier, ...identifierMap, } } const importSpecifiers = findImportSpecifiers(packageName) const identifierMap = {} for (const identifier of identifiers) { identifierMap[identifier] = findImportIdentifierOf( importSpecifiers, identifier, ) } return { namespace: null, ...identifierMap, } } const findAllMethodCalls = () => root // First, we need to find all method calls. .find(jscodeshift.CallExpression, { callee: { type: jscodeshift.MemberExpression.name, property: { type: jscodeshift.Identifier.name, }, }, }) const findQueryClientIdentifiers = (importIdentifiers) => root .find(jscodeshift.VariableDeclarator, {}) .filter((node) => { if (node.value.init) { const initializer = node.value.init return ( isClassInstantiationOf( initializer, getSelectorByImports(importIdentifiers, 'QueryClient'), ) || isFunctionCallOf( initializer, getSelectorByImports(importIdentifiers, 'useQueryClient'), ) ) } return false }) .paths() .map((node) => node.value.id.name) const isCallExpression = (node) => jscodeshift.match(node, { type: jscodeshift.CallExpression.name }) const isIdentifier = (node) => jscodeshift.match(node, { type: jscodeshift.Identifier.name }) const isMemberExpression = (node) => jscodeshift.match(node, { type: jscodeshift.MemberExpression.name }) const isNewExpression = (node) => jscodeshift.match(node, { type: jscodeshift.NewExpression.name }) const isArrayExpression = (node) => jscodeshift.match(node, { type: jscodeshift.ArrayExpression.name }) const isObjectExpression = (node) => jscodeshift.match(node, { type: jscodeshift.ObjectExpression.name }) const isObjectProperty = (node) => jscodeshift.match(node, { type: jscodeshift.ObjectProperty.name }) const isSpreadElement = (node) => jscodeshift.match(node, { type: jscodeshift.SpreadElement.name }) /** * @param {import('jscodeshift').Node} node * @returns {boolean} */ const isFunctionDefinition = (node) => { const isArrowFunctionExpression = jscodeshift.match(node, { type: jscodeshift.ArrowFunctionExpression.name, }) const isFunctionExpression = jscodeshift.match(node, { type: jscodeshift.FunctionExpression.name, }) return isArrowFunctionExpression || isFunctionExpression } const warn = (message) => { if (process.env.NODE_ENV !== 'test') { console.warn(message) } } const isClassInstantiationOf = (node, selector) => { if (!isNewExpression(node)) { return false } const parts = selector.split('.') return parts.length === 1 ? isIdentifier(node.callee) && node.callee.name === parts[0] : isMemberExpression(node.callee) && node.callee.object.name === parts[0] && node.callee.property.name === parts[1] } const isFunctionCallOf = (node, selector) => { if (!isCallExpression(node)) { return false } const parts = selector.split('.') return parts.length === 1 ? isIdentifier(node.callee) && node.callee.name === parts[0] : isMemberExpression(node.callee) && node.callee.object.name === parts[0] && node.callee.property.name === parts[1] } const getSelectorByImports = (imports, path) => imports.namespace ? `${imports.namespace.name}.${imports[path].name}` : imports[path].name return { findAllMethodCalls, getSelectorByImports, isCallExpression, isClassInstantiationOf, isFunctionCallOf, isIdentifier, isMemberExpression, isArrayExpression, isObjectExpression, isObjectProperty, isSpreadElement, isFunctionDefinition, locateImports, warn, queryClient: { findQueryClientIdentifiers, }, } } ================================================ FILE: packages/query-codemods/src/utils/transformers/query-cache-transformer.cjs ================================================ module.exports = ({ jscodeshift, utils, root, packageName = '@tanstack/react-query', }) => { const isGetQueryCacheMethodCall = ( initializer, importIdentifiers, knownQueryClientIds, ) => { const isKnownQueryClient = (node) => utils.isIdentifier(node) && knownQueryClientIds.includes(node.name) const isGetQueryCacheIdentifier = (node) => utils.isIdentifier(node) && node.name === 'getQueryCache' const isValidInitializer = (node) => utils.isCallExpression(node) && utils.isMemberExpression(node.callee) if (isValidInitializer(initializer)) { const instance = initializer.callee.object return ( isGetQueryCacheIdentifier(initializer.callee.property) && (isKnownQueryClient(instance) || utils.isFunctionCallOf( instance, utils.getSelectorByImports(importIdentifiers, 'useQueryClient'), )) ) } return false } const findQueryCacheInstantiations = ( importIdentifiers, knownQueryClientIds, ) => root.find(jscodeshift.VariableDeclarator, {}).filter((node) => { if (node.value.init) { const initializer = node.value.init return ( utils.isClassInstantiationOf( initializer, utils.getSelectorByImports(importIdentifiers, 'QueryCache'), ) || isGetQueryCacheMethodCall( initializer, importIdentifiers, knownQueryClientIds, ) ) } return false }) const filterQueryCacheMethodCalls = (node) => utils.isIdentifier(node) && ['find', 'findAll'].includes(node.name) const findQueryCacheMethodCalls = (importIdentifiers) => { /** * Here we collect all query client instantiations. We have to make aware of them because the query cache can be * accessed by the query client as well. */ const queryClientIdentifiers = utils.queryClient.findQueryClientIdentifiers(importIdentifiers) /** * Here we collect all query cache instantiations. The reason is simple: the methods can be called on query cache * instances, to locate the possible usages we need to be aware of the identifier names. */ const queryCacheIdentifiers = findQueryCacheInstantiations( importIdentifiers, queryClientIdentifiers, ) .paths() .map((node) => node.value.id.name) return ( utils // First, we need to find all method calls. .findAllMethodCalls() // Then we narrow the collection to all `fetch` and `fetchAll` methods. .filter((node) => filterQueryCacheMethodCalls(node.value.callee.property), ) .filter((node) => { const object = node.value.callee.object // If the method is called on a `QueryCache` instance, we keep it in the collection. if (utils.isIdentifier(object)) { return queryCacheIdentifiers.includes(object.name) } // If the method is called on a `QueryClient` instance, we keep it in the collection. if (utils.isCallExpression(object)) { return isGetQueryCacheMethodCall( object, importIdentifiers, queryClientIdentifiers, ) } return false }) ) } const execute = (replacer) => { findQueryCacheMethodCalls( utils.locateImports( ['QueryCache', 'QueryClient', 'useQueryClient'], packageName, ), ).replaceWith(replacer) } return { execute, } } ================================================ FILE: packages/query-codemods/src/utils/transformers/query-client-transformer.cjs ================================================ module.exports = ({ jscodeshift, utils, root, packageName = '@tanstack/react-query', }) => { const filterQueryClientMethodCalls = (node, methods) => utils.isIdentifier(node) && methods.includes(node.name) const findQueryClientMethodCalls = (importIdentifiers, methods) => { /** * Here we collect all query client instantiations. We have to make aware of them because some method calls might * be invoked on these instances. */ const queryClientIdentifiers = utils.queryClient.findQueryClientIdentifiers(importIdentifiers) return ( utils // First, we need to find all method calls. .findAllMethodCalls() // Then we narrow the collection to `QueryClient` methods. .filter((node) => filterQueryClientMethodCalls(node.value.callee.property, methods), ) .filter((node) => { const object = node.value.callee.object // If the method is called on a `QueryClient` instance, we keep it in the collection. if (utils.isIdentifier(object)) { return queryClientIdentifiers.includes(object.name) } // If the method is called on the return value of `useQueryClient` hook, we keep it in the collection. return utils.isFunctionCallOf( object, utils.getSelectorByImports(importIdentifiers, 'useQueryClient'), ) }) ) } const execute = (methods, replacer) => { findQueryClientMethodCalls( utils.locateImports(['QueryClient', 'useQueryClient'], packageName), methods, ).replaceWith(replacer) } return { execute, } } ================================================ FILE: packages/query-codemods/src/utils/transformers/use-query-like-transformer.cjs ================================================ module.exports = ({ jscodeshift, utils, root, packageName = '@tanstack/react-query', }) => { const filterUseQueryLikeHookCalls = (node, importIdentifiers, hooks) => { for (const hook of hooks) { const selector = utils.getSelectorByImports(importIdentifiers, hook) if (utils.isFunctionCallOf(node, selector)) { return true } } return false } const findUseQueryLikeHookCalls = (importIdentifiers, hooks) => root // First, we need to find all call expressions. .find(jscodeshift.CallExpression, {}) // Then we narrow the collection to the `useQuery` like hook calls. .filter((node) => filterUseQueryLikeHookCalls(node.value, importIdentifiers, hooks), ) const execute = (hooks, replacer) => { findUseQueryLikeHookCalls( utils.locateImports(hooks, packageName), hooks, ).replaceWith(replacer) } return { execute, } } ================================================ FILE: packages/query-codemods/src/v4/key-transformation.cjs ================================================ const createUtilsObject = require('../utils/index.cjs') const createKeyReplacer = require('./utils/replacers/key-replacer.cjs') const createUseQueryLikeTransformer = require('../utils/transformers/use-query-like-transformer.cjs') const createQueryClientTransformer = require('../utils/transformers/query-client-transformer.cjs') const createQueryCacheTransformer = require('../utils/transformers/query-cache-transformer.cjs') const transformQueryClientUsages = ({ jscodeshift, utils, root, filePath, packageName, }) => { const transformer = createQueryClientTransformer({ jscodeshift, utils, root, packageName, }) const replacer = createKeyReplacer({ jscodeshift, root, filePath }) transformer.execute( [ // Not object syntax-aware methods. 'getMutationDefaults', 'getQueriesData', 'getQueryData', 'getQueryDefaults', 'getQueryState', 'isFetching', 'setMutationDefaults', 'setQueriesData', 'setQueryData', 'setQueryDefaults', // Object syntax-aware methods. 'cancelQueries', 'fetchInfiniteQuery', 'fetchQuery', 'invalidateQueries', 'prefetchInfiniteQuery', 'prefetchQuery', 'refetchQueries', 'removeQueries', 'resetQueries', ], replacer, ) } const transformUseQueriesUsages = ({ jscodeshift, utils, root, packageName, }) => { const transformer = createUseQueryLikeTransformer({ jscodeshift, utils, root, packageName, }) const replacer = ({ node }) => { /** * When the node doesn't have the 'original' property, that means the codemod has been already applied, * so we don't need to do any changes. */ if (!node.original) { return node } const newCallExpression = jscodeshift.callExpression(node.original.callee, [ jscodeshift.objectExpression([ jscodeshift.property( 'init', jscodeshift.identifier('queries'), node.original.arguments[0], ), ]), ]) // TODO: This should be part of one function! if (node.typeParameters) { newCallExpression.typeArguments = node.typeParameters } return newCallExpression } transformer.execute(['useQueries'], replacer) } const transformUseQueryLikeUsages = ({ jscodeshift, utils, root, filePath, packageName, }) => { const transformer = createUseQueryLikeTransformer({ jscodeshift, utils, root, packageName, }) transformer.execute( ['useQuery', 'useInfiniteQuery', 'useIsFetching', 'useIsMutating'], createKeyReplacer({ jscodeshift, root, filePath, keyName: 'queryKey', }), ) transformer.execute( ['useMutation'], createKeyReplacer({ jscodeshift, root, filePath, keyName: 'mutationKey', }), ) } const transformQueryCacheUsages = ({ jscodeshift, utils, root, filePath, packageName, }) => { const transformer = createQueryCacheTransformer({ jscodeshift, utils, root, packageName, }) const replacer = createKeyReplacer({ jscodeshift, root, filePath }) transformer.execute(replacer) } module.exports = (file, api) => { const jscodeshift = api.jscodeshift const root = jscodeshift(file.source) // TODO: Execute the transformers only when it contains a `react-query` import! const utils = createUtilsObject({ root, jscodeshift }) const filePath = file.path const packageName = 'react-query' // This function transforms usages like `useQuery` and `useMutation`. transformUseQueryLikeUsages({ jscodeshift, utils, root, filePath, packageName, }) // This function transforms usages of `useQueries`. transformUseQueriesUsages({ jscodeshift, utils, root, packageName, }) // This function transforms usages of `QueryClient`. transformQueryClientUsages({ jscodeshift, utils, root, filePath, packageName, }) // This function transforms usages of `QueryCache`. transformQueryCacheUsages({ jscodeshift, utils, root, filePath, packageName }) return root.toSource({ quote: 'single', lineTerminator: '\n' }) } ================================================ FILE: packages/query-codemods/src/v4/replace-import-specifier.cjs ================================================ module.exports = (file, api) => { const jscodeshift = api.jscodeshift const root = jscodeshift(file.source) const replacements = [ { from: 'react-query', to: '@tanstack/react-query' }, { from: 'react-query/devtools', to: '@tanstack/react-query-devtools' }, ] replacements.forEach(({ from, to }) => { root .find(jscodeshift.ImportDeclaration, { source: { value: from, }, }) .replaceWith(({ node }) => { node.source.value = to return node }) }) return root.toSource({ quote: 'single', lineTerminator: '\n' }) } ================================================ FILE: packages/query-codemods/src/v4/__testfixtures__/default-import.input.tsx ================================================ import * as React from 'react' import { QueryCache, QueryClient, useInfiniteQuery, useIsFetching, useIsMutating, useMutation, useQueries, useQuery, useQueryClient, } from 'react-query' export const Examples = () => { useQuery('todos') useInfiniteQuery('todos') useMutation('todos') useIsFetching('todos') useIsMutating('todos') useQueries([query1, query2]) // QueryClient methods // --- Instantiated hook call. const queryClient = useQueryClient() queryClient.getMutationDefaults('todos') queryClient.getQueriesData('todos') queryClient.getQueryData('todos') queryClient.getQueryDefaults('todos') queryClient.getQueryState('todos') queryClient.isFetching('todos') queryClient.setMutationDefaults('todos', { mutationFn: async () => null }) queryClient.setQueriesData('todos', () => null) queryClient.setQueryData('todos', () => null) queryClient.setQueryDefaults('todos', { queryFn: async () => null }) queryClient.cancelQueries('todos') queryClient.fetchInfiniteQuery('todos') queryClient.fetchQuery('todos') queryClient.invalidateQueries('todos') queryClient.prefetchInfiniteQuery('todos') queryClient.prefetchQuery('todos') queryClient.refetchQueries('todos') queryClient.removeQueries('todos') queryClient.resetQueries('todos') // --- Direct hook call. useQueryClient().getMutationDefaults('todos') useQueryClient().getQueriesData('todos') useQueryClient().getQueryData('todos') useQueryClient().getQueryDefaults('todos') useQueryClient().getQueryState('todos') useQueryClient().isFetching('todos') useQueryClient().setMutationDefaults('todos', { mutationFn: async () => null, }) useQueryClient().setQueriesData('todos', () => null) useQueryClient().setQueryData('todos', () => null) useQueryClient().setQueryDefaults('todos', { queryFn: async () => null }) useQueryClient().cancelQueries('todos') useQueryClient().fetchInfiniteQuery('todos') useQueryClient().fetchQuery('todos') useQueryClient().invalidateQueries('todos') useQueryClient().prefetchInfiniteQuery('todos') useQueryClient().prefetchQuery('todos') useQueryClient().refetchQueries('todos') useQueryClient().removeQueries('todos') useQueryClient().resetQueries('todos') // QueryCache // --- NewExpression const queryCache1 = new QueryCache({ onError: (error) => console.log(error), onSuccess: (success) => console.log(success) }) queryCache1.find('todos') queryCache1.findAll('todos') // --- Instantiated hook call. const queryClient1 = useQueryClient() queryClient1.getQueryCache().find('todos') queryClient1.getQueryCache().findAll('todos') // const queryClient2 = new QueryClient({}) queryClient2.getQueryCache().find('todos') queryClient2.getQueryCache().findAll('todos') // const queryCache2 = queryClient1.getQueryCache() queryCache2.find('todos') queryCache2.findAll('todos') // --- Direct hook call. useQueryClient().getQueryCache().find('todos') useQueryClient().getQueryCache().findAll('todos') // const queryCache3 = useQueryClient().getQueryCache() queryCache3.find('todos') queryCache3.findAll('todos') return
Example Component
} ================================================ FILE: packages/query-codemods/src/v4/__testfixtures__/default-import.output.tsx ================================================ import * as React from 'react' import { QueryCache, QueryClient, useInfiniteQuery, useIsFetching, useIsMutating, useMutation, useQueries, useQuery, useQueryClient, } from 'react-query' export const Examples = () => { useQuery(['todos']) useInfiniteQuery(['todos']) useMutation(['todos']) useIsFetching(['todos']) useIsMutating(['todos']) useQueries({ queries: [query1, query2] }) // QueryClient methods // --- Instantiated hook call. const queryClient = useQueryClient() queryClient.getMutationDefaults(['todos']) queryClient.getQueriesData(['todos']) queryClient.getQueryData(['todos']) queryClient.getQueryDefaults(['todos']) queryClient.getQueryState(['todos']) queryClient.isFetching(['todos']) queryClient.setMutationDefaults(['todos'], { mutationFn: async () => null }) queryClient.setQueriesData(['todos'], () => null) queryClient.setQueryData(['todos'], () => null) queryClient.setQueryDefaults(['todos'], { queryFn: async () => null }) queryClient.cancelQueries(['todos']) queryClient.fetchInfiniteQuery(['todos']) queryClient.fetchQuery(['todos']) queryClient.invalidateQueries(['todos']) queryClient.prefetchInfiniteQuery(['todos']) queryClient.prefetchQuery(['todos']) queryClient.refetchQueries(['todos']) queryClient.removeQueries(['todos']) queryClient.resetQueries(['todos']) // --- Direct hook call. useQueryClient().getMutationDefaults(['todos']) useQueryClient().getQueriesData(['todos']) useQueryClient().getQueryData(['todos']) useQueryClient().getQueryDefaults(['todos']) useQueryClient().getQueryState(['todos']) useQueryClient().isFetching(['todos']) useQueryClient().setMutationDefaults(['todos'], { mutationFn: async () => null, }) useQueryClient().setQueriesData(['todos'], () => null) useQueryClient().setQueryData(['todos'], () => null) useQueryClient().setQueryDefaults(['todos'], { queryFn: async () => null }) useQueryClient().cancelQueries(['todos']) useQueryClient().fetchInfiniteQuery(['todos']) useQueryClient().fetchQuery(['todos']) useQueryClient().invalidateQueries(['todos']) useQueryClient().prefetchInfiniteQuery(['todos']) useQueryClient().prefetchQuery(['todos']) useQueryClient().refetchQueries(['todos']) useQueryClient().removeQueries(['todos']) useQueryClient().resetQueries(['todos']) // QueryCache // --- NewExpression const queryCache1 = new QueryCache({ onError: (error) => console.log(error), onSuccess: (success) => console.log(success) }) queryCache1.find(['todos']) queryCache1.findAll(['todos']) // --- Instantiated hook call. const queryClient1 = useQueryClient() queryClient1.getQueryCache().find(['todos']) queryClient1.getQueryCache().findAll(['todos']) // const queryClient2 = new QueryClient({}) queryClient2.getQueryCache().find(['todos']) queryClient2.getQueryCache().findAll(['todos']) // const queryCache2 = queryClient1.getQueryCache() queryCache2.find(['todos']) queryCache2.findAll(['todos']) // --- Direct hook call. useQueryClient().getQueryCache().find(['todos']) useQueryClient().getQueryCache().findAll(['todos']) // const queryCache3 = useQueryClient().getQueryCache() queryCache3.find(['todos']) queryCache3.findAll(['todos']) return
Example Component
} ================================================ FILE: packages/query-codemods/src/v4/__testfixtures__/named-import.input.tsx ================================================ import * as React from 'react' import { QueryCache as RenamedQueryCache, QueryClient as RenamedQueryClient, useInfiniteQuery as useRenamedInfiniteQuery, useIsFetching as useRenamedIsFetching, useIsMutating as useRenamedIsMutating, useMutation as useRenamedMutation, useQueries as useRenamedQueries, useQuery as useRenamedQuery, useQueryClient as useRenamedQueryClient, } from 'react-query' export const Examples = () => { useRenamedQuery('todos') useRenamedInfiniteQuery('todos') useRenamedMutation('todos') useRenamedIsFetching('todos') useRenamedIsMutating('todos') useRenamedQueries([query1, query2]) // QueryClient methods // --- Instantiated hook call. const queryClient = useRenamedQueryClient() queryClient.getMutationDefaults('todos') queryClient.getQueriesData('todos') queryClient.getQueryData('todos') queryClient.getQueryDefaults('todos') queryClient.getQueryState('todos') queryClient.isFetching('todos') queryClient.setMutationDefaults('todos', { mutationFn: async () => null }) queryClient.setQueriesData('todos', () => null) queryClient.setQueryData('todos', () => null) queryClient.setQueryDefaults('todos', { queryFn: async () => null }) queryClient.cancelQueries('todos') queryClient.fetchInfiniteQuery('todos') queryClient.fetchQuery('todos') queryClient.invalidateQueries('todos') queryClient.prefetchInfiniteQuery('todos') queryClient.prefetchQuery('todos') queryClient.refetchQueries('todos') queryClient.removeQueries('todos') queryClient.resetQueries('todos') // --- Direct hook call. useRenamedQueryClient().getMutationDefaults('todos') useRenamedQueryClient().getQueriesData('todos') useRenamedQueryClient().getQueryData('todos') useRenamedQueryClient().getQueryDefaults('todos') useRenamedQueryClient().getQueryState('todos') useRenamedQueryClient().isFetching('todos') useRenamedQueryClient().setMutationDefaults('todos', { mutationFn: async () => null, }) useRenamedQueryClient().setQueriesData('todos', () => null) useRenamedQueryClient().setQueryData('todos', () => null) useRenamedQueryClient().setQueryDefaults('todos', { queryFn: async () => null, }) useRenamedQueryClient().cancelQueries('todos') useRenamedQueryClient().fetchInfiniteQuery('todos') useRenamedQueryClient().fetchQuery('todos') useRenamedQueryClient().invalidateQueries('todos') useRenamedQueryClient().prefetchInfiniteQuery('todos') useRenamedQueryClient().prefetchQuery('todos') useRenamedQueryClient().refetchQueries('todos') useRenamedQueryClient().removeQueries('todos') useRenamedQueryClient().resetQueries('todos') // QueryCache // --- NewExpression const queryCache1 = new RenamedQueryCache({ onError: (error) => console.log(error), onSuccess: (success) => console.log(success) }) queryCache1.find('todos') queryCache1.findAll('todos') // --- Instantiated hook call. const queryClient1 = useRenamedQueryClient() queryClient1.getQueryCache().find('todos') queryClient1.getQueryCache().findAll('todos') // const queryClient2 = new RenamedQueryClient({}) queryClient2.getQueryCache().find('todos') queryClient2.getQueryCache().findAll('todos') // const queryCache2 = queryClient1.getQueryCache() queryCache2.find('todos') queryCache2.findAll('todos') // --- Direct hook call. useRenamedQueryClient().getQueryCache().find('todos') useRenamedQueryClient().getQueryCache().findAll('todos') // const queryCache3 = useRenamedQueryClient().getQueryCache() queryCache3.find('todos') queryCache3.findAll('todos') return
Example Component
} ================================================ FILE: packages/query-codemods/src/v4/__testfixtures__/named-import.output.tsx ================================================ import * as React from 'react' import { QueryCache as RenamedQueryCache, QueryClient as RenamedQueryClient, useInfiniteQuery as useRenamedInfiniteQuery, useIsFetching as useRenamedIsFetching, useIsMutating as useRenamedIsMutating, useMutation as useRenamedMutation, useQueries as useRenamedQueries, useQuery as useRenamedQuery, useQueryClient as useRenamedQueryClient, } from 'react-query' export const Examples = () => { useRenamedQuery(['todos']) useRenamedInfiniteQuery(['todos']) useRenamedMutation(['todos']) useRenamedIsFetching(['todos']) useRenamedIsMutating(['todos']) useRenamedQueries({ queries: [query1, query2] }) // QueryClient methods // --- Instantiated hook call. const queryClient = useRenamedQueryClient() queryClient.getMutationDefaults(['todos']) queryClient.getQueriesData(['todos']) queryClient.getQueryData(['todos']) queryClient.getQueryDefaults(['todos']) queryClient.getQueryState(['todos']) queryClient.isFetching(['todos']) queryClient.setMutationDefaults(['todos'], { mutationFn: async () => null }) queryClient.setQueriesData(['todos'], () => null) queryClient.setQueryData(['todos'], () => null) queryClient.setQueryDefaults(['todos'], { queryFn: async () => null }) queryClient.cancelQueries(['todos']) queryClient.fetchInfiniteQuery(['todos']) queryClient.fetchQuery(['todos']) queryClient.invalidateQueries(['todos']) queryClient.prefetchInfiniteQuery(['todos']) queryClient.prefetchQuery(['todos']) queryClient.refetchQueries(['todos']) queryClient.removeQueries(['todos']) queryClient.resetQueries(['todos']) // --- Direct hook call. useRenamedQueryClient().getMutationDefaults(['todos']) useRenamedQueryClient().getQueriesData(['todos']) useRenamedQueryClient().getQueryData(['todos']) useRenamedQueryClient().getQueryDefaults(['todos']) useRenamedQueryClient().getQueryState(['todos']) useRenamedQueryClient().isFetching(['todos']) useRenamedQueryClient().setMutationDefaults(['todos'], { mutationFn: async () => null, }) useRenamedQueryClient().setQueriesData(['todos'], () => null) useRenamedQueryClient().setQueryData(['todos'], () => null) useRenamedQueryClient().setQueryDefaults(['todos'], { queryFn: async () => null, }) useRenamedQueryClient().cancelQueries(['todos']) useRenamedQueryClient().fetchInfiniteQuery(['todos']) useRenamedQueryClient().fetchQuery(['todos']) useRenamedQueryClient().invalidateQueries(['todos']) useRenamedQueryClient().prefetchInfiniteQuery(['todos']) useRenamedQueryClient().prefetchQuery(['todos']) useRenamedQueryClient().refetchQueries(['todos']) useRenamedQueryClient().removeQueries(['todos']) useRenamedQueryClient().resetQueries(['todos']) // QueryCache // --- NewExpression const queryCache1 = new RenamedQueryCache({ onError: (error) => console.log(error), onSuccess: (success) => console.log(success) }) queryCache1.find(['todos']) queryCache1.findAll(['todos']) // --- Instantiated hook call. const queryClient1 = useRenamedQueryClient() queryClient1.getQueryCache().find(['todos']) queryClient1.getQueryCache().findAll(['todos']) // const queryClient2 = new RenamedQueryClient({}) queryClient2.getQueryCache().find(['todos']) queryClient2.getQueryCache().findAll(['todos']) // const queryCache2 = queryClient1.getQueryCache() queryCache2.find(['todos']) queryCache2.findAll(['todos']) // --- Direct hook call. useRenamedQueryClient().getQueryCache().find(['todos']) useRenamedQueryClient().getQueryCache().findAll(['todos']) // const queryCache3 = useRenamedQueryClient().getQueryCache() queryCache3.find(['todos']) queryCache3.findAll(['todos']) return
Example Component
} ================================================ FILE: packages/query-codemods/src/v4/__testfixtures__/namespaced-import.input.tsx ================================================ import * as React from 'react' import * as RQ from 'react-query' export const Examples = () => { RQ.useQuery('todos') RQ.useInfiniteQuery('todos') RQ.useMutation('todos') RQ.useIsFetching('todos') RQ.useIsMutating('todos') RQ.useQueries([query1, query2]) // QueryClient methods // --- Instantiated hook call. const queryClient = RQ.useQueryClient() queryClient.getMutationDefaults('todos') queryClient.getQueriesData('todos') queryClient.getQueryData('todos') queryClient.getQueryDefaults('todos') queryClient.getQueryState('todos') queryClient.isFetching('todos') queryClient.setMutationDefaults('todos', { mutationFn: async () => null }) queryClient.setQueriesData('todos', () => null) queryClient.setQueryData('todos', () => null) queryClient.setQueryDefaults('todos', { queryFn: async () => null }) queryClient.cancelQueries('todos') queryClient.fetchInfiniteQuery('todos') queryClient.fetchQuery('todos') queryClient.invalidateQueries('todos') queryClient.prefetchInfiniteQuery('todos') queryClient.prefetchQuery('todos') queryClient.refetchQueries('todos') queryClient.removeQueries('todos') queryClient.resetQueries('todos') // --- Direct hook call. RQ.useQueryClient().getMutationDefaults('todos') RQ.useQueryClient().getQueriesData('todos') RQ.useQueryClient().getQueryData('todos') RQ.useQueryClient().getQueryDefaults('todos') RQ.useQueryClient().getQueryState('todos') RQ.useQueryClient().isFetching('todos') RQ.useQueryClient().setMutationDefaults('todos', { mutationFn: async () => null, }) RQ.useQueryClient().setQueriesData('todos', () => null) RQ.useQueryClient().setQueryData('todos', () => null) RQ.useQueryClient().setQueryDefaults('todos', { queryFn: async () => null, }) RQ.useQueryClient().cancelQueries('todos') RQ.useQueryClient().fetchInfiniteQuery('todos') RQ.useQueryClient().fetchQuery('todos') RQ.useQueryClient().invalidateQueries('todos') RQ.useQueryClient().prefetchInfiniteQuery('todos') RQ.useQueryClient().prefetchQuery('todos') RQ.useQueryClient().refetchQueries('todos') RQ.useQueryClient().removeQueries('todos') RQ.useQueryClient().resetQueries('todos') // QueryCache // --- NewExpression const queryCache1 = new RQ.QueryCache({ onError: (error) => console.log(error), onSuccess: (success) => console.log(success) }) queryCache1.find('todos') queryCache1.findAll('todos') // --- Instantiated hook call. const queryClient1 = RQ.useQueryClient() queryClient1.getQueryCache().find('todos') queryClient1.getQueryCache().findAll('todos') // const queryClient2 = new RQ.QueryClient({}) queryClient2.getQueryCache().find('todos') queryClient2.getQueryCache().findAll('todos') // const queryCache2 = queryClient1.getQueryCache() queryCache2.find('todos') queryCache2.findAll('todos') // --- Direct hook call. RQ.useQueryClient().getQueryCache().find('todos') RQ.useQueryClient().getQueryCache().findAll('todos') // const queryCache3 = RQ.useQueryClient().getQueryCache() queryCache3.find('todos') queryCache3.findAll('todos') return
Example Component
} ================================================ FILE: packages/query-codemods/src/v4/__testfixtures__/namespaced-import.output.tsx ================================================ import * as React from 'react' import * as RQ from 'react-query' export const Examples = () => { RQ.useQuery(['todos']) RQ.useInfiniteQuery(['todos']) RQ.useMutation(['todos']) RQ.useIsFetching(['todos']) RQ.useIsMutating(['todos']) RQ.useQueries({ queries: [query1, query2] }) // QueryClient methods // --- Instantiated hook call. const queryClient = RQ.useQueryClient() queryClient.getMutationDefaults(['todos']) queryClient.getQueriesData(['todos']) queryClient.getQueryData(['todos']) queryClient.getQueryDefaults(['todos']) queryClient.getQueryState(['todos']) queryClient.isFetching(['todos']) queryClient.setMutationDefaults(['todos'], { mutationFn: async () => null }) queryClient.setQueriesData(['todos'], () => null) queryClient.setQueryData(['todos'], () => null) queryClient.setQueryDefaults(['todos'], { queryFn: async () => null }) queryClient.cancelQueries(['todos']) queryClient.fetchInfiniteQuery(['todos']) queryClient.fetchQuery(['todos']) queryClient.invalidateQueries(['todos']) queryClient.prefetchInfiniteQuery(['todos']) queryClient.prefetchQuery(['todos']) queryClient.refetchQueries(['todos']) queryClient.removeQueries(['todos']) queryClient.resetQueries(['todos']) // --- Direct hook call. RQ.useQueryClient().getMutationDefaults(['todos']) RQ.useQueryClient().getQueriesData(['todos']) RQ.useQueryClient().getQueryData(['todos']) RQ.useQueryClient().getQueryDefaults(['todos']) RQ.useQueryClient().getQueryState(['todos']) RQ.useQueryClient().isFetching(['todos']) RQ.useQueryClient().setMutationDefaults(['todos'], { mutationFn: async () => null, }) RQ.useQueryClient().setQueriesData(['todos'], () => null) RQ.useQueryClient().setQueryData(['todos'], () => null) RQ.useQueryClient().setQueryDefaults(['todos'], { queryFn: async () => null, }) RQ.useQueryClient().cancelQueries(['todos']) RQ.useQueryClient().fetchInfiniteQuery(['todos']) RQ.useQueryClient().fetchQuery(['todos']) RQ.useQueryClient().invalidateQueries(['todos']) RQ.useQueryClient().prefetchInfiniteQuery(['todos']) RQ.useQueryClient().prefetchQuery(['todos']) RQ.useQueryClient().refetchQueries(['todos']) RQ.useQueryClient().removeQueries(['todos']) RQ.useQueryClient().resetQueries(['todos']) // QueryCache // --- NewExpression const queryCache1 = new RQ.QueryCache({ onError: (error) => console.log(error), onSuccess: (success) => console.log(success) }) queryCache1.find(['todos']) queryCache1.findAll(['todos']) // --- Instantiated hook call. const queryClient1 = RQ.useQueryClient() queryClient1.getQueryCache().find(['todos']) queryClient1.getQueryCache().findAll(['todos']) // const queryClient2 = new RQ.QueryClient({}) queryClient2.getQueryCache().find(['todos']) queryClient2.getQueryCache().findAll(['todos']) // const queryCache2 = queryClient1.getQueryCache() queryCache2.find(['todos']) queryCache2.findAll(['todos']) // --- Direct hook call. RQ.useQueryClient().getQueryCache().find(['todos']) RQ.useQueryClient().getQueryCache().findAll(['todos']) // const queryCache3 = RQ.useQueryClient().getQueryCache() queryCache3.find(['todos']) queryCache3.findAll(['todos']) return
Example Component
} ================================================ FILE: packages/query-codemods/src/v4/__testfixtures__/parameter-is-identifier.input.tsx ================================================ import * as React from 'react' import { useMutation, useQuery, useQueryClient } from 'react-query' export const ExampleWithStringLiteralKey = () => { const stringLiteralKey = 'todos' useQuery(stringLiteralKey) useMutation(stringLiteralKey) // QueryClient methods // --- Instantiated hook call. const queryClient = useQueryClient() queryClient.cancelQueries(stringLiteralKey) // --- Direct hook call. useQueryClient().cancelQueries(stringLiteralKey) return
Example Component
} export const ExampleWithTemplateLiteral = () => { const templateLiteralKey = `todos` useQuery(templateLiteralKey) useMutation(templateLiteralKey) } export const ExampleWithArrayExpressionKey = () => { const arrayExpressionKey = ['todos'] useQuery(arrayExpressionKey) useMutation(arrayExpressionKey) // QueryClient methods // --- Instantiated hook call. const queryClient = useQueryClient() queryClient.cancelQueries(queryKey2) // --- Direct hook call. useQueryClient().cancelQueries(queryKey2) return
Example Component
} export const ExampleWithUnknownKey = () => { useQuery(unknownQueryKey) useMutation(unknownQueryKey) // QueryClient methods // --- Instantiated hook call. const queryClient = useQueryClient() queryClient.cancelQueries(unknownQueryKey) // --- Direct hook call. useQueryClient().cancelQueries(unknownQueryKey) return
Example Component
} ================================================ FILE: packages/query-codemods/src/v4/__testfixtures__/parameter-is-identifier.output.tsx ================================================ import * as React from 'react' import { useMutation, useQuery, useQueryClient } from 'react-query' export const ExampleWithStringLiteralKey = () => { const stringLiteralKey = 'todos' useQuery([stringLiteralKey]) useMutation([stringLiteralKey]) // QueryClient methods // --- Instantiated hook call. const queryClient = useQueryClient() queryClient.cancelQueries([stringLiteralKey]) // --- Direct hook call. useQueryClient().cancelQueries([stringLiteralKey]) return
Example Component
} export const ExampleWithTemplateLiteral = () => { const templateLiteralKey = `todos` useQuery([templateLiteralKey]) useMutation([templateLiteralKey]) } export const ExampleWithArrayExpressionKey = () => { const arrayExpressionKey = ['todos'] useQuery(arrayExpressionKey) useMutation(arrayExpressionKey) // QueryClient methods // --- Instantiated hook call. const queryClient = useQueryClient() queryClient.cancelQueries(queryKey2) // --- Direct hook call. useQueryClient().cancelQueries(queryKey2) return
Example Component
} export const ExampleWithUnknownKey = () => { useQuery(unknownQueryKey) useMutation(unknownQueryKey) // QueryClient methods // --- Instantiated hook call. const queryClient = useQueryClient() queryClient.cancelQueries(unknownQueryKey) // --- Direct hook call. useQueryClient().cancelQueries(unknownQueryKey) return
Example Component
} ================================================ FILE: packages/query-codemods/src/v4/__testfixtures__/parameter-is-object-expression.input.tsx ================================================ import * as React from 'react' import { useMutation, useQuery, useQueryClient } from 'react-query' const options = {} export const ExampleWithStringLiteral = () => { useQuery({ queryKey: 'todos', exact: true }, options) useMutation({ mutationKey: 'todos', exact: true }, options) // QueryClient methods // --- Instantiated hook call. const queryClient = useQueryClient() queryClient.cancelQueries({ queryKey: 'todos', exact: true }, options) // --- Direct hook call. useQueryClient().cancelQueries({ queryKey: 'todos', exact: true }, options) return
Example Component
} export const ExampleWithStringLiteralIdentifier = () => { const stringLiteralKey = 'todos' useQuery({ queryKey: stringLiteralKey, exact: true }, options) useMutation({ mutationKey: stringLiteralKey, exact: true }, options) // QueryClient methods // --- Instantiated hook call. const queryClient = useQueryClient() queryClient.cancelQueries({ queryKey: stringLiteralKey, exact: true }, options) // --- Direct hook call. useQueryClient().cancelQueries({ queryKey: stringLiteralKey, exact: true }, options) return
Example Component
} export const ExampleWithTemplateLiteral = () => { useQuery({ queryKey: `todos`, exact: true }, options) useMutation({ mutationKey: `todos`, exact: true }, options) // QueryClient methods // --- Instantiated hook call. const queryClient = useQueryClient() queryClient.cancelQueries({ queryKey: `todos`, exact: true }, options) // --- Direct hook call. useQueryClient().cancelQueries({ queryKey: `todos`, exact: true }, options) return
Example Component
} export const ExampleWithTemplateLiteralIdentifier = () => { const templateLiteralKey = `todos` useQuery({ queryKey: templateLiteralKey, exact: true }, options) useMutation({ mutationKey: templateLiteralKey, exact: true }, options) // QueryClient methods // --- Instantiated hook call. const queryClient = useQueryClient() queryClient.cancelQueries({ queryKey: templateLiteralKey, exact: true }, options) // --- Direct hook call. useQueryClient().cancelQueries({ queryKey: templateLiteralKey, exact: true }, options) return
Example Component
} export const ExampleWithArrayExpression = () => { useQuery({ queryKey: ['todos'], exact: true }, options) useMutation({ mutationKey: ['todos'], exact: true }, options) // QueryClient methods // --- Instantiated hook call. const queryClient = useQueryClient() queryClient.cancelQueries({ queryKey: ['todos'], exact: true }, options) // --- Direct hook call. useQueryClient().cancelQueries({ queryKey: ['todos'], exact: true }, options) return
Example Component
} export const ExampleWithArrayExpressionIdentifier = () => { const arrayExpressionKey = ['todos'] useQuery({ queryKey: arrayExpressionKey, exact: true }, options) useMutation({ mutationKey: arrayExpressionKey, exact: true }, options) // QueryClient methods // --- Instantiated hook call. const queryClient = useQueryClient() queryClient.cancelQueries({ queryKey: arrayExpressionKey, exact: true }, options) // --- Direct hook call. useQueryClient().cancelQueries({ queryKey: arrayExpressionKey, exact: true }, options) return
Example Component
} export const ExampleWithUnknownIdentifier1 = () => { const createKey = (id) => ['todos', id] const createdKey1 = createKey(1) useQuery({ queryKey: createdKey1, exact: true }, options) useMutation({ mutationKey: createdKey1, exact: true }, options) // QueryClient methods // --- Instantiated hook call. const queryClient = useQueryClient() queryClient.cancelQueries({ queryKey: createdKey1, exact: true }, options) // --- Direct hook call. useQueryClient().cancelQueries({ queryKey: createdKey1, exact: true }, options) return
Example Component
} export const ExampleWithUnknownIdentifier2 = () => { const createdKey2 = createKey() useQuery({ queryKey: createdKey2, exact: true }, options) useMutation({ mutationKey: createdKey2, exact: true }, options) // QueryClient methods // --- Instantiated hook call. const queryClient = useQueryClient() queryClient.cancelQueries({ queryKey: createdKey2, exact: true }, options) // --- Direct hook call. useQueryClient().cancelQueries({ queryKey: createdKey2, exact: true }, options) return
Example Component
} export const ExampleWithUnknownIdentifier3 = () => { useQuery({ queryKey: unknownQueryKey, exact: true }, options) useMutation({ mutationKey: unknownQueryKey, exact: true }, options) // QueryClient methods // --- Instantiated hook call. const queryClient = useQueryClient() queryClient.cancelQueries({ queryKey: unknownQueryKey, exact: true }, options) // --- Direct hook call. useQueryClient().cancelQueries({ queryKey: unknownQueryKey, exact: true }, options) return
Example Component
} ================================================ FILE: packages/query-codemods/src/v4/__testfixtures__/parameter-is-object-expression.output.tsx ================================================ import * as React from 'react' import { useMutation, useQuery, useQueryClient } from 'react-query' const options = {} export const ExampleWithStringLiteral = () => { useQuery({ queryKey: ['todos'], exact: true }, options) useMutation({ mutationKey: ['todos'], exact: true }, options) // QueryClient methods // --- Instantiated hook call. const queryClient = useQueryClient() queryClient.cancelQueries({ queryKey: ['todos'], exact: true }, options) // --- Direct hook call. useQueryClient().cancelQueries({ queryKey: ['todos'], exact: true }, options) return
Example Component
} export const ExampleWithStringLiteralIdentifier = () => { const stringLiteralKey = 'todos' useQuery({ queryKey: [stringLiteralKey], exact: true }, options) useMutation({ mutationKey: [stringLiteralKey], exact: true }, options) // QueryClient methods // --- Instantiated hook call. const queryClient = useQueryClient() queryClient.cancelQueries({ queryKey: [stringLiteralKey], exact: true }, options) // --- Direct hook call. useQueryClient().cancelQueries({ queryKey: [stringLiteralKey], exact: true }, options) return
Example Component
} export const ExampleWithTemplateLiteral = () => { useQuery({ queryKey: [`todos`], exact: true }, options) useMutation({ mutationKey: [`todos`], exact: true }, options) // QueryClient methods // --- Instantiated hook call. const queryClient = useQueryClient() queryClient.cancelQueries({ queryKey: [`todos`], exact: true }, options) // --- Direct hook call. useQueryClient().cancelQueries({ queryKey: [`todos`], exact: true }, options) return
Example Component
} export const ExampleWithTemplateLiteralIdentifier = () => { const templateLiteralKey = `todos` useQuery({ queryKey: [templateLiteralKey], exact: true }, options) useMutation({ mutationKey: [templateLiteralKey], exact: true }, options) // QueryClient methods // --- Instantiated hook call. const queryClient = useQueryClient() queryClient.cancelQueries({ queryKey: [templateLiteralKey], exact: true }, options) // --- Direct hook call. useQueryClient().cancelQueries({ queryKey: [templateLiteralKey], exact: true }, options) return
Example Component
} export const ExampleWithArrayExpression = () => { useQuery({ queryKey: ['todos'], exact: true }, options) useMutation({ mutationKey: ['todos'], exact: true }, options) // QueryClient methods // --- Instantiated hook call. const queryClient = useQueryClient() queryClient.cancelQueries({ queryKey: ['todos'], exact: true }, options) // --- Direct hook call. useQueryClient().cancelQueries({ queryKey: ['todos'], exact: true }, options) return
Example Component
} export const ExampleWithArrayExpressionIdentifier = () => { const arrayExpressionKey = ['todos'] useQuery({ queryKey: arrayExpressionKey, exact: true }, options) useMutation({ mutationKey: arrayExpressionKey, exact: true }, options) // QueryClient methods // --- Instantiated hook call. const queryClient = useQueryClient() queryClient.cancelQueries({ queryKey: arrayExpressionKey, exact: true }, options) // --- Direct hook call. useQueryClient().cancelQueries({ queryKey: arrayExpressionKey, exact: true }, options) return
Example Component
} export const ExampleWithUnknownIdentifier1 = () => { const createKey = (id) => ['todos', id] const createdKey1 = createKey(1) useQuery({ queryKey: createdKey1, exact: true }, options) useMutation({ mutationKey: createdKey1, exact: true }, options) // QueryClient methods // --- Instantiated hook call. const queryClient = useQueryClient() queryClient.cancelQueries({ queryKey: createdKey1, exact: true }, options) // --- Direct hook call. useQueryClient().cancelQueries({ queryKey: createdKey1, exact: true }, options) return
Example Component
} export const ExampleWithUnknownIdentifier2 = () => { const createdKey2 = createKey() useQuery({ queryKey: createdKey2, exact: true }, options) useMutation({ mutationKey: createdKey2, exact: true }, options) // QueryClient methods // --- Instantiated hook call. const queryClient = useQueryClient() queryClient.cancelQueries({ queryKey: createdKey2, exact: true }, options) // --- Direct hook call. useQueryClient().cancelQueries({ queryKey: createdKey2, exact: true }, options) return
Example Component
} export const ExampleWithUnknownIdentifier3 = () => { useQuery({ queryKey: unknownQueryKey, exact: true }, options) useMutation({ mutationKey: unknownQueryKey, exact: true }, options) // QueryClient methods // --- Instantiated hook call. const queryClient = useQueryClient() queryClient.cancelQueries({ queryKey: unknownQueryKey, exact: true }, options) // --- Direct hook call. useQueryClient().cancelQueries({ queryKey: unknownQueryKey, exact: true }, options) return
Example Component
} ================================================ FILE: packages/query-codemods/src/v4/__testfixtures__/replace-import-specifier.input.tsx ================================================ // React Query import { useQuery, useQueryClient } from 'react-query' import { useQuery as RenamedUseQuery, useQueryClient as RenamedUseQueryClient } from 'react-query' import DefaultReactQuery from 'react-query' import * as NamespacedReactQuery from 'react-query' // Devtools import { ReactQueryDevtools } from 'react-query/devtools' import { ReactQueryDevtools as RenamedReactQueryDevtools } from 'react-query/devtools' import DefaultReactQueryDevtools from 'react-query/devtools' import * as NamespacedReactQueryDevtools from 'react-query/devtools' ================================================ FILE: packages/query-codemods/src/v4/__testfixtures__/replace-import-specifier.output.tsx ================================================ // React Query import { useQuery, useQueryClient } from '@tanstack/react-query' import { useQuery as RenamedUseQuery, useQueryClient as RenamedUseQueryClient } from '@tanstack/react-query' import DefaultReactQuery from '@tanstack/react-query' import * as NamespacedReactQuery from '@tanstack/react-query' // Devtools import { ReactQueryDevtools } from '@tanstack/react-query-devtools' import { ReactQueryDevtools as RenamedReactQueryDevtools } from '@tanstack/react-query-devtools' import DefaultReactQueryDevtools from '@tanstack/react-query-devtools' import * as NamespacedReactQueryDevtools from '@tanstack/react-query-devtools' ================================================ FILE: packages/query-codemods/src/v4/__testfixtures__/type-arguments.input.tsx ================================================ import * as React from 'react' import { useQueries, useQuery, useQueryClient } from 'react-query' type Todos = { items: ReadonlyArray<{ id: string text: string }> ts: number } export const Examples = () => { useQuery('todos') useQueries>([query1, query2]) // QueryClient methods // --- Instantiated hook call. const queryClient = useQueryClient() queryClient.getQueriesData('todos') queryClient.getQueriesData({ queryKey: 'todos' }) // --- Direct hook call. useQueryClient().getQueriesData('todos') useQueryClient().getQueriesData({ queryKey: 'todos' }) return
Example Component
} ================================================ FILE: packages/query-codemods/src/v4/__testfixtures__/type-arguments.output.tsx ================================================ import * as React from 'react' import { useQueries, useQuery, useQueryClient } from 'react-query' type Todos = { items: ReadonlyArray<{ id: string text: string }> ts: number } export const Examples = () => { useQuery(['todos']) useQueries>({ queries: [query1, query2] }) // QueryClient methods // --- Instantiated hook call. const queryClient = useQueryClient() queryClient.getQueriesData(['todos']) queryClient.getQueriesData({ queryKey: ['todos'] }) // --- Direct hook call. useQueryClient().getQueriesData(['todos']) useQueryClient().getQueriesData({ queryKey: ['todos'] }) return
Example Component
} ================================================ FILE: packages/query-codemods/src/v4/__tests__/key-transformation.test.cjs ================================================ const defineTest = require('jscodeshift/dist/testUtils').defineTest defineTest(__dirname, 'key-transformation.cjs', null, 'default-import', { parser: 'tsx', }) defineTest(__dirname, 'key-transformation.cjs', null, 'named-import', { parser: 'tsx', }) defineTest(__dirname, 'key-transformation.cjs', null, 'namespaced-import', { parser: 'tsx', }) defineTest( __dirname, 'key-transformation.cjs', null, 'parameter-is-identifier', { parser: 'tsx', }, ) defineTest( __dirname, 'key-transformation.cjs', null, 'parameter-is-object-expression', { parser: 'tsx', }, ) defineTest(__dirname, 'key-transformation.cjs', null, 'type-arguments', { parser: 'tsx', }) ================================================ FILE: packages/query-codemods/src/v4/__tests__/replace-import-specifier.test.cjs ================================================ const defineTest = require('jscodeshift/dist/testUtils').defineTest defineTest( __dirname, 'replace-import-specifier.cjs', null, 'replace-import-specifier', { parser: 'tsx', }, ) ================================================ FILE: packages/query-codemods/src/v4/utils/replacers/key-replacer.cjs ================================================ class UnprocessableKeyError extends Error { constructor(message) { super(message) this.name = 'UnprocessableKeyError' } } module.exports = ({ jscodeshift, root, filePath, keyName = 'queryKey' }) => { const isArrayExpression = (node) => jscodeshift.match(node, { type: jscodeshift.ArrayExpression.name }) const isStringLiteral = (node) => jscodeshift.match(node, { type: jscodeshift.StringLiteral.name }) || jscodeshift.match(node, { type: jscodeshift.Literal.name }) const isTemplateLiteral = (node) => jscodeshift.match(node, { type: jscodeshift.TemplateLiteral.name }) const findVariableDeclaration = (node) => { const declarations = root .find(jscodeshift.VariableDeclarator, { id: { type: jscodeshift.Identifier.name, name: node.name, }, }) .paths() return declarations.length > 0 ? declarations[0] : null } const createKeyValue = (node) => { // When the node is a string literal we convert it into an array of strings. if (isStringLiteral(node)) { return jscodeshift.arrayExpression([ jscodeshift.stringLiteral(node.value), ]) } // When the node is a template literal we convert it into an array of template literals. if (isTemplateLiteral(node)) { return jscodeshift.arrayExpression([ jscodeshift.templateLiteral(node.quasis, node.expressions), ]) } if (jscodeshift.match(node, { type: jscodeshift.Identifier.name })) { // When the node is an identifier at first, we try to find its declaration, because we will try // to guess its type. const variableDeclaration = findVariableDeclaration(node) if (!variableDeclaration) { throw new UnprocessableKeyError( `In file ${filePath} at line ${node.loc.start.line} the type of identifier \`${node.name}\` couldn't be recognized, so the codemod couldn't be applied. Please migrate manually.`, ) } const initializer = variableDeclaration.value.init // When it's a string, we just wrap it into an array expression. if (isStringLiteral(initializer) || isTemplateLiteral(initializer)) { return jscodeshift.arrayExpression([node]) } } throw new UnprocessableKeyError( `In file ${filePath} at line ${node.loc.start.line} the type of the \`${keyName}\` couldn't be recognized, so the codemod couldn't be applied. Please migrate manually.`, ) } const createKeyProperty = (node) => jscodeshift.property( 'init', jscodeshift.identifier(keyName), createKeyValue(node), ) const getPropertyFromObjectExpression = (objectExpression, propertyName) => objectExpression.properties.find( (property) => property.key.name === propertyName, ) ?? null const buildWithTypeArguments = (node, builder) => { const newNode = builder(node) if (node.typeParameters) { newNode.typeArguments = node.typeParameters } return newNode } return ({ node }) => { // When the node doesn't have the 'original' property, that means the codemod has been already applied, // so we don't need to do any changes. if (!node.original) { return node } const methodArguments = node.arguments // The method call doesn't have any arguments, we have nothing to do in this case. if (methodArguments.length === 0) { return node } try { const [firstArgument, ...restOfTheArguments] = methodArguments if ( jscodeshift.match(firstArgument, { type: jscodeshift.ObjectExpression.name, }) ) { const originalKey = getPropertyFromObjectExpression( firstArgument, keyName, ) if (!originalKey) { throw new UnprocessableKeyError( `In file ${filePath} at line ${node.loc.start.line} the \`${keyName}\` couldn't be found. Did you forget to add it?`, ) } const restOfTheProperties = firstArgument.properties.filter( (item) => item.key.name !== keyName, ) return buildWithTypeArguments(node, (originalNode) => jscodeshift.callExpression(originalNode.original.callee, [ jscodeshift.objectExpression([ createKeyProperty(originalKey.value), ...restOfTheProperties, ]), ...restOfTheArguments, ]), ) } // When the node is an array expression we just simply return it because we want query keys to be arrays. if (isArrayExpression(firstArgument)) { return node } return buildWithTypeArguments(node, (originalNode) => jscodeshift.callExpression(originalNode.original.callee, [ createKeyValue(firstArgument), ...restOfTheArguments, ]), ) } catch (error) { if (error.name === 'UnprocessableKeyError') { if (process.env.NODE_ENV !== 'test') { console.warn(error.message) } return node } throw error } } } ================================================ FILE: packages/query-codemods/src/v5/is-loading/is-loading.cjs ================================================ const createUtilsObject = require('../../utils/index.cjs') const createUseQueryLikeTransformer = require('../../utils/transformers/use-query-like-transformer.cjs') const createQueryClientTransformer = require('../../utils/transformers/query-client-transformer.cjs') const originalName = 'isLoading' const newName = 'isPending' /** * @param {import('jscodeshift')} jscodeshift * @param {Object} utils * @param {import('jscodeshift').Collection} root * @param {string} filePath * @param {{keyName: "mutationKey"|"queryKey", queryClientMethods: ReadonlyArray, hooks: ReadonlyArray}} config */ const transformUsages = ({ jscodeshift, utils, root, filePath, config }) => { /** * @param {import('jscodeshift').CallExpression | import('jscodeshift').ExpressionStatement} node * @returns {{start: number, end: number}} */ const getNodeLocation = (node) => { const location = utils.isCallExpression(node) ? node.callee.loc : node.loc const start = location.start.line const end = location.end.line return { start, end } } /** * @param {import('jscodeshift').ASTNode} node * @returns {boolean} */ const isObjectExpression = (node) => { return jscodeshift.match(node, { type: jscodeshift.ObjectExpression.name, }) } /** * @param {import('jscodeshift').ASTNode} node * @returns {boolean} */ const isObjectPattern = (node) => { return jscodeshift.match(node, { type: jscodeshift.ObjectPattern.name, }) } /** * @param {import('jscodeshift').ASTNode} node * @returns {boolean} */ const isVariableDeclarator = (node) => { return jscodeshift.match(node, { type: jscodeshift.VariableDeclarator.name, }) } /** * @param {import('jscodeshift').Node} node * @param {import('jscodeshift').Identifier} identifier * @returns {Collection} */ const findIsLoadingPropertiesOfIdentifier = (node, identifier) => { return jscodeshift(node).find(jscodeshift.MemberExpression, { object: { type: jscodeshift.Identifier.name, name: identifier.name, }, property: { type: jscodeshift.Identifier.name, name: originalName, }, }) } /** * @param {import('jscodeshift').ObjectPattern} node * @returns {import('jscodeshift').ObjectProperty|null} */ const findIsLoadingObjectPropertyInObjectPattern = (node) => { return ( node.properties.find((property) => jscodeshift.match(property, { key: { type: jscodeshift.Identifier.name, name: originalName, }, }), ) ?? null ) } /** * @param {import('jscodeshift').ObjectPattern} node * @returns {import('jscodeshift').RestElement|null} */ const findRestElementInObjectPattern = (node) => { return ( node.properties.find((property) => jscodeshift.match(property, { type: jscodeshift.RestElement.name, }), ) ?? null ) } const replacer = (path, transformNode) => { const node = path.node const parentNode = path.parentPath.value const { start, end } = getNodeLocation(node) try { if (!isVariableDeclarator(parentNode)) { // The parent node is not a variable declarator, the transformation will be skipped. return node } const lookupNode = path.scope.node const variableDeclaratorId = parentNode.id if (isObjectPattern(variableDeclaratorId)) { const isLoadingObjectProperty = findIsLoadingObjectPropertyInObjectPattern(variableDeclaratorId) if (isLoadingObjectProperty) { jscodeshift(lookupNode) .find(jscodeshift.ObjectProperty, { key: { type: jscodeshift.Identifier.name, name: originalName, }, }) .replaceWith((mutablePath) => { if (isObjectPattern(mutablePath.parent)) { const affectedProperty = mutablePath.value.value.shorthand ? 'value' : 'key' mutablePath.value[affectedProperty].name = newName return mutablePath.value } if (isObjectExpression(mutablePath.parent)) { const affectedProperty = mutablePath.value.value.shorthand ? 'key' : 'value' mutablePath.value[affectedProperty].name = newName return mutablePath.value } return mutablePath.value }) // Renaming all other 'isLoading' references that are object properties. jscodeshift(lookupNode) .find(jscodeshift.Identifier, { name: originalName }) .replaceWith((mutablePath) => { if ( !jscodeshift.match(mutablePath.parent, { type: jscodeshift.ObjectProperty.name, }) ) { mutablePath.value.name = newName } return mutablePath.value }) } const restElement = findRestElementInObjectPattern(variableDeclaratorId) if (restElement) { findIsLoadingPropertiesOfIdentifier( lookupNode, restElement.argument, ).replaceWith(({ node: mutableNode }) => { mutableNode.property.name = newName return mutableNode }) } return node } if (utils.isIdentifier(variableDeclaratorId)) { findIsLoadingPropertiesOfIdentifier( lookupNode, variableDeclaratorId, ).replaceWith(({ node: mutableNode }) => { mutableNode.property.name = newName return mutableNode }) return node } utils.warn( `The usage in file "${filePath}" at line ${start}:${end} could not be transformed. Please migrate this usage manually.`, ) return node } catch (error) { utils.warn( `An unknown error occurred while processing the "${filePath}" file. Please review this file, because the codemod couldn't be applied.`, ) return node } } createUseQueryLikeTransformer({ jscodeshift, utils, root }).execute( config.hooks, replacer, ) createQueryClientTransformer({ jscodeshift, utils, root }).execute( config.queryClientMethods, replacer, ) } module.exports = (file, api) => { const jscodeshift = api.jscodeshift const root = jscodeshift(file.source) const utils = createUtilsObject({ root, jscodeshift }) const filePath = file.path const dependencies = { jscodeshift, utils, root, filePath } transformUsages({ ...dependencies, config: { hooks: ['useQuery', 'useMutation'], queryClientMethods: [], }, }) return root.toSource({ quote: 'single', lineTerminator: '\n' }) } ================================================ FILE: packages/query-codemods/src/v5/is-loading/__testfixtures__/default-import.input.tsx ================================================ import * as React from 'react' import { useQuery } from '@tanstack/react-query' export const WithObjectPattern = () => { const { isError, isLoading, isSuccess, status } = useQuery({ queryKey: ['queryKey'], queryFn: () => ['data'], }) if (isError || isLoading || isSuccess) { console.log('Do something') } return
{JSON.stringify({ isError, isLoading, isSuccess, status })}
} export const WithObjectPatternAndRename = () => { const { isError, isLoading: isLoadingRenamed, isSuccess, status } = useQuery({ queryKey: ['queryKey'], queryFn: () => ['data'], }) if (isError || isLoadingRenamed || isSuccess) { console.log('Do something') } return
{JSON.stringify({ isError, isLoadingRenamed, isSuccess, status })}
} export const WithObjectPatternAndRestElement = () => { const { isError, ...rest } = useQuery({ queryKey: ['queryKey'], queryFn: () => ['data'], }) if (isError || rest.isLoading || rest.isSuccess) { console.log('Do something') } return
{JSON.stringify({ isError, isLoading: rest.isLoading, isSuccess: rest.isSuccess })}
} export const WithIdentifier = () => { const queryResult = useQuery({ queryKey: ['queryKey'], queryFn: () => ['data'], }) if (queryResult.isError || queryResult.isLoading || queryResult.isSuccess) { console.log('Do something') } return
{JSON.stringify(queryResult)}
} export const WithCombinations = () => { function useSomethingElse() { const { isError, isLoading, isSuccess, status } = useQuery({ queryKey: ['somethingElse'], queryFn: () => ['data'], }) return { isError, isLoading, isSuccess, status } } function useAnotherThing() { const { isLoading, ...rest } = useQuery({ queryKey: ['anotherThing'], queryFn: () => ['anotherData'], }) return { isLoading: rest.isLoading, ...rest } } const { isError, isLoading: isLoadingRenamed, isSuccess, status } = useQuery({ queryKey: ['queryKey'], queryFn: () => ['data'], }) return isLoadingRenamed } ================================================ FILE: packages/query-codemods/src/v5/is-loading/__testfixtures__/default-import.output.tsx ================================================ import * as React from 'react' import { useQuery } from '@tanstack/react-query' export const WithObjectPattern = () => { const { isError, isPending, isSuccess, status } = useQuery({ queryKey: ['queryKey'], queryFn: () => ['data'], }) if (isError || isPending || isSuccess) { console.log('Do something') } return
{JSON.stringify({ isError, isPending, isSuccess, status })}
; } export const WithObjectPatternAndRename = () => { const { isError, isPending: isLoadingRenamed, isSuccess, status } = useQuery({ queryKey: ['queryKey'], queryFn: () => ['data'], }) if (isError || isLoadingRenamed || isSuccess) { console.log('Do something') } return
{JSON.stringify({ isError, isLoadingRenamed, isSuccess, status })}
} export const WithObjectPatternAndRestElement = () => { const { isError, ...rest } = useQuery({ queryKey: ['queryKey'], queryFn: () => ['data'], }) if (isError || rest.isPending || rest.isSuccess) { console.log('Do something') } return
{JSON.stringify({ isError, isLoading: rest.isPending, isSuccess: rest.isSuccess })}
; } export const WithIdentifier = () => { const queryResult = useQuery({ queryKey: ['queryKey'], queryFn: () => ['data'], }) if (queryResult.isError || queryResult.isPending || queryResult.isSuccess) { console.log('Do something') } return
{JSON.stringify(queryResult)}
} export const WithCombinations = () => { function useSomethingElse() { const { isError, isPending, isSuccess, status } = useQuery({ queryKey: ['somethingElse'], queryFn: () => ['data'], }) return { isError, isPending, isSuccess, status }; } function useAnotherThing() { const { isPending, ...rest } = useQuery({ queryKey: ['anotherThing'], queryFn: () => ['anotherData'], }) return { isLoading: rest.isPending, ...rest }; } const { isError, isPending: isLoadingRenamed, isSuccess, status } = useQuery({ queryKey: ['queryKey'], queryFn: () => ['data'], }) return isLoadingRenamed } ================================================ FILE: packages/query-codemods/src/v5/is-loading/__testfixtures__/named-import.input.tsx ================================================ import * as React from 'react' import { useQuery as useRenamedUseQuery } from '@tanstack/react-query' export const WithObjectPattern = () => { const { isError, isLoading, isSuccess, status } = useRenamedUseQuery({ queryKey: ['queryKey'], queryFn: () => ['data'], }) if (isError || isLoading || isSuccess) { console.log('Do something') } return
{JSON.stringify({ isError, isLoading, isSuccess, status })}
} export const WithObjectPatternAndRename = () => { const { isError, isLoading: isLoadingRenamed, isSuccess, status } = useRenamedUseQuery({ queryKey: ['queryKey'], queryFn: () => ['data'], }) if (isError || isLoadingRenamed || isSuccess) { console.log('Do something') } return
{JSON.stringify({ isError, isLoadingRenamed, isSuccess, status })}
} export const WithObjectPatternAndRestElement = () => { const { isError, ...rest } = useRenamedUseQuery({ queryKey: ['queryKey'], queryFn: () => ['data'], }) if (isError || rest.isLoading || rest.isSuccess) { console.log('Do something') } return
{JSON.stringify({ isError, isLoading: rest.isLoading, isSuccess: rest.isSuccess })}
} export const WithIdentifier = () => { const queryResult = useRenamedUseQuery({ queryKey: ['queryKey'], queryFn: () => ['data'], }) if (queryResult.isError || queryResult.isLoading || queryResult.isSuccess) { console.log('Do something') } return
{JSON.stringify(queryResult)}
} export const WithCombinations = () => { function useSomethingElse() { const { isError, isLoading, isSuccess, status } = useRenamedUseQuery({ queryKey: ['somethingElse'], queryFn: () => ['data'], }) return { isError, isLoading, isSuccess, status } } function useAnotherThing() { const { isLoading, ...rest } = useRenamedUseQuery({ queryKey: ['anotherThing'], queryFn: () => ['anotherData'], }) return { isLoading: rest.isLoading, ...rest } } const { isError, isLoading: isLoadingRenamed, isSuccess, status } = useRenamedUseQuery({ queryKey: ['queryKey'], queryFn: () => ['data'], }) return isLoadingRenamed } ================================================ FILE: packages/query-codemods/src/v5/is-loading/__testfixtures__/named-import.output.tsx ================================================ import * as React from 'react' import { useQuery as useRenamedUseQuery } from '@tanstack/react-query' export const WithObjectPattern = () => { const { isError, isPending, isSuccess, status } = useRenamedUseQuery({ queryKey: ['queryKey'], queryFn: () => ['data'], }) if (isError || isPending || isSuccess) { console.log('Do something') } return
{JSON.stringify({ isError, isPending, isSuccess, status })}
; } export const WithObjectPatternAndRename = () => { const { isError, isPending: isLoadingRenamed, isSuccess, status } = useRenamedUseQuery({ queryKey: ['queryKey'], queryFn: () => ['data'], }) if (isError || isLoadingRenamed || isSuccess) { console.log('Do something') } return
{JSON.stringify({ isError, isLoadingRenamed, isSuccess, status })}
} export const WithObjectPatternAndRestElement = () => { const { isError, ...rest } = useRenamedUseQuery({ queryKey: ['queryKey'], queryFn: () => ['data'], }) if (isError || rest.isPending || rest.isSuccess) { console.log('Do something') } return
{JSON.stringify({ isError, isLoading: rest.isPending, isSuccess: rest.isSuccess })}
; } export const WithIdentifier = () => { const queryResult = useRenamedUseQuery({ queryKey: ['queryKey'], queryFn: () => ['data'], }) if (queryResult.isError || queryResult.isPending || queryResult.isSuccess) { console.log('Do something') } return
{JSON.stringify(queryResult)}
} export const WithCombinations = () => { function useSomethingElse() { const { isError, isPending, isSuccess, status } = useRenamedUseQuery({ queryKey: ['somethingElse'], queryFn: () => ['data'], }) return { isError, isPending, isSuccess, status }; } function useAnotherThing() { const { isPending, ...rest } = useRenamedUseQuery({ queryKey: ['anotherThing'], queryFn: () => ['anotherData'], }) return { isLoading: rest.isPending, ...rest }; } const { isError, isPending: isLoadingRenamed, isSuccess, status } = useRenamedUseQuery({ queryKey: ['queryKey'], queryFn: () => ['data'], }) return isLoadingRenamed } ================================================ FILE: packages/query-codemods/src/v5/is-loading/__tests__/is-loading.test.cjs ================================================ const defineTest = require('jscodeshift/dist/testUtils').defineTest defineTest(__dirname, 'is-loading.cjs', null, 'default-import', { parser: 'tsx', }) defineTest(__dirname, 'is-loading.cjs', null, 'named-import', { parser: 'tsx', }) ================================================ FILE: packages/query-codemods/src/v5/keep-previous-data/README.md ================================================ ### Intro The prerequisite for this code mod is to migrate your usages to the new syntax, so overloads for hooks and `QueryClient` methods shouldn't be available anymore. ### Affected usages Please note, this code mod transforms usages only where the first argument is an object expression. The following usage should be transformed by the code mod: ```ts const { data } = useQuery({ queryKey: ['posts'], queryFn: queryFn, keepPreviousData: true, }) ``` But the following usage won't be transformed by the code mod, because the first argument an identifier: ```ts const hookArgument = { queryKey: ['posts'], queryFn: queryFn, keepPreviousData: true, } const { data } = useQuery(hookArgument) ``` ### Troubleshooting In case of any errors, feel free to reach us out via Discord or open an issue. If you open an issue, please provide a code snippet as well, because without a snippet we cannot find the bug in the code mod. ================================================ FILE: packages/query-codemods/src/v5/keep-previous-data/keep-previous-data.cjs ================================================ const createUtilsObject = require('../../utils/index.cjs') const createUseQueryLikeTransformer = require('../../utils/transformers/use-query-like-transformer.cjs') const createQueryClientTransformer = require('../../utils/transformers/query-client-transformer.cjs') const AlreadyHasPlaceholderDataProperty = require('./utils/already-has-placeholder-data-property.cjs') /** * @param {import('jscodeshift')} jscodeshift * @param {Object} utils * @param {import('jscodeshift').Collection} root * @param {string} filePath * @param {{keyName: "mutationKey"|"queryKey", queryClientMethods: ReadonlyArray, hooks: ReadonlyArray}} config */ const transformUsages = ({ jscodeshift, utils, root, filePath, config }) => { /** * @param {import('jscodeshift').CallExpression | import('jscodeshift').ExpressionStatement} node * @returns {{start: number, end: number}} */ const getNodeLocation = (node) => { const location = utils.isCallExpression(node) ? node.callee.loc : node.loc const start = location.start.line const end = location.end.line return { start, end } } /** * @param {import('jscodeshift').ObjectProperty} objectProperty * @returns {boolean} */ const isKeepPreviousDataObjectProperty = (objectProperty) => { return jscodeshift.match(objectProperty.key, { type: jscodeshift.Identifier.name, name: 'keepPreviousData', }) } /** * @param {import('jscodeshift').ObjectProperty} objectProperty * @returns {boolean} */ const isObjectPropertyHasTrueBooleanLiteralValue = (objectProperty) => { return jscodeshift.match(objectProperty.value, { type: jscodeshift.BooleanLiteral.name, value: true, }) } /** * @param {import('jscodeshift').ObjectExpression} objectExpression * @returns {Array} */ const filterKeepPreviousDataProperty = (objectExpression) => { return objectExpression.properties.filter((objectProperty) => { return !isKeepPreviousDataObjectProperty(objectProperty) }) } const createPlaceholderDataObjectProperty = () => { return jscodeshift.objectProperty( jscodeshift.identifier('placeholderData'), jscodeshift.identifier('keepPreviousData'), ) } /** * @param {import('jscodeshift').ObjectExpression} objectExpression * @returns {boolean} */ const hasPlaceholderDataProperty = (objectExpression) => { return ( objectExpression.properties.findIndex((objectProperty) => { return jscodeshift.match(objectProperty.key, { type: jscodeshift.Identifier.name, name: 'placeholderData', }) }) !== -1 ) } /** * @param {import('jscodeshift').ObjectExpression} objectExpression * @returns {import('jscodeshift').ObjectProperty | undefined} */ const getKeepPreviousDataProperty = (objectExpression) => { return objectExpression.properties.find(isKeepPreviousDataObjectProperty) } let shouldAddKeepPreviousDataImport = false const replacer = (path, resolveTargetArgument, transformNode) => { const node = path.node const { start, end } = getNodeLocation(node) try { const targetArgument = resolveTargetArgument(node) if (targetArgument && utils.isObjectExpression(targetArgument)) { const isPlaceholderDataPropertyPresent = hasPlaceholderDataProperty(targetArgument) if (hasPlaceholderDataProperty(targetArgument)) { throw new AlreadyHasPlaceholderDataProperty(node, filePath) } const keepPreviousDataProperty = getKeepPreviousDataProperty(targetArgument) const keepPreviousDataPropertyHasTrueValue = isObjectPropertyHasTrueBooleanLiteralValue(keepPreviousDataProperty) if (!keepPreviousDataPropertyHasTrueValue) { utils.warn( `The usage in file "${filePath}" at line ${start}:${end} already contains a "keepPreviousData" property but its value is not "true". Please migrate this usage manually.`, ) return node } if (keepPreviousDataPropertyHasTrueValue) { // Removing the `keepPreviousData` property from the object. const mutableObjectExpressionProperties = filterKeepPreviousDataProperty(targetArgument) if (!isPlaceholderDataPropertyPresent) { shouldAddKeepPreviousDataImport = true // When the `placeholderData` property is not present, the `placeholderData: keepPreviousData` property will be added. mutableObjectExpressionProperties.push( createPlaceholderDataObjectProperty(), ) } return transformNode( node, jscodeshift.objectExpression(mutableObjectExpressionProperties), ) } } utils.warn( `The usage in file "${filePath}" at line ${start}:${end} could not be transformed, because the first parameter is not an object expression. Please migrate this usage manually.`, ) return node } catch (error) { utils.warn( error.name === AlreadyHasPlaceholderDataProperty.name ? error.message : `An unknown error occurred while processing the "${filePath}" file. Please review this file, because the codemod couldn't be applied.`, ) return node } } createUseQueryLikeTransformer({ jscodeshift, utils, root }).execute( config.hooks, (path) => { const resolveTargetArgument = (node) => node.arguments[0] ?? null const transformNode = (node, transformedArgument) => jscodeshift.callExpression(node.original.callee, [transformedArgument]) return replacer(path, resolveTargetArgument, transformNode) }, ) createQueryClientTransformer({ jscodeshift, utils, root }).execute( config.queryClientMethods, (path) => { const resolveTargetArgument = (node) => node.arguments[1] ?? null const transformNode = (node, transformedArgument) => { return jscodeshift.callExpression(node.original.callee, [ node.arguments[0], transformedArgument, ...node.arguments.slice(2, 0), ]) } return replacer(path, resolveTargetArgument, transformNode) }, ) const importIdentifierOfQueryClient = utils.getSelectorByImports( utils.locateImports(['QueryClient']), 'QueryClient', ) root .find(jscodeshift.ExpressionStatement, { expression: { type: jscodeshift.NewExpression.name, callee: { type: jscodeshift.Identifier.name, name: importIdentifierOfQueryClient, }, }, }) .filter((path) => path.node.expression) .replaceWith((path) => { const resolveTargetArgument = (node) => { const paths = jscodeshift(node) .find(jscodeshift.ObjectProperty, { key: { type: jscodeshift.Identifier.name, name: 'keepPreviousData', }, }) .paths() return paths.length > 0 ? paths[0].parent.node : null } const transformNode = (node, transformedArgument) => { jscodeshift(node.expression) .find(jscodeshift.ObjectProperty, { key: { type: jscodeshift.Identifier.name, name: 'queries', }, }) .replaceWith(({ node: mutableNode }) => { mutableNode.value.properties = transformedArgument.properties return mutableNode }) return node } return replacer(path, resolveTargetArgument, transformNode) }) return { shouldAddKeepPreviousDataImport } } module.exports = (file, api) => { const jscodeshift = api.jscodeshift const root = jscodeshift(file.source) const utils = createUtilsObject({ root, jscodeshift }) const filePath = file.path const dependencies = { jscodeshift, utils, root, filePath } const { shouldAddKeepPreviousDataImport } = transformUsages({ ...dependencies, config: { hooks: ['useInfiniteQuery', 'useQueries', 'useQuery'], queryClientMethods: ['setQueryDefaults'], }, }) if (shouldAddKeepPreviousDataImport) { root .find(jscodeshift.ImportDeclaration, { source: { value: '@tanstack/react-query', }, }) .replaceWith(({ node: mutableNode }) => { mutableNode.specifiers = [ jscodeshift.importSpecifier( jscodeshift.identifier('keepPreviousData'), ), ...mutableNode.specifiers, ] return mutableNode }) } return root.toSource({ quote: 'single', lineTerminator: '\n' }) } ================================================ FILE: packages/query-codemods/src/v5/keep-previous-data/__testfixtures__/default.input.tsx ================================================ import * as React from 'react' import axios from 'axios' import { QueryClient, useQueries, useQuery, useQueryClient } from '@tanstack/react-query' type Post = { id: number title: string body: string } const queryFn = async (): Promise> => { const { data } = await axios.get( 'https://jsonplaceholder.typicode.com/posts', ) return data } /** * The object expression has a `keepPreviousData` property with value `true`, so the codemod should transform * this usage. */ export const Example1 = () => { const { data } = useQuery({ queryKey: ['posts'], queryFn: queryFn, keepPreviousData: true, }) return
{JSON.stringify(data)}
} /** * The object expression has a `keepPreviousData` property with value `true`, but the `placeholderData` is a function. * The codemod shouldn't transform this case, only warn the user about the manual migration. */ export const Example2 = () => { const { data } = useQueries({ queryKey: ['posts'], queryFn: queryFn, keepPreviousData: true, placeholderData: () => previousData }) return
{JSON.stringify(data)}
} /** * The object expression has a `keepPreviousData` property with value `true`, but the `placeholderData` is a string. * The codemod shouldn't transform this case, only warn the user about the manual migration. */ export const Example3 = () => { const { data } = useQueries({ queryKey: ['posts'], queryFn: queryFn, keepPreviousData: true, placeholderData: 'somePlaceholderData' }) return
{JSON.stringify(data)}
} /** * The object expression has a `keepPreviousData` property with value `false`. * The codemod shouldn't transform this case, only warn the user about the manual migration. */ export const Example4 = () => { const { data } = useQueries({ queryKey: ['posts'], queryFn: queryFn, keepPreviousData: false, }) return
{JSON.stringify(data)}
} /** * The object expression has a `keepPreviousData` property with which is an identifier. * The codemod shouldn't transform this case, only warn the user about the manual migration. */ export const Example5 = () => { const keepPreviousDataIdentifier = false const { data } = useQueries({ queryKey: ['posts'], queryFn: queryFn, keepPreviousData: keepPreviousDataIdentifier, placeholderData: () => previousData }) return
{JSON.stringify(data)}
} /** * The object expression has a `keepPreviousData` property with value `true`, so the codemod should transform * this usage. */ export const Example6 = () => { const queryClient = new QueryClient() queryClient.setQueryDefaults(['key'], { keepPreviousData: true }) useQueryClient().setQueryDefaults(['key'], { keepPreviousData: true }) const anotherQueryClient = useQueryClient() anotherQueryClient.setQueryDefaults(['key'], { keepPreviousData: true }) } /** * The object expression has a `keepPreviousData` property with value `true`, but the `placeholderData` is a function. * The codemod shouldn't transform this case, only warn the user about the manual migration. */ export const Example7 = () => { const queryClient = new QueryClient() queryClient.setQueryDefaults(['key'], { keepPreviousData: true, placeholderData: () => previousData }) useQueryClient().setQueryDefaults(['key'], { keepPreviousData: true, placeholderData: () => previousData }) const anotherQueryClient = useQueryClient() anotherQueryClient.setQueryDefaults(['key'], { keepPreviousData: true, placeholderData: () => previousData }) } /** * The object expression has a `keepPreviousData` property with value `true`, but the `placeholderData` is a string. * The codemod shouldn't transform this case, only warn the user about the manual migration. */ export const Example8 = () => { const queryClient = new QueryClient() queryClient.setQueryDefaults(['key'], { keepPreviousData: true, placeholderData: 'somePlaceholderData' }) useQueryClient().setQueryDefaults(['key'], { keepPreviousData: true, placeholderData: 'somePlaceholderData' }) const anotherQueryClient = useQueryClient() anotherQueryClient.setQueryDefaults(['key'], { keepPreviousData: true, placeholderData: 'somePlaceholderData' }) } /** * The object expression has a `keepPreviousData` property with value `false`. * The codemod shouldn't transform this case, only warn the user about the manual migration. */ export const Example9 = () => { const queryClient = new QueryClient() queryClient.setQueryDefaults(['key'], { keepPreviousData: false, }) useQueryClient().setQueryDefaults(['key'], { keepPreviousData: false, }) const anotherQueryClient = useQueryClient() anotherQueryClient.setQueryDefaults(['key'], { keepPreviousData: false, }) } /** * The object expression has a `keepPreviousData` property with which is an identifier. * The codemod shouldn't transform this case, only warn the user about the manual migration. */ export const Example10 = () => { const queryClient = new QueryClient() const keepPreviousDataIdentifier = false queryClient.setQueryDefaults(['key'], { keepPreviousData: keepPreviousDataIdentifier, placeholderData: () => previousData }) useQueryClient().setQueryDefaults(['key'], { keepPreviousData: keepPreviousDataIdentifier, placeholderData: () => previousData }) const anotherQueryClient = useQueryClient() anotherQueryClient.setQueryDefaults(['key'], { keepPreviousData: keepPreviousDataIdentifier, placeholderData: () => previousData }) } /** * The object expression has a `keepPreviousData` property with value `true`, so the codemod should transform * this usage. */ export const Example11 = () => { new QueryClient({ defaultOptions: { queries: { keepPreviousData: true } } }) } /** * The object expression has a `keepPreviousData` property with value `true`, but the `placeholderData` is a function. * The codemod shouldn't transform this case, only warn the user about the manual migration. */ export const Example12 = () => { new QueryClient({ defaultOptions: { queries: { keepPreviousData: true, placeholderData: () => previousData } } }) } /** * The object expression has a `keepPreviousData` property with value `true`, but the `placeholderData` is a string. * The codemod shouldn't transform this case, only warn the user about the manual migration. */ export const Example13 = () => { new QueryClient({ defaultOptions: { queries: { keepPreviousData: true, placeholderData: 'somePlaceholderData' } } }) } /** * The object expression has a `keepPreviousData` property with value `false`. * The codemod shouldn't transform this case, only warn the user about the manual migration. */ export const Example14 = () => { new QueryClient({ defaultOptions: { queries: { keepPreviousData: false, } } }) } /** * The object expression has a `keepPreviousData` property with which is an identifier. * The codemod shouldn't transform this case, only warn the user about the manual migration. */ export const Example15 = () => { const keepPreviousDataIdentifier = false new QueryClient({ defaultOptions: { queries: { keepPreviousData: keepPreviousDataIdentifier, placeholderData: () => previousData } } }) } ================================================ FILE: packages/query-codemods/src/v5/keep-previous-data/__testfixtures__/default.output.tsx ================================================ import * as React from 'react' import axios from 'axios' import { keepPreviousData, QueryClient, useQueries, useQuery, useQueryClient } from '@tanstack/react-query'; type Post = { id: number title: string body: string } const queryFn = async (): Promise> => { const { data } = await axios.get( 'https://jsonplaceholder.typicode.com/posts', ) return data } /** * The object expression has a `keepPreviousData` property with value `true`, so the codemod should transform * this usage. */ export const Example1 = () => { const { data } = useQuery({ queryKey: ['posts'], queryFn: queryFn, placeholderData: keepPreviousData }) return
{JSON.stringify(data)}
} /** * The object expression has a `keepPreviousData` property with value `true`, but the `placeholderData` is a function. * The codemod shouldn't transform this case, only warn the user about the manual migration. */ export const Example2 = () => { const { data } = useQueries({ queryKey: ['posts'], queryFn: queryFn, keepPreviousData: true, placeholderData: () => previousData }) return
{JSON.stringify(data)}
} /** * The object expression has a `keepPreviousData` property with value `true`, but the `placeholderData` is a string. * The codemod shouldn't transform this case, only warn the user about the manual migration. */ export const Example3 = () => { const { data } = useQueries({ queryKey: ['posts'], queryFn: queryFn, keepPreviousData: true, placeholderData: 'somePlaceholderData' }) return
{JSON.stringify(data)}
} /** * The object expression has a `keepPreviousData` property with value `false`. * The codemod shouldn't transform this case, only warn the user about the manual migration. */ export const Example4 = () => { const { data } = useQueries({ queryKey: ['posts'], queryFn: queryFn, keepPreviousData: false, }) return
{JSON.stringify(data)}
} /** * The object expression has a `keepPreviousData` property with which is an identifier. * The codemod shouldn't transform this case, only warn the user about the manual migration. */ export const Example5 = () => { const keepPreviousDataIdentifier = false const { data } = useQueries({ queryKey: ['posts'], queryFn: queryFn, keepPreviousData: keepPreviousDataIdentifier, placeholderData: () => previousData }) return
{JSON.stringify(data)}
} /** * The object expression has a `keepPreviousData` property with value `true`, so the codemod should transform * this usage. */ export const Example6 = () => { const queryClient = new QueryClient() queryClient.setQueryDefaults(['key'], { placeholderData: keepPreviousData }) useQueryClient().setQueryDefaults(['key'], { placeholderData: keepPreviousData }) const anotherQueryClient = useQueryClient() anotherQueryClient.setQueryDefaults(['key'], { placeholderData: keepPreviousData }) } /** * The object expression has a `keepPreviousData` property with value `true`, but the `placeholderData` is a function. * The codemod shouldn't transform this case, only warn the user about the manual migration. */ export const Example7 = () => { const queryClient = new QueryClient() queryClient.setQueryDefaults(['key'], { keepPreviousData: true, placeholderData: () => previousData }) useQueryClient().setQueryDefaults(['key'], { keepPreviousData: true, placeholderData: () => previousData }) const anotherQueryClient = useQueryClient() anotherQueryClient.setQueryDefaults(['key'], { keepPreviousData: true, placeholderData: () => previousData }) } /** * The object expression has a `keepPreviousData` property with value `true`, but the `placeholderData` is a string. * The codemod shouldn't transform this case, only warn the user about the manual migration. */ export const Example8 = () => { const queryClient = new QueryClient() queryClient.setQueryDefaults(['key'], { keepPreviousData: true, placeholderData: 'somePlaceholderData' }) useQueryClient().setQueryDefaults(['key'], { keepPreviousData: true, placeholderData: 'somePlaceholderData' }) const anotherQueryClient = useQueryClient() anotherQueryClient.setQueryDefaults(['key'], { keepPreviousData: true, placeholderData: 'somePlaceholderData' }) } /** * The object expression has a `keepPreviousData` property with value `false`. * The codemod shouldn't transform this case, only warn the user about the manual migration. */ export const Example9 = () => { const queryClient = new QueryClient() queryClient.setQueryDefaults(['key'], { keepPreviousData: false, }) useQueryClient().setQueryDefaults(['key'], { keepPreviousData: false, }) const anotherQueryClient = useQueryClient() anotherQueryClient.setQueryDefaults(['key'], { keepPreviousData: false, }) } /** * The object expression has a `keepPreviousData` property with which is an identifier. * The codemod shouldn't transform this case, only warn the user about the manual migration. */ export const Example10 = () => { const queryClient = new QueryClient() const keepPreviousDataIdentifier = false queryClient.setQueryDefaults(['key'], { keepPreviousData: keepPreviousDataIdentifier, placeholderData: () => previousData }) useQueryClient().setQueryDefaults(['key'], { keepPreviousData: keepPreviousDataIdentifier, placeholderData: () => previousData }) const anotherQueryClient = useQueryClient() anotherQueryClient.setQueryDefaults(['key'], { keepPreviousData: keepPreviousDataIdentifier, placeholderData: () => previousData }) } /** * The object expression has a `keepPreviousData` property with value `true`, so the codemod should transform * this usage. */ export const Example11 = () => { new QueryClient({ defaultOptions: { queries: { placeholderData: keepPreviousData } } }) } /** * The object expression has a `keepPreviousData` property with value `true`, but the `placeholderData` is a function. * The codemod shouldn't transform this case, only warn the user about the manual migration. */ export const Example12 = () => { new QueryClient({ defaultOptions: { queries: { keepPreviousData: true, placeholderData: () => previousData } } }) } /** * The object expression has a `keepPreviousData` property with value `true`, but the `placeholderData` is a string. * The codemod shouldn't transform this case, only warn the user about the manual migration. */ export const Example13 = () => { new QueryClient({ defaultOptions: { queries: { keepPreviousData: true, placeholderData: 'somePlaceholderData' } } }) } /** * The object expression has a `keepPreviousData` property with value `false`. * The codemod shouldn't transform this case, only warn the user about the manual migration. */ export const Example14 = () => { new QueryClient({ defaultOptions: { queries: { keepPreviousData: false, } } }) } /** * The object expression has a `keepPreviousData` property with which is an identifier. * The codemod shouldn't transform this case, only warn the user about the manual migration. */ export const Example15 = () => { const keepPreviousDataIdentifier = false new QueryClient({ defaultOptions: { queries: { keepPreviousData: keepPreviousDataIdentifier, placeholderData: () => previousData } } }) } ================================================ FILE: packages/query-codemods/src/v5/keep-previous-data/__testfixtures__/named.input.tsx ================================================ import * as React from 'react' import axios from 'axios' import { QueryClient as RenamedQueryClient, useQueries as useRenamedUseQueries, useQuery as useRenamedUseQuery, useQueryClient as useRenamedUseQueryClient } from '@tanstack/react-query' type Post = { id: number title: string body: string } const queryFn = async (): Promise> => { const { data } = await axios.get( 'https://jsonplaceholder.typicode.com/posts', ) return data } /** * The object expression has a `keepPreviousData` property with value `true`, so the codemod should transform * this usage. */ export const Example1 = () => { const { data } = useRenamedUseQuery({ queryKey: ['posts'], queryFn: queryFn, keepPreviousData: true, }) return
{JSON.stringify(data)}
} /** * The object expression has a `keepPreviousData` property with value `true`, but the `placeholderData` is a function. * The codemod shouldn't transform this case, only warn the user about the manual migration. */ export const Example2 = () => { const { data } = useRenamedUseQuery({ queryKey: ['posts'], queryFn: queryFn, keepPreviousData: true, placeholderData: () => previousData }) return
{JSON.stringify(data)}
} /** * The object expression has a `keepPreviousData` property with value `true`, but the `placeholderData` is a string. * The codemod shouldn't transform this case, only warn the user about the manual migration. */ export const Example3 = () => { const { data } = useRenamedUseQueries({ queryKey: ['posts'], queryFn: queryFn, keepPreviousData: true, placeholderData: 'somePlaceholderData' }) return
{JSON.stringify(data)}
} /** * The object expression has a `keepPreviousData` property with value `false`. * The codemod shouldn't transform this case, only warn the user about the manual migration. */ export const Example4 = () => { const { data } = useRenamedUseQueries({ queryKey: ['posts'], queryFn: queryFn, keepPreviousData: false, }) return
{JSON.stringify(data)}
} /** * The object expression has a `keepPreviousData` property with which is an identifier. * The codemod shouldn't transform this case, only warn the user about the manual migration. */ export const Example5 = () => { const keepPreviousDataIdentifier = false const { data } = useRenamedUseQueries({ queryKey: ['posts'], queryFn: queryFn, keepPreviousData: keepPreviousDataIdentifier, placeholderData: () => previousData }) return
{JSON.stringify(data)}
} /** * The object expression has a `keepPreviousData` property with value `true`, so the codemod should transform * this usage. */ export const Example6 = () => { const queryClient = new RenamedQueryClient() queryClient.setQueryDefaults(['key'], { keepPreviousData: true }) useRenamedUseQueryClient().setQueryDefaults(['key'], { keepPreviousData: true }) const anotherQueryClient = useRenamedUseQueryClient() anotherQueryClient.setQueryDefaults(['key'], { keepPreviousData: true }) } /** * The object expression has a `keepPreviousData` property with value `true`, but the `placeholderData` is a function. * The codemod shouldn't transform this case, only warn the user about the manual migration. */ export const Example7 = () => { const queryClient = new RenamedQueryClient() queryClient.setQueryDefaults(['key'], { keepPreviousData: true, placeholderData: () => previousData }) useRenamedUseQueryClient().setQueryDefaults(['key'], { keepPreviousData: true, placeholderData: () => previousData }) const anotherQueryClient = useRenamedUseQueryClient() anotherQueryClient.setQueryDefaults(['key'], { keepPreviousData: true, placeholderData: () => previousData }) } /** * The object expression has a `keepPreviousData` property with value `true`, but the `placeholderData` is a string. * The codemod shouldn't transform this case, only warn the user about the manual migration. */ export const Example8 = () => { const queryClient = new RenamedQueryClient() queryClient.setQueryDefaults(['key'], { keepPreviousData: true, placeholderData: 'somePlaceholderData' }) useRenamedUseQueryClient().setQueryDefaults(['key'], { keepPreviousData: true, placeholderData: 'somePlaceholderData' }) const anotherQueryClient = useRenamedUseQueryClient() anotherQueryClient.setQueryDefaults(['key'], { keepPreviousData: true, placeholderData: 'somePlaceholderData' }) } /** * The object expression has a `keepPreviousData` property with value `false`. * The codemod shouldn't transform this case, only warn the user about the manual migration. */ export const Example9 = () => { const queryClient = new RenamedQueryClient() queryClient.setQueryDefaults(['key'], { keepPreviousData: false, }) useRenamedUseQueryClient().setQueryDefaults(['key'], { keepPreviousData: false, }) const anotherQueryClient = useRenamedUseQueryClient() anotherQueryClient.setQueryDefaults(['key'], { keepPreviousData: false, }) } /** * The object expression has a `keepPreviousData` property with which is an identifier. * The codemod shouldn't transform this case, only warn the user about the manual migration. */ export const Example10 = () => { const queryClient = new RenamedQueryClient() const keepPreviousDataIdentifier = false queryClient.setQueryDefaults(['key'], { keepPreviousData: keepPreviousDataIdentifier, placeholderData: () => previousData }) useRenamedUseQueryClient().setQueryDefaults(['key'], { keepPreviousData: keepPreviousDataIdentifier, placeholderData: () => previousData }) const anotherQueryClient = useRenamedUseQueryClient() anotherQueryClient.setQueryDefaults(['key'], { keepPreviousData: keepPreviousDataIdentifier, placeholderData: () => previousData }) } /** * The object expression has a `keepPreviousData` property with value `true`, so the codemod should transform * this usage. */ export const Example11 = () => { new RenamedQueryClient({ defaultOptions: { queries: { keepPreviousData: true } } }) } /** * The object expression has a `keepPreviousData` property with value `true`, but the `placeholderData` is a function. * The codemod shouldn't transform this case, only warn the user about the manual migration. */ export const Example12 = () => { new RenamedQueryClient({ defaultOptions: { queries: { keepPreviousData: true, placeholderData: () => previousData } } }) } /** * The object expression has a `keepPreviousData` property with value `true`, but the `placeholderData` is a string. * The codemod shouldn't transform this case, only warn the user about the manual migration. */ export const Example13 = () => { new RenamedQueryClient({ defaultOptions: { queries: { keepPreviousData: true, placeholderData: 'somePlaceholderData' } } }) } /** * The object expression has a `keepPreviousData` property with value `false`. * The codemod shouldn't transform this case, only warn the user about the manual migration. */ export const Example14 = () => { new RenamedQueryClient({ defaultOptions: { queries: { keepPreviousData: false, } } }) } /** * The object expression has a `keepPreviousData` property with which is an identifier. * The codemod shouldn't transform this case, only warn the user about the manual migration. */ export const Example15 = () => { const keepPreviousDataIdentifier = false new RenamedQueryClient({ defaultOptions: { queries: { keepPreviousData: keepPreviousDataIdentifier, placeholderData: () => previousData } } }) } ================================================ FILE: packages/query-codemods/src/v5/keep-previous-data/__testfixtures__/named.output.tsx ================================================ import * as React from 'react' import axios from 'axios' import { keepPreviousData, QueryClient as RenamedQueryClient, useQueries as useRenamedUseQueries, useQuery as useRenamedUseQuery, useQueryClient as useRenamedUseQueryClient, } from '@tanstack/react-query'; type Post = { id: number title: string body: string } const queryFn = async (): Promise> => { const { data } = await axios.get( 'https://jsonplaceholder.typicode.com/posts', ) return data } /** * The object expression has a `keepPreviousData` property with value `true`, so the codemod should transform * this usage. */ export const Example1 = () => { const { data } = useRenamedUseQuery({ queryKey: ['posts'], queryFn: queryFn, placeholderData: keepPreviousData }) return
{JSON.stringify(data)}
} /** * The object expression has a `keepPreviousData` property with value `true`, but the `placeholderData` is a function. * The codemod shouldn't transform this case, only warn the user about the manual migration. */ export const Example2 = () => { const { data } = useRenamedUseQuery({ queryKey: ['posts'], queryFn: queryFn, keepPreviousData: true, placeholderData: () => previousData }) return
{JSON.stringify(data)}
} /** * The object expression has a `keepPreviousData` property with value `true`, but the `placeholderData` is a string. * The codemod shouldn't transform this case, only warn the user about the manual migration. */ export const Example3 = () => { const { data } = useRenamedUseQueries({ queryKey: ['posts'], queryFn: queryFn, keepPreviousData: true, placeholderData: 'somePlaceholderData' }) return
{JSON.stringify(data)}
} /** * The object expression has a `keepPreviousData` property with value `false`. * The codemod shouldn't transform this case, only warn the user about the manual migration. */ export const Example4 = () => { const { data } = useRenamedUseQueries({ queryKey: ['posts'], queryFn: queryFn, keepPreviousData: false, }) return
{JSON.stringify(data)}
} /** * The object expression has a `keepPreviousData` property with which is an identifier. * The codemod shouldn't transform this case, only warn the user about the manual migration. */ export const Example5 = () => { const keepPreviousDataIdentifier = false const { data } = useRenamedUseQueries({ queryKey: ['posts'], queryFn: queryFn, keepPreviousData: keepPreviousDataIdentifier, placeholderData: () => previousData }) return
{JSON.stringify(data)}
} /** * The object expression has a `keepPreviousData` property with value `true`, so the codemod should transform * this usage. */ export const Example6 = () => { const queryClient = new RenamedQueryClient() queryClient.setQueryDefaults(['key'], { placeholderData: keepPreviousData }) useRenamedUseQueryClient().setQueryDefaults(['key'], { placeholderData: keepPreviousData }) const anotherQueryClient = useRenamedUseQueryClient() anotherQueryClient.setQueryDefaults(['key'], { placeholderData: keepPreviousData }) } /** * The object expression has a `keepPreviousData` property with value `true`, but the `placeholderData` is a function. * The codemod shouldn't transform this case, only warn the user about the manual migration. */ export const Example7 = () => { const queryClient = new RenamedQueryClient() queryClient.setQueryDefaults(['key'], { keepPreviousData: true, placeholderData: () => previousData }) useRenamedUseQueryClient().setQueryDefaults(['key'], { keepPreviousData: true, placeholderData: () => previousData }) const anotherQueryClient = useRenamedUseQueryClient() anotherQueryClient.setQueryDefaults(['key'], { keepPreviousData: true, placeholderData: () => previousData }) } /** * The object expression has a `keepPreviousData` property with value `true`, but the `placeholderData` is a string. * The codemod shouldn't transform this case, only warn the user about the manual migration. */ export const Example8 = () => { const queryClient = new RenamedQueryClient() queryClient.setQueryDefaults(['key'], { keepPreviousData: true, placeholderData: 'somePlaceholderData' }) useRenamedUseQueryClient().setQueryDefaults(['key'], { keepPreviousData: true, placeholderData: 'somePlaceholderData' }) const anotherQueryClient = useRenamedUseQueryClient() anotherQueryClient.setQueryDefaults(['key'], { keepPreviousData: true, placeholderData: 'somePlaceholderData' }) } /** * The object expression has a `keepPreviousData` property with value `false`. * The codemod shouldn't transform this case, only warn the user about the manual migration. */ export const Example9 = () => { const queryClient = new RenamedQueryClient() queryClient.setQueryDefaults(['key'], { keepPreviousData: false, }) useRenamedUseQueryClient().setQueryDefaults(['key'], { keepPreviousData: false, }) const anotherQueryClient = useRenamedUseQueryClient() anotherQueryClient.setQueryDefaults(['key'], { keepPreviousData: false, }) } /** * The object expression has a `keepPreviousData` property with which is an identifier. * The codemod shouldn't transform this case, only warn the user about the manual migration. */ export const Example10 = () => { const queryClient = new RenamedQueryClient() const keepPreviousDataIdentifier = false queryClient.setQueryDefaults(['key'], { keepPreviousData: keepPreviousDataIdentifier, placeholderData: () => previousData }) useRenamedUseQueryClient().setQueryDefaults(['key'], { keepPreviousData: keepPreviousDataIdentifier, placeholderData: () => previousData }) const anotherQueryClient = useRenamedUseQueryClient() anotherQueryClient.setQueryDefaults(['key'], { keepPreviousData: keepPreviousDataIdentifier, placeholderData: () => previousData }) } /** * The object expression has a `keepPreviousData` property with value `true`, so the codemod should transform * this usage. */ export const Example11 = () => { new RenamedQueryClient({ defaultOptions: { queries: { placeholderData: keepPreviousData } } }) } /** * The object expression has a `keepPreviousData` property with value `true`, but the `placeholderData` is a function. * The codemod shouldn't transform this case, only warn the user about the manual migration. */ export const Example12 = () => { new RenamedQueryClient({ defaultOptions: { queries: { keepPreviousData: true, placeholderData: () => previousData } } }) } /** * The object expression has a `keepPreviousData` property with value `true`, but the `placeholderData` is a string. * The codemod shouldn't transform this case, only warn the user about the manual migration. */ export const Example13 = () => { new RenamedQueryClient({ defaultOptions: { queries: { keepPreviousData: true, placeholderData: 'somePlaceholderData' } } }) } /** * The object expression has a `keepPreviousData` property with value `false`. * The codemod shouldn't transform this case, only warn the user about the manual migration. */ export const Example14 = () => { new RenamedQueryClient({ defaultOptions: { queries: { keepPreviousData: false, } } }) } /** * The object expression has a `keepPreviousData` property with which is an identifier. * The codemod shouldn't transform this case, only warn the user about the manual migration. */ export const Example15 = () => { const keepPreviousDataIdentifier = false new RenamedQueryClient({ defaultOptions: { queries: { keepPreviousData: keepPreviousDataIdentifier, placeholderData: () => previousData } } }) } ================================================ FILE: packages/query-codemods/src/v5/keep-previous-data/__tests__/keep-previous-data.test.cjs ================================================ const defineTest = require('jscodeshift/dist/testUtils').defineTest defineTest(__dirname, 'keep-previous-data.cjs', null, 'default', { parser: 'tsx', }) defineTest(__dirname, 'keep-previous-data.cjs', null, 'named', { parser: 'tsx', }) ================================================ FILE: packages/query-codemods/src/v5/keep-previous-data/utils/already-has-placeholder-data-property.cjs ================================================ class AlreadyHasPlaceholderDataProperty extends Error { /** * @param {import('jscodeshift').CallExpression} callExpression * @param {string} filePath */ constructor(callExpression, filePath) { super('') this.message = this.buildMessage(callExpression, filePath) this.name = 'AlreadyHasPlaceholderDataProperty' } /** * @param {import('jscodeshift').CallExpression} callExpression * @param {string} filePath * @returns {string} */ buildMessage(callExpression, filePath) { const location = callExpression.callee.loc const start = location.start.line const end = location.end.line return `The usage in file "${filePath}" at line ${start}:${end} already contains a a "placeholderData" property. Please migrate this usage manually.` } } module.exports = AlreadyHasPlaceholderDataProperty ================================================ FILE: packages/query-codemods/src/v5/remove-overloads/remove-overloads.cjs ================================================ const createUtilsObject = require('../../utils/index.cjs') const transformFilterAwareUsages = require('./transformers/filter-aware-usage-transformer.cjs') const transformQueryFnAwareUsages = require('./transformers/query-fn-aware-usage-transformer.cjs') module.exports = (file, api) => { const jscodeshift = api.jscodeshift const root = jscodeshift(file.source) const utils = createUtilsObject({ root, jscodeshift }) const filePath = file.path const dependencies = { jscodeshift, utils, root, filePath } transformFilterAwareUsages({ ...dependencies, config: { keyName: 'queryKey', fnName: 'queryFn', queryClientMethods: [ 'cancelQueries', 'getQueriesData', 'invalidateQueries', 'isFetching', 'refetchQueries', 'removeQueries', 'resetQueries', // 'setQueriesData', ], hooks: ['useIsFetching', 'useQuery'], }, }) transformFilterAwareUsages({ ...dependencies, config: { keyName: 'mutationKey', fnName: 'mutationFn', queryClientMethods: [], hooks: ['useIsMutating', 'useMutation'], }, }) transformQueryFnAwareUsages({ ...dependencies, config: { keyName: 'queryKey', queryClientMethods: [ 'ensureQueryData', 'fetchQuery', 'prefetchQuery', 'fetchInfiniteQuery', 'prefetchInfiniteQuery', ], hooks: [], }, }) return root.toSource({ quote: 'single', lineTerminator: '\n' }) } ================================================ FILE: packages/query-codemods/src/v5/remove-overloads/__testfixtures__/bug-reports.input.tsx ================================================ // From: https://github.com/TanStack/query/issues/6204 export function useDetails({ groupId }: { groupId?: string }): any { const cacheKey = ["details", groupId]; const query = () => fetch(`/group/${groupId}`); const queryObject = useQuery(cacheKey, query, { enabled: !!groupId, }); return queryObject; } // Based on: https://github.com/TanStack/query/issues/6204 export function useDetailsContainsIdentifierAsThirdArgument({ groupId }: { groupId?: string }): any { const cacheKey = ["details", groupId]; const query = () => fetch(`/group/${groupId}`); const opts = { enabled: !!groupId } const queryObject = useQuery(cacheKey, query, opts); return queryObject; } // From: https://twitter.com/samcook_/status/1715063150184132902 export function useWhatever({ thing }: { thing: string }) { return useQuery( ['some-string', someVar], async () => 'foo', { enabled: Boolean(thing) }, ); } // From: https://github.com/TanStack/query/issues/6548 export function useDeleteSomething(): any { return useMutation(({ groupId }: { groupId: string }) => { return fetch(`/api/groups/${groupId}`, { method: 'DELETE', }); }); } // From: https://github.com/TanStack/query/issues/6548 export function useDeleteSomethingWithOnError(): any { return useMutation( ({ groupId }: { groupId: string }) => { return fetch(`/api/groups/${groupId}`, { method: 'DELETE', }); }, { onError: (_error, _variables, context) => { // An error happened! console.log( `rolling back optimistic delete with id ${context.id}` ); }, } ); } ================================================ FILE: packages/query-codemods/src/v5/remove-overloads/__testfixtures__/bug-reports.output.tsx ================================================ // From: https://github.com/TanStack/query/issues/6204 export function useDetails({ groupId }: { groupId?: string }): any { const cacheKey = ["details", groupId]; const query = () => fetch(`/group/${groupId}`); const queryObject = useQuery({ queryKey: cacheKey, queryFn: query, enabled: !!groupId }); return queryObject; } // Based on: https://github.com/TanStack/query/issues/6204 export function useDetailsContainsIdentifierAsThirdArgument({ groupId }: { groupId?: string }): any { const cacheKey = ["details", groupId]; const query = () => fetch(`/group/${groupId}`); const opts = { enabled: !!groupId } const queryObject = useQuery({ queryKey: cacheKey, queryFn: query, ...opts }); return queryObject; } // From: https://twitter.com/samcook_/status/1715063150184132902 export function useWhatever({ thing }: { thing: string }) { return useQuery({ queryKey: ['some-string', someVar], queryFn: async () => 'foo', enabled: Boolean(thing) }); } // From: https://github.com/TanStack/query/issues/6548 export function useDeleteSomething(): any { return useMutation({ mutationFn: ({ groupId }: { groupId: string }) => { return fetch(`/api/groups/${groupId}`, { method: 'DELETE', }); } }); } // From: https://github.com/TanStack/query/issues/6548 export function useDeleteSomethingWithOnError(): any { return useMutation({ mutationFn: ({ groupId }: { groupId: string }) => { return fetch(`/api/groups/${groupId}`, { method: 'DELETE', }); }, onError: (_error, _variables, context) => { // An error happened! console.log( `rolling back optimistic delete with id ${context.id}` ); } }); } ================================================ FILE: packages/query-codemods/src/v5/remove-overloads/__testfixtures__/default-import.input.tsx ================================================ import * as React from 'react' import { useIsFetching, useIsMutating, useQuery, useQueryClient, } from '@tanstack/react-query' import { queryKeysFromAnotherModule } from '../another/module' export const WithKnownParameters = () => { useIsFetching(['foo', 'bar']) useIsFetching(['foo', 'bar'], { queryKey: ['todos'], type: 'all', exact: true }) useIsFetching(['foo', 'bar'], { queryKey: ['todos'], type: 'all', exact: true }, { context: undefined }) useIsFetching(['foo', 'bar'], { type: 'all', exact: true }) useIsFetching(['foo', 'bar'], { type: 'all', exact: true }, { context: undefined }) useIsFetching({ queryKey: ['foo', 'bar'], type: 'all', exact: true }) useIsFetching({ queryKey: ['foo', 'bar'], type: 'all', exact: true }, { context: undefined }) useIsMutating(['foo', 'bar']) useIsMutating(['foo', 'bar'], { exact: true }) useIsMutating(['foo', 'bar'], { exact: true }, { context: undefined }) useIsMutating({ mutationKey: ['foo', 'bar'], exact: true }) useIsMutating({ mutationKey: ['foo', 'bar'], exact: true }, { context: undefined }) // QueryClient methods // --- Instantiated hook call. const queryClient = useQueryClient() queryClient.cancelQueries(['foo', 'bar']) queryClient.cancelQueries(['foo', 'bar'], { queryKey: ['todos'], type: 'all', exact: true }) queryClient.cancelQueries(['foo', 'bar'], { queryKey: ['todos'], type: 'all', exact: true }, { silent: true }) queryClient.cancelQueries(['foo', 'bar'], { type: 'all', exact: true }) queryClient.cancelQueries(['foo', 'bar'], { type: 'all', exact: true }, { silent: true }) queryClient.cancelQueries({ queryKey: ['foo', 'bar'], type: 'all', exact: true }) queryClient.cancelQueries({ queryKey: ['foo', 'bar'], type: 'all', exact: true }, { silent: true }) queryClient.getQueriesData(['foo', 'bar']) queryClient.getQueriesData({ queryKey: ['foo', 'bar'], type: 'all', exact: true }) queryClient.invalidateQueries(['foo', 'bar']) queryClient.invalidateQueries(['foo', 'bar'], { queryKey: ['todos'], type: 'all', exact: true }) queryClient.invalidateQueries(['foo', 'bar'], { queryKey: ['todos'], type: 'all', exact: true }, { cancelRefetch: false, throwOnError: true }) queryClient.invalidateQueries(['foo', 'bar'], { type: 'all', exact: true }) queryClient.invalidateQueries(['foo', 'bar'], { type: 'all', exact: true }, { cancelRefetch: false, throwOnError: true }) queryClient.invalidateQueries({ queryKey: ['foo', 'bar'], type: 'all', exact: true }) queryClient.invalidateQueries({ queryKey: ['foo', 'bar'], type: 'all', exact: true }, { cancelRefetch: false, throwOnError: true }) queryClient.isFetching(['foo', 'bar']) queryClient.isFetching(['foo', 'bar'], { queryKey: ['todos'], type: 'all', exact: true }) queryClient.isFetching(['foo', 'bar'], { type: 'all', exact: true }) queryClient.isFetching({ queryKey: ['foo', 'bar'], type: 'all', exact: true }) queryClient.refetchQueries(['foo', 'bar']) queryClient.refetchQueries(['foo', 'bar'], { queryKey: ['todos'], type: 'all', exact: true }) queryClient.refetchQueries(['foo', 'bar'], { queryKey: ['todos'], type: 'all', exact: true }, { cancelRefetch: false, throwOnError: true }) queryClient.refetchQueries(['foo', 'bar'], { type: 'all', exact: true }) queryClient.refetchQueries(['foo', 'bar'], { type: 'all', exact: true }, { cancelRefetch: false, throwOnError: true }) queryClient.refetchQueries({ queryKey: ['foo', 'bar'], type: 'all', exact: true }) queryClient.refetchQueries({ queryKey: ['foo', 'bar'], type: 'all', exact: true }, { cancelRefetch: false, throwOnError: true }) queryClient.removeQueries(['foo', 'bar']) queryClient.removeQueries(['foo', 'bar'], { queryKey: ['todos'], type: 'all', exact: true }) queryClient.removeQueries({ queryKey: ['foo', 'bar'], type: 'all', exact: true }) queryClient.resetQueries(['foo', 'bar']) queryClient.resetQueries(['foo', 'bar'], { queryKey: ['todos'], type: 'all', exact: true }) queryClient.resetQueries(['foo', 'bar'], { queryKey: ['todos'], type: 'all', exact: true }, { cancelRefetch: false, throwOnError: true }) queryClient.resetQueries(['foo', 'bar'], { type: 'all', exact: true }) queryClient.resetQueries(['foo', 'bar'], { type: 'all', exact: true }, { cancelRefetch: false, throwOnError: true }) queryClient.resetQueries({ queryKey: ['foo', 'bar'], exact: true }) queryClient.resetQueries({ queryKey: ['foo', 'bar'], exact: true }, { cancelRefetch: false, throwOnError: true }) queryClient.setQueriesData(['foo', 'bar'], null) queryClient.setQueriesData(['foo', 'bar'], null, { updatedAt: 1000 }) queryClient.setQueriesData({ queryKey: ['foo', 'bar'] }, null) queryClient.setQueriesData({ queryKey: ['foo', 'bar'] }, null, { updatedAt: 1000 }) queryClient.fetchQuery(['foo', 'bar']) queryClient.fetchQuery(['foo', 'bar'], { queryKey: ['todos'], staleTime: 1000 }) queryClient.fetchQuery(['foo', 'bar'], { queryKey: ['todos'], queryFn: () => 'data', staleTime: 1000 }) queryClient.fetchQuery(['foo', 'bar'], () => 'data', { queryKey: ['todos'], staleTime: 1000 }) queryClient.fetchQuery(['foo', 'bar'], function myFn() { return 'data' }, { queryKey: ['todos'], staleTime: 1000 }) queryClient.fetchQuery({ queryKey: ['foo', 'bar'], queryFn: () => 'data', retry: true }) const queryCache = queryClient.getQueryCache() queryCache.find(['foo', 'bar']) queryCache.find(['foo', 'bar'], { queryKey: ['todos'], type: 'all', exact: true }) queryCache.find(['foo', 'bar'], { type: 'all', exact: true }) queryCache.findAll(['foo', 'bar']) queryCache.findAll(['foo', 'bar'], { type: 'all', exact: true }) queryCache.findAll(['foo', 'bar'], { queryKey: ['todos'], type: 'all', exact: true }) queryCache.findAll({ queryKey: ['foo', 'bar'], type: 'all', exact: true }) return
Example Component
} const globalQueryKey = ['module', 'level'] export const WithIdentifiers = () => { const queryKey = ['foo', 'bar'] const mutationKey = ['posts', 'articles'] const filters = { type: 'all', exact: true } as const const options = { context: undefined } as const const mutationOptions = { exact: true, fetching: false } as const const cancelOptions = { silent: true } as const const invalidateOptions = { cancelRefetch: true, throwOnError: true } as const const refetchOptions = { cancelRefetch: false, throwOnError: true } as const const resetOptions = { cancelRefetch: false, throwOnError: true } as const const fetchOptions = { queryFn: () => 'data', retry: true } as const const queryFn = () => 'data' useIsFetching(queryKey) useIsFetching(queryKey, filters) useIsFetching(queryKey, filters, options) useIsFetching(queryKey, { type: 'all', exact: true }) useIsFetching(queryKey, { type: 'all', exact: true }, { context: undefined }) useIsFetching(queryKey, { queryKey: ['todos'], ...filters }, options) useIsFetching({ queryKey: queryKey, ...filters }) useIsFetching({ queryKey: queryKey, ...filters }, { context: undefined }) useIsMutating(mutationKey) useIsMutating(mutationKey, { exact: true, status: 'idle' }) useIsMutating(mutationKey, { ...mutationOptions, exact: false }) useIsMutating({ mutationKey, ...mutationOptions }) useIsMutating({ mutationKey: ['foo', 'bar'], exact: true, status: 'idle' }) // QueryClient methods // --- Instantiated hook call. const queryClient = useQueryClient() queryClient.cancelQueries(queryKey) queryClient.cancelQueries(queryKey, filters) queryClient.cancelQueries(queryKey, filters, cancelOptions) queryClient.cancelQueries(queryKey, { type: 'all', exact: true }) queryClient.cancelQueries(queryKey, { type: 'all', exact: true }, { revert: true }) queryClient.cancelQueries(queryKey, { queryKey: ['todos'], ...filters }, cancelOptions) queryClient.cancelQueries({ queryKey: queryKey, type: 'all', exact: true }) queryClient.cancelQueries({ queryKey: ['foo', 'bar'], ...filters }, cancelOptions) queryClient.getQueriesData(globalQueryKey) queryClient.getQueriesData({ queryKey: globalQueryKey, ...filters }) queryClient.getQueriesData({ queryKey: ['foo', 'bar'], type: 'all' }) queryClient.invalidateQueries(queryKey) queryClient.invalidateQueries(queryKey, filters) queryClient.invalidateQueries(queryKey, filters, invalidateOptions) queryClient.invalidateQueries(queryKey, { queryKey: ['todos'], stale: true, ...filters }) queryClient.invalidateQueries(queryKey, { queryKey: ['todos'], stale: true, ...filters }, invalidateOptions) queryClient.invalidateQueries({ queryKey: globalQueryKey, ...filters, stale: true }) queryClient.invalidateQueries({ queryKey: globalQueryKey, ...filters, stale: true }, invalidateOptions) queryClient.isFetching(globalQueryKey) queryClient.isFetching(globalQueryKey, filters) queryClient.isFetching(globalQueryKey, { queryKey: ['todos'], type: 'all', exact: true }) queryClient.isFetching(globalQueryKey, { queryKey: ['todos'], ...filters }) queryClient.isFetching({ queryKey: globalQueryKey, ...filters, stale: true }) // Stays as it is because the code couldn't infer the type of the "queryKeysFromAnotherModule" identifier. queryClient.isFetching(queryKeysFromAnotherModule) queryClient.refetchQueries(queryKey) queryClient.refetchQueries(queryKey, filters) queryClient.refetchQueries(queryKey, filters, refetchOptions) queryClient.refetchQueries(queryKey, { queryKey: ['todos'], ...filters }, { ...refetchOptions, cancelRefetch: true }) queryClient.refetchQueries({ queryKey: queryKey, ...filters }) queryClient.refetchQueries({ queryKey: queryKey, ...filters }, { ...refetchOptions, cancelRefetch: true }) // Stays as it is because the code couldn't infer the type of the "queryKeysFromAnotherModule" identifier. queryClient.refetchQueries(queryKeysFromAnotherModule) queryClient.refetchQueries(queryKeysFromAnotherModule, filters) queryClient.refetchQueries(queryKeysFromAnotherModule, filters, refetchOptions) queryClient.removeQueries(queryKey) queryClient.removeQueries(queryKey, filters) queryClient.removeQueries(queryKey, { queryKey: ['todos'], ...filters, stale: true }) queryClient.removeQueries({ queryKey, ...filters, stale: true }) // Stays as it is because the code couldn't infer the type of the "queryKeysFromAnotherModule" identifier. queryClient.removeQueries(queryKeysFromAnotherModule) queryClient.removeQueries(queryKeysFromAnotherModule, filters) queryClient.resetQueries(queryKey) queryClient.resetQueries(queryKey, filters) queryClient.resetQueries(queryKey, filters, resetOptions) queryClient.resetQueries(queryKey, { queryKey: ['todos'], ...filters, stale: true }) queryClient.resetQueries(queryKey, { queryKey: ['todos'], ...filters, stale: true }, resetOptions) queryClient.resetQueries({ queryKey, ...filters, stale: true }) queryClient.resetQueries({ queryKey, ...filters, stale: true }, resetOptions) // Stays as it is because the code couldn't infer the type of the "queryKeysFromAnotherModule" identifier. queryClient.resetQueries(queryKeysFromAnotherModule) queryClient.resetQueries(queryKeysFromAnotherModule, filters) queryClient.resetQueries(queryKeysFromAnotherModule, filters, resetOptions) queryClient.fetchQuery(queryKey) queryClient.fetchQuery(queryKey, fetchOptions) queryClient.fetchQuery(queryKey, { networkMode: 'always', ...fetchOptions }) queryClient.fetchQuery(queryKey, queryFn, fetchOptions) queryClient.fetchQuery(queryKey, () => 'data', { networkMode: 'always', ...fetchOptions }) // Stays as it is because the code couldn't infer the type of the "queryKeysFromAnotherModule" identifier. queryClient.fetchQuery(queryKeysFromAnotherModule) queryClient.fetchQuery(queryKeysFromAnotherModule, fetchOptions) queryClient.fetchQuery(queryKeysFromAnotherModule, queryFn, fetchOptions) } export const SecondArgumentIsAFunctionExample = () => { useQuery(ordersCacheKeys.groupOrders(id), () => api.getPatientGroupOrders(id).then((r) => r.data)) const rest = 'rest' const of = 1 const functionArguments = { foo: 'bar' } useQuery(ordersCacheKeys.groupOrders(id), () => api.getPatientGroupOrders(id).then((r) => r.data), rest, of, functionArguments) } ================================================ FILE: packages/query-codemods/src/v5/remove-overloads/__testfixtures__/default-import.output.tsx ================================================ import * as React from 'react' import { useIsFetching, useIsMutating, useQuery, useQueryClient, } from '@tanstack/react-query' import { queryKeysFromAnotherModule } from '../another/module' export const WithKnownParameters = () => { useIsFetching({ queryKey: ['foo', 'bar'] }) useIsFetching({ queryKey: ['foo', 'bar'], type: 'all', exact: true }) useIsFetching({ queryKey: ['foo', 'bar'], type: 'all', exact: true }, { context: undefined }) useIsFetching({ queryKey: ['foo', 'bar'], type: 'all', exact: true }) useIsFetching({ queryKey: ['foo', 'bar'], type: 'all', exact: true }, { context: undefined }) useIsFetching({ queryKey: ['foo', 'bar'], type: 'all', exact: true }) useIsFetching({ queryKey: ['foo', 'bar'], type: 'all', exact: true }, { context: undefined }) useIsMutating({ mutationKey: ['foo', 'bar'] }) useIsMutating({ mutationKey: ['foo', 'bar'], exact: true }) useIsMutating({ mutationKey: ['foo', 'bar'], exact: true }, { context: undefined }) useIsMutating({ mutationKey: ['foo', 'bar'], exact: true }) useIsMutating({ mutationKey: ['foo', 'bar'], exact: true }, { context: undefined }) // QueryClient methods // --- Instantiated hook call. const queryClient = useQueryClient() queryClient.cancelQueries({ queryKey: ['foo', 'bar'] }) queryClient.cancelQueries({ queryKey: ['foo', 'bar'], type: 'all', exact: true }) queryClient.cancelQueries({ queryKey: ['foo', 'bar'], type: 'all', exact: true }, { silent: true }) queryClient.cancelQueries({ queryKey: ['foo', 'bar'], type: 'all', exact: true }) queryClient.cancelQueries({ queryKey: ['foo', 'bar'], type: 'all', exact: true }, { silent: true }) queryClient.cancelQueries({ queryKey: ['foo', 'bar'], type: 'all', exact: true }) queryClient.cancelQueries({ queryKey: ['foo', 'bar'], type: 'all', exact: true }, { silent: true }) queryClient.getQueriesData({ queryKey: ['foo', 'bar'] }) queryClient.getQueriesData({ queryKey: ['foo', 'bar'], type: 'all', exact: true }) queryClient.invalidateQueries({ queryKey: ['foo', 'bar'] }) queryClient.invalidateQueries({ queryKey: ['foo', 'bar'], type: 'all', exact: true }) queryClient.invalidateQueries({ queryKey: ['foo', 'bar'], type: 'all', exact: true }, { cancelRefetch: false, throwOnError: true }) queryClient.invalidateQueries({ queryKey: ['foo', 'bar'], type: 'all', exact: true }) queryClient.invalidateQueries({ queryKey: ['foo', 'bar'], type: 'all', exact: true }, { cancelRefetch: false, throwOnError: true }) queryClient.invalidateQueries({ queryKey: ['foo', 'bar'], type: 'all', exact: true }) queryClient.invalidateQueries({ queryKey: ['foo', 'bar'], type: 'all', exact: true }, { cancelRefetch: false, throwOnError: true }) queryClient.isFetching({ queryKey: ['foo', 'bar'] }) queryClient.isFetching({ queryKey: ['foo', 'bar'], type: 'all', exact: true }) queryClient.isFetching({ queryKey: ['foo', 'bar'], type: 'all', exact: true }) queryClient.isFetching({ queryKey: ['foo', 'bar'], type: 'all', exact: true }) queryClient.refetchQueries({ queryKey: ['foo', 'bar'] }) queryClient.refetchQueries({ queryKey: ['foo', 'bar'], type: 'all', exact: true }) queryClient.refetchQueries({ queryKey: ['foo', 'bar'], type: 'all', exact: true }, { cancelRefetch: false, throwOnError: true }) queryClient.refetchQueries({ queryKey: ['foo', 'bar'], type: 'all', exact: true }) queryClient.refetchQueries({ queryKey: ['foo', 'bar'], type: 'all', exact: true }, { cancelRefetch: false, throwOnError: true }) queryClient.refetchQueries({ queryKey: ['foo', 'bar'], type: 'all', exact: true }) queryClient.refetchQueries({ queryKey: ['foo', 'bar'], type: 'all', exact: true }, { cancelRefetch: false, throwOnError: true }) queryClient.removeQueries({ queryKey: ['foo', 'bar'] }) queryClient.removeQueries({ queryKey: ['foo', 'bar'], type: 'all', exact: true }) queryClient.removeQueries({ queryKey: ['foo', 'bar'], type: 'all', exact: true }) queryClient.resetQueries({ queryKey: ['foo', 'bar'] }) queryClient.resetQueries({ queryKey: ['foo', 'bar'], type: 'all', exact: true }) queryClient.resetQueries({ queryKey: ['foo', 'bar'], type: 'all', exact: true }, { cancelRefetch: false, throwOnError: true }) queryClient.resetQueries({ queryKey: ['foo', 'bar'], type: 'all', exact: true }) queryClient.resetQueries({ queryKey: ['foo', 'bar'], type: 'all', exact: true }, { cancelRefetch: false, throwOnError: true }) queryClient.resetQueries({ queryKey: ['foo', 'bar'], exact: true }) queryClient.resetQueries({ queryKey: ['foo', 'bar'], exact: true }, { cancelRefetch: false, throwOnError: true }) queryClient.setQueriesData(['foo', 'bar'], null) queryClient.setQueriesData(['foo', 'bar'], null, { updatedAt: 1000 }) queryClient.setQueriesData({ queryKey: ['foo', 'bar'] }, null) queryClient.setQueriesData({ queryKey: ['foo', 'bar'] }, null, { updatedAt: 1000 }) queryClient.fetchQuery({ queryKey: ['foo', 'bar'] }) queryClient.fetchQuery({ queryKey: ['foo', 'bar'], staleTime: 1000 }) queryClient.fetchQuery({ queryKey: ['foo', 'bar'], queryFn: () => 'data', staleTime: 1000 }) queryClient.fetchQuery({ queryKey: ['foo', 'bar'], queryFn: () => 'data', staleTime: 1000 }) queryClient.fetchQuery({ queryKey: ['foo', 'bar'], queryFn: function myFn() { return 'data' }, staleTime: 1000 }) queryClient.fetchQuery({ queryKey: ['foo', 'bar'], queryFn: () => 'data', retry: true }) const queryCache = queryClient.getQueryCache() queryCache.find({ queryKey: ['foo', 'bar'] }) queryCache.find({ queryKey: ['foo', 'bar'], type: 'all', exact: true }) queryCache.find({ queryKey: ['foo', 'bar'], type: 'all', exact: true }) queryCache.findAll({ queryKey: ['foo', 'bar'] }) queryCache.findAll({ queryKey: ['foo', 'bar'], type: 'all', exact: true }) queryCache.findAll({ queryKey: ['foo', 'bar'], type: 'all', exact: true }) queryCache.findAll({ queryKey: ['foo', 'bar'], type: 'all', exact: true }) return
Example Component
} const globalQueryKey = ['module', 'level'] export const WithIdentifiers = () => { const queryKey = ['foo', 'bar'] const mutationKey = ['posts', 'articles'] const filters = { type: 'all', exact: true } as const const options = { context: undefined } as const const mutationOptions = { exact: true, fetching: false } as const const cancelOptions = { silent: true } as const const invalidateOptions = { cancelRefetch: true, throwOnError: true } as const const refetchOptions = { cancelRefetch: false, throwOnError: true } as const const resetOptions = { cancelRefetch: false, throwOnError: true } as const const fetchOptions = { queryFn: () => 'data', retry: true } as const const queryFn = () => 'data' useIsFetching({ queryKey: queryKey }) useIsFetching({ queryKey: queryKey, ...filters }) useIsFetching({ queryKey: queryKey, ...filters }, options) useIsFetching({ queryKey: queryKey, type: 'all', exact: true }) useIsFetching({ queryKey: queryKey, type: 'all', exact: true }, { context: undefined }) useIsFetching({ queryKey: queryKey, ...filters }, options) useIsFetching({ queryKey: queryKey, ...filters }) useIsFetching({ queryKey: queryKey, ...filters }, { context: undefined }) useIsMutating({ mutationKey: mutationKey }) useIsMutating({ mutationKey: mutationKey, exact: true, status: 'idle' }) useIsMutating({ mutationKey: mutationKey, ...mutationOptions, exact: false }) useIsMutating({ mutationKey, ...mutationOptions }) useIsMutating({ mutationKey: ['foo', 'bar'], exact: true, status: 'idle' }) // QueryClient methods // --- Instantiated hook call. const queryClient = useQueryClient() queryClient.cancelQueries({ queryKey: queryKey }) queryClient.cancelQueries({ queryKey: queryKey, ...filters }) queryClient.cancelQueries({ queryKey: queryKey, ...filters }, cancelOptions) queryClient.cancelQueries({ queryKey: queryKey, type: 'all', exact: true }) queryClient.cancelQueries({ queryKey: queryKey, type: 'all', exact: true }, { revert: true }) queryClient.cancelQueries({ queryKey: queryKey, ...filters }, cancelOptions) queryClient.cancelQueries({ queryKey: queryKey, type: 'all', exact: true }) queryClient.cancelQueries({ queryKey: ['foo', 'bar'], ...filters }, cancelOptions) queryClient.getQueriesData({ queryKey: globalQueryKey }) queryClient.getQueriesData({ queryKey: globalQueryKey, ...filters }) queryClient.getQueriesData({ queryKey: ['foo', 'bar'], type: 'all' }) queryClient.invalidateQueries({ queryKey: queryKey }) queryClient.invalidateQueries({ queryKey: queryKey, ...filters }) queryClient.invalidateQueries({ queryKey: queryKey, ...filters }, invalidateOptions) queryClient.invalidateQueries({ queryKey: queryKey, stale: true, ...filters }) queryClient.invalidateQueries({ queryKey: queryKey, stale: true, ...filters }, invalidateOptions) queryClient.invalidateQueries({ queryKey: globalQueryKey, ...filters, stale: true }) queryClient.invalidateQueries({ queryKey: globalQueryKey, ...filters, stale: true }, invalidateOptions) queryClient.isFetching({ queryKey: globalQueryKey }) queryClient.isFetching({ queryKey: globalQueryKey, ...filters }) queryClient.isFetching({ queryKey: globalQueryKey, type: 'all', exact: true }) queryClient.isFetching({ queryKey: globalQueryKey, ...filters }) queryClient.isFetching({ queryKey: globalQueryKey, ...filters, stale: true }) // Stays as it is because the code couldn't infer the type of the "queryKeysFromAnotherModule" identifier. queryClient.isFetching(queryKeysFromAnotherModule) queryClient.refetchQueries({ queryKey: queryKey }) queryClient.refetchQueries({ queryKey: queryKey, ...filters }) queryClient.refetchQueries({ queryKey: queryKey, ...filters }, refetchOptions) queryClient.refetchQueries({ queryKey: queryKey, ...filters }, { ...refetchOptions, cancelRefetch: true }) queryClient.refetchQueries({ queryKey: queryKey, ...filters }) queryClient.refetchQueries({ queryKey: queryKey, ...filters }, { ...refetchOptions, cancelRefetch: true }) // Stays as it is because the code couldn't infer the type of the "queryKeysFromAnotherModule" identifier. queryClient.refetchQueries(queryKeysFromAnotherModule) queryClient.refetchQueries(queryKeysFromAnotherModule, filters) queryClient.refetchQueries(queryKeysFromAnotherModule, filters, refetchOptions) queryClient.removeQueries({ queryKey: queryKey }) queryClient.removeQueries({ queryKey: queryKey, ...filters }) queryClient.removeQueries({ queryKey: queryKey, ...filters, stale: true }) queryClient.removeQueries({ queryKey, ...filters, stale: true }) // Stays as it is because the code couldn't infer the type of the "queryKeysFromAnotherModule" identifier. queryClient.removeQueries(queryKeysFromAnotherModule) queryClient.removeQueries(queryKeysFromAnotherModule, filters) queryClient.resetQueries({ queryKey: queryKey }) queryClient.resetQueries({ queryKey: queryKey, ...filters }) queryClient.resetQueries({ queryKey: queryKey, ...filters }, resetOptions) queryClient.resetQueries({ queryKey: queryKey, ...filters, stale: true }) queryClient.resetQueries({ queryKey: queryKey, ...filters, stale: true }, resetOptions) queryClient.resetQueries({ queryKey, ...filters, stale: true }) queryClient.resetQueries({ queryKey, ...filters, stale: true }, resetOptions) // Stays as it is because the code couldn't infer the type of the "queryKeysFromAnotherModule" identifier. queryClient.resetQueries(queryKeysFromAnotherModule) queryClient.resetQueries(queryKeysFromAnotherModule, filters) queryClient.resetQueries(queryKeysFromAnotherModule, filters, resetOptions) queryClient.fetchQuery({ queryKey: queryKey }) queryClient.fetchQuery({ queryKey: queryKey, ...fetchOptions }) queryClient.fetchQuery({ queryKey: queryKey, networkMode: 'always', ...fetchOptions }) queryClient.fetchQuery({ queryKey: queryKey, queryFn: queryFn, ...fetchOptions }) queryClient.fetchQuery({ queryKey: queryKey, queryFn: () => 'data', networkMode: 'always', ...fetchOptions }) // Stays as it is because the code couldn't infer the type of the "queryKeysFromAnotherModule" identifier. queryClient.fetchQuery(queryKeysFromAnotherModule) queryClient.fetchQuery(queryKeysFromAnotherModule, fetchOptions) queryClient.fetchQuery(queryKeysFromAnotherModule, queryFn, fetchOptions) } export const SecondArgumentIsAFunctionExample = () => { useQuery({ queryKey: ordersCacheKeys.groupOrders(id), queryFn: () => api.getPatientGroupOrders(id).then((r) => r.data) }) const rest = 'rest' const of = 1 const functionArguments = { foo: 'bar' } useQuery({ queryKey: ordersCacheKeys.groupOrders(id), queryFn: () => api.getPatientGroupOrders(id).then((r) => r.data) }, rest, of, functionArguments) } ================================================ FILE: packages/query-codemods/src/v5/remove-overloads/__tests__/remove-overloads.test.cjs ================================================ const defineTest = require('jscodeshift/dist/testUtils').defineTest defineTest(__dirname, 'remove-overloads.cjs', null, 'default-import', { parser: 'tsx', }) defineTest(__dirname, 'remove-overloads.cjs', null, 'bug-reports', { parser: 'tsx', }) ================================================ FILE: packages/query-codemods/src/v5/remove-overloads/transformers/filter-aware-usage-transformer.cjs ================================================ const createV5UtilsObject = require('../utils/index.cjs') const UnknownUsageError = require('../utils/unknown-usage-error.cjs') const createQueryClientTransformer = require('../../../utils/transformers/query-client-transformer.cjs') const createQueryCacheTransformer = require('../../../utils/transformers/query-cache-transformer.cjs') const createUseQueryLikeTransformer = require('../../../utils/transformers/use-query-like-transformer.cjs') /** * @param {import('jscodeshift').api} jscodeshift * @param {Object} utils * @param {import('jscodeshift').Collection} root * @param {string} filePath * @param {{keyName: "mutationKey"|"queryKey", fnName: "mutationFn"|"queryFn", queryClientMethods: ReadonlyArray, hooks: ReadonlyArray}} config */ const transformFilterAwareUsages = ({ jscodeshift, utils, root, filePath, config, }) => { const v5Utils = createV5UtilsObject({ jscodeshift, utils }) /** * @param {import('jscodeshift').CallExpression} node * @param {"mutationKey"|"queryKey"} keyName * @param {"mutationFn"|"queryFn"} fnName * @returns {boolean} */ const canSkipReplacement = (node, keyName, fnName) => { const callArguments = node.arguments const hasKeyOrFnProperty = () => callArguments[0].properties.some( (property) => utils.isObjectProperty(property) && property.key.name !== keyName && property.key.name !== fnName, ) /** * This call has at least one argument. If it's an object expression and contains the "queryKey" or "mutationKey" * field, the transformation can be skipped, because it's already matching the expected signature. */ return ( callArguments.length > 0 && utils.isObjectExpression(callArguments[0]) && hasKeyOrFnProperty() ) } /** * This function checks whether the given object property is a spread element or a property that's not named * "queryKey" or "mutationKey". * * @param {import('jscodeshift').ObjectProperty} property * @returns {boolean} */ const predicate = (property) => { const isSpreadElement = utils.isSpreadElement(property) const isObjectProperty = utils.isObjectProperty(property) return ( isSpreadElement || (isObjectProperty && property.key.name !== config.keyName) ) } const replacer = (path) => { const node = path.node const isFunctionDefinition = (functionArgument) => { if (utils.isFunctionDefinition(functionArgument)) { return true } if (utils.isIdentifier(functionArgument)) { const binding = v5Utils.getBindingFromScope( path, functionArgument.name, filePath, ) const isVariableDeclarator = jscodeshift.match(binding, { type: jscodeshift.VariableDeclarator.name, }) return isVariableDeclarator && utils.isFunctionDefinition(binding.init) } } try { // If the given method/function call matches certain criteria, the node doesn't need to be replaced, this step can be skipped. if (canSkipReplacement(node, config.keyName, config.fnName)) { return node } /** * Here we attempt to determine the first parameter of the function call. * If it's a function definition, we can create an object property from it (the mutation fn). */ const firstArgument = node.arguments[0] if (isFunctionDefinition(firstArgument)) { const objectExpression = jscodeshift.objectExpression([ jscodeshift.property( 'init', jscodeshift.identifier(config.fnName), firstArgument, ), ]) const secondArgument = node.arguments[1] if (secondArgument) { // If it's an object expression, we can copy the properties from it to the newly created object expression. if (utils.isObjectExpression(secondArgument)) { v5Utils.copyPropertiesFromSource( secondArgument, objectExpression, predicate, ) } else { // Otherwise, we simply spread the second argument in the newly created object expression. objectExpression.properties.push( jscodeshift.spreadElement(secondArgument), ) } } return jscodeshift.callExpression(node.original.callee, [ objectExpression, ]) } /** * If, instead, the first parameter is an array expression or an identifier that references * an array expression, then we create an object property from it (the query or mutation key). * * @type {import('jscodeshift').Property|undefined} */ const keyProperty = v5Utils.transformArgumentToKey( path, node.arguments[0], config.keyName, filePath, ) /** * The first parameter couldn't be transformed into an object property, so it's time to throw an exception, * it will notify the consumers that they need to rewrite this usage manually. */ if (!keyProperty) { const secondArgument = node.arguments.length > 1 ? node.arguments[1] : null if (!secondArgument) { throw new UnknownUsageError(node, filePath) } if (utils.isFunctionDefinition(secondArgument)) { const originalArguments = node.arguments const firstArgument = jscodeshift.objectExpression([ jscodeshift.property( 'init', jscodeshift.identifier(config.keyName), originalArguments[0], ), jscodeshift.property( 'init', jscodeshift.identifier(config.fnName), secondArgument, ), ]) return jscodeshift.callExpression(node.original.callee, [ firstArgument, ...originalArguments.slice(2), ]) } } const functionArguments = [jscodeshift.objectExpression([keyProperty])] const secondParameter = node.arguments[1] if (secondParameter) { const createdObjectExpression = functionArguments[0] if (isFunctionDefinition(secondParameter)) { const objectExpression = jscodeshift.objectExpression([ jscodeshift.property( 'init', jscodeshift.identifier(config.keyName), node.arguments[0], ), jscodeshift.property( 'init', jscodeshift.identifier(config.fnName), secondParameter, ), ]) const thirdArgument = node.arguments[2] if (thirdArgument) { // If it's an object expression, we can copy the properties from it to the newly created object expression. if (utils.isObjectExpression(thirdArgument)) { v5Utils.copyPropertiesFromSource( thirdArgument, objectExpression, predicate, ) } else { // Otherwise, we simply spread the third argument in the newly created object expression. objectExpression.properties.push( jscodeshift.spreadElement(thirdArgument), ) } } return jscodeshift.callExpression(node.original.callee, [ objectExpression, ]) } /** * If it has a second argument, and it's an object expression, then we get the properties from it * (except the "queryKey" or "mutationKey" properties), because these arguments will also be moved to the * newly created object expression. */ if (utils.isObjectExpression(secondParameter)) { v5Utils.copyPropertiesFromSource( secondParameter, createdObjectExpression, predicate, ) } else { // Otherwise, we simply spread the second parameter in the newly created object expression. createdObjectExpression.properties.push( jscodeshift.spreadElement(secondParameter), ) } } // The rest of the function arguments can be simply pushed to the function arguments object so all will be kept. functionArguments.push(...node.arguments.slice(2)) return jscodeshift.callExpression(node.original.callee, functionArguments) } catch (error) { utils.warn( error.name === UnknownUsageError.name ? error.message : `An unknown error occurred while processing the "${filePath}" file. Please review this file, because the codemod couldn't be applied.`, ) return node } } createQueryClientTransformer({ jscodeshift, utils, root }).execute( config.queryClientMethods, replacer, ) createUseQueryLikeTransformer({ jscodeshift, utils, root }).execute( config.hooks, replacer, ) createQueryCacheTransformer({ jscodeshift, utils, root }).execute(replacer) } module.exports = transformFilterAwareUsages ================================================ FILE: packages/query-codemods/src/v5/remove-overloads/transformers/query-fn-aware-usage-transformer.cjs ================================================ const createV5UtilsObject = require('../utils/index.cjs') const UnknownUsageError = require('../utils/unknown-usage-error.cjs') const createQueryClientTransformer = require('../../../utils/transformers/query-client-transformer.cjs') /** * @param {import('jscodeshift').api} jscodeshift * @param {Object} utils * @param {import('jscodeshift').Collection} root * @param {string} filePath * @param {{keyName: "mutationKey"|"queryKey", queryClientMethods: ReadonlyArray, hooks: ReadonlyArray}} config */ const transformQueryFnAwareUsages = ({ jscodeshift, utils, root, filePath, config, }) => { const v5Utils = createV5UtilsObject({ jscodeshift, utils }) /** * @param {import('jscodeshift').CallExpression} node * @returns {boolean} */ const canSkipReplacement = (node) => { const callArguments = node.arguments const hasKeyProperty = () => callArguments[0].properties.some( (property) => utils.isObjectProperty(property) && [config.keyName, 'queryFn'].includes(property.key.name), ) return ( callArguments.length > 0 && utils.isObjectExpression(callArguments[0]) && hasKeyProperty() ) } const predicate = (property) => { const isSpreadElement = utils.isSpreadElement(property) const isObjectProperty = utils.isObjectProperty(property) return ( isSpreadElement || (isObjectProperty && property.key.name !== config.keyName) ) } const transformArgumentToQueryFunction = (path, node) => { const isIdentifier = utils.isIdentifier(node) const isFunctionDefinition = utils.isFunctionDefinition(node) if (!isIdentifier && !isFunctionDefinition) { return undefined } if (isFunctionDefinition) { return jscodeshift.property( 'init', jscodeshift.identifier('queryFn'), node, ) } const binding = v5Utils.getBindingFromScope(path, node.name, filePath) const initializer = v5Utils.getInitializerByDeclarator(binding) if (!utils.isFunctionDefinition(initializer)) { return undefined } return jscodeshift.property( 'init', jscodeshift.identifier('queryFn'), binding.id, ) } const transformArgumentToOptionsObject = (path, node) => { if (!utils.isIdentifier(node)) { return undefined } const binding = v5Utils.getBindingFromScope(path, node.name, filePath) const initializer = v5Utils.getInitializerByDeclarator(binding) if (utils.isObjectExpression(initializer)) { return jscodeshift.spreadElement(binding.id) } } const replacer = (path) => { const node = path.node try { // If the given method/function call matches certain criteria, the node doesn't need to be replaced, this step can be skipped. if (canSkipReplacement(node)) { return node } const keyProperty = v5Utils.transformArgumentToKey( path, node.arguments[0], config.keyName, filePath, ) if (!keyProperty) { throw new UnknownUsageError(node, filePath) } const parameters = [jscodeshift.objectExpression([keyProperty])] const createdObjectExpression = parameters[0] const secondParameter = node.arguments[1] if (secondParameter) { const queryFnProperty = transformArgumentToQueryFunction( path, secondParameter, ) if (queryFnProperty) { createdObjectExpression.properties.push(queryFnProperty) const thirdParameter = node.arguments[2] if (utils.isObjectExpression(thirdParameter)) { v5Utils.copyPropertiesFromSource( thirdParameter, createdObjectExpression, predicate, ) } else { createdObjectExpression.properties.push( jscodeshift.spreadElement(thirdParameter), ) } return jscodeshift.callExpression(node.original.callee, parameters) } const optionsProperty = transformArgumentToOptionsObject( path, secondParameter, ) if (optionsProperty) { createdObjectExpression.properties.push(optionsProperty) return jscodeshift.callExpression(node.original.callee, parameters) } if (utils.isObjectExpression(secondParameter)) { v5Utils.copyPropertiesFromSource( secondParameter, createdObjectExpression, predicate, ) } return jscodeshift.callExpression(node.original.callee, parameters) } return jscodeshift.callExpression(node.original.callee, parameters) } catch (error) { utils.warn( error.name === UnknownUsageError.name ? error.message : `An unknown error occurred while processing the "${filePath}" file. Please review this file, because the codemod couldn't be applied.`, ) return node } } createQueryClientTransformer({ jscodeshift, utils, root }).execute( config.queryClientMethods, replacer, ) } module.exports = transformQueryFnAwareUsages ================================================ FILE: packages/query-codemods/src/v5/remove-overloads/utils/index.cjs ================================================ const UnknownUsageError = require('./unknown-usage-error.cjs') module.exports = ({ jscodeshift, utils }) => { /** * * @param {import('jscodeshift').ObjectExpression} source * @param {import('jscodeshift').ObjectExpression} target * @param {(node: import('jscodeshift').Node) => boolean} predicate */ const copyPropertiesFromSource = (source, target, predicate) => { source.properties.forEach((property) => { if (predicate(property)) { target.properties.push(property) } }) } /** * @param {import('jscodeshift').NodePath} path * @param {string} argumentName * @param {string} filePath * @returns {*} */ const getBindingFromScope = (path, argumentName, filePath) => { /** * If the current scope contains the declaration then we can use the actual one else we attempt to find the * binding from above. */ const scope = path.scope.declares(argumentName) ? path.scope : path.scope.lookup(argumentName) /** * The declaration couldn't be found for some reason, time to move on. We warn the user it needs to be rewritten * by themselves. */ if (!scope) { return undefined } const binding = scope.bindings[argumentName] .filter((item) => utils.isIdentifier(item.value)) .map((item) => item.parentPath.value) .at(0) if (!binding) { throw new UnknownUsageError(path.node, filePath) } return binding } /** * @param {import('jscodeshift').VariableDeclarator} binding * @returns {import('jscodeshift').Node|undefined} */ const getInitializerByDeclarator = (binding) => { const isVariableDeclaration = jscodeshift.match(binding, { type: jscodeshift.VariableDeclarator.name, }) if (!isVariableDeclaration) { return undefined } const isTSAsExpression = jscodeshift.match(binding.init, { type: jscodeshift.TSAsExpression.name, }) return isTSAsExpression ? binding.init.expression : binding.init } /** * @param {import('jscodeshift').Node} node * @returns {boolean} */ const isArrayExpressionVariable = (node) => jscodeshift.match(node, { type: jscodeshift.VariableDeclarator.name, init: { type: jscodeshift.ArrayExpression.name, }, }) /** * @param {import('jscodeshift').NodePath} path * @param {import('jscodeshift').Node} node * @param {"queryKey"|"mutationKey"} keyName * @param {string} filePath * @returns {import('jscodeshift').Property|undefined} */ const transformArgumentToKey = (path, node, keyName, filePath) => { // If the first argument is an identifier we have to infer its type if possible. if (utils.isIdentifier(node)) { const binding = getBindingFromScope(path, node.name, filePath) if (isArrayExpressionVariable(binding)) { return jscodeshift.property( 'init', jscodeshift.identifier(keyName), jscodeshift.identifier(binding.id.name), ) } } // If the first argument is an array, then it matches the following overload: // methodName(queryKey?: QueryKey, firstObject?: TFirstObject, secondObject?: TSecondObject) if (utils.isArrayExpression(node)) { // Then we create the 'queryKey' property based on it, because it will be passed to the first argument // that should be an object according to the new signature. return jscodeshift.property('init', jscodeshift.identifier(keyName), node) } return undefined } return { copyPropertiesFromSource, getInitializerByDeclarator, getBindingFromScope, transformArgumentToKey, } } ================================================ FILE: packages/query-codemods/src/v5/remove-overloads/utils/unknown-usage-error.cjs ================================================ class UnknownUsageError extends Error { /** * @param {import('jscodeshift').CallExpression} callExpression * @param {string} filePath */ constructor(callExpression, filePath) { super('') this.message = this.buildMessage(callExpression, filePath) this.name = 'UnknownUsageError' } /** * * @param {import('jscodeshift').CallExpression} callExpression * @param {string} filePath * @returns {string} */ buildMessage(callExpression, filePath) { const location = callExpression.callee.loc const start = location.start.line const end = location.end.line return `The usage in file "${filePath}" at line ${start}:${end} could not be transformed into the new syntax. Please do this manually.` } } module.exports = UnknownUsageError ================================================ FILE: packages/query-codemods/src/v5/rename-hydrate/rename-hydrate.cjs ================================================ module.exports = (file, api) => { const jscodeshift = api.jscodeshift const root = jscodeshift(file.source) const importSpecifiers = root .find(jscodeshift.ImportDeclaration, { source: { value: '@tanstack/react-query', }, }) .find(jscodeshift.ImportSpecifier, { imported: { name: 'Hydrate', }, }) if (importSpecifiers.length > 0) { const names = { searched: 'Hydrate', // By default, we want to replace the `Hydrate` usages. target: 'HydrationBoundary', // We want to replace them with `HydrationBoundary`. } importSpecifiers.replaceWith(({ node: mutableNode }) => { /** * When the local and imported names match which means the code doesn't contain import aliases, we need * to replace only the import specifier. * @type {boolean} */ const usesDefaultImport = mutableNode.local.name === mutableNode.imported.name if (!usesDefaultImport) { // If the code uses import aliases, we must re-use the alias. names.searched = mutableNode.local.name names.target = mutableNode.local.name } // Override the import specifier. mutableNode.imported.name = 'HydrationBoundary' return mutableNode }) root .findJSXElements(names.searched) .replaceWith(({ node: mutableNode }) => { mutableNode.openingElement.name.name = names.target mutableNode.closingElement.name.name = names.target return mutableNode }) } return root.toSource({ quote: 'single', lineTerminator: '\n' }) } ================================================ FILE: packages/query-codemods/src/v5/rename-hydrate/__testfixtures__/default-import.input.tsx ================================================ import * as React from 'react' import { Hydrate, QueryClient, QueryClientProvider, } from '@tanstack/react-query' import { ReactQueryDevtools } from '@tanstack/react-query-devtools' export default function MyApp({ Component, pageProps }) { const [queryClient] = React.useState(() => new QueryClient()) return ( ) } ================================================ FILE: packages/query-codemods/src/v5/rename-hydrate/__testfixtures__/default-import.output.tsx ================================================ import * as React from 'react' import { HydrationBoundary, QueryClient, QueryClientProvider, } from '@tanstack/react-query' import { ReactQueryDevtools } from '@tanstack/react-query-devtools' export default function MyApp({ Component, pageProps }) { const [queryClient] = React.useState(() => new QueryClient()) return ( ); } ================================================ FILE: packages/query-codemods/src/v5/rename-hydrate/__testfixtures__/named-import.input.tsx ================================================ import * as React from 'react' import { Hydrate as RenamedHydrate, QueryClient as RenamedQueryClient, QueryClientProvider as RenamedQueryClientProvider, } from '@tanstack/react-query' import { ReactQueryDevtools } from '@tanstack/react-query-devtools' export default function MyApp({ Component, pageProps }) { const [queryClient] = React.useState(() => new RenamedQueryClient()) return ( ) } ================================================ FILE: packages/query-codemods/src/v5/rename-hydrate/__testfixtures__/named-import.output.tsx ================================================ import * as React from 'react' import { HydrationBoundary as RenamedHydrate, QueryClient as RenamedQueryClient, QueryClientProvider as RenamedQueryClientProvider, } from '@tanstack/react-query' import { ReactQueryDevtools } from '@tanstack/react-query-devtools' export default function MyApp({ Component, pageProps }) { const [queryClient] = React.useState(() => new RenamedQueryClient()) return ( ) } ================================================ FILE: packages/query-codemods/src/v5/rename-hydrate/__tests__/rename-hydrate.test.cjs ================================================ const defineTest = require('jscodeshift/dist/testUtils').defineTest defineTest(__dirname, 'rename-hydrate.cjs', null, 'default-import', { parser: 'tsx', }) defineTest(__dirname, 'rename-hydrate.cjs', null, 'named-import', { parser: 'tsx', }) ================================================ FILE: packages/query-codemods/src/v5/rename-properties/rename-properties.cjs ================================================ module.exports = (file, api) => { const jscodeshift = api.jscodeshift const root = jscodeshift(file.source) const baseRenameLogic = (kind, from, to) => { root .find(kind, (node) => { return ( node.computed === false && (node.key?.name === from || node.key?.value === from) ) }) .replaceWith(({ node: mutableNode }) => { if (mutableNode.key.name !== undefined) { mutableNode.key.name = to } if (mutableNode.key.value !== undefined) { mutableNode.key.value = to } return mutableNode }) } const renameObjectProperty = (from, to) => { baseRenameLogic(jscodeshift.ObjectProperty, from, to) } const renameTypeScriptPropertySignature = (from, to) => { baseRenameLogic(jscodeshift.TSPropertySignature, from, to) } renameObjectProperty('cacheTime', 'gcTime') renameObjectProperty('useErrorBoundary', 'throwOnError') renameTypeScriptPropertySignature('cacheTime', 'gcTime') renameTypeScriptPropertySignature('useErrorBoundary', 'throwOnError') return root.toSource({ quote: 'single', lineTerminator: '\n' }) } ================================================ FILE: packages/query-codemods/src/v5/rename-properties/__testfixtures__/rename-cache-time.input.tsx ================================================ import * as React from 'react' import { QueryClient, useInfiniteQuery } from '@tanstack/react-query' import { search } from './algolia' // Since the `cacheTime` property is string literal and not computed, the codemod should change it. export type UseAlgoliaOptionsButWithStringLiterals = { 'indexName': string 'query': string 'hitsPerPage'?: number 'staleTime'?: number 'cacheTime'?: number 'enabled'?: boolean } // Since the `cacheTime` property is an identifier and not computed, the codemod should change it. export type UseAlgoliaOptions = { indexName: string query: string hitsPerPage?: number staleTime?: number cacheTime?: number enabled?: boolean } // Since the `cacheTime` property is an identifier and not computed, and shorthand, the codemod should change it. export function useAlgolia({ indexName, query, hitsPerPage = 10, staleTime, cacheTime, enabled, }: UseAlgoliaOptions) { const queryInfo = useInfiniteQuery({ queryKey: ['algolia', indexName, query, hitsPerPage], queryFn: ({ pageParam }) => search({ indexName, query, pageParam, hitsPerPage }), initialPageParam: 0, getNextPageParam: (lastPage) => lastPage?.nextPage, staleTime, cacheTime, enabled, }) const hits = queryInfo.data?.pages.map((page) => page.hits).flat() return { ...queryInfo, hits } } // Since the `cacheTime` property is an identifier and not computed, the codemod should change it. export const asIdentifierExample = () => new QueryClient({ defaultOptions: { queries: { cacheTime: 1000 * 60 * 60 * 24, // 24 hours }, }, }) // Since the `cacheTime` property is a string literal and not computed, the codemod should change it. export const asStringLiteralExample = () => new QueryClient({ defaultOptions: { queries: { 'cacheTime': 1000 * 60 * 60 * 24, // 24 hours }, }, }) // Since the `cacheTime` property is computed, the codemod shouldn't touch this example. export const asComputedExample = () => { const cacheTime = 'foo' return new QueryClient({ defaultOptions: { queries: { [cacheTime]: 1000 * 60 * 60 * 24, // 24 hours }, }, }) } ================================================ FILE: packages/query-codemods/src/v5/rename-properties/__testfixtures__/rename-cache-time.output.tsx ================================================ import * as React from 'react' import { QueryClient, useInfiniteQuery } from '@tanstack/react-query' import { search } from './algolia' // Since the `cacheTime` property is string literal and not computed, the codemod should change it. export type UseAlgoliaOptionsButWithStringLiterals = { 'indexName': string 'query': string 'hitsPerPage'?: number 'staleTime'?: number 'gcTime'?: number 'enabled'?: boolean } // Since the `cacheTime` property is an identifier and not computed, the codemod should change it. export type UseAlgoliaOptions = { indexName: string query: string hitsPerPage?: number staleTime?: number gcTime?: number enabled?: boolean } // Since the `cacheTime` property is an identifier and not computed, and shorthand, the codemod should change it. export function useAlgolia({ indexName, query, hitsPerPage = 10, staleTime, gcTime, enabled, }: UseAlgoliaOptions) { const queryInfo = useInfiniteQuery({ queryKey: ['algolia', indexName, query, hitsPerPage], queryFn: ({ pageParam }) => search({ indexName, query, pageParam, hitsPerPage }), initialPageParam: 0, getNextPageParam: (lastPage) => lastPage?.nextPage, staleTime, gcTime, enabled, }) const hits = queryInfo.data?.pages.map((page) => page.hits).flat() return { ...queryInfo, hits } } // Since the `cacheTime` property is an identifier and not computed, the codemod should change it. export const asIdentifierExample = () => new QueryClient({ defaultOptions: { queries: { gcTime: 1000 * 60 * 60 * 24, // 24 hours }, }, }) // Since the `cacheTime` property is a string literal and not computed, the codemod should change it. export const asStringLiteralExample = () => new QueryClient({ defaultOptions: { queries: { 'gcTime': 1000 * 60 * 60 * 24, // 24 hours }, }, }) // Since the `cacheTime` property is computed, the codemod shouldn't touch this example. export const asComputedExample = () => { const cacheTime = 'foo' return new QueryClient({ defaultOptions: { queries: { [cacheTime]: 1000 * 60 * 60 * 24, // 24 hours }, }, }) } ================================================ FILE: packages/query-codemods/src/v5/rename-properties/__testfixtures__/rename-use-error-boundary.input.tsx ================================================ import * as React from 'react' // Since the `useErrorBoundary` property is string literal and not computed, the codemod should change it. export type Type1 = { 'useErrorBoundary'?: boolean } // Since the `useErrorBoundary` property is an identifier and not computed, the codemod should change it. export type Type2 = { useErrorBoundary?: boolean } // Since the `useErrorBoundary` property is an identifier and not computed, and shorthand, the codemod should change it. export function useSomething() { const queryInfo = useSomethingElse({ useErrorBoundary, enabled, }) return queryInfo } // Since the `useErrorBoundary` property is an identifier and not computed, the codemod should change it. export const asIdentifierExample = () => { return { useErrorBoundary: true } } // Since the `useErrorBoundary` property is a string literal and not computed, the codemod should change it. export const asStringLiteralExample = () => { return { 'useErrorBoundary': true } } // Since the `useErrorBoundary` property is computed, the codemod shouldn't touch this example. export const asComputedExample = () => { const useErrorBoundary = 'foo' return { [useErrorBoundary]: false } } ================================================ FILE: packages/query-codemods/src/v5/rename-properties/__testfixtures__/rename-use-error-boundary.output.tsx ================================================ import * as React from 'react' // Since the `useErrorBoundary` property is string literal and not computed, the codemod should change it. export type Type1 = { 'throwOnError'?: boolean } // Since the `useErrorBoundary` property is an identifier and not computed, the codemod should change it. export type Type2 = { throwOnError?: boolean } // Since the `useErrorBoundary` property is an identifier and not computed, and shorthand, the codemod should change it. export function useSomething() { const queryInfo = useSomethingElse({ throwOnError, enabled, }) return queryInfo } // Since the `useErrorBoundary` property is an identifier and not computed, the codemod should change it. export const asIdentifierExample = () => { return { throwOnError: true }; } // Since the `useErrorBoundary` property is a string literal and not computed, the codemod should change it. export const asStringLiteralExample = () => { return { 'throwOnError': true }; } // Since the `useErrorBoundary` property is computed, the codemod shouldn't touch this example. export const asComputedExample = () => { const useErrorBoundary = 'foo' return { [useErrorBoundary]: false } } ================================================ FILE: packages/query-codemods/src/v5/rename-properties/__tests__/rename-properties.test.cjs ================================================ const defineTest = require('jscodeshift/dist/testUtils').defineTest defineTest(__dirname, 'rename-properties.cjs', null, 'rename-cache-time', { parser: 'tsx', }) defineTest( __dirname, 'rename-properties.cjs', null, 'rename-use-error-boundary', { parser: 'tsx', }, ) ================================================ FILE: packages/query-core/eslint.config.js ================================================ // @ts-check import rootConfig from './root.eslint.config.js' export default [...rootConfig] ================================================ FILE: packages/query-core/package.json ================================================ { "name": "@tanstack/query-core", "version": "5.89.0", "description": "The framework agnostic core that powers TanStack Query", "author": "tannerlinsley", "license": "MIT", "repository": { "type": "git", "url": "https://github.com/TanStack/query.git", "directory": "packages/query-core" }, "homepage": "https://tanstack.com/query", "funding": { "type": "github", "url": "https://github.com/sponsors/tannerlinsley" }, "scripts": { "clean": "premove ./build ./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 tsconfig.legacy.json", "test:types:ts51": "node ../../node_modules/typescript51/lib/tsc.js --build tsconfig.legacy.json", "test:types:ts52": "node ../../node_modules/typescript52/lib/tsc.js --build tsconfig.legacy.json", "test:types:ts53": "node ../../node_modules/typescript53/lib/tsc.js --build tsconfig.legacy.json", "test:types:ts54": "node ../../node_modules/typescript54/lib/tsc.js --build tsconfig.legacy.json", "test:types:ts55": "node ../../node_modules/typescript55/lib/tsc.js --build tsconfig.legacy.json", "test:types:ts56": "node ../../node_modules/typescript56/lib/tsc.js --build tsconfig.legacy.json", "test:types:ts57": "node ../../node_modules/typescript57/lib/tsc.js --build tsconfig.legacy.json", "test:types:tscurrent": "tsc --build", "test:lib": "vitest", "test:lib:dev": "pnpm run test:lib --watch", "test:build": "publint --strict && attw --pack", "build": "tsup --tsconfig tsconfig.prod.json" }, "type": "module", "types": "build/legacy/index.d.ts", "main": "build/legacy/index.cjs", "module": "build/legacy/index.js", "react-native": "src/index.ts", "exports": { ".": { "@tanstack/custom-condition": "./src/index.ts", "import": { "types": "./build/modern/index.d.ts", "default": "./build/modern/index.js" }, "require": { "types": "./build/modern/index.d.cts", "default": "./build/modern/index.cjs" } }, "./package.json": "./package.json" }, "sideEffects": false, "files": [ "build", "src", "!src/__tests__" ], "devDependencies": { "@tanstack/query-test-utils": "workspace:*", "npm-run-all2": "^5.0.0" } } ================================================ FILE: packages/query-core/tsconfig.json ================================================ { "extends": "../../tsconfig.json", "compilerOptions": { "outDir": "./dist-ts", "rootDir": "." }, "include": ["src", "*.config.*", "package.json"] } ================================================ FILE: packages/query-core/tsconfig.legacy.json ================================================ { "extends": "./tsconfig.json", "compilerOptions": { "outDir": "./dist-ts/legacy" }, "include": ["src"], "exclude": ["src/__tests__"] } ================================================ FILE: packages/query-core/tsconfig.prod.json ================================================ { "extends": "./tsconfig.json", "compilerOptions": { "incremental": false, "composite": false, "rootDir": "../../" } } ================================================ FILE: packages/query-core/tsup.config.ts ================================================ import { defineConfig } from 'tsup' import { legacyConfig, modernConfig } from './root.tsup.config.js' export default defineConfig([ modernConfig({ entry: ['src/*.ts'] }), legacyConfig({ entry: ['src/*.ts'] }), ]) ================================================ FILE: packages/query-core/vite.config.ts ================================================ import { defineConfig } from 'vitest/config' import packageJson from './package.json' export default 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', coverage: { enabled: true, provider: 'istanbul', include: ['src/**/*'] }, typecheck: { enabled: true }, restoreMocks: true, }, }) ================================================ SYMLINK: packages/query-core/root.eslint.config.js -> eslint.config.js ================================================ ================================================ SYMLINK: packages/query-core/root.tsup.config.js -> getTsupConfig.js ================================================ ================================================ FILE: packages/query-core/src/focusManager.ts ================================================ import { Subscribable } from './subscribable' import { isServer } from './utils' type Listener = (focused: boolean) => void type SetupFn = ( setFocused: (focused?: boolean) => void, ) => (() => void) | undefined export class FocusManager extends Subscribable { #focused?: boolean #cleanup?: () => void #setup: SetupFn constructor() { super() this.#setup = (onFocus) => { // addEventListener does not exist in React Native, but window does // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (!isServer && window.addEventListener) { const listener = () => onFocus() // Listen to visibilitychange window.addEventListener('visibilitychange', listener, false) return () => { // Be sure to unsubscribe if a new handler is set window.removeEventListener('visibilitychange', listener) } } return } } protected onSubscribe(): void { if (!this.#cleanup) { this.setEventListener(this.#setup) } } protected onUnsubscribe() { if (!this.hasListeners()) { this.#cleanup?.() this.#cleanup = undefined } } setEventListener(setup: SetupFn): void { this.#setup = setup this.#cleanup?.() this.#cleanup = setup((focused) => { if (typeof focused === 'boolean') { this.setFocused(focused) } else { this.onFocus() } }) } setFocused(focused?: boolean): void { const changed = this.#focused !== focused if (changed) { this.#focused = focused this.onFocus() } } onFocus(): void { const isFocused = this.isFocused() this.listeners.forEach((listener) => { listener(isFocused) }) } isFocused(): boolean { if (typeof this.#focused === 'boolean') { return this.#focused } // document global can be unavailable in react native // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition return globalThis.document?.visibilityState !== 'hidden' } } export const focusManager = new FocusManager() ================================================ FILE: packages/query-core/src/hydration.ts ================================================ import { tryResolveSync } from './thenable' import type { DefaultError, MutationKey, MutationMeta, MutationOptions, MutationScope, QueryKey, QueryMeta, QueryOptions, } from './types' import type { QueryClient } from './queryClient' import type { Query, QueryState } from './query' import type { Mutation, MutationState } from './mutation' // TYPES type TransformerFn = (data: any) => any function defaultTransformerFn(data: any): any { return data } export interface DehydrateOptions { serializeData?: TransformerFn shouldDehydrateMutation?: (mutation: Mutation) => boolean shouldDehydrateQuery?: (query: Query) => boolean shouldRedactErrors?: (error: unknown) => boolean } export interface HydrateOptions { defaultOptions?: { deserializeData?: TransformerFn queries?: QueryOptions mutations?: MutationOptions } } interface DehydratedMutation { mutationKey?: MutationKey state: MutationState meta?: MutationMeta scope?: MutationScope } interface DehydratedQuery { queryHash: string queryKey: QueryKey state: QueryState promise?: Promise meta?: QueryMeta // This is only optional because older versions of Query might have dehydrated // without it which we need to handle for backwards compatibility. // This should be changed to required in the future. dehydratedAt?: number } export interface DehydratedState { mutations: Array queries: Array } // FUNCTIONS function dehydrateMutation(mutation: Mutation): DehydratedMutation { return { mutationKey: mutation.options.mutationKey, state: mutation.state, ...(mutation.options.scope && { scope: mutation.options.scope }), ...(mutation.meta && { meta: mutation.meta }), } } // Most config is not dehydrated but instead meant to configure again when // consuming the de/rehydrated data, typically with useQuery on the client. // Sometimes it might make sense to prefetch data on the server and include // in the html-payload, but not consume it on the initial render. function dehydrateQuery( query: Query, serializeData: TransformerFn, shouldRedactErrors: (error: unknown) => boolean, ): DehydratedQuery { return { dehydratedAt: Date.now(), state: { ...query.state, ...(query.state.data !== undefined && { data: serializeData(query.state.data), }), }, queryKey: query.queryKey, queryHash: query.queryHash, ...(query.state.status === 'pending' && { promise: query.promise?.then(serializeData).catch((error) => { if (!shouldRedactErrors(error)) { // Reject original error if it should not be redacted return Promise.reject(error) } // If not in production, log original error before rejecting redacted error if (process.env.NODE_ENV !== 'production') { console.error( `A query that was dehydrated as pending ended up rejecting. [${query.queryHash}]: ${error}; The error will be redacted in production builds`, ) } return Promise.reject(new Error('redacted')) }), }), ...(query.meta && { meta: query.meta }), } } export function defaultShouldDehydrateMutation(mutation: Mutation) { return mutation.state.isPaused } export function defaultShouldDehydrateQuery(query: Query) { return query.state.status === 'success' } function defaultShouldRedactErrors(_: unknown) { return true } export function dehydrate( client: QueryClient, options: DehydrateOptions = {}, ): DehydratedState { const filterMutation = options.shouldDehydrateMutation ?? client.getDefaultOptions().dehydrate?.shouldDehydrateMutation ?? defaultShouldDehydrateMutation const mutations = client .getMutationCache() .getAll() .flatMap((mutation) => filterMutation(mutation) ? [dehydrateMutation(mutation)] : [], ) const filterQuery = options.shouldDehydrateQuery ?? client.getDefaultOptions().dehydrate?.shouldDehydrateQuery ?? defaultShouldDehydrateQuery const shouldRedactErrors = options.shouldRedactErrors ?? client.getDefaultOptions().dehydrate?.shouldRedactErrors ?? defaultShouldRedactErrors const serializeData = options.serializeData ?? client.getDefaultOptions().dehydrate?.serializeData ?? defaultTransformerFn const queries = client .getQueryCache() .getAll() .flatMap((query) => filterQuery(query) ? [dehydrateQuery(query, serializeData, shouldRedactErrors)] : [], ) return { mutations, queries } } export function hydrate( client: QueryClient, dehydratedState: unknown, options?: HydrateOptions, ): void { if (typeof dehydratedState !== 'object' || dehydratedState === null) { return } const mutationCache = client.getMutationCache() const queryCache = client.getQueryCache() const deserializeData = options?.defaultOptions?.deserializeData ?? client.getDefaultOptions().hydrate?.deserializeData ?? defaultTransformerFn // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition const mutations = (dehydratedState as DehydratedState).mutations || [] // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition const queries = (dehydratedState as DehydratedState).queries || [] mutations.forEach(({ state, ...mutationOptions }) => { mutationCache.build( client, { ...client.getDefaultOptions().hydrate?.mutations, ...options?.defaultOptions?.mutations, ...mutationOptions, }, state, ) }) queries.forEach( ({ queryKey, state, queryHash, meta, promise, dehydratedAt }) => { const syncData = promise ? tryResolveSync(promise) : undefined const rawData = state.data === undefined ? syncData?.data : state.data const data = rawData === undefined ? rawData : deserializeData(rawData) let query = queryCache.get(queryHash) const existingQueryIsPending = query?.state.status === 'pending' const existingQueryIsFetching = query?.state.fetchStatus === 'fetching' // Do not hydrate if an existing query exists with newer data if (query) { const hasNewerSyncData = syncData && // We only need this undefined check to handle older dehydration // payloads that might not have dehydratedAt dehydratedAt !== undefined && dehydratedAt > query.state.dataUpdatedAt if ( state.dataUpdatedAt > query.state.dataUpdatedAt || hasNewerSyncData ) { // omit fetchStatus from dehydrated state // so that query stays in its current fetchStatus const { fetchStatus: _ignored, ...serializedState } = state query.setState({ ...serializedState, data, }) } } else { // Restore query query = queryCache.build( client, { ...client.getDefaultOptions().hydrate?.queries, ...options?.defaultOptions?.queries, queryKey, queryHash, meta, }, // Reset fetch status to idle to avoid // query being stuck in fetching state upon hydration { ...state, data, fetchStatus: 'idle', status: data !== undefined ? 'success' : state.status, }, ) } if ( promise && !existingQueryIsPending && !existingQueryIsFetching && // Only hydrate if dehydration is newer than any existing data, // this is always true for new queries (dehydratedAt === undefined || dehydratedAt > query.state.dataUpdatedAt) ) { // This doesn't actually fetch - it just creates a retryer // which will re-use the passed `initialPromise` // Note that we need to call these even when data was synchronously // available, as we still need to set up the retryer void query.fetch(undefined, { // RSC transformed promises are not thenable initialPromise: Promise.resolve(promise).then(deserializeData), }) } }, ) } ================================================ FILE: packages/query-core/src/index.ts ================================================ /* istanbul ignore file */ export { focusManager } from './focusManager' export { defaultShouldDehydrateMutation, defaultShouldDehydrateQuery, dehydrate, hydrate, } from './hydration' export { InfiniteQueryObserver } from './infiniteQueryObserver' export { MutationCache } from './mutationCache' export type { MutationCacheNotifyEvent } from './mutationCache' export { MutationObserver } from './mutationObserver' export { defaultScheduler, notifyManager } from './notifyManager' export { onlineManager } from './onlineManager' export { QueriesObserver } from './queriesObserver' export { QueryCache } from './queryCache' export type { QueryCacheNotifyEvent } from './queryCache' export { QueryClient } from './queryClient' export { QueryObserver } from './queryObserver' export { CancelledError, isCancelledError } from './retryer' export { timeoutManager, type ManagedTimerId, type TimeoutCallback, type TimeoutProvider, } from './timeoutManager' export { hashKey, isServer, keepPreviousData, matchMutation, matchQuery, noop, partialMatchKey, replaceEqualDeep, shouldThrowError, skipToken, } from './utils' export type { MutationFilters, QueryFilters, SkipToken, Updater } from './utils' export { streamedQuery as experimental_streamedQuery } from './streamedQuery' // Types export type { DehydratedState, DehydrateOptions, HydrateOptions, } from './hydration' export { Mutation } from './mutation' export type { MutationState } from './mutation' export type { QueriesObserverOptions } from './queriesObserver' export { Query } from './query' export type { QueryState } from './query' export * from './types' ================================================ FILE: packages/query-core/src/infiniteQueryBehavior.ts ================================================ import { addToEnd, addToStart, ensureQueryFn } from './utils' import type { QueryBehavior } from './query' import type { InfiniteData, InfiniteQueryPageParamsOptions, OmitKeyof, QueryFunctionContext, QueryKey, } from './types' export function infiniteQueryBehavior( pages?: number, ): QueryBehavior> { return { onFetch: (context, query) => { const options = context.options as InfiniteQueryPageParamsOptions const direction = context.fetchOptions?.meta?.fetchMore?.direction const oldPages = context.state.data?.pages || [] const oldPageParams = context.state.data?.pageParams || [] let result: InfiniteData = { pages: [], pageParams: [] } let currentPage = 0 const fetchFn = async () => { let cancelled = false const addSignalProperty = (object: unknown) => { Object.defineProperty(object, 'signal', { enumerable: true, get: () => { if (context.signal.aborted) { cancelled = true } else { context.signal.addEventListener('abort', () => { cancelled = true }) } return context.signal }, }) } const queryFn = ensureQueryFn(context.options, context.fetchOptions) // Create function to fetch a page const fetchPage = async ( data: InfiniteData, param: unknown, previous?: boolean, ): Promise> => { if (cancelled) { return Promise.reject() } if (param == null && data.pages.length) { return Promise.resolve(data) } const createQueryFnContext = () => { const queryFnContext: OmitKeyof< QueryFunctionContext, 'signal' > = { client: context.client, queryKey: context.queryKey, pageParam: param, direction: previous ? 'backward' : 'forward', meta: context.options.meta, } addSignalProperty(queryFnContext) return queryFnContext as QueryFunctionContext } const queryFnContext = createQueryFnContext() const page = await queryFn(queryFnContext) const { maxPages } = context.options const addTo = previous ? addToStart : addToEnd return { pages: addTo(data.pages, page, maxPages), pageParams: addTo(data.pageParams, param, maxPages), } } // fetch next / previous page? if (direction && oldPages.length) { const previous = direction === 'backward' const pageParamFn = previous ? getPreviousPageParam : getNextPageParam const oldData = { pages: oldPages, pageParams: oldPageParams, } const param = pageParamFn(options, oldData) result = await fetchPage(oldData, param, previous) } else { const remainingPages = pages ?? oldPages.length // Fetch all pages do { const param = currentPage === 0 ? (oldPageParams[0] ?? options.initialPageParam) : getNextPageParam(options, result) if (currentPage > 0 && param == null) { break } result = await fetchPage(result, param) currentPage++ } while (currentPage < remainingPages) } return result } if (context.options.persister) { context.fetchFn = () => { return context.options.persister?.( fetchFn as any, { client: context.client, queryKey: context.queryKey, meta: context.options.meta, signal: context.signal, }, query, ) } } else { context.fetchFn = fetchFn } }, } } function getNextPageParam( options: InfiniteQueryPageParamsOptions, { pages, pageParams }: InfiniteData, ): unknown | undefined { const lastIndex = pages.length - 1 return pages.length > 0 ? options.getNextPageParam( pages[lastIndex], pages, pageParams[lastIndex], pageParams, ) : undefined } function getPreviousPageParam( options: InfiniteQueryPageParamsOptions, { pages, pageParams }: InfiniteData, ): unknown | undefined { return pages.length > 0 ? options.getPreviousPageParam?.(pages[0], pages, pageParams[0], pageParams) : undefined } /** * Checks if there is a next page. */ export function hasNextPage( options: InfiniteQueryPageParamsOptions, data?: InfiniteData, ): boolean { if (!data) return false return getNextPageParam(options, data) != null } /** * Checks if there is a previous page. */ export function hasPreviousPage( options: InfiniteQueryPageParamsOptions, data?: InfiniteData, ): boolean { if (!data || !options.getPreviousPageParam) return false return getPreviousPageParam(options, data) != null } ================================================ FILE: packages/query-core/src/infiniteQueryObserver.ts ================================================ import { QueryObserver } from './queryObserver' import { hasNextPage, hasPreviousPage, infiniteQueryBehavior, } from './infiniteQueryBehavior' import type { Subscribable } from './subscribable' import type { DefaultError, DefaultedInfiniteQueryObserverOptions, FetchNextPageOptions, FetchPreviousPageOptions, InfiniteData, InfiniteQueryObserverBaseResult, InfiniteQueryObserverOptions, InfiniteQueryObserverResult, QueryKey, } from './types' import type { QueryClient } from './queryClient' import type { Query } from './query' type InfiniteQueryObserverListener = ( result: InfiniteQueryObserverResult, ) => void export class InfiniteQueryObserver< TQueryFnData = unknown, TError = DefaultError, TData = InfiniteData, TQueryKey extends QueryKey = QueryKey, TPageParam = unknown, > extends QueryObserver< TQueryFnData, TError, TData, InfiniteData, TQueryKey > { // Type override subscribe!: Subscribable< InfiniteQueryObserverListener >['subscribe'] // Type override getCurrentResult!: ReplaceReturnType< QueryObserver< TQueryFnData, TError, TData, InfiniteData, TQueryKey >['getCurrentResult'], InfiniteQueryObserverResult > // Type override protected fetch!: ReplaceReturnType< QueryObserver< TQueryFnData, TError, TData, InfiniteData, TQueryKey >['fetch'], Promise> > constructor( client: QueryClient, options: InfiniteQueryObserverOptions< TQueryFnData, TError, TData, TQueryKey, TPageParam >, ) { super(client, options) } protected bindMethods(): void { super.bindMethods() this.fetchNextPage = this.fetchNextPage.bind(this) this.fetchPreviousPage = this.fetchPreviousPage.bind(this) } setOptions( options: InfiniteQueryObserverOptions< TQueryFnData, TError, TData, TQueryKey, TPageParam >, ): void { super.setOptions({ ...options, behavior: infiniteQueryBehavior(), }) } getOptimisticResult( options: DefaultedInfiniteQueryObserverOptions< TQueryFnData, TError, TData, TQueryKey, TPageParam >, ): InfiniteQueryObserverResult { options.behavior = infiniteQueryBehavior() return super.getOptimisticResult(options) as InfiniteQueryObserverResult< TData, TError > } fetchNextPage( options?: FetchNextPageOptions, ): Promise> { return this.fetch({ ...options, meta: { fetchMore: { direction: 'forward' }, }, }) } fetchPreviousPage( options?: FetchPreviousPageOptions, ): Promise> { return this.fetch({ ...options, meta: { fetchMore: { direction: 'backward' }, }, }) } protected createResult( query: Query< TQueryFnData, TError, InfiniteData, TQueryKey >, options: InfiniteQueryObserverOptions< TQueryFnData, TError, TData, TQueryKey, TPageParam >, ): InfiniteQueryObserverResult { const { state } = query const parentResult = super.createResult(query, options) const { isFetching, isRefetching, isError, isRefetchError } = parentResult const fetchDirection = state.fetchMeta?.fetchMore?.direction const isFetchNextPageError = isError && fetchDirection === 'forward' const isFetchingNextPage = isFetching && fetchDirection === 'forward' const isFetchPreviousPageError = isError && fetchDirection === 'backward' const isFetchingPreviousPage = isFetching && fetchDirection === 'backward' const result: InfiniteQueryObserverBaseResult = { ...parentResult, fetchNextPage: this.fetchNextPage, fetchPreviousPage: this.fetchPreviousPage, hasNextPage: hasNextPage(options, state.data), hasPreviousPage: hasPreviousPage(options, state.data), isFetchNextPageError, isFetchingNextPage, isFetchPreviousPageError, isFetchingPreviousPage, isRefetchError: isRefetchError && !isFetchNextPageError && !isFetchPreviousPageError, isRefetching: isRefetching && !isFetchingNextPage && !isFetchingPreviousPage, } return result as InfiniteQueryObserverResult } } type ReplaceReturnType< TFunction extends (...args: Array) => unknown, TReturn, > = (...args: Parameters) => TReturn ================================================ FILE: packages/query-core/src/mutation.ts ================================================ import { notifyManager } from './notifyManager' import { Removable } from './removable' import { createRetryer } from './retryer' import type { DefaultError, MutationFunctionContext, MutationMeta, MutationOptions, MutationStatus, } from './types' import type { MutationCache } from './mutationCache' import type { MutationObserver } from './mutationObserver' import type { Retryer } from './retryer' import type { QueryClient } from './queryClient' // TYPES interface MutationConfig { client: QueryClient mutationId: number mutationCache: MutationCache options: MutationOptions state?: MutationState } export interface MutationState< TData = unknown, TError = DefaultError, TVariables = unknown, TOnMutateResult = unknown, > { context: TOnMutateResult | undefined data: TData | undefined error: TError | null failureCount: number failureReason: TError | null isPaused: boolean status: MutationStatus variables: TVariables | undefined submittedAt: number } interface FailedAction { type: 'failed' failureCount: number error: TError | null } interface PendingAction { type: 'pending' isPaused: boolean variables?: TVariables context?: TOnMutateResult } interface SuccessAction { type: 'success' data: TData } interface ErrorAction { type: 'error' error: TError } interface PauseAction { type: 'pause' } interface ContinueAction { type: 'continue' } export type Action = | ContinueAction | ErrorAction | FailedAction | PendingAction | PauseAction | SuccessAction // CLASS export class Mutation< TData = unknown, TError = DefaultError, TVariables = unknown, TOnMutateResult = unknown, > extends Removable { state: MutationState options!: MutationOptions readonly mutationId: number #client: QueryClient #observers: Array< MutationObserver > #mutationCache: MutationCache #retryer?: Retryer constructor( config: MutationConfig, ) { super() this.#client = config.client this.mutationId = config.mutationId this.#mutationCache = config.mutationCache this.#observers = [] this.state = config.state || getDefaultState() this.setOptions(config.options) this.scheduleGc() } setOptions( options: MutationOptions, ): void { this.options = options this.updateGcTime(this.options.gcTime) } get meta(): MutationMeta | undefined { return this.options.meta } addObserver(observer: MutationObserver): void { if (!this.#observers.includes(observer)) { this.#observers.push(observer) // Stop the mutation from being garbage collected this.clearGcTimeout() this.#mutationCache.notify({ type: 'observerAdded', mutation: this, observer, }) } } removeObserver(observer: MutationObserver): void { this.#observers = this.#observers.filter((x) => x !== observer) this.scheduleGc() this.#mutationCache.notify({ type: 'observerRemoved', mutation: this, observer, }) } protected optionalRemove() { if (!this.#observers.length) { if (this.state.status === 'pending') { this.scheduleGc() } else { this.#mutationCache.remove(this) } } } continue(): Promise { return ( this.#retryer?.continue() ?? // continuing a mutation assumes that variables are set, mutation must have been dehydrated before this.execute(this.state.variables!) ) } async execute(variables: TVariables): Promise { const onContinue = () => { this.#dispatch({ type: 'continue' }) } const mutationFnContext = { client: this.#client, meta: this.options.meta, mutationKey: this.options.mutationKey, } satisfies MutationFunctionContext this.#retryer = createRetryer({ fn: () => { if (!this.options.mutationFn) { return Promise.reject(new Error('No mutationFn found')) } return this.options.mutationFn(variables, mutationFnContext) }, onFail: (failureCount, error) => { this.#dispatch({ type: 'failed', failureCount, error }) }, onPause: () => { this.#dispatch({ type: 'pause' }) }, onContinue, retry: this.options.retry ?? 0, retryDelay: this.options.retryDelay, networkMode: this.options.networkMode, canRun: () => this.#mutationCache.canRun(this), }) const restored = this.state.status === 'pending' const isPaused = !this.#retryer.canStart() try { if (restored) { // Dispatch continue action to unpause restored mutation onContinue() } else { this.#dispatch({ type: 'pending', variables, isPaused }) // Notify cache callback await this.#mutationCache.config.onMutate?.( variables, this as Mutation, mutationFnContext, ) const context = await this.options.onMutate?.( variables, mutationFnContext, ) if (context !== this.state.context) { this.#dispatch({ type: 'pending', context, variables, isPaused, }) } } const data = await this.#retryer.start() // Notify cache callback await this.#mutationCache.config.onSuccess?.( data, variables, this.state.context, this as Mutation, mutationFnContext, ) await this.options.onSuccess?.( data, variables, this.state.context, mutationFnContext, ) // Notify cache callback await this.#mutationCache.config.onSettled?.( data, null, this.state.variables, this.state.context, this as Mutation, mutationFnContext, ) await this.options.onSettled?.( data, null, variables, this.state.context, mutationFnContext, ) this.#dispatch({ type: 'success', data }) return data } catch (error) { try { // Notify cache callback await this.#mutationCache.config.onError?.( error as any, variables, this.state.context, this as Mutation, mutationFnContext, ) await this.options.onError?.( error as TError, variables, this.state.context, mutationFnContext, ) // Notify cache callback await this.#mutationCache.config.onSettled?.( undefined, error as any, this.state.variables, this.state.context, this as Mutation, mutationFnContext, ) await this.options.onSettled?.( undefined, error as TError, variables, this.state.context, mutationFnContext, ) throw error } finally { this.#dispatch({ type: 'error', error: error as TError }) } } finally { this.#mutationCache.runNext(this) } } #dispatch(action: Action): void { const reducer = ( state: MutationState, ): MutationState => { switch (action.type) { case 'failed': return { ...state, failureCount: action.failureCount, failureReason: action.error, } case 'pause': return { ...state, isPaused: true, } case 'continue': return { ...state, isPaused: false, } case 'pending': return { ...state, context: action.context, data: undefined, failureCount: 0, failureReason: null, error: null, isPaused: action.isPaused, status: 'pending', variables: action.variables, submittedAt: Date.now(), } case 'success': return { ...state, data: action.data, failureCount: 0, failureReason: null, error: null, status: 'success', isPaused: false, } case 'error': return { ...state, data: undefined, error: action.error, failureCount: state.failureCount + 1, failureReason: action.error, isPaused: false, status: 'error', } } } this.state = reducer(this.state) notifyManager.batch(() => { this.#observers.forEach((observer) => { observer.onMutationUpdate(action) }) this.#mutationCache.notify({ mutation: this, type: 'updated', action, }) }) } } export function getDefaultState< TData, TError, TVariables, TOnMutateResult, >(): MutationState { return { context: undefined, data: undefined, error: null, failureCount: 0, failureReason: null, isPaused: false, status: 'idle', variables: undefined, submittedAt: 0, } } ================================================ FILE: packages/query-core/src/mutationCache.ts ================================================ import { notifyManager } from './notifyManager' import { Mutation } from './mutation' import { matchMutation, noop } from './utils' import { Subscribable } from './subscribable' import type { MutationObserver } from './mutationObserver' import type { DefaultError, MutationFunctionContext, MutationOptions, NotifyEvent, } from './types' import type { QueryClient } from './queryClient' import type { Action, MutationState } from './mutation' import type { MutationFilters } from './utils' // TYPES interface MutationCacheConfig { onError?: ( error: DefaultError, variables: unknown, onMutateResult: unknown, mutation: Mutation, context: MutationFunctionContext, ) => Promise | unknown onSuccess?: ( data: unknown, variables: unknown, onMutateResult: unknown, mutation: Mutation, context: MutationFunctionContext, ) => Promise | unknown onMutate?: ( variables: unknown, mutation: Mutation, context: MutationFunctionContext, ) => Promise | unknown onSettled?: ( data: unknown | undefined, error: DefaultError | null, variables: unknown, onMutateResult: unknown, mutation: Mutation, context: MutationFunctionContext, ) => Promise | unknown } interface NotifyEventMutationAdded extends NotifyEvent { type: 'added' mutation: Mutation } interface NotifyEventMutationRemoved extends NotifyEvent { type: 'removed' mutation: Mutation } interface NotifyEventMutationObserverAdded extends NotifyEvent { type: 'observerAdded' mutation: Mutation observer: MutationObserver } interface NotifyEventMutationObserverRemoved extends NotifyEvent { type: 'observerRemoved' mutation: Mutation observer: MutationObserver } interface NotifyEventMutationObserverOptionsUpdated extends NotifyEvent { type: 'observerOptionsUpdated' mutation?: Mutation observer: MutationObserver } interface NotifyEventMutationUpdated extends NotifyEvent { type: 'updated' mutation: Mutation action: Action } export type MutationCacheNotifyEvent = | NotifyEventMutationAdded | NotifyEventMutationRemoved | NotifyEventMutationObserverAdded | NotifyEventMutationObserverRemoved | NotifyEventMutationObserverOptionsUpdated | NotifyEventMutationUpdated type MutationCacheListener = (event: MutationCacheNotifyEvent) => void // CLASS export class MutationCache extends Subscribable { #mutations: Set> #scopes: Map>> #mutationId: number constructor(public config: MutationCacheConfig = {}) { super() this.#mutations = new Set() this.#scopes = new Map() this.#mutationId = 0 } build( client: QueryClient, options: MutationOptions, state?: MutationState, ): Mutation { const mutation = new Mutation({ client, mutationCache: this, mutationId: ++this.#mutationId, options: client.defaultMutationOptions(options), state, }) this.add(mutation) return mutation } add(mutation: Mutation): void { this.#mutations.add(mutation) const scope = scopeFor(mutation) if (typeof scope === 'string') { const scopedMutations = this.#scopes.get(scope) if (scopedMutations) { scopedMutations.push(mutation) } else { this.#scopes.set(scope, [mutation]) } } this.notify({ type: 'added', mutation }) } remove(mutation: Mutation): void { if (this.#mutations.delete(mutation)) { const scope = scopeFor(mutation) if (typeof scope === 'string') { const scopedMutations = this.#scopes.get(scope) if (scopedMutations) { if (scopedMutations.length > 1) { const index = scopedMutations.indexOf(mutation) if (index !== -1) { scopedMutations.splice(index, 1) } } else if (scopedMutations[0] === mutation) { this.#scopes.delete(scope) } } } } // Currently we notify the removal even if the mutation was already removed. // Consider making this an error or not notifying of the removal depending on the desired semantics. this.notify({ type: 'removed', mutation }) } canRun(mutation: Mutation): boolean { const scope = scopeFor(mutation) if (typeof scope === 'string') { const mutationsWithSameScope = this.#scopes.get(scope) const firstPendingMutation = mutationsWithSameScope?.find( (m) => m.state.status === 'pending', ) // we can run if there is no current pending mutation (start use-case) // or if WE are the first pending mutation (continue use-case) return !firstPendingMutation || firstPendingMutation === mutation } else { // For unscoped mutations there are never any pending mutations in front of the // current mutation return true } } runNext(mutation: Mutation): Promise { const scope = scopeFor(mutation) if (typeof scope === 'string') { const foundMutation = this.#scopes .get(scope) ?.find((m) => m !== mutation && m.state.isPaused) return foundMutation?.continue() ?? Promise.resolve() } else { return Promise.resolve() } } clear(): void { notifyManager.batch(() => { this.#mutations.forEach((mutation) => { this.notify({ type: 'removed', mutation }) }) this.#mutations.clear() this.#scopes.clear() }) } getAll(): Array { return Array.from(this.#mutations) } find< TData = unknown, TError = DefaultError, TVariables = any, TOnMutateResult = unknown, >( filters: MutationFilters, ): Mutation | undefined { const defaultedFilters = { exact: true, ...filters } return this.getAll().find((mutation) => matchMutation(defaultedFilters, mutation), ) as Mutation | undefined } findAll(filters: MutationFilters = {}): Array { return this.getAll().filter((mutation) => matchMutation(filters, mutation)) } notify(event: MutationCacheNotifyEvent) { notifyManager.batch(() => { this.listeners.forEach((listener) => { listener(event) }) }) } resumePausedMutations(): Promise { const pausedMutations = this.getAll().filter((x) => x.state.isPaused) return notifyManager.batch(() => Promise.all( pausedMutations.map((mutation) => mutation.continue().catch(noop)), ), ) } } function scopeFor(mutation: Mutation) { return mutation.options.scope?.id } ================================================ FILE: packages/query-core/src/mutationObserver.ts ================================================ import { getDefaultState } from './mutation' import { notifyManager } from './notifyManager' import { Subscribable } from './subscribable' import { hashKey, shallowEqualObjects } from './utils' import type { QueryClient } from './queryClient' import type { DefaultError, MutateOptions, MutationFunctionContext, MutationObserverOptions, MutationObserverResult, } from './types' import type { Action, Mutation } from './mutation' // TYPES type MutationObserverListener = ( result: MutationObserverResult, ) => void // CLASS export class MutationObserver< TData = unknown, TError = DefaultError, TVariables = void, TOnMutateResult = unknown, > extends Subscribable< MutationObserverListener > { options!: MutationObserverOptions #client: QueryClient #currentResult: MutationObserverResult< TData, TError, TVariables, TOnMutateResult > = undefined! #currentMutation?: Mutation #mutateOptions?: MutateOptions constructor( client: QueryClient, options: MutationObserverOptions< TData, TError, TVariables, TOnMutateResult >, ) { super() this.#client = client this.setOptions(options) this.bindMethods() this.#updateResult() } protected bindMethods(): void { this.mutate = this.mutate.bind(this) this.reset = this.reset.bind(this) } setOptions( options: MutationObserverOptions< TData, TError, TVariables, TOnMutateResult >, ) { const prevOptions = this.options as | MutationObserverOptions | undefined this.options = this.#client.defaultMutationOptions(options) if (!shallowEqualObjects(this.options, prevOptions)) { this.#client.getMutationCache().notify({ type: 'observerOptionsUpdated', mutation: this.#currentMutation, observer: this, }) } if ( prevOptions?.mutationKey && this.options.mutationKey && hashKey(prevOptions.mutationKey) !== hashKey(this.options.mutationKey) ) { this.reset() } else if (this.#currentMutation?.state.status === 'pending') { this.#currentMutation.setOptions(this.options) } } protected onUnsubscribe(): void { if (!this.hasListeners()) { this.#currentMutation?.removeObserver(this) } } onMutationUpdate( action: Action, ): void { this.#updateResult() this.#notify(action) } getCurrentResult(): MutationObserverResult< TData, TError, TVariables, TOnMutateResult > { return this.#currentResult } reset(): void { // reset needs to remove the observer from the mutation because there is no way to "get it back" // another mutate call will yield a new mutation! this.#currentMutation?.removeObserver(this) this.#currentMutation = undefined this.#updateResult() this.#notify() } mutate( variables: TVariables, options?: MutateOptions, ): Promise { this.#mutateOptions = options this.#currentMutation?.removeObserver(this) this.#currentMutation = this.#client .getMutationCache() .build(this.#client, this.options) this.#currentMutation.addObserver(this) return this.#currentMutation.execute(variables) } #updateResult(): void { const state = this.#currentMutation?.state ?? getDefaultState() this.#currentResult = { ...state, isPending: state.status === 'pending', isSuccess: state.status === 'success', isError: state.status === 'error', isIdle: state.status === 'idle', mutate: this.mutate, reset: this.reset, } as MutationObserverResult } #notify(action?: Action): void { notifyManager.batch(() => { // First trigger the mutate callbacks if (this.#mutateOptions && this.hasListeners()) { const variables = this.#currentResult.variables! const onMutateResult = this.#currentResult.context const context = { client: this.#client, meta: this.options.meta, mutationKey: this.options.mutationKey, } satisfies MutationFunctionContext if (action?.type === 'success') { this.#mutateOptions.onSuccess?.( action.data, variables, onMutateResult, context, ) this.#mutateOptions.onSettled?.( action.data, null, variables, onMutateResult, context, ) } else if (action?.type === 'error') { this.#mutateOptions.onError?.( action.error, variables, onMutateResult, context, ) this.#mutateOptions.onSettled?.( undefined, action.error, variables, onMutateResult, context, ) } } // Then trigger the listeners this.listeners.forEach((listener) => { listener(this.#currentResult) }) }) } } ================================================ FILE: packages/query-core/src/notifyManager.ts ================================================ // TYPES import { systemSetTimeoutZero } from './timeoutManager' type NotifyCallback = () => void type NotifyFunction = (callback: () => void) => void type BatchNotifyFunction = (callback: () => void) => void type BatchCallsCallback> = (...args: T) => void type ScheduleFunction = (callback: () => void) => void export const defaultScheduler: ScheduleFunction = systemSetTimeoutZero export function createNotifyManager() { let queue: Array = [] let transactions = 0 let notifyFn: NotifyFunction = (callback) => { callback() } let batchNotifyFn: BatchNotifyFunction = (callback: () => void) => { callback() } let scheduleFn = defaultScheduler const schedule = (callback: NotifyCallback): void => { if (transactions) { queue.push(callback) } else { scheduleFn(() => { notifyFn(callback) }) } } const flush = (): void => { const originalQueue = queue queue = [] if (originalQueue.length) { scheduleFn(() => { batchNotifyFn(() => { originalQueue.forEach((callback) => { notifyFn(callback) }) }) }) } } return { batch: (callback: () => T): T => { let result transactions++ try { result = callback() } finally { transactions-- if (!transactions) { flush() } } return result }, /** * All calls to the wrapped function will be batched. */ batchCalls: >( callback: BatchCallsCallback, ): BatchCallsCallback => { return (...args) => { schedule(() => { callback(...args) }) } }, schedule, /** * Use this method to set a custom notify function. * This can be used to for example wrap notifications with `React.act` while running tests. */ setNotifyFunction: (fn: NotifyFunction) => { notifyFn = fn }, /** * Use this method to set a custom function to batch notifications together into a single tick. * By default React Query will use the batch function provided by ReactDOM or React Native. */ setBatchNotifyFunction: (fn: BatchNotifyFunction) => { batchNotifyFn = fn }, setScheduler: (fn: ScheduleFunction) => { scheduleFn = fn }, } as const } // SINGLETON export const notifyManager = createNotifyManager() ================================================ FILE: packages/query-core/src/onlineManager.ts ================================================ import { Subscribable } from './subscribable' import { isServer } from './utils' type Listener = (online: boolean) => void type SetupFn = (setOnline: Listener) => (() => void) | undefined export class OnlineManager extends Subscribable { #online = true #cleanup?: () => void #setup: SetupFn constructor() { super() this.#setup = (onOnline) => { // addEventListener does not exist in React Native, but window does // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (!isServer && window.addEventListener) { const onlineListener = () => onOnline(true) const offlineListener = () => onOnline(false) // Listen to online window.addEventListener('online', onlineListener, false) window.addEventListener('offline', offlineListener, false) return () => { // Be sure to unsubscribe if a new handler is set window.removeEventListener('online', onlineListener) window.removeEventListener('offline', offlineListener) } } return } } protected onSubscribe(): void { if (!this.#cleanup) { this.setEventListener(this.#setup) } } protected onUnsubscribe() { if (!this.hasListeners()) { this.#cleanup?.() this.#cleanup = undefined } } setEventListener(setup: SetupFn): void { this.#setup = setup this.#cleanup?.() this.#cleanup = setup(this.setOnline.bind(this)) } setOnline(online: boolean): void { const changed = this.#online !== online if (changed) { this.#online = online this.listeners.forEach((listener) => { listener(online) }) } } isOnline(): boolean { return this.#online } } export const onlineManager = new OnlineManager() ================================================ FILE: packages/query-core/src/queriesObserver.ts ================================================ import { notifyManager } from './notifyManager' import { QueryObserver } from './queryObserver' import { Subscribable } from './subscribable' import { replaceEqualDeep, shallowEqualObjects } from './utils' import type { DefaultedQueryObserverOptions, QueryObserverOptions, QueryObserverResult, } from './types' import type { QueryClient } from './queryClient' function difference(array1: Array, array2: Array): Array { const excludeSet = new Set(array2) return array1.filter((x) => !excludeSet.has(x)) } function replaceAt(array: Array, index: number, value: T): Array { const copy = array.slice(0) copy[index] = value return copy } type QueriesObserverListener = (result: Array) => void type CombineFn = ( result: Array, ) => TCombinedResult export interface QueriesObserverOptions< TCombinedResult = Array, > { combine?: CombineFn } export class QueriesObserver< TCombinedResult = Array, > extends Subscribable { #client: QueryClient #result!: Array #queries: Array #options?: QueriesObserverOptions #observers: Array #combinedResult?: TCombinedResult #lastCombine?: CombineFn #lastResult?: Array #observerMatches: Array = [] constructor( client: QueryClient, queries: Array>, options?: QueriesObserverOptions, ) { super() this.#client = client this.#options = options this.#queries = [] this.#observers = [] this.#result = [] this.setQueries(queries) } protected onSubscribe(): void { if (this.listeners.size === 1) { this.#observers.forEach((observer) => { observer.subscribe((result) => { this.#onUpdate(observer, result) }) }) } } protected onUnsubscribe(): void { if (!this.listeners.size) { this.destroy() } } destroy(): void { this.listeners = new Set() this.#observers.forEach((observer) => { observer.destroy() }) } setQueries( queries: Array, options?: QueriesObserverOptions, ): void { this.#queries = queries this.#options = options if (process.env.NODE_ENV !== 'production') { const queryHashes = queries.map( (query) => this.#client.defaultQueryOptions(query).queryHash, ) if (new Set(queryHashes).size !== queryHashes.length) { console.warn( '[QueriesObserver]: Duplicate Queries found. This might result in unexpected behavior.', ) } } notifyManager.batch(() => { const prevObservers = this.#observers const newObserverMatches = this.#findMatchingObservers(this.#queries) this.#observerMatches = newObserverMatches // set options for the new observers to notify of changes newObserverMatches.forEach((match) => match.observer.setOptions(match.defaultedQueryOptions), ) const newObservers = newObserverMatches.map((match) => match.observer) const newResult = newObservers.map((observer) => observer.getCurrentResult(), ) const hasLengthChange = prevObservers.length !== newObservers.length const hasIndexChange = newObservers.some( (observer, index) => observer !== prevObservers[index], ) const hasStructuralChange = hasLengthChange || hasIndexChange const hasResultChange = hasStructuralChange ? true : newResult.some((result, index) => { const prev = this.#result[index] return !prev || !shallowEqualObjects(result, prev) }) if (!hasStructuralChange && !hasResultChange) return if (hasStructuralChange) { this.#observers = newObservers } this.#result = newResult if (!this.hasListeners()) return if (hasStructuralChange) { difference(prevObservers, newObservers).forEach((observer) => { observer.destroy() }) difference(newObservers, prevObservers).forEach((observer) => { observer.subscribe((result) => { this.#onUpdate(observer, result) }) }) } this.#notify() }) } getCurrentResult(): Array { return this.#result } getQueries() { return this.#observers.map((observer) => observer.getCurrentQuery()) } getObservers() { return this.#observers } getOptimisticResult( queries: Array, combine: CombineFn | undefined, ): [ rawResult: Array, combineResult: (r?: Array) => TCombinedResult, trackResult: () => Array, ] { const matches = this.#findMatchingObservers(queries) const result = matches.map((match) => match.observer.getOptimisticResult(match.defaultedQueryOptions), ) return [ result, (r?: Array) => { return this.#combineResult(r ?? result, combine) }, () => { return this.#trackResult(result, matches) }, ] } #trackResult( result: Array, matches: Array, ) { return matches.map((match, index) => { const observerResult = result[index]! return !match.defaultedQueryOptions.notifyOnChangeProps ? match.observer.trackResult(observerResult, (accessedProp) => { // track property on all observers to ensure proper (synchronized) tracking (#7000) matches.forEach((m) => { m.observer.trackProp(accessedProp) }) }) : observerResult }) } #combineResult( input: Array, combine: CombineFn | undefined, ): TCombinedResult { if (combine) { if ( !this.#combinedResult || this.#result !== this.#lastResult || combine !== this.#lastCombine ) { this.#lastCombine = combine this.#lastResult = this.#result this.#combinedResult = replaceEqualDeep( this.#combinedResult, combine(input), ) } return this.#combinedResult } return input as any } #findMatchingObservers( queries: Array, ): Array { const prevObserversMap = new Map( this.#observers.map((observer) => [observer.options.queryHash, observer]), ) const observers: Array = [] queries.forEach((options) => { const defaultedOptions = this.#client.defaultQueryOptions(options) const match = prevObserversMap.get(defaultedOptions.queryHash) if (match) { observers.push({ defaultedQueryOptions: defaultedOptions, observer: match, }) } else { observers.push({ defaultedQueryOptions: defaultedOptions, observer: new QueryObserver(this.#client, defaultedOptions), }) } }) return observers } #onUpdate(observer: QueryObserver, result: QueryObserverResult): void { const index = this.#observers.indexOf(observer) if (index !== -1) { this.#result = replaceAt(this.#result, index, result) this.#notify() } } #notify(): void { if (this.hasListeners()) { const previousResult = this.#combinedResult const newTracked = this.#trackResult(this.#result, this.#observerMatches) const newResult = this.#combineResult(newTracked, this.#options?.combine) if (previousResult !== newResult) { notifyManager.batch(() => { this.listeners.forEach((listener) => { listener(this.#result) }) }) } } } } type QueryObserverMatch = { defaultedQueryOptions: DefaultedQueryObserverOptions observer: QueryObserver } ================================================ FILE: packages/query-core/src/query.ts ================================================ import { ensureQueryFn, noop, replaceData, resolveEnabled, resolveStaleTime, skipToken, timeUntilStale, } from './utils' import { notifyManager } from './notifyManager' import { CancelledError, canFetch, createRetryer } from './retryer' import { Removable } from './removable' import type { QueryCache } from './queryCache' import type { QueryClient } from './queryClient' import type { CancelOptions, DefaultError, FetchStatus, InitialDataFunction, OmitKeyof, QueryFunctionContext, QueryKey, QueryMeta, QueryOptions, QueryStatus, SetDataOptions, StaleTime, } from './types' import type { QueryObserver } from './queryObserver' import type { Retryer } from './retryer' // TYPES interface QueryConfig< TQueryFnData, TError, TData, TQueryKey extends QueryKey = QueryKey, > { client: QueryClient queryKey: TQueryKey queryHash: string options?: QueryOptions defaultOptions?: QueryOptions state?: QueryState } export interface QueryState { data: TData | undefined dataUpdateCount: number dataUpdatedAt: number error: TError | null errorUpdateCount: number errorUpdatedAt: number fetchFailureCount: number fetchFailureReason: TError | null fetchMeta: FetchMeta | null isInvalidated: boolean status: QueryStatus fetchStatus: FetchStatus } export interface FetchContext< TQueryFnData, TError, TData, TQueryKey extends QueryKey = QueryKey, > { fetchFn: () => unknown | Promise fetchOptions?: FetchOptions signal: AbortSignal options: QueryOptions client: QueryClient queryKey: TQueryKey state: QueryState } export interface QueryBehavior< TQueryFnData = unknown, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, > { onFetch: ( context: FetchContext, query: Query, ) => void } export type FetchDirection = 'forward' | 'backward' export interface FetchMeta { fetchMore?: { direction: FetchDirection } } export interface FetchOptions { cancelRefetch?: boolean meta?: FetchMeta initialPromise?: Promise } interface FailedAction { type: 'failed' failureCount: number error: TError } interface FetchAction { type: 'fetch' meta?: FetchMeta } interface SuccessAction { data: TData | undefined type: 'success' dataUpdatedAt?: number manual?: boolean } interface ErrorAction { type: 'error' error: TError } interface InvalidateAction { type: 'invalidate' } interface PauseAction { type: 'pause' } interface ContinueAction { type: 'continue' } interface SetStateAction { type: 'setState' state: Partial> setStateOptions?: SetStateOptions } export type Action = | ContinueAction | ErrorAction | FailedAction | FetchAction | InvalidateAction | PauseAction | SetStateAction | SuccessAction export interface SetStateOptions { meta?: any } // CLASS export class Query< TQueryFnData = unknown, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, > extends Removable { queryKey: TQueryKey queryHash: string options!: QueryOptions state: QueryState #initialState: QueryState #revertState?: QueryState #cache: QueryCache #client: QueryClient #retryer?: Retryer observers: Array> #defaultOptions?: QueryOptions #abortSignalConsumed: boolean constructor(config: QueryConfig) { super() this.#abortSignalConsumed = false this.#defaultOptions = config.defaultOptions this.setOptions(config.options) this.observers = [] this.#client = config.client this.#cache = this.#client.getQueryCache() this.queryKey = config.queryKey this.queryHash = config.queryHash this.#initialState = getDefaultState(this.options) this.state = config.state ?? this.#initialState this.scheduleGc() } get meta(): QueryMeta | undefined { return this.options.meta } get promise(): Promise | undefined { return this.#retryer?.promise } setOptions( options?: QueryOptions, ): void { this.options = { ...this.#defaultOptions, ...options } this.updateGcTime(this.options.gcTime) // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (this.state && this.state.data === undefined) { const defaultState = getDefaultState(this.options) if (defaultState.data !== undefined) { this.setData(defaultState.data, { updatedAt: defaultState.dataUpdatedAt, manual: true, }) this.#initialState = defaultState } } } protected optionalRemove() { if (!this.observers.length && this.state.fetchStatus === 'idle') { this.#cache.remove(this) } } setData( newData: TData, options?: SetDataOptions & { manual: boolean }, ): TData { const data = replaceData(this.state.data, newData, this.options) // Set data and mark it as cached this.#dispatch({ data, type: 'success', dataUpdatedAt: options?.updatedAt, manual: options?.manual, }) return data } setState( state: Partial>, setStateOptions?: SetStateOptions, ): void { this.#dispatch({ type: 'setState', state, setStateOptions }) } cancel(options?: CancelOptions): Promise { const promise = this.#retryer?.promise this.#retryer?.cancel(options) return promise ? promise.then(noop).catch(noop) : Promise.resolve() } destroy(): void { super.destroy() this.cancel({ silent: true }) } reset(): void { this.destroy() this.setState(this.#initialState) } isActive(): boolean { return this.observers.some( (observer) => resolveEnabled(observer.options.enabled, this) !== false, ) } isDisabled(): boolean { if (this.getObserversCount() > 0) { return !this.isActive() } // if a query has no observers, it should still be considered disabled if it never attempted a fetch return ( this.options.queryFn === skipToken || this.state.dataUpdateCount + this.state.errorUpdateCount === 0 ) } isStatic(): boolean { if (this.getObserversCount() > 0) { return this.observers.some( (observer) => resolveStaleTime(observer.options.staleTime, this) === 'static', ) } return false } isStale(): boolean { // check observers first, their `isStale` has the source of truth // calculated with `isStaleByTime` and it takes `enabled` into account if (this.getObserversCount() > 0) { return this.observers.some( (observer) => observer.getCurrentResult().isStale, ) } return this.state.data === undefined || this.state.isInvalidated } isStaleByTime(staleTime: StaleTime = 0): boolean { // no data is always stale if (this.state.data === undefined) { return true } // static is never stale if (staleTime === 'static') { return false } // if the query is invalidated, it is stale if (this.state.isInvalidated) { return true } return !timeUntilStale(this.state.dataUpdatedAt, staleTime) } onFocus(): void { const observer = this.observers.find((x) => x.shouldFetchOnWindowFocus()) observer?.refetch({ cancelRefetch: false }) // Continue fetch if currently paused this.#retryer?.continue() } onOnline(): void { const observer = this.observers.find((x) => x.shouldFetchOnReconnect()) observer?.refetch({ cancelRefetch: false }) // Continue fetch if currently paused this.#retryer?.continue() } addObserver(observer: QueryObserver): void { if (!this.observers.includes(observer)) { this.observers.push(observer) // Stop the query from being garbage collected this.clearGcTimeout() this.#cache.notify({ type: 'observerAdded', query: this, observer }) } } removeObserver(observer: QueryObserver): void { if (this.observers.includes(observer)) { this.observers = this.observers.filter((x) => x !== observer) if (!this.observers.length) { // If the transport layer does not support cancellation // we'll let the query continue so the result can be cached if (this.#retryer) { if (this.#abortSignalConsumed) { this.#retryer.cancel({ revert: true }) } else { this.#retryer.cancelRetry() } } this.scheduleGc() } this.#cache.notify({ type: 'observerRemoved', query: this, observer }) } } getObserversCount(): number { return this.observers.length } invalidate(): void { if (!this.state.isInvalidated) { this.#dispatch({ type: 'invalidate' }) } } async fetch( options?: QueryOptions, fetchOptions?: FetchOptions, ): Promise { if ( this.state.fetchStatus !== 'idle' && // If the promise in the retyer is already rejected, we have to definitely // re-start the fetch; there is a chance that the query is still in a // pending state when that happens this.#retryer?.status() !== 'rejected' ) { if (this.state.data !== undefined && fetchOptions?.cancelRefetch) { // Silently cancel current fetch if the user wants to cancel refetch this.cancel({ silent: true }) } else if (this.#retryer) { // make sure that retries that were potentially cancelled due to unmounts can continue this.#retryer.continueRetry() // Return current promise if we are already fetching return this.#retryer.promise } } // Update config if passed, otherwise the config from the last execution is used if (options) { this.setOptions(options) } // Use the options from the first observer with a query function if no function is found. // This can happen when the query is hydrated or created with setQueryData. if (!this.options.queryFn) { const observer = this.observers.find((x) => x.options.queryFn) if (observer) { this.setOptions(observer.options) } } if (process.env.NODE_ENV !== 'production') { if (!Array.isArray(this.options.queryKey)) { console.error( `As of v4, queryKey needs to be an Array. If you are using a string like 'repoData', please change it to an Array, e.g. ['repoData']`, ) } } const abortController = new AbortController() // Adds an enumerable signal property to the object that // which sets abortSignalConsumed to true when the signal // is read. const addSignalProperty = (object: unknown) => { Object.defineProperty(object, 'signal', { enumerable: true, get: () => { this.#abortSignalConsumed = true return abortController.signal }, }) } // Create fetch function const fetchFn = () => { const queryFn = ensureQueryFn(this.options, fetchOptions) // Create query function context const createQueryFnContext = (): QueryFunctionContext => { const queryFnContext: OmitKeyof< QueryFunctionContext, 'signal' > = { client: this.#client, queryKey: this.queryKey, meta: this.meta, } addSignalProperty(queryFnContext) return queryFnContext as QueryFunctionContext } const queryFnContext = createQueryFnContext() this.#abortSignalConsumed = false if (this.options.persister) { return this.options.persister( queryFn, queryFnContext, this as unknown as Query, ) } return queryFn(queryFnContext) } // Trigger behavior hook const createFetchContext = (): FetchContext< TQueryFnData, TError, TData, TQueryKey > => { const context: OmitKeyof< FetchContext, 'signal' > = { fetchOptions, options: this.options, queryKey: this.queryKey, client: this.#client, state: this.state, fetchFn, } addSignalProperty(context) return context as FetchContext } const context = createFetchContext() this.options.behavior?.onFetch(context, this as unknown as Query) // Store state in case the current fetch needs to be reverted this.#revertState = this.state // Set to fetching state if not already in it if ( this.state.fetchStatus === 'idle' || this.state.fetchMeta !== context.fetchOptions?.meta ) { this.#dispatch({ type: 'fetch', meta: context.fetchOptions?.meta }) } // Try to fetch the data this.#retryer = createRetryer({ initialPromise: fetchOptions?.initialPromise as | Promise | undefined, fn: context.fetchFn as () => Promise, onCancel: (error) => { if (error instanceof CancelledError && error.revert) { this.setState({ ...this.#revertState, fetchStatus: 'idle' as const, }) } abortController.abort() }, onFail: (failureCount, error) => { this.#dispatch({ type: 'failed', failureCount, error }) }, onPause: () => { this.#dispatch({ type: 'pause' }) }, onContinue: () => { this.#dispatch({ type: 'continue' }) }, retry: context.options.retry, retryDelay: context.options.retryDelay, networkMode: context.options.networkMode, canRun: () => true, }) try { const data = await this.#retryer.start() // this is more of a runtime guard // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (data === undefined) { if (process.env.NODE_ENV !== 'production') { console.error( `Query data cannot be undefined. Please make sure to return a value other than undefined from your query function. Affected query key: ${this.queryHash}`, ) } throw new Error(`${this.queryHash} data is undefined`) } this.setData(data) // Notify cache callback this.#cache.config.onSuccess?.(data, this as Query) this.#cache.config.onSettled?.( data, this.state.error as any, this as Query, ) return data } catch (error) { if (error instanceof CancelledError) { if (error.silent) { // silent cancellation implies a new fetch is going to be started, // so we piggyback onto that promise return this.#retryer.promise } else if (error.revert) { // transform error into reverted state data // if the initial fetch was cancelled, we have no data, so we have // to get reject with a CancelledError if (this.state.data === undefined) { throw error } return this.state.data } } this.#dispatch({ type: 'error', error: error as TError, }) // Notify cache callback this.#cache.config.onError?.( error as any, this as Query, ) this.#cache.config.onSettled?.( this.state.data, error as any, this as Query, ) throw error // rethrow the error for further handling } finally { // Schedule query gc after fetching this.scheduleGc() } } #dispatch(action: Action): void { const reducer = ( state: QueryState, ): QueryState => { switch (action.type) { case 'failed': return { ...state, fetchFailureCount: action.failureCount, fetchFailureReason: action.error, } case 'pause': return { ...state, fetchStatus: 'paused', } case 'continue': return { ...state, fetchStatus: 'fetching', } case 'fetch': return { ...state, ...fetchState(state.data, this.options), fetchMeta: action.meta ?? null, } case 'success': const newState = { ...state, data: action.data, dataUpdateCount: state.dataUpdateCount + 1, dataUpdatedAt: action.dataUpdatedAt ?? Date.now(), error: null, isInvalidated: false, status: 'success' as const, ...(!action.manual && { fetchStatus: 'idle' as const, fetchFailureCount: 0, fetchFailureReason: null, }), } // If fetching ends successfully, we don't need revertState as a fallback anymore. // For manual updates, capture the state to revert to it in case of a cancellation. this.#revertState = action.manual ? newState : undefined return newState case 'error': const error = action.error return { ...state, error, errorUpdateCount: state.errorUpdateCount + 1, errorUpdatedAt: Date.now(), fetchFailureCount: state.fetchFailureCount + 1, fetchFailureReason: error, fetchStatus: 'idle', status: 'error', } case 'invalidate': return { ...state, isInvalidated: true, } case 'setState': return { ...state, ...action.state, } } } this.state = reducer(this.state) notifyManager.batch(() => { this.observers.forEach((observer) => { observer.onQueryUpdate() }) this.#cache.notify({ query: this, type: 'updated', action }) }) } } export function fetchState< TQueryFnData, TError, TData, TQueryKey extends QueryKey, >( data: TData | undefined, options: QueryOptions, ) { return { fetchFailureCount: 0, fetchFailureReason: null, fetchStatus: canFetch(options.networkMode) ? 'fetching' : 'paused', ...(data === undefined && ({ error: null, status: 'pending', } as const)), } as const } function getDefaultState< TQueryFnData, TError, TData, TQueryKey extends QueryKey, >( options: QueryOptions, ): QueryState { const data = typeof options.initialData === 'function' ? (options.initialData as InitialDataFunction)() : options.initialData const hasData = data !== undefined const initialDataUpdatedAt = hasData ? typeof options.initialDataUpdatedAt === 'function' ? (options.initialDataUpdatedAt as () => number | undefined)() : options.initialDataUpdatedAt : 0 return { data, dataUpdateCount: 0, dataUpdatedAt: hasData ? (initialDataUpdatedAt ?? Date.now()) : 0, error: null, errorUpdateCount: 0, errorUpdatedAt: 0, fetchFailureCount: 0, fetchFailureReason: null, fetchMeta: null, isInvalidated: false, status: hasData ? 'success' : 'pending', fetchStatus: 'idle', } } ================================================ FILE: packages/query-core/src/queryCache.ts ================================================ import { hashQueryKeyByOptions, matchQuery } from './utils' import { Query } from './query' import { notifyManager } from './notifyManager' import { Subscribable } from './subscribable' import type { QueryFilters } from './utils' import type { Action, QueryState } from './query' import type { DefaultError, NotifyEvent, QueryKey, QueryOptions, WithRequired, } from './types' import type { QueryClient } from './queryClient' import type { QueryObserver } from './queryObserver' // TYPES interface QueryCacheConfig { onError?: ( error: DefaultError, query: Query, ) => void onSuccess?: (data: unknown, query: Query) => void onSettled?: ( data: unknown | undefined, error: DefaultError | null, query: Query, ) => void } interface NotifyEventQueryAdded extends NotifyEvent { type: 'added' query: Query } interface NotifyEventQueryRemoved extends NotifyEvent { type: 'removed' query: Query } interface NotifyEventQueryUpdated extends NotifyEvent { type: 'updated' query: Query action: Action } interface NotifyEventQueryObserverAdded extends NotifyEvent { type: 'observerAdded' query: Query observer: QueryObserver } interface NotifyEventQueryObserverRemoved extends NotifyEvent { type: 'observerRemoved' query: Query observer: QueryObserver } interface NotifyEventQueryObserverResultsUpdated extends NotifyEvent { type: 'observerResultsUpdated' query: Query } interface NotifyEventQueryObserverOptionsUpdated extends NotifyEvent { type: 'observerOptionsUpdated' query: Query observer: QueryObserver } export type QueryCacheNotifyEvent = | NotifyEventQueryAdded | NotifyEventQueryRemoved | NotifyEventQueryUpdated | NotifyEventQueryObserverAdded | NotifyEventQueryObserverRemoved | NotifyEventQueryObserverResultsUpdated | NotifyEventQueryObserverOptionsUpdated type QueryCacheListener = (event: QueryCacheNotifyEvent) => void export interface QueryStore { has: (queryHash: string) => boolean set: (queryHash: string, query: Query) => void get: (queryHash: string) => Query | undefined delete: (queryHash: string) => void values: () => IterableIterator } // CLASS export class QueryCache extends Subscribable { #queries: QueryStore constructor(public config: QueryCacheConfig = {}) { super() this.#queries = new Map() } build< TQueryFnData = unknown, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, >( client: QueryClient, options: WithRequired< QueryOptions, 'queryKey' >, state?: QueryState, ): Query { const queryKey = options.queryKey const queryHash = options.queryHash ?? hashQueryKeyByOptions(queryKey, options) let query = this.get(queryHash) if (!query) { query = new Query({ client, queryKey, queryHash, options: client.defaultQueryOptions(options), state, defaultOptions: client.getQueryDefaults(queryKey), }) this.add(query) } return query } add(query: Query): void { if (!this.#queries.has(query.queryHash)) { this.#queries.set(query.queryHash, query) this.notify({ type: 'added', query, }) } } remove(query: Query): void { const queryInMap = this.#queries.get(query.queryHash) if (queryInMap) { query.destroy() if (queryInMap === query) { this.#queries.delete(query.queryHash) } this.notify({ type: 'removed', query }) } } clear(): void { notifyManager.batch(() => { this.getAll().forEach((query) => { this.remove(query) }) }) } get< TQueryFnData = unknown, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, >( queryHash: string, ): Query | undefined { return this.#queries.get(queryHash) as | Query | undefined } getAll(): Array { return [...this.#queries.values()] } find( filters: WithRequired, ): Query | undefined { const defaultedFilters = { exact: true, ...filters } return this.getAll().find((query) => matchQuery(defaultedFilters, query), ) as Query | undefined } findAll(filters: QueryFilters = {}): Array { const queries = this.getAll() return Object.keys(filters).length > 0 ? queries.filter((query) => matchQuery(filters, query)) : queries } notify(event: QueryCacheNotifyEvent): void { notifyManager.batch(() => { this.listeners.forEach((listener) => { listener(event) }) }) } onFocus(): void { notifyManager.batch(() => { this.getAll().forEach((query) => { query.onFocus() }) }) } onOnline(): void { notifyManager.batch(() => { this.getAll().forEach((query) => { query.onOnline() }) }) } } ================================================ FILE: packages/query-core/src/queryClient.ts ================================================ import { functionalUpdate, hashKey, hashQueryKeyByOptions, noop, partialMatchKey, resolveStaleTime, skipToken, } from './utils' import { QueryCache } from './queryCache' import { MutationCache } from './mutationCache' import { focusManager } from './focusManager' import { onlineManager } from './onlineManager' import { notifyManager } from './notifyManager' import { infiniteQueryBehavior } from './infiniteQueryBehavior' import type { CancelOptions, DefaultError, DefaultOptions, DefaultedQueryObserverOptions, EnsureInfiniteQueryDataOptions, EnsureQueryDataOptions, FetchInfiniteQueryOptions, FetchQueryOptions, InferDataFromTag, InferErrorFromTag, InfiniteData, InvalidateOptions, InvalidateQueryFilters, MutationKey, MutationObserverOptions, MutationOptions, NoInfer, OmitKeyof, QueryClientConfig, QueryKey, QueryObserverOptions, QueryOptions, RefetchOptions, RefetchQueryFilters, ResetOptions, SetDataOptions, } from './types' import type { QueryState } from './query' import type { MutationFilters, QueryFilters, Updater } from './utils' // TYPES interface QueryDefaults { queryKey: QueryKey defaultOptions: OmitKeyof, 'queryKey'> } interface MutationDefaults { mutationKey: MutationKey defaultOptions: MutationOptions } // CLASS export class QueryClient { #queryCache: QueryCache #mutationCache: MutationCache #defaultOptions: DefaultOptions #queryDefaults: Map #mutationDefaults: Map #mountCount: number #unsubscribeFocus?: () => void #unsubscribeOnline?: () => void constructor(config: QueryClientConfig = {}) { this.#queryCache = config.queryCache || new QueryCache() this.#mutationCache = config.mutationCache || new MutationCache() this.#defaultOptions = config.defaultOptions || {} this.#queryDefaults = new Map() this.#mutationDefaults = new Map() this.#mountCount = 0 } mount(): void { this.#mountCount++ if (this.#mountCount !== 1) return this.#unsubscribeFocus = focusManager.subscribe(async (focused) => { if (focused) { await this.resumePausedMutations() this.#queryCache.onFocus() } }) this.#unsubscribeOnline = onlineManager.subscribe(async (online) => { if (online) { await this.resumePausedMutations() this.#queryCache.onOnline() } }) } unmount(): void { this.#mountCount-- if (this.#mountCount !== 0) return this.#unsubscribeFocus?.() this.#unsubscribeFocus = undefined this.#unsubscribeOnline?.() this.#unsubscribeOnline = undefined } isFetching = QueryFilters>( filters?: TQueryFilters, ): number { return this.#queryCache.findAll({ ...filters, fetchStatus: 'fetching' }) .length } isMutating< TMutationFilters extends MutationFilters = MutationFilters, >(filters?: TMutationFilters): number { return this.#mutationCache.findAll({ ...filters, status: 'pending' }).length } /** * Imperative (non-reactive) way to retrieve data for a QueryKey. * Should only be used in callbacks or functions where reading the latest data is necessary, e.g. for optimistic updates. * * Hint: Do not use this function inside a component, because it won't receive updates. * Use `useQuery` to create a `QueryObserver` that subscribes to changes. */ getQueryData< TQueryFnData = unknown, TTaggedQueryKey extends QueryKey = QueryKey, TInferredQueryFnData = InferDataFromTag, >(queryKey: TTaggedQueryKey): TInferredQueryFnData | undefined { const options = this.defaultQueryOptions({ queryKey }) return this.#queryCache.get(options.queryHash)?.state .data } ensureQueryData< TQueryFnData, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, >( options: EnsureQueryDataOptions, ): Promise { const defaultedOptions = this.defaultQueryOptions(options) const query = this.#queryCache.build(this, defaultedOptions) const cachedData = query.state.data if (cachedData === undefined) { return this.fetchQuery(options) } if ( options.revalidateIfStale && query.isStaleByTime(resolveStaleTime(defaultedOptions.staleTime, query)) ) { void this.prefetchQuery(defaultedOptions) } return Promise.resolve(cachedData) } getQueriesData< TQueryFnData = unknown, TQueryFilters extends QueryFilters = QueryFilters, >(filters: TQueryFilters): Array<[QueryKey, TQueryFnData | undefined]> { return this.#queryCache.findAll(filters).map(({ queryKey, state }) => { const data = state.data as TQueryFnData | undefined return [queryKey, data] }) } setQueryData< TQueryFnData = unknown, TTaggedQueryKey extends QueryKey = QueryKey, TInferredQueryFnData = InferDataFromTag, >( queryKey: TTaggedQueryKey, updater: Updater< NoInfer | undefined, NoInfer | undefined >, options?: SetDataOptions, ): NoInfer | undefined { const defaultedOptions = this.defaultQueryOptions< any, any, unknown, any, QueryKey >({ queryKey }) const query = this.#queryCache.get( defaultedOptions.queryHash, ) const prevData = query?.state.data const data = functionalUpdate(updater, prevData) if (data === undefined) { return undefined } return this.#queryCache .build(this, defaultedOptions) .setData(data, { ...options, manual: true }) } setQueriesData< TQueryFnData, TQueryFilters extends QueryFilters = QueryFilters, >( filters: TQueryFilters, updater: Updater< NoInfer | undefined, NoInfer | undefined >, options?: SetDataOptions, ): Array<[QueryKey, TQueryFnData | undefined]> { return notifyManager.batch(() => this.#queryCache .findAll(filters) .map(({ queryKey }) => [ queryKey, this.setQueryData(queryKey, updater, options), ]), ) } getQueryState< TQueryFnData = unknown, TError = DefaultError, TTaggedQueryKey extends QueryKey = QueryKey, TInferredQueryFnData = InferDataFromTag, TInferredError = InferErrorFromTag, >( queryKey: TTaggedQueryKey, ): QueryState | undefined { const options = this.defaultQueryOptions({ queryKey }) return this.#queryCache.get( options.queryHash, )?.state } removeQueries( filters?: QueryFilters, ): void { const queryCache = this.#queryCache notifyManager.batch(() => { queryCache.findAll(filters).forEach((query) => { queryCache.remove(query) }) }) } resetQueries( filters?: QueryFilters, options?: ResetOptions, ): Promise { const queryCache = this.#queryCache return notifyManager.batch(() => { queryCache.findAll(filters).forEach((query) => { query.reset() }) return this.refetchQueries( { type: 'active', ...filters, }, options, ) }) } cancelQueries( filters?: QueryFilters, cancelOptions: CancelOptions = {}, ): Promise { const defaultedCancelOptions = { revert: true, ...cancelOptions } const promises = notifyManager.batch(() => this.#queryCache .findAll(filters) .map((query) => query.cancel(defaultedCancelOptions)), ) return Promise.all(promises).then(noop).catch(noop) } invalidateQueries( filters?: InvalidateQueryFilters, options: InvalidateOptions = {}, ): Promise { return notifyManager.batch(() => { this.#queryCache.findAll(filters).forEach((query) => { query.invalidate() }) if (filters?.refetchType === 'none') { return Promise.resolve() } return this.refetchQueries( { ...filters, type: filters?.refetchType ?? filters?.type ?? 'active', }, options, ) }) } refetchQueries( filters?: RefetchQueryFilters, options: RefetchOptions = {}, ): Promise { const fetchOptions = { ...options, cancelRefetch: options.cancelRefetch ?? true, } const promises = notifyManager.batch(() => this.#queryCache .findAll(filters) .filter((query) => !query.isDisabled() && !query.isStatic()) .map((query) => { let promise = query.fetch(undefined, fetchOptions) if (!fetchOptions.throwOnError) { promise = promise.catch(noop) } return query.state.fetchStatus === 'paused' ? Promise.resolve() : promise }), ) return Promise.all(promises).then(noop) } fetchQuery< TQueryFnData, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, TPageParam = never, >( options: FetchQueryOptions< TQueryFnData, TError, TData, TQueryKey, TPageParam >, ): Promise { const defaultedOptions = this.defaultQueryOptions(options) // https://github.com/tannerlinsley/react-query/issues/652 if (defaultedOptions.retry === undefined) { defaultedOptions.retry = false } const query = this.#queryCache.build(this, defaultedOptions) return query.isStaleByTime( resolveStaleTime(defaultedOptions.staleTime, query), ) ? query.fetch(defaultedOptions) : Promise.resolve(query.state.data as TData) } prefetchQuery< TQueryFnData = unknown, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, >( options: FetchQueryOptions, ): Promise { return this.fetchQuery(options).then(noop).catch(noop) } fetchInfiniteQuery< TQueryFnData, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, TPageParam = unknown, >( options: FetchInfiniteQueryOptions< TQueryFnData, TError, TData, TQueryKey, TPageParam >, ): Promise> { options.behavior = infiniteQueryBehavior< TQueryFnData, TError, TData, TPageParam >(options.pages) return this.fetchQuery(options as any) } prefetchInfiniteQuery< TQueryFnData, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, TPageParam = unknown, >( options: FetchInfiniteQueryOptions< TQueryFnData, TError, TData, TQueryKey, TPageParam >, ): Promise { return this.fetchInfiniteQuery(options).then(noop).catch(noop) } ensureInfiniteQueryData< TQueryFnData, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, TPageParam = unknown, >( options: EnsureInfiniteQueryDataOptions< TQueryFnData, TError, TData, TQueryKey, TPageParam >, ): Promise> { options.behavior = infiniteQueryBehavior< TQueryFnData, TError, TData, TPageParam >(options.pages) return this.ensureQueryData(options as any) } resumePausedMutations(): Promise { if (onlineManager.isOnline()) { return this.#mutationCache.resumePausedMutations() } return Promise.resolve() } getQueryCache(): QueryCache { return this.#queryCache } getMutationCache(): MutationCache { return this.#mutationCache } getDefaultOptions(): DefaultOptions { return this.#defaultOptions } setDefaultOptions(options: DefaultOptions): void { this.#defaultOptions = options } setQueryDefaults< TQueryFnData = unknown, TError = DefaultError, TData = TQueryFnData, TQueryData = TQueryFnData, >( queryKey: QueryKey, options: Partial< OmitKeyof< QueryObserverOptions, 'queryKey' > >, ): void { this.#queryDefaults.set(hashKey(queryKey), { queryKey, defaultOptions: options, }) } getQueryDefaults( queryKey: QueryKey, ): OmitKeyof, 'queryKey'> { const defaults = [...this.#queryDefaults.values()] const result: OmitKeyof< QueryObserverOptions, 'queryKey' > = {} defaults.forEach((queryDefault) => { if (partialMatchKey(queryKey, queryDefault.queryKey)) { Object.assign(result, queryDefault.defaultOptions) } }) return result } setMutationDefaults< TData = unknown, TError = DefaultError, TVariables = void, TOnMutateResult = unknown, >( mutationKey: MutationKey, options: OmitKeyof< MutationObserverOptions, 'mutationKey' >, ): void { this.#mutationDefaults.set(hashKey(mutationKey), { mutationKey, defaultOptions: options, }) } getMutationDefaults( mutationKey: MutationKey, ): OmitKeyof, 'mutationKey'> { const defaults = [...this.#mutationDefaults.values()] const result: OmitKeyof< MutationObserverOptions, 'mutationKey' > = {} defaults.forEach((queryDefault) => { if (partialMatchKey(mutationKey, queryDefault.mutationKey)) { Object.assign(result, queryDefault.defaultOptions) } }) return result } defaultQueryOptions< TQueryFnData = unknown, TError = DefaultError, TData = TQueryFnData, TQueryData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, TPageParam = never, >( options: | QueryObserverOptions< TQueryFnData, TError, TData, TQueryData, TQueryKey, TPageParam > | DefaultedQueryObserverOptions< TQueryFnData, TError, TData, TQueryData, TQueryKey >, ): DefaultedQueryObserverOptions< TQueryFnData, TError, TData, TQueryData, TQueryKey > { if (options._defaulted) { return options as DefaultedQueryObserverOptions< TQueryFnData, TError, TData, TQueryData, TQueryKey > } const defaultedOptions = { ...this.#defaultOptions.queries, ...this.getQueryDefaults(options.queryKey), ...options, _defaulted: true, } if (!defaultedOptions.queryHash) { defaultedOptions.queryHash = hashQueryKeyByOptions( defaultedOptions.queryKey, defaultedOptions, ) } // dependent default values if (defaultedOptions.refetchOnReconnect === undefined) { defaultedOptions.refetchOnReconnect = defaultedOptions.networkMode !== 'always' } if (defaultedOptions.throwOnError === undefined) { defaultedOptions.throwOnError = !!defaultedOptions.suspense } if (!defaultedOptions.networkMode && defaultedOptions.persister) { defaultedOptions.networkMode = 'offlineFirst' } if (defaultedOptions.queryFn === skipToken) { defaultedOptions.enabled = false } return defaultedOptions as DefaultedQueryObserverOptions< TQueryFnData, TError, TData, TQueryData, TQueryKey > } defaultMutationOptions>( options?: T, ): T { if (options?._defaulted) { return options } return { ...this.#defaultOptions.mutations, ...(options?.mutationKey && this.getMutationDefaults(options.mutationKey)), ...options, _defaulted: true, } as T } clear(): void { this.#queryCache.clear() this.#mutationCache.clear() } } ================================================ FILE: packages/query-core/src/queryObserver.ts ================================================ import { focusManager } from './focusManager' import { notifyManager } from './notifyManager' import { fetchState } from './query' import { Subscribable } from './subscribable' import { pendingThenable } from './thenable' import { isServer, isValidTimeout, noop, replaceData, resolveEnabled, resolveStaleTime, shallowEqualObjects, timeUntilStale, } from './utils' import { timeoutManager } from './timeoutManager' import type { ManagedTimerId } from './timeoutManager' import type { FetchOptions, Query, QueryState } from './query' import type { QueryClient } from './queryClient' import type { PendingThenable, Thenable } from './thenable' import type { DefaultError, DefaultedQueryObserverOptions, PlaceholderDataFunction, QueryKey, QueryObserverBaseResult, QueryObserverOptions, QueryObserverResult, QueryOptions, RefetchOptions, } from './types' type QueryObserverListener = ( result: QueryObserverResult, ) => void interface ObserverFetchOptions extends FetchOptions { throwOnError?: boolean } export class QueryObserver< TQueryFnData = unknown, TError = DefaultError, TData = TQueryFnData, TQueryData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, > extends Subscribable> { #client: QueryClient #currentQuery: Query = undefined! #currentQueryInitialState: QueryState = undefined! #currentResult: QueryObserverResult = undefined! #currentResultState?: QueryState #currentResultOptions?: QueryObserverOptions< TQueryFnData, TError, TData, TQueryData, TQueryKey > #currentThenable: Thenable #selectError: TError | null #selectFn?: (data: TQueryData) => TData #selectResult?: TData // This property keeps track of the last query with defined data. // It will be used to pass the previous data and query to the placeholder function between renders. #lastQueryWithDefinedData?: Query #staleTimeoutId?: ManagedTimerId #refetchIntervalId?: ManagedTimerId #currentRefetchInterval?: number | false #trackedProps = new Set() constructor( client: QueryClient, public options: QueryObserverOptions< TQueryFnData, TError, TData, TQueryData, TQueryKey >, ) { super() this.#client = client this.#selectError = null this.#currentThenable = pendingThenable() this.bindMethods() this.setOptions(options) } protected bindMethods(): void { this.refetch = this.refetch.bind(this) } protected onSubscribe(): void { if (this.listeners.size === 1) { this.#currentQuery.addObserver(this) if (shouldFetchOnMount(this.#currentQuery, this.options)) { this.#executeFetch() } else { this.updateResult() } this.#updateTimers() } } protected onUnsubscribe(): void { if (!this.hasListeners()) { this.destroy() } } shouldFetchOnReconnect(): boolean { return shouldFetchOn( this.#currentQuery, this.options, this.options.refetchOnReconnect, ) } shouldFetchOnWindowFocus(): boolean { return shouldFetchOn( this.#currentQuery, this.options, this.options.refetchOnWindowFocus, ) } destroy(): void { this.listeners = new Set() this.#clearStaleTimeout() this.#clearRefetchInterval() this.#currentQuery.removeObserver(this) } setOptions( options: QueryObserverOptions< TQueryFnData, TError, TData, TQueryData, TQueryKey >, ): void { const prevOptions = this.options const prevQuery = this.#currentQuery this.options = this.#client.defaultQueryOptions(options) if ( this.options.enabled !== undefined && typeof this.options.enabled !== 'boolean' && typeof this.options.enabled !== 'function' && typeof resolveEnabled(this.options.enabled, this.#currentQuery) !== 'boolean' ) { throw new Error( 'Expected enabled to be a boolean or a callback that returns a boolean', ) } this.#updateQuery() this.#currentQuery.setOptions(this.options) if ( prevOptions._defaulted && !shallowEqualObjects(this.options, prevOptions) ) { this.#client.getQueryCache().notify({ type: 'observerOptionsUpdated', query: this.#currentQuery, observer: this, }) } const mounted = this.hasListeners() // Fetch if there are subscribers if ( mounted && shouldFetchOptionally( this.#currentQuery, prevQuery, this.options, prevOptions, ) ) { this.#executeFetch() } // Update result this.updateResult() // Update stale interval if needed if ( mounted && (this.#currentQuery !== prevQuery || resolveEnabled(this.options.enabled, this.#currentQuery) !== resolveEnabled(prevOptions.enabled, this.#currentQuery) || resolveStaleTime(this.options.staleTime, this.#currentQuery) !== resolveStaleTime(prevOptions.staleTime, this.#currentQuery)) ) { this.#updateStaleTimeout() } const nextRefetchInterval = this.#computeRefetchInterval() // Update refetch interval if needed if ( mounted && (this.#currentQuery !== prevQuery || resolveEnabled(this.options.enabled, this.#currentQuery) !== resolveEnabled(prevOptions.enabled, this.#currentQuery) || nextRefetchInterval !== this.#currentRefetchInterval) ) { this.#updateRefetchInterval(nextRefetchInterval) } } getOptimisticResult( options: DefaultedQueryObserverOptions< TQueryFnData, TError, TData, TQueryData, TQueryKey >, ): QueryObserverResult { const query = this.#client.getQueryCache().build(this.#client, options) const result = this.createResult(query, options) if (shouldAssignObserverCurrentProperties(this, result)) { // this assigns the optimistic result to the current Observer // because if the query function changes, useQuery will be performing // an effect where it would fetch again. // When the fetch finishes, we perform a deep data cloning in order // to reuse objects references. This deep data clone is performed against // the `observer.currentResult.data` property // When QueryKey changes, we refresh the query and get new `optimistic` // result, while we leave the `observer.currentResult`, so when new data // arrives, it finds the old `observer.currentResult` which is related // to the old QueryKey. Which means that currentResult and selectData are // out of sync already. // To solve this, we move the cursor of the currentResult every time // an observer reads an optimistic value. // When keeping the previous data, the result doesn't change until new // data arrives. this.#currentResult = result this.#currentResultOptions = this.options this.#currentResultState = this.#currentQuery.state } return result } getCurrentResult(): QueryObserverResult { return this.#currentResult } trackResult( result: QueryObserverResult, onPropTracked?: (key: keyof QueryObserverResult) => void, ): QueryObserverResult { return new Proxy(result, { get: (target, key) => { this.trackProp(key as keyof QueryObserverResult) onPropTracked?.(key as keyof QueryObserverResult) if ( key === 'promise' && !this.options.experimental_prefetchInRender && this.#currentThenable.status === 'pending' ) { this.#currentThenable.reject( new Error( 'experimental_prefetchInRender feature flag is not enabled', ), ) } return Reflect.get(target, key) }, }) } trackProp(key: keyof QueryObserverResult) { this.#trackedProps.add(key) } getCurrentQuery(): Query { return this.#currentQuery } refetch({ ...options }: RefetchOptions = {}): Promise< QueryObserverResult > { return this.fetch({ ...options, }) } fetchOptimistic( options: QueryObserverOptions< TQueryFnData, TError, TData, TQueryData, TQueryKey >, ): Promise> { const defaultedOptions = this.#client.defaultQueryOptions(options) const query = this.#client .getQueryCache() .build(this.#client, defaultedOptions) return query.fetch().then(() => this.createResult(query, defaultedOptions)) } protected fetch( fetchOptions: ObserverFetchOptions, ): Promise> { return this.#executeFetch({ ...fetchOptions, cancelRefetch: fetchOptions.cancelRefetch ?? true, }).then(() => { this.updateResult() return this.#currentResult }) } #executeFetch( fetchOptions?: Omit, ): Promise { // Make sure we reference the latest query as the current one might have been removed this.#updateQuery() // Fetch let promise: Promise = this.#currentQuery.fetch( this.options as QueryOptions, fetchOptions, ) if (!fetchOptions?.throwOnError) { promise = promise.catch(noop) } return promise } #updateStaleTimeout(): void { this.#clearStaleTimeout() const staleTime = resolveStaleTime( this.options.staleTime, this.#currentQuery, ) if (isServer || this.#currentResult.isStale || !isValidTimeout(staleTime)) { return } const time = timeUntilStale(this.#currentResult.dataUpdatedAt, staleTime) // The timeout is sometimes triggered 1 ms before the stale time expiration. // To mitigate this issue we always add 1 ms to the timeout. const timeout = time + 1 this.#staleTimeoutId = timeoutManager.setTimeout(() => { if (!this.#currentResult.isStale) { this.updateResult() } }, timeout) } #computeRefetchInterval() { return ( (typeof this.options.refetchInterval === 'function' ? this.options.refetchInterval(this.#currentQuery) : this.options.refetchInterval) ?? false ) } #updateRefetchInterval(nextInterval: number | false): void { this.#clearRefetchInterval() this.#currentRefetchInterval = nextInterval if ( isServer || resolveEnabled(this.options.enabled, this.#currentQuery) === false || !isValidTimeout(this.#currentRefetchInterval) || this.#currentRefetchInterval === 0 ) { return } this.#refetchIntervalId = timeoutManager.setInterval(() => { if ( this.options.refetchIntervalInBackground || focusManager.isFocused() ) { this.#executeFetch() } }, this.#currentRefetchInterval) } #updateTimers(): void { this.#updateStaleTimeout() this.#updateRefetchInterval(this.#computeRefetchInterval()) } #clearStaleTimeout(): void { if (this.#staleTimeoutId) { timeoutManager.clearTimeout(this.#staleTimeoutId) this.#staleTimeoutId = undefined } } #clearRefetchInterval(): void { if (this.#refetchIntervalId) { timeoutManager.clearInterval(this.#refetchIntervalId) this.#refetchIntervalId = undefined } } protected createResult( query: Query, options: QueryObserverOptions< TQueryFnData, TError, TData, TQueryData, TQueryKey >, ): QueryObserverResult { const prevQuery = this.#currentQuery const prevOptions = this.options const prevResult = this.#currentResult as | QueryObserverResult | undefined const prevResultState = this.#currentResultState const prevResultOptions = this.#currentResultOptions const queryChange = query !== prevQuery const queryInitialState = queryChange ? query.state : this.#currentQueryInitialState const { state } = query let newState = { ...state } let isPlaceholderData = false let data: TData | undefined // Optimistically set result in fetching state if needed if (options._optimisticResults) { const mounted = this.hasListeners() const fetchOnMount = !mounted && shouldFetchOnMount(query, options) const fetchOptionally = mounted && shouldFetchOptionally(query, prevQuery, options, prevOptions) if (fetchOnMount || fetchOptionally) { newState = { ...newState, ...fetchState(state.data, query.options), } } if (options._optimisticResults === 'isRestoring') { newState.fetchStatus = 'idle' } } let { error, errorUpdatedAt, status } = newState // Per default, use query data data = newState.data as unknown as TData let skipSelect = false // use placeholderData if needed if ( options.placeholderData !== undefined && data === undefined && status === 'pending' ) { let placeholderData // Memoize placeholder data if ( prevResult?.isPlaceholderData && options.placeholderData === prevResultOptions?.placeholderData ) { placeholderData = prevResult.data // we have to skip select when reading this memoization // because prevResult.data is already "selected" skipSelect = true } else { // compute placeholderData placeholderData = typeof options.placeholderData === 'function' ? ( options.placeholderData as unknown as PlaceholderDataFunction )( this.#lastQueryWithDefinedData?.state.data, this.#lastQueryWithDefinedData as any, ) : options.placeholderData } if (placeholderData !== undefined) { status = 'success' data = replaceData( prevResult?.data, placeholderData as unknown, options, ) as TData isPlaceholderData = true } } // Select data if needed // this also runs placeholderData through the select function if (options.select && data !== undefined && !skipSelect) { // Memoize select result if ( prevResult && data === prevResultState?.data && options.select === this.#selectFn ) { data = this.#selectResult } else { try { this.#selectFn = options.select data = options.select(data as any) data = replaceData(prevResult?.data, data, options) this.#selectResult = data this.#selectError = null } catch (selectError) { this.#selectError = selectError as TError } } } if (this.#selectError) { error = this.#selectError as any data = this.#selectResult errorUpdatedAt = Date.now() status = 'error' } const isFetching = newState.fetchStatus === 'fetching' const isPending = status === 'pending' const isError = status === 'error' const isLoading = isPending && isFetching const hasData = data !== undefined const result: QueryObserverBaseResult = { status, fetchStatus: newState.fetchStatus, isPending, isSuccess: status === 'success', isError, isInitialLoading: isLoading, isLoading, data, dataUpdatedAt: newState.dataUpdatedAt, error, errorUpdatedAt, failureCount: newState.fetchFailureCount, failureReason: newState.fetchFailureReason, errorUpdateCount: newState.errorUpdateCount, isFetched: newState.dataUpdateCount > 0 || newState.errorUpdateCount > 0, isFetchedAfterMount: newState.dataUpdateCount > queryInitialState.dataUpdateCount || newState.errorUpdateCount > queryInitialState.errorUpdateCount, isFetching, isRefetching: isFetching && !isPending, isLoadingError: isError && !hasData, isPaused: newState.fetchStatus === 'paused', isPlaceholderData, isRefetchError: isError && hasData, isStale: isStale(query, options), refetch: this.refetch, promise: this.#currentThenable, isEnabled: resolveEnabled(options.enabled, query) !== false, } const nextResult = result as QueryObserverResult if (this.options.experimental_prefetchInRender) { const finalizeThenableIfPossible = (thenable: PendingThenable) => { if (nextResult.status === 'error') { thenable.reject(nextResult.error) } else if (nextResult.data !== undefined) { thenable.resolve(nextResult.data) } } /** * Create a new thenable and result promise when the results have changed */ const recreateThenable = () => { const pending = (this.#currentThenable = nextResult.promise = pendingThenable()) finalizeThenableIfPossible(pending) } const prevThenable = this.#currentThenable switch (prevThenable.status) { case 'pending': // Finalize the previous thenable if it was pending // and we are still observing the same query if (query.queryHash === prevQuery.queryHash) { finalizeThenableIfPossible(prevThenable) } break case 'fulfilled': if ( nextResult.status === 'error' || nextResult.data !== prevThenable.value ) { recreateThenable() } break case 'rejected': if ( nextResult.status !== 'error' || nextResult.error !== prevThenable.reason ) { recreateThenable() } break } } return nextResult } updateResult(): void { const prevResult = this.#currentResult as | QueryObserverResult | undefined const nextResult = this.createResult(this.#currentQuery, this.options) this.#currentResultState = this.#currentQuery.state this.#currentResultOptions = this.options if (this.#currentResultState.data !== undefined) { this.#lastQueryWithDefinedData = this.#currentQuery } // Only notify and update result if something has changed if (shallowEqualObjects(nextResult, prevResult)) { return } this.#currentResult = nextResult const shouldNotifyListeners = (): boolean => { if (!prevResult) { return true } const { notifyOnChangeProps } = this.options const notifyOnChangePropsValue = typeof notifyOnChangeProps === 'function' ? notifyOnChangeProps() : notifyOnChangeProps if ( notifyOnChangePropsValue === 'all' || (!notifyOnChangePropsValue && !this.#trackedProps.size) ) { return true } const includedProps = new Set( notifyOnChangePropsValue ?? this.#trackedProps, ) if (this.options.throwOnError) { includedProps.add('error') } return Object.keys(this.#currentResult).some((key) => { const typedKey = key as keyof QueryObserverResult const changed = this.#currentResult[typedKey] !== prevResult[typedKey] return changed && includedProps.has(typedKey) }) } this.#notify({ listeners: shouldNotifyListeners() }) } #updateQuery(): void { const query = this.#client.getQueryCache().build(this.#client, this.options) if (query === this.#currentQuery) { return } const prevQuery = this.#currentQuery as | Query | undefined this.#currentQuery = query this.#currentQueryInitialState = query.state if (this.hasListeners()) { prevQuery?.removeObserver(this) query.addObserver(this) } } onQueryUpdate(): void { this.updateResult() if (this.hasListeners()) { this.#updateTimers() } } #notify(notifyOptions: { listeners: boolean }): void { notifyManager.batch(() => { // First, trigger the listeners if (notifyOptions.listeners) { this.listeners.forEach((listener) => { listener(this.#currentResult) }) } // Then the cache listeners this.#client.getQueryCache().notify({ query: this.#currentQuery, type: 'observerResultsUpdated', }) }) } } function shouldLoadOnMount( query: Query, options: QueryObserverOptions, ): boolean { return ( resolveEnabled(options.enabled, query) !== false && query.state.data === undefined && !(query.state.status === 'error' && options.retryOnMount === false) ) } function shouldFetchOnMount( query: Query, options: QueryObserverOptions, ): boolean { return ( shouldLoadOnMount(query, options) || (query.state.data !== undefined && shouldFetchOn(query, options, options.refetchOnMount)) ) } function shouldFetchOn( query: Query, options: QueryObserverOptions, field: (typeof options)['refetchOnMount'] & (typeof options)['refetchOnWindowFocus'] & (typeof options)['refetchOnReconnect'], ) { if ( resolveEnabled(options.enabled, query) !== false && resolveStaleTime(options.staleTime, query) !== 'static' ) { const value = typeof field === 'function' ? field(query) : field return value === 'always' || (value !== false && isStale(query, options)) } return false } function shouldFetchOptionally( query: Query, prevQuery: Query, options: QueryObserverOptions, prevOptions: QueryObserverOptions, ): boolean { return ( (query !== prevQuery || resolveEnabled(prevOptions.enabled, query) === false) && (!options.suspense || query.state.status !== 'error') && isStale(query, options) ) } function isStale( query: Query, options: QueryObserverOptions, ): boolean { return ( resolveEnabled(options.enabled, query) !== false && query.isStaleByTime(resolveStaleTime(options.staleTime, query)) ) } // this function would decide if we will update the observer's 'current' // properties after an optimistic reading via getOptimisticResult function shouldAssignObserverCurrentProperties< TQueryFnData = unknown, TError = unknown, TData = TQueryFnData, TQueryData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, >( observer: QueryObserver, optimisticResult: QueryObserverResult, ) { // if the newly created result isn't what the observer is holding as current, // then we'll need to update the properties as well if (!shallowEqualObjects(observer.getCurrentResult(), optimisticResult)) { return true } // basically, just keep previous properties if nothing changed return false } ================================================ FILE: packages/query-core/src/removable.ts ================================================ import { timeoutManager } from './timeoutManager' import { isServer, isValidTimeout } from './utils' import type { ManagedTimerId } from './timeoutManager' export abstract class Removable { gcTime!: number #gcTimeout?: ManagedTimerId destroy(): void { this.clearGcTimeout() } protected scheduleGc(): void { this.clearGcTimeout() if (isValidTimeout(this.gcTime)) { this.#gcTimeout = timeoutManager.setTimeout(() => { this.optionalRemove() }, this.gcTime) } } protected updateGcTime(newGcTime: number | undefined): void { // Default to 5 minutes (Infinity for server-side) if no gcTime is set this.gcTime = Math.max( this.gcTime || 0, newGcTime ?? (isServer ? Infinity : 5 * 60 * 1000), ) } protected clearGcTimeout() { if (this.#gcTimeout) { timeoutManager.clearTimeout(this.#gcTimeout) this.#gcTimeout = undefined } } protected abstract optionalRemove(): void } ================================================ FILE: packages/query-core/src/retryer.ts ================================================ import { focusManager } from './focusManager' import { onlineManager } from './onlineManager' import { pendingThenable } from './thenable' import { isServer, sleep } from './utils' import type { Thenable } from './thenable' import type { CancelOptions, DefaultError, NetworkMode } from './types' // TYPES interface RetryerConfig { fn: () => TData | Promise initialPromise?: Promise onCancel?: (error: TError) => void onFail?: (failureCount: number, error: TError) => void onPause?: () => void onContinue?: () => void retry?: RetryValue retryDelay?: RetryDelayValue networkMode: NetworkMode | undefined canRun: () => boolean } export interface Retryer { promise: Promise cancel: (cancelOptions?: CancelOptions) => void continue: () => Promise cancelRetry: () => void continueRetry: () => void canStart: () => boolean start: () => Promise status: () => 'pending' | 'resolved' | 'rejected' } export type RetryValue = boolean | number | ShouldRetryFunction type ShouldRetryFunction = ( failureCount: number, error: TError, ) => boolean export type RetryDelayValue = number | RetryDelayFunction type RetryDelayFunction = ( failureCount: number, error: TError, ) => number function defaultRetryDelay(failureCount: number) { return Math.min(1000 * 2 ** failureCount, 30000) } export function canFetch(networkMode: NetworkMode | undefined): boolean { return (networkMode ?? 'online') === 'online' ? onlineManager.isOnline() : true } export class CancelledError extends Error { revert?: boolean silent?: boolean constructor(options?: CancelOptions) { super('CancelledError') this.revert = options?.revert this.silent = options?.silent } } /** * @deprecated Use instanceof `CancelledError` instead. */ export function isCancelledError(value: any): value is CancelledError { return value instanceof CancelledError } export function createRetryer( config: RetryerConfig, ): Retryer { let isRetryCancelled = false let failureCount = 0 let continueFn: ((value?: unknown) => void) | undefined const thenable = pendingThenable() const isResolved = () => (thenable.status as Thenable['status']) !== 'pending' const cancel = (cancelOptions?: CancelOptions): void => { if (!isResolved()) { const error = new CancelledError(cancelOptions) as TError reject(error) config.onCancel?.(error) } } const cancelRetry = () => { isRetryCancelled = true } const continueRetry = () => { isRetryCancelled = false } const canContinue = () => focusManager.isFocused() && (config.networkMode === 'always' || onlineManager.isOnline()) && config.canRun() const canStart = () => canFetch(config.networkMode) && config.canRun() const resolve = (value: any) => { if (!isResolved()) { continueFn?.() thenable.resolve(value) } } const reject = (value: any) => { if (!isResolved()) { continueFn?.() thenable.reject(value) } } const pause = () => { return new Promise((continueResolve) => { continueFn = (value) => { if (isResolved() || canContinue()) { continueResolve(value) } } config.onPause?.() }).then(() => { continueFn = undefined if (!isResolved()) { config.onContinue?.() } }) } // Create loop function const run = () => { // Do nothing if already resolved if (isResolved()) { return } let promiseOrValue: any // we can re-use config.initialPromise on the first call of run() const initialPromise = failureCount === 0 ? config.initialPromise : undefined // Execute query try { promiseOrValue = initialPromise ?? config.fn() } catch (error) { promiseOrValue = Promise.reject(error) } Promise.resolve(promiseOrValue) .then(resolve) .catch((error) => { // Stop if the fetch is already resolved if (isResolved()) { return } // Do we need to retry the request? const retry = config.retry ?? (isServer ? 0 : 3) const retryDelay = config.retryDelay ?? defaultRetryDelay const delay = typeof retryDelay === 'function' ? retryDelay(failureCount, error) : retryDelay const shouldRetry = retry === true || (typeof retry === 'number' && failureCount < retry) || (typeof retry === 'function' && retry(failureCount, error)) if (isRetryCancelled || !shouldRetry) { // We are done if the query does not need to be retried reject(error) return } failureCount++ // Notify on fail config.onFail?.(failureCount, error) // Delay sleep(delay) // Pause if the document is not visible or when the device is offline .then(() => { return canContinue() ? undefined : pause() }) .then(() => { if (isRetryCancelled) { reject(error) } else { run() } }) }) } return { promise: thenable, status: () => thenable.status, cancel, continue: () => { continueFn?.() return thenable }, cancelRetry, continueRetry, canStart, start: () => { // Start loop if (canStart()) { run() } else { pause().then(run) } return thenable }, } } ================================================ FILE: packages/query-core/src/streamedQuery.ts ================================================ import { addToEnd } from './utils' import type { QueryFunction, QueryFunctionContext, QueryKey } from './types' type BaseStreamedQueryParams = { streamFn: ( context: QueryFunctionContext, ) => AsyncIterable | Promise> refetchMode?: 'append' | 'reset' | 'replace' } type SimpleStreamedQueryParams< TQueryFnData, TQueryKey extends QueryKey, > = BaseStreamedQueryParams & { reducer?: never initialValue?: never } type ReducibleStreamedQueryParams< TQueryFnData, TData, TQueryKey extends QueryKey, > = BaseStreamedQueryParams & { reducer: (acc: TData, chunk: TQueryFnData) => TData initialValue: TData } type StreamedQueryParams = | SimpleStreamedQueryParams | ReducibleStreamedQueryParams /** * This is a helper function to create a query function that streams data from an AsyncIterable. * Data will be an Array of all the chunks received. * The query will be in a 'pending' state until the first chunk of data is received, but will go to 'success' after that. * The query will stay in fetchStatus 'fetching' until the stream ends. * @param queryFn - The function that returns an AsyncIterable to stream data from. * @param refetchMode - Defines how re-fetches are handled. * Defaults to `'reset'`, erases all data and puts the query back into `pending` state. * Set to `'append'` to append new data to the existing data. * Set to `'replace'` to write all data to the cache once the stream ends. * @param reducer - A function to reduce the streamed chunks into the final data. * Defaults to a function that appends chunks to the end of the array. * @param initialValue - Initial value to be used while the first chunk is being fetched. */ export function streamedQuery< TQueryFnData = unknown, TData = Array, TQueryKey extends QueryKey = QueryKey, >({ streamFn, refetchMode = 'reset', reducer = (items, chunk) => addToEnd(items as Array, chunk) as TData, initialValue = [] as TData, }: StreamedQueryParams): QueryFunction< TData, TQueryKey > { return async (context) => { const query = context.client .getQueryCache() .find({ queryKey: context.queryKey, exact: true }) const isRefetch = !!query && query.state.data !== undefined if (isRefetch && refetchMode === 'reset') { query.setState({ status: 'pending', data: undefined, error: null, fetchStatus: 'fetching', }) } let result = initialValue const stream = await streamFn(context) for await (const chunk of stream) { if (context.signal.aborted) { break } // don't append to the cache directly when replace-refetching if (!isRefetch || refetchMode !== 'replace') { context.client.setQueryData(context.queryKey, (prev) => reducer(prev === undefined ? initialValue : prev, chunk), ) } result = reducer(result, chunk) } // finalize result: replace-refetching needs to write to the cache if (isRefetch && refetchMode === 'replace' && !context.signal.aborted) { context.client.setQueryData(context.queryKey, result) } return context.client.getQueryData(context.queryKey)! } } ================================================ FILE: packages/query-core/src/subscribable.ts ================================================ export class Subscribable { protected listeners = new Set() constructor() { this.subscribe = this.subscribe.bind(this) } subscribe(listener: TListener): () => void { this.listeners.add(listener) this.onSubscribe() return () => { this.listeners.delete(listener) this.onUnsubscribe() } } hasListeners(): boolean { return this.listeners.size > 0 } protected onSubscribe(): void { // Do nothing } protected onUnsubscribe(): void { // Do nothing } } ================================================ FILE: packages/query-core/src/thenable.ts ================================================ /** * Thenable types which matches React's types for promises * * React seemingly uses `.status`, `.value` and `.reason` properties on a promises to optimistically unwrap data from promises * * @see https://github.com/facebook/react/blob/main/packages/shared/ReactTypes.js#L112-L138 * @see https://github.com/facebook/react/blob/4f604941569d2e8947ce1460a0b2997e835f37b9/packages/react-debug-tools/src/ReactDebugHooks.js#L224-L227 */ import { noop } from './utils' interface Fulfilled { status: 'fulfilled' value: T } interface Rejected { status: 'rejected' reason: unknown } interface Pending { status: 'pending' /** * Resolve the promise with a value. * Will remove the `resolve` and `reject` properties from the promise. */ resolve: (value: T) => void /** * Reject the promise with a reason. * Will remove the `resolve` and `reject` properties from the promise. */ reject: (reason: unknown) => void } export type FulfilledThenable = Promise & Fulfilled export type RejectedThenable = Promise & Rejected export type PendingThenable = Promise & Pending export type Thenable = | FulfilledThenable | RejectedThenable | PendingThenable export function pendingThenable(): PendingThenable { let resolve: Pending['resolve'] let reject: Pending['reject'] // this could use `Promise.withResolvers()` in the future const thenable = new Promise((_resolve, _reject) => { resolve = _resolve reject = _reject }) as PendingThenable thenable.status = 'pending' thenable.catch(() => { // prevent unhandled rejection errors }) function finalize(data: Fulfilled | Rejected) { Object.assign(thenable, data) // clear pending props props to avoid calling them twice delete (thenable as Partial>).resolve delete (thenable as Partial>).reject } thenable.resolve = (value) => { finalize({ status: 'fulfilled', value, }) resolve(value) } thenable.reject = (reason) => { finalize({ status: 'rejected', reason, }) reject(reason) } return thenable } /** * This function takes a Promise-like input and detects whether the data * is synchronously available or not. * * It does not inspect .status, .value or .reason properties of the promise, * as those are not always available, and the .status of React's promises * should not be considered part of the public API. */ export function tryResolveSync(promise: Promise | Thenable) { let data: unknown promise .then((result) => { data = result return result }, noop) // .catch can be unavailable on certain kinds of thenable's // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition ?.catch(noop) if (data !== undefined) { return { data } } return undefined } ================================================ FILE: packages/query-core/src/timeoutManager.ts ================================================ /** * {@link TimeoutManager} does not support passing arguments to the callback. * * `(_: void)` is the argument type inferred by TypeScript's default typings for * `setTimeout(cb, number)`. * If we don't accept a single void argument, then * `new Promise(resolve => timeoutManager.setTimeout(resolve, N))` is a type error. */ export type TimeoutCallback = (_: void) => void /** * Wrapping `setTimeout` is awkward from a typing perspective because platform * typings may extend the return type of `setTimeout`. For example, NodeJS * typings add `NodeJS.Timeout`; but a non-default `timeoutManager` may not be * able to return such a type. */ export type ManagedTimerId = number | { [Symbol.toPrimitive]: () => number } /** * Backend for timer functions. */ export type TimeoutProvider = { readonly setTimeout: (callback: TimeoutCallback, delay: number) => TTimerId readonly clearTimeout: (timeoutId: TTimerId | undefined) => void readonly setInterval: (callback: TimeoutCallback, delay: number) => TTimerId readonly clearInterval: (intervalId: TTimerId | undefined) => void } export const defaultTimeoutProvider: TimeoutProvider< ReturnType > = { // We need the wrapper function syntax below instead of direct references to // global setTimeout etc. // // BAD: `setTimeout: setTimeout` // GOOD: `setTimeout: (cb, delay) => setTimeout(cb, delay)` // // If we use direct references here, then anything that wants to spy on or // replace the global setTimeout (like tests) won't work since we'll already // have a hard reference to the original implementation at the time when this // file was imported. setTimeout: (callback, delay) => setTimeout(callback, delay), clearTimeout: (timeoutId) => clearTimeout(timeoutId), setInterval: (callback, delay) => setInterval(callback, delay), clearInterval: (intervalId) => clearInterval(intervalId), } /** * Allows customization of how timeouts are created. * * @tanstack/query-core makes liberal use of timeouts to implement `staleTime` * and `gcTime`. The default TimeoutManager provider uses the platform's global * `setTimeout` implementation, which is known to have scalability issues with * thousands of timeouts on the event loop. * * If you hit this limitation, consider providing a custom TimeoutProvider that * coalesces timeouts. */ export class TimeoutManager implements Omit { // We cannot have TimeoutManager as we must instantiate it with a concrete // type at app boot; and if we leave that type, then any new timer provider // would need to support ReturnType, which is infeasible. // // We settle for type safety for the TimeoutProvider type, and accept that // this class is unsafe internally to allow for extension. #provider: TimeoutProvider = defaultTimeoutProvider #providerCalled = false setTimeoutProvider( provider: TimeoutProvider, ): void { if (process.env.NODE_ENV !== 'production') { if (this.#providerCalled && provider !== this.#provider) { // After changing providers, `clearTimeout` will not work as expected for // timeouts from the previous provider. // // Since they may allocate the same timeout ID, clearTimeout may cancel an // arbitrary different timeout, or unexpected no-op. // // We could protect against this by mixing the timeout ID bits // deterministically with some per-provider bits. // // We could internally queue `setTimeout` calls to `TimeoutManager` until // some API call to set the initial provider. console.error( `[timeoutManager]: Switching provider after calls to previous provider might result in unexpected behavior.`, { previous: this.#provider, provider }, ) } } this.#provider = provider if (process.env.NODE_ENV !== 'production') { this.#providerCalled = false } } setTimeout(callback: TimeoutCallback, delay: number): ManagedTimerId { if (process.env.NODE_ENV !== 'production') { this.#providerCalled = true } return this.#provider.setTimeout(callback, delay) } clearTimeout(timeoutId: ManagedTimerId | undefined): void { this.#provider.clearTimeout(timeoutId) } setInterval(callback: TimeoutCallback, delay: number): ManagedTimerId { if (process.env.NODE_ENV !== 'production') { this.#providerCalled = true } return this.#provider.setInterval(callback, delay) } clearInterval(intervalId: ManagedTimerId | undefined): void { this.#provider.clearInterval(intervalId) } } export const timeoutManager = new TimeoutManager() /** * In many cases code wants to delay to the next event loop tick; this is not * mediated by {@link timeoutManager}. * * This function is provided to make auditing the `tanstack/query-core` for * incorrect use of system `setTimeout` easier. */ export function systemSetTimeoutZero(callback: TimeoutCallback): void { setTimeout(callback, 0) } ================================================ FILE: packages/query-core/src/types.ts ================================================ /* istanbul ignore file */ import type { QueryClient } from './queryClient' import type { DehydrateOptions, HydrateOptions } from './hydration' import type { MutationState } from './mutation' import type { FetchDirection, Query, QueryBehavior } from './query' import type { RetryDelayValue, RetryValue } from './retryer' import type { QueryFilters, QueryTypeFilter, SkipToken } from './utils' import type { QueryCache } from './queryCache' import type { MutationCache } from './mutationCache' export type NonUndefinedGuard = T extends undefined ? never : T export type DistributiveOmit< TObject, TKey extends keyof TObject, > = TObject extends any ? Omit : never export type OmitKeyof< TObject, TKey extends TStrictly extends 'safely' ? | keyof TObject | (string & Record) | (number & Record) | (symbol & Record) : keyof TObject, TStrictly extends 'strictly' | 'safely' = 'strictly', > = Omit export type Override = { [AKey in keyof TTargetA]: AKey extends keyof TTargetB ? TTargetB[AKey] : TTargetA[AKey] } export type NoInfer = [T][T extends any ? 0 : never] export interface Register { // defaultError: Error // queryMeta: Record // mutationMeta: Record // queryKey: ReadonlyArray // mutationKey: ReadonlyArray } export type DefaultError = Register extends { defaultError: infer TError } ? TError : Error export type QueryKey = Register extends { queryKey: infer TQueryKey } ? TQueryKey extends ReadonlyArray ? TQueryKey : TQueryKey extends Array ? TQueryKey : ReadonlyArray : ReadonlyArray export const dataTagSymbol = Symbol('dataTagSymbol') export type dataTagSymbol = typeof dataTagSymbol export const dataTagErrorSymbol = Symbol('dataTagErrorSymbol') export type dataTagErrorSymbol = typeof dataTagErrorSymbol export const unsetMarker = Symbol('unsetMarker') export type UnsetMarker = typeof unsetMarker export type AnyDataTag = { [dataTagSymbol]: any [dataTagErrorSymbol]: any } export type DataTag< TType, TValue, TError = UnsetMarker, > = TType extends AnyDataTag ? TType : TType & { [dataTagSymbol]: TValue [dataTagErrorSymbol]: TError } export type InferDataFromTag = TTaggedQueryKey extends DataTag ? TaggedValue : TQueryFnData export type InferErrorFromTag = TTaggedQueryKey extends DataTag ? TaggedError extends UnsetMarker ? TError : TaggedError : TError export type QueryFunction< T = unknown, TQueryKey extends QueryKey = QueryKey, TPageParam = never, > = (context: QueryFunctionContext) => T | Promise export type StaleTime = number | 'static' export type StaleTimeFunction< TQueryFnData = unknown, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, > = | StaleTime | ((query: Query) => StaleTime) export type Enabled< TQueryFnData = unknown, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, > = | boolean | ((query: Query) => boolean) export type QueryPersister< T = unknown, TQueryKey extends QueryKey = QueryKey, TPageParam = never, > = [TPageParam] extends [never] ? ( queryFn: QueryFunction, context: QueryFunctionContext, query: Query, ) => T | Promise : ( queryFn: QueryFunction, context: QueryFunctionContext, query: Query, ) => T | Promise export type QueryFunctionContext< TQueryKey extends QueryKey = QueryKey, TPageParam = never, > = [TPageParam] extends [never] ? { client: QueryClient queryKey: TQueryKey signal: AbortSignal meta: QueryMeta | undefined pageParam?: unknown /** * @deprecated * if you want access to the direction, you can add it to the pageParam */ direction?: unknown } : { client: QueryClient queryKey: TQueryKey signal: AbortSignal pageParam: TPageParam /** * @deprecated * if you want access to the direction, you can add it to the pageParam */ direction: FetchDirection meta: QueryMeta | undefined } export type InitialDataFunction = () => T | undefined type NonFunctionGuard = T extends Function ? never : T export type PlaceholderDataFunction< TQueryFnData = unknown, TError = DefaultError, TQueryData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, > = ( previousData: TQueryData | undefined, previousQuery: Query | undefined, ) => TQueryData | undefined export type QueriesPlaceholderDataFunction = ( previousData: undefined, previousQuery: undefined, ) => TQueryData | undefined export type QueryKeyHashFunction = ( queryKey: TQueryKey, ) => string export type GetPreviousPageParamFunction = ( firstPage: TQueryFnData, allPages: Array, firstPageParam: TPageParam, allPageParams: Array, ) => TPageParam | undefined | null export type GetNextPageParamFunction = ( lastPage: TQueryFnData, allPages: Array, lastPageParam: TPageParam, allPageParams: Array, ) => TPageParam | undefined | null export interface InfiniteData { pages: Array pageParams: Array } export type QueryMeta = Register extends { queryMeta: infer TQueryMeta } ? TQueryMeta extends Record ? TQueryMeta : Record : Record export type NetworkMode = 'online' | 'always' | 'offlineFirst' export type NotifyOnChangeProps = | Array | 'all' | undefined | (() => Array | 'all' | undefined) export interface QueryOptions< TQueryFnData = unknown, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, TPageParam = never, > { /** * If `false`, failed queries will not retry by default. * If `true`, failed queries will retry infinitely., failureCount: num * If set to an integer number, e.g. 3, failed queries will retry until the failed query count meets that number. * If set to a function `(failureCount, error) => boolean` failed queries will retry until the function returns false. */ retry?: RetryValue retryDelay?: RetryDelayValue networkMode?: NetworkMode /** * The time in milliseconds that unused/inactive cache data remains in memory. * When a query's cache becomes unused or inactive, that cache data will be garbage collected after this duration. * When different garbage collection times are specified, the longest one will be used. * Setting it to `Infinity` will disable garbage collection. */ gcTime?: number queryFn?: QueryFunction | SkipToken persister?: QueryPersister< NoInfer, NoInfer, NoInfer > queryHash?: string queryKey?: TQueryKey queryKeyHashFn?: QueryKeyHashFunction initialData?: TData | InitialDataFunction initialDataUpdatedAt?: number | (() => number | undefined) behavior?: QueryBehavior /** * Set this to `false` to disable structural sharing between query results. * Set this to a function which accepts the old and new data and returns resolved data of the same type to implement custom structural sharing logic. * Defaults to `true`. */ structuralSharing?: | boolean | ((oldData: unknown | undefined, newData: unknown) => unknown) _defaulted?: boolean /** * Additional payload to be stored on each query. * Use this property to pass information that can be used in other places. */ meta?: QueryMeta /** * Maximum number of pages to store in the data of an infinite query. */ maxPages?: number } export interface InitialPageParam { initialPageParam: TPageParam } export interface InfiniteQueryPageParamsOptions< TQueryFnData = unknown, TPageParam = unknown, > extends InitialPageParam { /** * This function can be set to automatically get the previous cursor for infinite queries. * The result will also be used to determine the value of `hasPreviousPage`. */ getPreviousPageParam?: GetPreviousPageParamFunction /** * This function can be set to automatically get the next cursor for infinite queries. * The result will also be used to determine the value of `hasNextPage`. */ getNextPageParam: GetNextPageParamFunction } export type ThrowOnError< TQueryFnData, TError, TQueryData, TQueryKey extends QueryKey, > = | boolean | (( error: TError, query: Query, ) => boolean) export interface QueryObserverOptions< TQueryFnData = unknown, TError = DefaultError, TData = TQueryFnData, TQueryData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, TPageParam = never, > extends WithRequired< QueryOptions, 'queryKey' > { /** * Set this to `false` or a function that returns `false` to disable automatic refetching when the query mounts or changes query keys. * To refetch the query, use the `refetch` method returned from the `useQuery` instance. * Accepts a boolean or function that returns a boolean. * Defaults to `true`. */ enabled?: Enabled /** * The time in milliseconds after data is considered stale. * If set to `Infinity`, the data will never be considered stale. * If set to a function, the function will be executed with the query to compute a `staleTime`. * Defaults to `0`. */ staleTime?: StaleTimeFunction /** * If set to a number, the query will continuously refetch at this frequency in milliseconds. * If set to a function, the function will be executed with the latest data and query to compute a frequency * Defaults to `false`. */ refetchInterval?: | number | false | (( query: Query, ) => number | false | undefined) /** * If set to `true`, the query will continue to refetch while their tab/window is in the background. * Defaults to `false`. */ refetchIntervalInBackground?: boolean /** * If set to `true`, the query will refetch on window focus if the data is stale. * If set to `false`, the query will not refetch on window focus. * If set to `'always'`, the query will always refetch on window focus. * If set to a function, the function will be executed with the latest data and query to compute the value. * Defaults to `true`. */ refetchOnWindowFocus?: | boolean | 'always' | (( query: Query, ) => boolean | 'always') /** * If set to `true`, the query will refetch on reconnect if the data is stale. * If set to `false`, the query will not refetch on reconnect. * If set to `'always'`, the query will always refetch on reconnect. * If set to a function, the function will be executed with the latest data and query to compute the value. * Defaults to the value of `networkOnline` (`true`) */ refetchOnReconnect?: | boolean | 'always' | (( query: Query, ) => boolean | 'always') /** * If set to `true`, the query will refetch on mount if the data is stale. * If set to `false`, will disable additional instances of a query to trigger background refetch. * If set to `'always'`, the query will always refetch on mount. * If set to a function, the function will be executed with the latest data and query to compute the value * Defaults to `true`. */ refetchOnMount?: | boolean | 'always' | (( query: Query, ) => boolean | 'always') /** * If set to `false`, the query will not be retried on mount if it contains an error. * Defaults to `true`. */ retryOnMount?: boolean /** * If set, the component will only re-render if any of the listed properties change. * When set to `['data', 'error']`, the component will only re-render when the `data` or `error` properties change. * When set to `'all'`, the component will re-render whenever a query is updated. * When set to a function, the function will be executed to compute the list of properties. * By default, access to properties will be tracked, and the component will only re-render when one of the tracked properties change. */ notifyOnChangeProps?: NotifyOnChangeProps /** * Whether errors should be thrown instead of setting the `error` property. * If set to `true` or `suspense` is `true`, all errors will be thrown to the error boundary. * If set to `false` and `suspense` is `false`, errors are returned as state. * If set to a function, it will be passed the error and the query, and it should return a boolean indicating whether to show the error in an error boundary (`true`) or return the error as state (`false`). * Defaults to `false`. */ throwOnError?: ThrowOnError /** * This option can be used to transform or select a part of the data returned by the query function. */ select?: (data: TQueryData) => TData /** * If set to `true`, the query will suspend when `status === 'pending'` * and throw errors when `status === 'error'`. * Defaults to `false`. */ suspense?: boolean /** * If set, this value will be used as the placeholder data for this particular query observer while the query is still in the `loading` data and no initialData has been provided. */ placeholderData?: | NonFunctionGuard | PlaceholderDataFunction< NonFunctionGuard, TError, NonFunctionGuard, TQueryKey > _optimisticResults?: 'optimistic' | 'isRestoring' /** * Enable prefetching during rendering */ experimental_prefetchInRender?: boolean } export type WithRequired = TTarget & { [_ in TKey]: {} } export type DefaultedQueryObserverOptions< TQueryFnData = unknown, TError = DefaultError, TData = TQueryFnData, TQueryData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, > = WithRequired< QueryObserverOptions, 'throwOnError' | 'refetchOnReconnect' | 'queryHash' > export interface InfiniteQueryObserverOptions< TQueryFnData = unknown, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, TPageParam = unknown, > extends QueryObserverOptions< TQueryFnData, TError, TData, InfiniteData, TQueryKey, TPageParam >, InfiniteQueryPageParamsOptions {} export type DefaultedInfiniteQueryObserverOptions< TQueryFnData = unknown, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, TPageParam = unknown, > = WithRequired< InfiniteQueryObserverOptions< TQueryFnData, TError, TData, TQueryKey, TPageParam >, 'throwOnError' | 'refetchOnReconnect' | 'queryHash' > export interface FetchQueryOptions< TQueryFnData = unknown, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, TPageParam = never, > extends WithRequired< QueryOptions, 'queryKey' > { initialPageParam?: never /** * The time in milliseconds after data is considered stale. * If the data is fresh it will be returned from the cache. */ staleTime?: StaleTimeFunction } export interface EnsureQueryDataOptions< TQueryFnData = unknown, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, TPageParam = never, > extends FetchQueryOptions< TQueryFnData, TError, TData, TQueryKey, TPageParam > { revalidateIfStale?: boolean } export type EnsureInfiniteQueryDataOptions< TQueryFnData = unknown, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, TPageParam = unknown, > = FetchInfiniteQueryOptions< TQueryFnData, TError, TData, TQueryKey, TPageParam > & { revalidateIfStale?: boolean } type FetchInfiniteQueryPages = | { pages?: never } | { pages: number getNextPageParam: GetNextPageParamFunction } export type FetchInfiniteQueryOptions< TQueryFnData = unknown, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, TPageParam = unknown, > = Omit< FetchQueryOptions< TQueryFnData, TError, InfiniteData, TQueryKey, TPageParam >, 'initialPageParam' > & InitialPageParam & FetchInfiniteQueryPages export interface ResultOptions { throwOnError?: boolean } export interface RefetchOptions extends ResultOptions { /** * If set to `true`, a currently running request will be cancelled before a new request is made * * If set to `false`, no refetch will be made if there is already a request running. * * Defaults to `true`. */ cancelRefetch?: boolean } export interface InvalidateQueryFilters extends QueryFilters { refetchType?: QueryTypeFilter | 'none' } export interface RefetchQueryFilters extends QueryFilters {} export interface InvalidateOptions extends RefetchOptions {} export interface ResetOptions extends RefetchOptions {} export interface FetchNextPageOptions extends ResultOptions { /** * If set to `true`, calling `fetchNextPage` repeatedly will invoke `queryFn` every time, * whether the previous invocation has resolved or not. Also, the result from previous invocations will be ignored. * * If set to `false`, calling `fetchNextPage` repeatedly won't have any effect until the first invocation has resolved. * * Defaults to `true`. */ cancelRefetch?: boolean } export interface FetchPreviousPageOptions extends ResultOptions { /** * If set to `true`, calling `fetchPreviousPage` repeatedly will invoke `queryFn` every time, * whether the previous invocation has resolved or not. Also, the result from previous invocations will be ignored. * * If set to `false`, calling `fetchPreviousPage` repeatedly won't have any effect until the first invocation has resolved. * * Defaults to `true`. */ cancelRefetch?: boolean } export type QueryStatus = 'pending' | 'error' | 'success' export type FetchStatus = 'fetching' | 'paused' | 'idle' export interface QueryObserverBaseResult< TData = unknown, TError = DefaultError, > { /** * The last successfully resolved data for the query. */ data: TData | undefined /** * The timestamp for when the query most recently returned the `status` as `"success"`. */ dataUpdatedAt: number /** * The error object for the query, if an error was thrown. * - Defaults to `null`. */ error: TError | null /** * The timestamp for when the query most recently returned the `status` as `"error"`. */ errorUpdatedAt: number /** * The failure count for the query. * - Incremented every time the query fails. * - Reset to `0` when the query succeeds. */ failureCount: number /** * The failure reason for the query retry. * - Reset to `null` when the query succeeds. */ failureReason: TError | null /** * The sum of all errors. */ errorUpdateCount: number /** * A derived boolean from the `status` variable, provided for convenience. * - `true` if the query attempt resulted in an error. */ isError: boolean /** * Will be `true` if the query has been fetched. */ isFetched: boolean /** * Will be `true` if the query has been fetched after the component mounted. * - This property can be used to not show any previously cached data. */ isFetchedAfterMount: boolean /** * A derived boolean from the `fetchStatus` variable, provided for convenience. * - `true` whenever the `queryFn` is executing, which includes initial `pending` as well as background refetch. */ isFetching: boolean /** * Is `true` whenever the first fetch for a query is in-flight. * - Is the same as `isFetching && isPending`. */ isLoading: boolean /** * Will be `pending` if there's no cached data and no query attempt was finished yet. */ isPending: boolean /** * Will be `true` if the query failed while fetching for the first time. */ isLoadingError: boolean /** * @deprecated `isInitialLoading` is being deprecated in favor of `isLoading` * and will be removed in the next major version. */ isInitialLoading: boolean /** * A derived boolean from the `fetchStatus` variable, provided for convenience. * - The query wanted to fetch, but has been `paused`. */ isPaused: boolean /** * Will be `true` if the data shown is the placeholder data. */ isPlaceholderData: boolean /** * Will be `true` if the query failed while refetching. */ isRefetchError: boolean /** * Is `true` whenever a background refetch is in-flight, which _does not_ include initial `pending`. * - Is the same as `isFetching && !isPending`. */ isRefetching: boolean /** * Will be `true` if the data in the cache is invalidated or if the data is older than the given `staleTime`. */ isStale: boolean /** * A derived boolean from the `status` variable, provided for convenience. * - `true` if the query has received a response with no errors and is ready to display its data. */ isSuccess: boolean /** * `true` if this observer is enabled, `false` otherwise. */ isEnabled: boolean /** * A function to manually refetch the query. */ refetch: ( options?: RefetchOptions, ) => Promise> /** * The status of the query. * - Will be: * - `pending` if there's no cached data and no query attempt was finished yet. * - `error` if the query attempt resulted in an error. * - `success` if the query has received a response with no errors and is ready to display its data. */ status: QueryStatus /** * The fetch status of the query. * - `fetching`: Is `true` whenever the queryFn is executing, which includes initial `pending` as well as background refetch. * - `paused`: The query wanted to fetch, but has been `paused`. * - `idle`: The query is not fetching. * - See [Network Mode](https://tanstack.com/query/latest/docs/framework/react/guides/network-mode) for more information. */ fetchStatus: FetchStatus /** * A stable promise that will be resolved with the data of the query. * Requires the `experimental_prefetchInRender` feature flag to be enabled. * @example * * ### Enabling the feature flag * ```ts * const client = new QueryClient({ * defaultOptions: { * queries: { * experimental_prefetchInRender: true, * }, * }, * }) * ``` * * ### Usage * ```tsx * import { useQuery } from '@tanstack/react-query' * import React from 'react' * import { fetchTodos, type Todo } from './api' * * function TodoList({ query }: { query: UseQueryResult }) { * const data = React.use(query.promise) * * return ( *
    * {data.map(todo => ( *
  • {todo.title}
  • * ))} *
* ) * } * * export function App() { * const query = useQuery({ queryKey: ['todos'], queryFn: fetchTodos }) * * return ( * <> *

Todos

* Loading...}> * * * * ) * } * ``` */ promise: Promise } export interface QueryObserverPendingResult< TData = unknown, TError = DefaultError, > extends QueryObserverBaseResult { data: undefined error: null isError: false isPending: true isLoadingError: false isRefetchError: false isSuccess: false isPlaceholderData: false status: 'pending' } export interface QueryObserverLoadingResult< TData = unknown, TError = DefaultError, > extends QueryObserverBaseResult { data: undefined error: null isError: false isPending: true isLoading: true isLoadingError: false isRefetchError: false isSuccess: false isPlaceholderData: false status: 'pending' } export interface QueryObserverLoadingErrorResult< TData = unknown, TError = DefaultError, > extends QueryObserverBaseResult { data: undefined error: TError isError: true isPending: false isLoading: false isLoadingError: true isRefetchError: false isSuccess: false isPlaceholderData: false status: 'error' } export interface QueryObserverRefetchErrorResult< TData = unknown, TError = DefaultError, > extends QueryObserverBaseResult { data: TData error: TError isError: true isPending: false isLoading: false isLoadingError: false isRefetchError: true isSuccess: false isPlaceholderData: false status: 'error' } export interface QueryObserverSuccessResult< TData = unknown, TError = DefaultError, > extends QueryObserverBaseResult { data: TData error: null isError: false isPending: false isLoading: false isLoadingError: false isRefetchError: false isSuccess: true isPlaceholderData: false status: 'success' } export interface QueryObserverPlaceholderResult< TData = unknown, TError = DefaultError, > extends QueryObserverBaseResult { data: TData isError: false error: null isPending: false isLoading: false isLoadingError: false isRefetchError: false isSuccess: true isPlaceholderData: true status: 'success' } export type DefinedQueryObserverResult< TData = unknown, TError = DefaultError, > = | QueryObserverRefetchErrorResult | QueryObserverSuccessResult export type QueryObserverResult = | DefinedQueryObserverResult | QueryObserverLoadingErrorResult | QueryObserverLoadingResult | QueryObserverPendingResult | QueryObserverPlaceholderResult export interface InfiniteQueryObserverBaseResult< TData = unknown, TError = DefaultError, > extends QueryObserverBaseResult { /** * This function allows you to fetch the next "page" of results. */ fetchNextPage: ( options?: FetchNextPageOptions, ) => Promise> /** * This function allows you to fetch the previous "page" of results. */ fetchPreviousPage: ( options?: FetchPreviousPageOptions, ) => Promise> /** * Will be `true` if there is a next page to be fetched (known via the `getNextPageParam` option). */ hasNextPage: boolean /** * Will be `true` if there is a previous page to be fetched (known via the `getPreviousPageParam` option). */ hasPreviousPage: boolean /** * Will be `true` if the query failed while fetching the next page. */ isFetchNextPageError: boolean /** * Will be `true` while fetching the next page with `fetchNextPage`. */ isFetchingNextPage: boolean /** * Will be `true` if the query failed while fetching the previous page. */ isFetchPreviousPageError: boolean /** * Will be `true` while fetching the previous page with `fetchPreviousPage`. */ isFetchingPreviousPage: boolean } export interface InfiniteQueryObserverPendingResult< TData = unknown, TError = DefaultError, > extends InfiniteQueryObserverBaseResult { data: undefined error: null isError: false isPending: true isLoadingError: false isRefetchError: false isFetchNextPageError: false isFetchPreviousPageError: false isSuccess: false isPlaceholderData: false status: 'pending' } export interface InfiniteQueryObserverLoadingResult< TData = unknown, TError = DefaultError, > extends InfiniteQueryObserverBaseResult { data: undefined error: null isError: false isPending: true isLoading: true isLoadingError: false isRefetchError: false isFetchNextPageError: false isFetchPreviousPageError: false isSuccess: false isPlaceholderData: false status: 'pending' } export interface InfiniteQueryObserverLoadingErrorResult< TData = unknown, TError = DefaultError, > extends InfiniteQueryObserverBaseResult { data: undefined error: TError isError: true isPending: false isLoading: false isLoadingError: true isRefetchError: false isFetchNextPageError: false isFetchPreviousPageError: false isSuccess: false isPlaceholderData: false status: 'error' } export interface InfiniteQueryObserverRefetchErrorResult< TData = unknown, TError = DefaultError, > extends InfiniteQueryObserverBaseResult { data: TData error: TError isError: true isPending: false isLoading: false isLoadingError: false isRefetchError: true isSuccess: false isPlaceholderData: false status: 'error' } export interface InfiniteQueryObserverSuccessResult< TData = unknown, TError = DefaultError, > extends InfiniteQueryObserverBaseResult { data: TData error: null isError: false isPending: false isLoading: false isLoadingError: false isRefetchError: false isFetchNextPageError: false isFetchPreviousPageError: false isSuccess: true isPlaceholderData: false status: 'success' } export interface InfiniteQueryObserverPlaceholderResult< TData = unknown, TError = DefaultError, > extends InfiniteQueryObserverBaseResult { data: TData isError: false error: null isPending: false isLoading: false isLoadingError: false isRefetchError: false isSuccess: true isPlaceholderData: true isFetchNextPageError: false isFetchPreviousPageError: false status: 'success' } export type DefinedInfiniteQueryObserverResult< TData = unknown, TError = DefaultError, > = | InfiniteQueryObserverRefetchErrorResult | InfiniteQueryObserverSuccessResult export type InfiniteQueryObserverResult< TData = unknown, TError = DefaultError, > = | DefinedInfiniteQueryObserverResult | InfiniteQueryObserverLoadingErrorResult | InfiniteQueryObserverLoadingResult | InfiniteQueryObserverPendingResult | InfiniteQueryObserverPlaceholderResult export type MutationKey = Register extends { mutationKey: infer TMutationKey } ? TMutationKey extends Array ? TMutationKey : TMutationKey extends Array ? TMutationKey : ReadonlyArray : ReadonlyArray export type MutationStatus = 'idle' | 'pending' | 'success' | 'error' export type MutationScope = { id: string } export type MutationMeta = Register extends { mutationMeta: infer TMutationMeta } ? TMutationMeta extends Record ? TMutationMeta : Record : Record export type MutationFunctionContext = { client: QueryClient meta: MutationMeta | undefined mutationKey?: MutationKey } export type MutationFunction = ( variables: TVariables, context: MutationFunctionContext, ) => Promise export interface MutationOptions< TData = unknown, TError = DefaultError, TVariables = void, TOnMutateResult = unknown, > { mutationFn?: MutationFunction mutationKey?: MutationKey onMutate?: ( variables: TVariables, context: MutationFunctionContext, ) => Promise | TOnMutateResult | undefined onSuccess?: ( data: TData, variables: TVariables, onMutateResult: TOnMutateResult | undefined, context: MutationFunctionContext, ) => Promise | unknown onError?: ( error: TError, variables: TVariables, onMutateResult: TOnMutateResult | undefined, context: MutationFunctionContext, ) => Promise | unknown onSettled?: ( data: TData | undefined, error: TError | null, variables: TVariables, onMutateResult: TOnMutateResult | undefined, context: MutationFunctionContext, ) => Promise | unknown retry?: RetryValue retryDelay?: RetryDelayValue networkMode?: NetworkMode gcTime?: number _defaulted?: boolean meta?: MutationMeta scope?: MutationScope } export interface MutationObserverOptions< TData = unknown, TError = DefaultError, TVariables = void, TOnMutateResult = unknown, > extends MutationOptions { throwOnError?: boolean | ((error: TError) => boolean) } export interface MutateOptions< TData = unknown, TError = DefaultError, TVariables = void, TOnMutateResult = unknown, > { onSuccess?: ( data: TData, variables: TVariables, onMutateResult: TOnMutateResult | undefined, context: MutationFunctionContext, ) => void onError?: ( error: TError, variables: TVariables, onMutateResult: TOnMutateResult | undefined, context: MutationFunctionContext, ) => void onSettled?: ( data: TData | undefined, error: TError | null, variables: TVariables, onMutateResult: TOnMutateResult | undefined, context: MutationFunctionContext, ) => void } export type MutateFunction< TData = unknown, TError = DefaultError, TVariables = void, TOnMutateResult = unknown, > = ( variables: TVariables, options?: MutateOptions, ) => Promise export interface MutationObserverBaseResult< TData = unknown, TError = DefaultError, TVariables = void, TOnMutateResult = unknown, > extends MutationState { /** * The last successfully resolved data for the mutation. */ data: TData | undefined /** * The variables object passed to the `mutationFn`. */ variables: TVariables | undefined /** * The error object for the mutation, if an error was encountered. * - Defaults to `null`. */ error: TError | null /** * A boolean variable derived from `status`. * - `true` if the last mutation attempt resulted in an error. */ isError: boolean /** * A boolean variable derived from `status`. * - `true` if the mutation is in its initial state prior to executing. */ isIdle: boolean /** * A boolean variable derived from `status`. * - `true` if the mutation is currently executing. */ isPending: boolean /** * A boolean variable derived from `status`. * - `true` if the last mutation attempt was successful. */ isSuccess: boolean /** * The status of the mutation. * - Will be: * - `idle` initial status prior to the mutation function executing. * - `pending` if the mutation is currently executing. * - `error` if the last mutation attempt resulted in an error. * - `success` if the last mutation attempt was successful. */ status: MutationStatus /** * The mutation function you can call with variables to trigger the mutation and optionally hooks on additional callback options. * @param variables - The variables object to pass to the `mutationFn`. * @param options.onSuccess - This function will fire when the mutation is successful and will be passed the mutation's result. * @param options.onError - This function will fire if the mutation encounters an error and will be passed the error. * @param options.onSettled - This function will fire when the mutation is either successfully fetched or encounters an error and be passed either the data or error. * @remarks * - If you make multiple requests, `onSuccess` will fire only after the latest call you've made. * - All the callback functions (`onSuccess`, `onError`, `onSettled`) are void functions, and the returned value will be ignored. */ mutate: MutateFunction /** * A function to clean the mutation internal state (i.e., it resets the mutation to its initial state). */ reset: () => void } export interface MutationObserverIdleResult< TData = unknown, TError = DefaultError, TVariables = void, TOnMutateResult = unknown, > extends MutationObserverBaseResult< TData, TError, TVariables, TOnMutateResult > { data: undefined variables: undefined error: null isError: false isIdle: true isPending: false isSuccess: false status: 'idle' } export interface MutationObserverLoadingResult< TData = unknown, TError = DefaultError, TVariables = void, TOnMutateResult = unknown, > extends MutationObserverBaseResult< TData, TError, TVariables, TOnMutateResult > { data: undefined variables: TVariables error: null isError: false isIdle: false isPending: true isSuccess: false status: 'pending' } export interface MutationObserverErrorResult< TData = unknown, TError = DefaultError, TVariables = void, TOnMutateResult = unknown, > extends MutationObserverBaseResult< TData, TError, TVariables, TOnMutateResult > { data: undefined error: TError variables: TVariables isError: true isIdle: false isPending: false isSuccess: false status: 'error' } export interface MutationObserverSuccessResult< TData = unknown, TError = DefaultError, TVariables = void, TOnMutateResult = unknown, > extends MutationObserverBaseResult< TData, TError, TVariables, TOnMutateResult > { data: TData error: null variables: TVariables isError: false isIdle: false isPending: false isSuccess: true status: 'success' } export type MutationObserverResult< TData = unknown, TError = DefaultError, TVariables = void, TOnMutateResult = unknown, > = | MutationObserverIdleResult | MutationObserverLoadingResult | MutationObserverErrorResult | MutationObserverSuccessResult export interface QueryClientConfig { queryCache?: QueryCache mutationCache?: MutationCache defaultOptions?: DefaultOptions } export interface DefaultOptions { queries?: OmitKeyof< QueryObserverOptions, 'suspense' | 'queryKey' > mutations?: MutationObserverOptions hydrate?: HydrateOptions['defaultOptions'] dehydrate?: DehydrateOptions } export interface CancelOptions { revert?: boolean silent?: boolean } export interface SetDataOptions { updatedAt?: number } export type NotifyEventType = | 'added' | 'removed' | 'updated' | 'observerAdded' | 'observerRemoved' | 'observerResultsUpdated' | 'observerOptionsUpdated' export interface NotifyEvent { type: NotifyEventType } ================================================ FILE: packages/query-core/src/utils.ts ================================================ import { timeoutManager } from './timeoutManager' import type { DefaultError, Enabled, FetchStatus, MutationKey, MutationStatus, QueryFunction, QueryKey, QueryOptions, StaleTime, StaleTimeFunction, } from './types' import type { Mutation } from './mutation' import type { FetchOptions, Query } from './query' // TYPES export interface QueryFilters { /** * Filter to active queries, inactive queries or all queries */ type?: QueryTypeFilter /** * Match query key exactly */ exact?: boolean /** * Include queries matching this predicate function */ predicate?: (query: Query) => boolean /** * Include queries matching this query key */ queryKey?: TQueryKey /** * Include or exclude stale queries */ stale?: boolean /** * Include queries matching their fetchStatus */ fetchStatus?: FetchStatus } export interface MutationFilters< TData = unknown, TError = DefaultError, TVariables = unknown, TOnMutateResult = unknown, > { /** * Match mutation key exactly */ exact?: boolean /** * Include mutations matching this predicate function */ predicate?: ( mutation: Mutation, ) => boolean /** * Include mutations matching this mutation key */ mutationKey?: MutationKey /** * Filter by mutation status */ status?: MutationStatus } export type Updater = TOutput | ((input: TInput) => TOutput) export type QueryTypeFilter = 'all' | 'active' | 'inactive' // UTILS export const isServer = typeof window === 'undefined' || 'Deno' in globalThis export function noop(): void export function noop(): undefined export function noop() {} export function functionalUpdate( updater: Updater, input: TInput, ): TOutput { return typeof updater === 'function' ? (updater as (_: TInput) => TOutput)(input) : updater } export function isValidTimeout(value: unknown): value is number { return typeof value === 'number' && value >= 0 && value !== Infinity } export function timeUntilStale(updatedAt: number, staleTime?: number): number { return Math.max(updatedAt + (staleTime || 0) - Date.now(), 0) } export function resolveStaleTime< TQueryFnData = unknown, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, >( staleTime: | undefined | StaleTimeFunction, query: Query, ): StaleTime | undefined { return typeof staleTime === 'function' ? staleTime(query) : staleTime } export function resolveEnabled< TQueryFnData = unknown, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, >( enabled: undefined | Enabled, query: Query, ): boolean | undefined { return typeof enabled === 'function' ? enabled(query) : enabled } export function matchQuery( filters: QueryFilters, query: Query, ): boolean { const { type = 'all', exact, fetchStatus, predicate, queryKey, stale, } = filters if (queryKey) { if (exact) { if (query.queryHash !== hashQueryKeyByOptions(queryKey, query.options)) { return false } } else if (!partialMatchKey(query.queryKey, queryKey)) { return false } } if (type !== 'all') { const isActive = query.isActive() if (type === 'active' && !isActive) { return false } if (type === 'inactive' && isActive) { return false } } if (typeof stale === 'boolean' && query.isStale() !== stale) { return false } if (fetchStatus && fetchStatus !== query.state.fetchStatus) { return false } if (predicate && !predicate(query)) { return false } return true } export function matchMutation( filters: MutationFilters, mutation: Mutation, ): boolean { const { exact, status, predicate, mutationKey } = filters if (mutationKey) { if (!mutation.options.mutationKey) { return false } if (exact) { if (hashKey(mutation.options.mutationKey) !== hashKey(mutationKey)) { return false } } else if (!partialMatchKey(mutation.options.mutationKey, mutationKey)) { return false } } if (status && mutation.state.status !== status) { return false } if (predicate && !predicate(mutation)) { return false } return true } export function hashQueryKeyByOptions( queryKey: TQueryKey, options?: Pick, 'queryKeyHashFn'>, ): string { const hashFn = options?.queryKeyHashFn || hashKey return hashFn(queryKey) } /** * Default query & mutation keys hash function. * Hashes the value into a stable hash. */ export function hashKey(queryKey: QueryKey | MutationKey): string { return JSON.stringify(queryKey, (_, val) => isPlainObject(val) ? Object.keys(val) .sort() .reduce((result, key) => { result[key] = val[key] return result }, {} as any) : val, ) } /** * Checks if key `b` partially matches with key `a`. */ export function partialMatchKey(a: QueryKey, b: QueryKey): boolean export function partialMatchKey(a: any, b: any): boolean { if (a === b) { return true } if (typeof a !== typeof b) { return false } if (a && b && typeof a === 'object' && typeof b === 'object') { return Object.keys(b).every((key) => partialMatchKey(a[key], b[key])) } return false } const hasOwn = Object.prototype.hasOwnProperty /** * This function returns `a` if `b` is deeply equal. * If not, it will replace any deeply equal children of `b` with those of `a`. * This can be used for structural sharing between JSON values for example. */ export function replaceEqualDeep(a: unknown, b: T): T export function replaceEqualDeep(a: any, b: any): any { if (a === b) { return a } const array = isPlainArray(a) && isPlainArray(b) if (!array && !(isPlainObject(a) && isPlainObject(b))) return b const aItems = array ? a : Object.keys(a) const aSize = aItems.length const bItems = array ? b : Object.keys(b) const bSize = bItems.length const copy: any = array ? new Array(bSize) : {} let equalItems = 0 for (let i = 0; i < bSize; i++) { const key: any = array ? i : bItems[i] const aItem = a[key] const bItem = b[key] if (aItem === bItem) { copy[key] = aItem if (array ? i < aSize : hasOwn.call(a, key)) equalItems++ continue } if ( aItem === null || bItem === null || typeof aItem !== 'object' || typeof bItem !== 'object' ) { copy[key] = bItem continue } const v = replaceEqualDeep(aItem, bItem) copy[key] = v if (v === aItem) equalItems++ } return aSize === bSize && equalItems === aSize ? a : copy } /** * Shallow compare objects. */ export function shallowEqualObjects>( a: T, b: T | undefined, ): boolean { if (!b || Object.keys(a).length !== Object.keys(b).length) { return false } for (const key in a) { if (a[key] !== b[key]) { return false } } return true } export function isPlainArray(value: unknown): value is Array { return Array.isArray(value) && value.length === Object.keys(value).length } // Copied from: https://github.com/jonschlinkert/is-plain-object export function isPlainObject(o: any): o is Record { if (!hasObjectPrototype(o)) { return false } // If has no constructor const ctor = o.constructor if (ctor === undefined) { return true } // If has modified prototype const prot = ctor.prototype if (!hasObjectPrototype(prot)) { return false } // If constructor does not have an Object-specific method if (!prot.hasOwnProperty('isPrototypeOf')) { return false } // Handles Objects created by Object.create() if (Object.getPrototypeOf(o) !== Object.prototype) { return false } // Most likely a plain Object return true } function hasObjectPrototype(o: any): boolean { return Object.prototype.toString.call(o) === '[object Object]' } export function sleep(timeout: number): Promise { return new Promise((resolve) => { timeoutManager.setTimeout(resolve, timeout) }) } export function replaceData< TData, TOptions extends QueryOptions, >(prevData: TData | undefined, data: TData, options: TOptions): TData { if (typeof options.structuralSharing === 'function') { return options.structuralSharing(prevData, data) as TData } else if (options.structuralSharing !== false) { if (process.env.NODE_ENV !== 'production') { try { return replaceEqualDeep(prevData, data) } catch (error) { console.error( `Structural sharing requires data to be JSON serializable. To fix this, turn off structuralSharing or return JSON-serializable data from your queryFn. [${options.queryHash}]: ${error}`, ) // Prevent the replaceEqualDeep from being called again down below. throw error } } // Structurally share data between prev and new data if needed return replaceEqualDeep(prevData, data) } return data } export function keepPreviousData( previousData: T | undefined, ): T | undefined { return previousData } export function addToEnd(items: Array, item: T, max = 0): Array { const newItems = [...items, item] return max && newItems.length > max ? newItems.slice(1) : newItems } export function addToStart(items: Array, item: T, max = 0): Array { const newItems = [item, ...items] return max && newItems.length > max ? newItems.slice(0, -1) : newItems } export const skipToken = Symbol() export type SkipToken = typeof skipToken export function ensureQueryFn< TQueryFnData = unknown, TQueryKey extends QueryKey = QueryKey, >( options: { queryFn?: QueryFunction | SkipToken queryHash?: string }, fetchOptions?: FetchOptions, ): QueryFunction { if (process.env.NODE_ENV !== 'production') { if (options.queryFn === skipToken) { console.error( `Attempted to invoke queryFn when set to skipToken. This is likely a configuration error. Query hash: '${options.queryHash}'`, ) } } // if we attempt to retry a fetch that was triggered from an initialPromise // when we don't have a queryFn yet, we can't retry, so we just return the already rejected initialPromise // if an observer has already mounted, we will be able to retry with that queryFn if (!options.queryFn && fetchOptions?.initialPromise) { return () => fetchOptions.initialPromise! } if (!options.queryFn || options.queryFn === skipToken) { return () => Promise.reject(new Error(`Missing queryFn: '${options.queryHash}'`)) } return options.queryFn } export function shouldThrowError) => boolean>( throwOnError: boolean | T | undefined, params: Parameters, ): boolean { // Allow throwOnError function to override throwing behavior on a per-error basis if (typeof throwOnError === 'function') { return throwOnError(...params) } return !!throwOnError } ================================================ FILE: packages/query-core/src/__tests__/focusManager.test.tsx ================================================ import { afterEach, beforeEach, describe, expect, it, test, vi } from 'vitest' import { FocusManager } from '../focusManager' import { setIsServer } from './utils' describe('focusManager', () => { let focusManager: FocusManager beforeEach(() => { vi.useFakeTimers() vi.resetModules() focusManager = new FocusManager() }) afterEach(() => { vi.useRealTimers() }) it('should call previous remove handler when replacing an event listener', () => { const remove1Spy = vi.fn() const remove2Spy = vi.fn() focusManager.setEventListener(() => remove1Spy) focusManager.setEventListener(() => remove2Spy) expect(remove1Spy).toHaveBeenCalledTimes(1) expect(remove2Spy).not.toHaveBeenCalled() }) it('should use focused boolean arg', () => { let count = 0 const setup = (setFocused: (focused?: boolean) => void) => { setTimeout(() => { count++ setFocused(true) }, 20) return () => void 0 } focusManager.setEventListener(setup) vi.advanceTimersByTime(20) expect(count).toEqual(1) expect(focusManager.isFocused()).toBeTruthy() }) it('should return true for isFocused if document is undefined', () => { const { document } = globalThis // @ts-expect-error delete globalThis.document focusManager.setFocused() expect(focusManager.isFocused()).toBeTruthy() globalThis.document = document }) test('cleanup (removeEventListener) should not be called if window is not defined', () => { const restoreIsServer = setIsServer(true) const removeEventListenerSpy = vi.spyOn(globalThis, 'removeEventListener') const unsubscribe = focusManager.subscribe(() => undefined) unsubscribe() expect(removeEventListenerSpy).not.toHaveBeenCalled() restoreIsServer() }) test('cleanup (removeEventListener) should not be called if window.addEventListener is not defined', () => { const { addEventListener } = globalThis.window // @ts-expect-error globalThis.window.addEventListener = undefined const removeEventListenerSpy = vi.spyOn(globalThis, 'removeEventListener') const unsubscribe = focusManager.subscribe(() => undefined) unsubscribe() expect(removeEventListenerSpy).not.toHaveBeenCalled() globalThis.window.addEventListener = addEventListener }) it('should replace default window listener when a new event listener is set', () => { const unsubscribeSpy = vi.fn().mockImplementation(() => undefined) const handlerSpy = vi.fn().mockImplementation(() => unsubscribeSpy) focusManager.setEventListener(() => handlerSpy()) const unsubscribe = focusManager.subscribe(() => undefined) // Should call the custom event once expect(handlerSpy).toHaveBeenCalledTimes(1) unsubscribe() // Should unsubscribe our event event expect(unsubscribeSpy).toHaveBeenCalledTimes(1) handlerSpy.mockRestore() unsubscribeSpy.mockRestore() }) test('should call removeEventListener when last listener unsubscribes', () => { const addEventListenerSpy = vi.spyOn(globalThis.window, 'addEventListener') const removeEventListenerSpy = vi.spyOn( globalThis.window, 'removeEventListener', ) const unsubscribe1 = focusManager.subscribe(() => undefined) const unsubscribe2 = focusManager.subscribe(() => undefined) expect(addEventListenerSpy).toHaveBeenCalledTimes(1) // visibilitychange event unsubscribe1() expect(removeEventListenerSpy).toHaveBeenCalledTimes(0) unsubscribe2() expect(removeEventListenerSpy).toHaveBeenCalledTimes(1) // visibilitychange event }) test('should keep setup function even if last listener unsubscribes', () => { const setupSpy = vi.fn().mockImplementation(() => () => undefined) focusManager.setEventListener(setupSpy) const unsubscribe1 = focusManager.subscribe(() => undefined) expect(setupSpy).toHaveBeenCalledTimes(1) unsubscribe1() const unsubscribe2 = focusManager.subscribe(() => undefined) expect(setupSpy).toHaveBeenCalledTimes(2) unsubscribe2() }) test('should call listeners when setFocused is called', () => { const listener = vi.fn() focusManager.subscribe(listener) focusManager.setFocused(true) focusManager.setFocused(true) expect(listener).toHaveBeenCalledTimes(1) expect(listener).toHaveBeenNthCalledWith(1, true) focusManager.setFocused(false) focusManager.setFocused(false) expect(listener).toHaveBeenCalledTimes(2) expect(listener).toHaveBeenNthCalledWith(2, false) focusManager.setFocused(undefined) focusManager.setFocused(undefined) expect(listener).toHaveBeenCalledTimes(3) expect(listener).toHaveBeenNthCalledWith(3, true) }) }) ================================================ FILE: packages/query-core/src/__tests__/hydration.test.tsx ================================================ import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest' import { sleep } from '@tanstack/query-test-utils' import { QueryClient } from '../queryClient' import { QueryCache } from '../queryCache' import { dehydrate, hydrate } from '../hydration' import { MutationCache } from '../mutationCache' import { executeMutation, mockOnlineManagerIsOnline } from './utils' describe('dehydration and rehydration', () => { beforeEach(() => { vi.useFakeTimers() }) afterEach(() => { vi.useRealTimers() }) test('should work with serializable values', async () => { const queryCache = new QueryCache() const queryClient = new QueryClient({ queryCache }) await vi.waitFor(() => queryClient.prefetchQuery({ queryKey: ['string'], queryFn: () => sleep(0).then(() => 'string'), }), ) await vi.waitFor(() => queryClient.prefetchQuery({ queryKey: ['number'], queryFn: () => sleep(0).then(() => 1), }), ) await vi.waitFor(() => queryClient.prefetchQuery({ queryKey: ['boolean'], queryFn: () => sleep(0).then(() => true), }), ) await vi.waitFor(() => queryClient.prefetchQuery({ queryKey: ['null'], queryFn: () => sleep(0).then(() => null), }), ) await vi.waitFor(() => queryClient.prefetchQuery({ queryKey: ['array'], queryFn: () => sleep(0).then(() => ['string', 0]), }), ) await vi.waitFor(() => queryClient.prefetchQuery({ queryKey: ['nested'], queryFn: () => sleep(0).then(() => ({ key: [{ nestedKey: 1 }] })), }), ) const dehydrated = dehydrate(queryClient) const stringified = JSON.stringify(dehydrated) // --- const parsed = JSON.parse(stringified) const hydrationCache = new QueryCache() const hydrationClient = new QueryClient({ queryCache: hydrationCache, }) hydrate(hydrationClient, parsed) expect(hydrationCache.find({ queryKey: ['string'] })?.state.data).toBe( 'string', ) expect(hydrationCache.find({ queryKey: ['number'] })?.state.data).toBe(1) expect(hydrationCache.find({ queryKey: ['boolean'] })?.state.data).toBe( true, ) expect(hydrationCache.find({ queryKey: ['null'] })?.state.data).toBe(null) expect(hydrationCache.find({ queryKey: ['array'] })?.state.data).toEqual([ 'string', 0, ]) expect(hydrationCache.find({ queryKey: ['nested'] })?.state.data).toEqual({ key: [{ nestedKey: 1 }], }) const fetchDataAfterHydration = vi.fn<(...args: Array) => unknown>() await hydrationClient.prefetchQuery({ queryKey: ['string'], queryFn: fetchDataAfterHydration, staleTime: 1000, }) await hydrationClient.prefetchQuery({ queryKey: ['number'], queryFn: fetchDataAfterHydration, staleTime: 1000, }) await hydrationClient.prefetchQuery({ queryKey: ['boolean'], queryFn: fetchDataAfterHydration, staleTime: 1000, }) await hydrationClient.prefetchQuery({ queryKey: ['null'], queryFn: fetchDataAfterHydration, staleTime: 1000, }) await hydrationClient.prefetchQuery({ queryKey: ['array'], queryFn: fetchDataAfterHydration, staleTime: 1000, }) await hydrationClient.prefetchQuery({ queryKey: ['nested'], queryFn: fetchDataAfterHydration, staleTime: 1000, }) expect(fetchDataAfterHydration).toHaveBeenCalledTimes(0) queryClient.clear() hydrationClient.clear() }) test('should not dehydrate queries if dehydrateQueries is set to false', () => { const queryCache = new QueryCache() const queryClient = new QueryClient({ queryCache }) vi.waitFor(() => queryClient.prefetchQuery({ queryKey: ['string'], queryFn: () => sleep(0).then(() => 'string'), }), ) const dehydrated = dehydrate(queryClient, { shouldDehydrateQuery: () => false, }) expect(dehydrated.queries.length).toBe(0) queryClient.clear() }) test('should use the garbage collection time from the client', async () => { const queryCache = new QueryCache() const queryClient = new QueryClient({ queryCache }) await vi.waitFor(() => queryClient.prefetchQuery({ queryKey: ['string'], queryFn: () => sleep(0).then(() => 'string'), gcTime: 50, }), ) const dehydrated = dehydrate(queryClient) const stringified = JSON.stringify(dehydrated) await vi.advanceTimersByTimeAsync(20) // --- const parsed = JSON.parse(stringified) const hydrationCache = new QueryCache() const hydrationClient = new QueryClient({ queryCache: hydrationCache }) hydrate(hydrationClient, parsed) expect(hydrationCache.find({ queryKey: ['string'] })?.state.data).toBe( 'string', ) await vi.advanceTimersByTimeAsync(100) expect(hydrationCache.find({ queryKey: ['string'] })).toBeTruthy() queryClient.clear() hydrationClient.clear() }) test('should be able to provide default options for the hydrated queries', async () => { const queryCache = new QueryCache() const queryClient = new QueryClient({ queryCache }) await vi.waitFor(() => queryClient.prefetchQuery({ queryKey: ['string'], queryFn: () => sleep(0).then(() => 'string'), }), ) const dehydrated = dehydrate(queryClient) const stringified = JSON.stringify(dehydrated) const parsed = JSON.parse(stringified) const hydrationCache = new QueryCache() const hydrationClient = new QueryClient({ queryCache: hydrationCache }) hydrate(hydrationClient, parsed, { defaultOptions: { queries: { retry: 10 } }, }) expect(hydrationCache.find({ queryKey: ['string'] })?.options.retry).toBe( 10, ) queryClient.clear() hydrationClient.clear() }) test('should respect query defaultOptions specified on the QueryClient', async () => { const queryCache = new QueryCache() const queryClient = new QueryClient({ queryCache, defaultOptions: { dehydrate: { shouldDehydrateQuery: () => true }, }, }) await vi.waitFor(() => queryClient.prefetchQuery({ queryKey: ['string'], retry: 0, queryFn: () => Promise.reject(new Error('error')), }), ) const dehydrated = dehydrate(queryClient) expect(dehydrated.queries.length).toBe(1) expect(dehydrated.queries[0]?.state.error).toStrictEqual(new Error('error')) const stringified = JSON.stringify(dehydrated) const parsed = JSON.parse(stringified) const hydrationCache = new QueryCache() const hydrationClient = new QueryClient({ queryCache: hydrationCache, defaultOptions: { hydrate: { queries: { retry: 10 } } }, }) hydrate(hydrationClient, parsed, { defaultOptions: { queries: { gcTime: 10 } }, }) expect(hydrationCache.find({ queryKey: ['string'] })?.options.retry).toBe( 10, ) expect(hydrationCache.find({ queryKey: ['string'] })?.options.gcTime).toBe( 10, ) queryClient.clear() hydrationClient.clear() }) test('should respect mutation defaultOptions specified on the QueryClient', async () => { const mutationCache = new MutationCache() const queryClient = new QueryClient({ mutationCache, defaultOptions: { dehydrate: { shouldDehydrateMutation: (mutation) => mutation.state.data === 'done', }, }, }) await executeMutation( queryClient, { mutationKey: ['string'], mutationFn: () => Promise.resolve('done'), }, undefined, ) const dehydrated = dehydrate(queryClient) expect(dehydrated.mutations.length).toBe(1) expect(dehydrated.mutations[0]?.state.data).toBe('done') const stringified = JSON.stringify(dehydrated) const parsed = JSON.parse(stringified) const hydrationCache = new MutationCache() const hydrationClient = new QueryClient({ mutationCache: hydrationCache, defaultOptions: { hydrate: { mutations: { retry: 10 } } }, }) hydrate(hydrationClient, parsed, { defaultOptions: { mutations: { gcTime: 10 } }, }) expect( hydrationCache.find({ mutationKey: ['string'] })?.options.retry, ).toBe(10) expect( hydrationCache.find({ mutationKey: ['string'] })?.options.gcTime, ).toBe(10) queryClient.clear() hydrationClient.clear() }) test('should work with complex keys', async () => { const queryCache = new QueryCache() const queryClient = new QueryClient({ queryCache }) await vi.waitFor(() => queryClient.prefetchQuery({ queryKey: ['string', { key: ['string'], key2: 0 }], queryFn: () => sleep(0).then(() => 'string'), }), ) const dehydrated = dehydrate(queryClient) const stringified = JSON.stringify(dehydrated) // --- const parsed = JSON.parse(stringified) const hydrationCache = new QueryCache() const hydrationClient = new QueryClient({ queryCache: hydrationCache }) hydrate(hydrationClient, parsed) expect( hydrationCache.find({ queryKey: ['string', { key: ['string'], key2: 0 }], })?.state.data, ).toBe('string') const fetchDataAfterHydration = vi.fn<(...args: Array) => unknown>() await vi.waitFor(() => hydrationClient.prefetchQuery({ queryKey: ['string', { key: ['string'], key2: 0 }], queryFn: fetchDataAfterHydration, staleTime: 100, }), ) expect(fetchDataAfterHydration).toHaveBeenCalledTimes(0) queryClient.clear() hydrationClient.clear() }) test('should only hydrate successful queries by default', async () => { const consoleMock = vi.spyOn(console, 'error') consoleMock.mockImplementation(() => undefined) const queryCache = new QueryCache() const queryClient = new QueryClient({ queryCache }) await vi.waitFor(() => queryClient.prefetchQuery({ queryKey: ['success'], queryFn: () => sleep(0).then(() => 'success'), }), ) queryClient.prefetchQuery({ queryKey: ['loading'], queryFn: () => sleep(10000).then(() => 'loading'), }) await vi.waitFor(() => queryClient.prefetchQuery({ queryKey: ['error'], queryFn: () => { throw new Error() }, }), ) const dehydrated = dehydrate(queryClient) const stringified = JSON.stringify(dehydrated) // --- const parsed = JSON.parse(stringified) const hydrationCache = new QueryCache() const hydrationClient = new QueryClient({ queryCache: hydrationCache }) hydrate(hydrationClient, parsed) expect(hydrationCache.find({ queryKey: ['success'] })).toBeTruthy() expect(hydrationCache.find({ queryKey: ['loading'] })).toBeFalsy() expect(hydrationCache.find({ queryKey: ['error'] })).toBeFalsy() queryClient.clear() hydrationClient.clear() consoleMock.mockRestore() }) test('should filter queries via dehydrateQuery', async () => { const queryCache = new QueryCache() const queryClient = new QueryClient({ queryCache }) await vi.waitFor(() => queryClient.prefetchQuery({ queryKey: ['string'], queryFn: () => sleep(0).then(() => 'string'), }), ) await vi.waitFor(() => queryClient.prefetchQuery({ queryKey: ['number'], queryFn: () => sleep(0).then(() => 1), }), ) const dehydrated = dehydrate(queryClient, { shouldDehydrateQuery: (query) => query.queryKey[0] !== 'string', }) // This is testing implementation details that can change and are not // part of the public API, but is important for keeping the payload small const dehydratedQuery = dehydrated.queries.find( (query) => query.queryKey[0] === 'string', ) expect(dehydratedQuery).toBeUndefined() const stringified = JSON.stringify(dehydrated) // --- const parsed = JSON.parse(stringified) const hydrationCache = new QueryCache() const hydrationClient = new QueryClient({ queryCache: hydrationCache }) hydrate(hydrationClient, parsed) expect(hydrationCache.find({ queryKey: ['string'] })).toBeUndefined() expect(hydrationCache.find({ queryKey: ['number'] })?.state.data).toBe(1) queryClient.clear() hydrationClient.clear() }) test('should not overwrite query in cache if hydrated query is older', async () => { const queryCache = new QueryCache() const queryClient = new QueryClient({ queryCache }) await vi.waitFor(() => queryClient.prefetchQuery({ queryKey: ['string'], queryFn: () => sleep(5).then(() => 'string-older'), }), ) const dehydrated = dehydrate(queryClient) const stringified = JSON.stringify(dehydrated) // --- const parsed = JSON.parse(stringified) const hydrationCache = new QueryCache() const hydrationClient = new QueryClient({ queryCache: hydrationCache }) await vi.waitFor(() => hydrationClient.prefetchQuery({ queryKey: ['string'], queryFn: () => sleep(5).then(() => 'string-newer'), }), ) hydrate(hydrationClient, parsed) expect(hydrationCache.find({ queryKey: ['string'] })?.state.data).toBe( 'string-newer', ) queryClient.clear() hydrationClient.clear() }) test('should overwrite query in cache if hydrated query is newer', async () => { const hydrationCache = new QueryCache() const hydrationClient = new QueryClient({ queryCache: hydrationCache }) await vi.waitFor(() => hydrationClient.prefetchQuery({ queryKey: ['string'], queryFn: () => sleep(5).then(() => 'string-older'), }), ) // --- const queryCache = new QueryCache() const queryClient = new QueryClient({ queryCache }) await vi.waitFor(() => queryClient.prefetchQuery({ queryKey: ['string'], queryFn: () => sleep(5).then(() => 'string-newer'), }), ) const dehydrated = dehydrate(queryClient) const stringified = JSON.stringify(dehydrated) // --- const parsed = JSON.parse(stringified) hydrate(hydrationClient, parsed) expect(hydrationCache.find({ queryKey: ['string'] })?.state.data).toBe( 'string-newer', ) queryClient.clear() hydrationClient.clear() }) test('should be able to dehydrate mutations and continue on hydration', async () => { const consoleMock = vi.spyOn(console, 'error') consoleMock.mockImplementation(() => undefined) const onlineMock = mockOnlineManagerIsOnline(false) const serverAddTodo = vi .fn() .mockImplementation(() => Promise.reject(new Error('offline'))) const serverOnMutate = vi .fn() .mockImplementation((variables: { text: string }) => { const optimisticTodo = { id: 1, text: variables.text } return { optimisticTodo } }) const serverOnSuccess = vi.fn() const serverClient = new QueryClient() serverClient.setMutationDefaults(['addTodo'], { mutationFn: serverAddTodo, onMutate: serverOnMutate, onSuccess: serverOnSuccess, retry: 3, retryDelay: 10, }) executeMutation( serverClient, { mutationKey: ['addTodo'], }, { text: 'text' }, ).catch(() => undefined) await vi.advanceTimersByTimeAsync(50) const dehydrated = dehydrate(serverClient) const stringified = JSON.stringify(dehydrated) serverClient.clear() // --- onlineMock.mockReturnValue(true) const parsed = JSON.parse(stringified) const client = new QueryClient() const clientAddTodo = vi .fn() .mockImplementation((variables: { text: string }) => { return { id: 2, text: variables.text } }) const clientOnMutate = vi .fn() .mockImplementation((variables: { text: string }) => { const optimisticTodo = { id: 1, text: variables.text } return { optimisticTodo } }) const clientOnSuccess = vi.fn() client.setMutationDefaults(['addTodo'], { mutationFn: clientAddTodo, onMutate: clientOnMutate, onSuccess: clientOnSuccess, retry: 3, retryDelay: 10, }) hydrate(client, parsed) await client.resumePausedMutations() expect(clientAddTodo).toHaveBeenCalledTimes(1) expect(clientOnMutate).not.toHaveBeenCalled() expect(clientOnSuccess).toHaveBeenCalledTimes(1) expect(clientOnSuccess).toHaveBeenCalledWith( { id: 2, text: 'text' }, { text: 'text' }, { optimisticTodo: { id: 1, text: 'text' } }, { client: client, meta: undefined, mutationKey: ['addTodo'] }, ) client.clear() consoleMock.mockRestore() onlineMock.mockRestore() }) test('should not dehydrate mutations if dehydrateMutations is set to false', async () => { const consoleMock = vi.spyOn(console, 'error') consoleMock.mockImplementation(() => undefined) const serverAddTodo = vi .fn() .mockImplementation(() => Promise.reject(new Error('offline'))) const queryClient = new QueryClient() queryClient.setMutationDefaults(['addTodo'], { mutationFn: serverAddTodo, retry: false, }) executeMutation( queryClient, { mutationKey: ['addTodo'], }, { text: 'text' }, ).catch(() => undefined) await vi.advanceTimersByTimeAsync(1) const dehydrated = dehydrate(queryClient, { shouldDehydrateMutation: () => false, }) expect(dehydrated.mutations.length).toBe(0) queryClient.clear() consoleMock.mockRestore() }) test('should not dehydrate mutation if mutation state is set to pause', async () => { const consoleMock = vi.spyOn(console, 'error') consoleMock.mockImplementation(() => undefined) const serverAddTodo = vi .fn() .mockImplementation(() => Promise.reject(new Error('offline'))) const queryClient = new QueryClient() queryClient.setMutationDefaults(['addTodo'], { mutationFn: serverAddTodo, retry: 1, retryDelay: 20, }) executeMutation( queryClient, { mutationKey: ['addTodo'], }, { text: 'text' }, ).catch(() => undefined) // Dehydrate mutation between retries await vi.advanceTimersByTimeAsync(1) const dehydrated = dehydrate(queryClient) expect(dehydrated.mutations.length).toBe(0) await vi.advanceTimersByTimeAsync(30) queryClient.clear() consoleMock.mockRestore() }) test('should not hydrate if the hydratedState is null or is not an object', () => { const queryCache = new QueryCache() const queryClient = new QueryClient({ queryCache }) expect(() => hydrate(queryClient, null)).not.toThrow() expect(() => hydrate(queryClient, 'invalid')).not.toThrow() queryClient.clear() }) test('should support hydratedState with undefined queries and mutations', () => { const queryCache = new QueryCache() const queryClient = new QueryClient({ queryCache }) expect(() => hydrate(queryClient, {})).not.toThrow() expect(() => hydrate(queryClient, {})).not.toThrow() queryClient.clear() }) test('should set the fetchStatus to idle when creating a query with dehydrate', async () => { const queryCache = new QueryCache() const queryClient = new QueryClient({ queryCache }) let isInitialFetch = true let resolvePromise: (value: unknown) => void = () => undefined const customFetchData = () => { const promise = new Promise((resolve) => { resolvePromise = resolve }) // Resolve the promise in initial fetch // because we are awaiting the query first time if (isInitialFetch) { resolvePromise('string') } isInitialFetch = false return promise } await queryClient.prefetchQuery({ queryKey: ['string'], queryFn: () => customFetchData(), }) queryClient.refetchQueries({ queryKey: ['string'] }) const dehydrated = dehydrate(queryClient) resolvePromise('string') expect( dehydrated.queries.find((q) => q.queryHash === '["string"]')?.state .fetchStatus, ).toBe('fetching') const stringified = JSON.stringify(dehydrated) // --- const parsed = JSON.parse(stringified) const hydrationCache = new QueryCache() const hydrationClient = new QueryClient({ queryCache: hydrationCache }) hydrate(hydrationClient, parsed) expect( hydrationCache.find({ queryKey: ['string'] })?.state.fetchStatus, ).toBe('idle') }) test('should dehydrate and hydrate meta for queries', async () => { const queryCache = new QueryCache() const queryClient = new QueryClient({ queryCache }) await vi.waitFor(() => queryClient.prefetchQuery({ queryKey: ['meta'], queryFn: () => Promise.resolve('meta'), meta: { some: 'meta', }, }), ) await vi.waitFor(() => queryClient.prefetchQuery({ queryKey: ['no-meta'], queryFn: () => Promise.resolve('no-meta'), }), ) const dehydrated = dehydrate(queryClient) expect( dehydrated.queries.find((q) => q.queryHash === '["meta"]')?.meta, ).toEqual({ some: 'meta', }) expect( dehydrated.queries.find((q) => q.queryHash === '["no-meta"]')?.meta, ).toEqual(undefined) expect( Object.keys( dehydrated.queries.find((q) => q.queryHash === '["no-meta"]')!, ), ).not.toEqual(expect.arrayContaining(['meta'])) const stringified = JSON.stringify(dehydrated) // --- const parsed = JSON.parse(stringified) const hydrationCache = new QueryCache() const hydrationClient = new QueryClient({ queryCache: hydrationCache, }) hydrate(hydrationClient, parsed) expect(hydrationCache.find({ queryKey: ['meta'] })?.meta).toEqual({ some: 'meta', }) expect(hydrationCache.find({ queryKey: ['no-meta'] })?.meta).toEqual( undefined, ) }) test('should dehydrate and hydrate meta for mutations', async () => { const mutationCache = new MutationCache() const queryClient = new QueryClient({ mutationCache }) await executeMutation( queryClient, { mutationKey: ['meta'], mutationFn: () => Promise.resolve('meta'), meta: { some: 'meta', }, }, undefined, ) await executeMutation( queryClient, { mutationKey: ['no-meta'], mutationFn: () => Promise.resolve('no-meta'), }, undefined, ) const dehydrated = dehydrate(queryClient, { shouldDehydrateMutation: () => true, }) expect(Object.keys(dehydrated.mutations[0]!)).toEqual( expect.arrayContaining(['meta']), ) expect(dehydrated.mutations[0]?.meta).toEqual({ some: 'meta', }) expect(Object.keys(dehydrated.mutations[1]!)).not.toEqual( expect.arrayContaining(['meta']), ) expect(dehydrated.mutations[1]?.meta).toEqual(undefined) const stringified = JSON.stringify(dehydrated) // --- const parsed = JSON.parse(stringified) const hydrationCache = new MutationCache() const hydrationClient = new QueryClient({ mutationCache: hydrationCache, }) hydrate(hydrationClient, parsed) expect(hydrationCache.find({ mutationKey: ['meta'] })?.meta).toEqual({ some: 'meta', }) expect(hydrationCache.find({ mutationKey: ['no-meta'] })?.meta).toEqual( undefined, ) }) test('should not change fetchStatus when updating a query with dehydrate', async () => { const queryClient = new QueryClient() const options = { queryKey: ['string'], queryFn: async () => { await sleep(10) return 'string' }, } as const await vi.waitFor(() => queryClient.prefetchQuery(options)) const dehydrated = dehydrate(queryClient) expect( dehydrated.queries.find((q) => q.queryHash === '["string"]')?.state .fetchStatus, ).toBe('idle') const stringified = JSON.stringify(dehydrated) // --- const parsed = JSON.parse(stringified) const hydrationCache = new QueryCache() const hydrationClient = new QueryClient({ queryCache: hydrationCache }) const promise = hydrationClient.prefetchQuery(options) hydrate(hydrationClient, parsed) expect( hydrationCache.find({ queryKey: ['string'] })?.state.fetchStatus, ).toBe('fetching') await vi.waitFor(() => promise) expect( hydrationCache.find({ queryKey: ['string'] })?.state.fetchStatus, ).toBe('idle') }) test('should dehydrate and hydrate mutation scopes', () => { const queryClient = new QueryClient() const onlineMock = mockOnlineManagerIsOnline(false) void executeMutation( queryClient, { mutationKey: ['mutation'], mutationFn: () => { return Promise.resolve('mutation') }, scope: { id: 'scope', }, }, 'vars', ) const dehydrated = dehydrate(queryClient) expect(dehydrated.mutations[0]?.scope?.id).toBe('scope') const stringified = JSON.stringify(dehydrated) // --- const parsed = JSON.parse(stringified) const hydrationCache = new MutationCache() const hydrationClient = new QueryClient({ mutationCache: hydrationCache }) hydrate(hydrationClient, parsed) expect(dehydrated.mutations[0]?.scope?.id).toBe('scope') onlineMock.mockRestore() }) test('should dehydrate promises for pending queries', async () => { const queryCache = new QueryCache() const queryClient = new QueryClient({ queryCache, defaultOptions: { dehydrate: { shouldDehydrateQuery: () => true } }, }) await vi.waitFor(() => queryClient.prefetchQuery({ queryKey: ['success'], queryFn: () => sleep(0).then(() => 'success'), }), ) const promise = queryClient.prefetchQuery({ queryKey: ['pending'], queryFn: () => sleep(10).then(() => 'pending'), }) const dehydrated = dehydrate(queryClient) expect(dehydrated.queries[0]?.promise).toBeUndefined() expect(dehydrated.queries[1]?.promise).toBeInstanceOf(Promise) await vi.waitFor(() => promise) queryClient.clear() }) test('should hydrate promises even without observers', async () => { const queryCache = new QueryCache() const queryClient = new QueryClient({ queryCache, defaultOptions: { dehydrate: { shouldDehydrateQuery: () => true } }, }) await vi.waitFor(() => queryClient.prefetchQuery({ queryKey: ['success'], queryFn: () => sleep(0).then(() => 'success'), }), ) void queryClient.prefetchQuery({ queryKey: ['pending'], queryFn: () => sleep(20).then(() => 'pending'), }) const dehydrated = dehydrate(queryClient) // no stringify/parse here because promises can't be serialized to json // but nextJs still can do it const hydrationCache = new QueryCache() const hydrationClient = new QueryClient({ queryCache: hydrationCache, }) hydrate(hydrationClient, dehydrated) expect(hydrationCache.find({ queryKey: ['success'] })?.state.data).toBe( 'success', ) expect(hydrationCache.find({ queryKey: ['pending'] })?.state).toMatchObject( { data: undefined, dataUpdateCount: 0, dataUpdatedAt: 0, error: null, errorUpdateCount: 0, errorUpdatedAt: 0, fetchFailureCount: 0, fetchFailureReason: null, fetchMeta: null, fetchStatus: 'fetching', isInvalidated: false, status: 'pending', }, ) await vi.waitFor(() => expect( hydrationCache.find({ queryKey: ['pending'] })?.state, ).toMatchObject({ data: 'pending', dataUpdateCount: 1, dataUpdatedAt: expect.any(Number), error: null, errorUpdateCount: 0, errorUpdatedAt: 0, fetchFailureCount: 0, fetchFailureReason: null, fetchMeta: null, fetchStatus: 'idle', isInvalidated: false, status: 'success', }), ) }) test('should transform promise result', async () => { const queryClient = new QueryClient({ defaultOptions: { dehydrate: { shouldDehydrateQuery: () => true, serializeData: (data) => data.toISOString(), }, }, }) const promise = queryClient.prefetchQuery({ queryKey: ['transformedStringToDate'], queryFn: () => sleep(20).then(() => new Date('2024-01-01T00:00:00.000Z')), }) const dehydrated = dehydrate(queryClient) expect(dehydrated.queries[0]?.promise).toBeInstanceOf(Promise) const hydrationClient = new QueryClient({ defaultOptions: { hydrate: { deserializeData: (data) => new Date(data), }, }, }) hydrate(hydrationClient, dehydrated) await vi.waitFor(() => promise) await vi.waitFor(() => expect( hydrationClient.getQueryData(['transformedStringToDate']), ).toBeInstanceOf(Date), ) queryClient.clear() }) test('should transform query data if promise is already resolved', async () => { const queryClient = new QueryClient({ defaultOptions: { dehydrate: { shouldDehydrateQuery: () => true, serializeData: (data) => data.toISOString(), }, }, }) const promise = queryClient.prefetchQuery({ queryKey: ['transformedStringToDate'], queryFn: () => sleep(0).then(() => new Date('2024-01-01T00:00:00.000Z')), }) await vi.advanceTimersByTimeAsync(20) const dehydrated = dehydrate(queryClient) const hydrationClient = new QueryClient({ defaultOptions: { hydrate: { deserializeData: (data) => new Date(data), }, }, }) hydrate(hydrationClient, dehydrated) await vi.waitFor(() => promise) await vi.waitFor(() => expect( hydrationClient.getQueryData(['transformedStringToDate']), ).toBeInstanceOf(Date), ) queryClient.clear() }) test('should overwrite query in cache if hydrated query is newer (with transformation)', async () => { const hydrationClient = new QueryClient({ defaultOptions: { hydrate: { deserializeData: (data) => new Date(data), }, }, }) await vi.waitFor(() => hydrationClient.prefetchQuery({ queryKey: ['date'], queryFn: () => sleep(5).then(() => new Date('2024-01-01T00:00:00.000Z')), }), ) // --- const queryClient = new QueryClient({ defaultOptions: { dehydrate: { shouldDehydrateQuery: () => true, serializeData: (data) => data.toISOString(), }, }, }) await vi.waitFor(() => queryClient.prefetchQuery({ queryKey: ['date'], queryFn: () => sleep(10).then(() => new Date('2024-01-02T00:00:00.000Z')), }), ) const dehydrated = dehydrate(queryClient) // --- hydrate(hydrationClient, dehydrated) expect(hydrationClient.getQueryData(['date'])).toStrictEqual( new Date('2024-01-02T00:00:00.000Z'), ) queryClient.clear() hydrationClient.clear() }) test('should overwrite query in cache if hydrated query is newer (with promise)', async () => { // --- server --- const serverQueryClient = new QueryClient({ defaultOptions: { dehydrate: { shouldDehydrateQuery: () => true, }, }, }) const promise = serverQueryClient.prefetchQuery({ queryKey: ['data'], queryFn: async () => { await sleep(10) return 'server data' }, }) const dehydrated = dehydrate(serverQueryClient) // --- client --- const clientQueryClient = new QueryClient() clientQueryClient.setQueryData(['data'], 'old data', { updatedAt: 10 }) hydrate(clientQueryClient, dehydrated) await vi.waitFor(() => promise) await vi.waitFor(() => expect(clientQueryClient.getQueryData(['data'])).toBe('server data'), ) clientQueryClient.clear() serverQueryClient.clear() }) test('should not overwrite query in cache if existing query is newer (with promise)', async () => { // --- server --- const serverQueryClient = new QueryClient({ defaultOptions: { dehydrate: { shouldDehydrateQuery: () => true, }, }, }) const promise = serverQueryClient.prefetchQuery({ queryKey: ['data'], queryFn: async () => { await sleep(10) return 'server data' }, }) const dehydrated = dehydrate(serverQueryClient) await vi.advanceTimersByTimeAsync(10) await promise // Pretend the output of this server part is cached for a long time // --- client --- await vi.advanceTimersByTimeAsync(10_000) // Arbitrary time in the future const clientQueryClient = new QueryClient() clientQueryClient.setQueryData(['data'], 'newer data', { updatedAt: Date.now(), }) hydrate(clientQueryClient, dehydrated) // If the query was hydrated in error, it would still take some time for it // to end up in the cache, so for the test to fail properly on regressions, // wait for the fetchStatus to be idle await vi.waitFor(() => expect(clientQueryClient.getQueryState(['data'])?.fetchStatus).toBe( 'idle', ), ) await vi.waitFor(() => expect(clientQueryClient.getQueryData(['data'])).toBe('newer data'), ) clientQueryClient.clear() serverQueryClient.clear() }) test('should overwrite data when a new promise is streamed in', async () => { const serializeDataMock = vi.fn((data: any) => data) const deserializeDataMock = vi.fn((data: any) => data) const countRef = { current: 0 } // --- server --- const serverQueryClient = new QueryClient({ defaultOptions: { dehydrate: { shouldDehydrateQuery: () => true, serializeData: serializeDataMock, }, }, }) const query = { queryKey: ['data'], queryFn: async () => { await sleep(10) return countRef.current }, } const promise = serverQueryClient.prefetchQuery(query) let dehydrated = dehydrate(serverQueryClient) // --- client --- const clientQueryClient = new QueryClient({ defaultOptions: { hydrate: { deserializeData: deserializeDataMock, }, }, }) hydrate(clientQueryClient, dehydrated) await vi.waitFor(() => promise) await vi.waitFor(() => expect(clientQueryClient.getQueryData(query.queryKey)).toBe(0), ) expect(serializeDataMock).toHaveBeenCalledTimes(1) expect(serializeDataMock).toHaveBeenCalledWith(0) expect(deserializeDataMock).toHaveBeenCalledTimes(1) expect(deserializeDataMock).toHaveBeenCalledWith(0) // --- server --- countRef.current++ serverQueryClient.clear() const promise2 = serverQueryClient.prefetchQuery(query) dehydrated = dehydrate(serverQueryClient) // --- client --- hydrate(clientQueryClient, dehydrated) await vi.waitFor(() => promise2) await vi.waitFor(() => expect(clientQueryClient.getQueryData(query.queryKey)).toBe(1), ) expect(serializeDataMock).toHaveBeenCalledTimes(2) expect(serializeDataMock).toHaveBeenCalledWith(1) expect(deserializeDataMock).toHaveBeenCalledTimes(2) expect(deserializeDataMock).toHaveBeenCalledWith(1) clientQueryClient.clear() serverQueryClient.clear() }) test('should not redact errors when shouldRedactErrors returns false', async () => { const queryCache = new QueryCache() const queryClient = new QueryClient({ queryCache, defaultOptions: { dehydrate: { shouldDehydrateQuery: () => true, shouldRedactErrors: () => false, }, }, }) const testError = new Error('original error') const promise = queryClient .prefetchQuery({ queryKey: ['error'], queryFn: () => Promise.reject(testError), retry: false, }) .catch(() => undefined) const dehydrated = dehydrate(queryClient) expect(dehydrated.queries[0]?.promise).toBeInstanceOf(Promise) await expect(dehydrated.queries[0]?.promise).rejects.toBe(testError) await promise }) test('should handle errors in promises for pending queries', async () => { const consoleMock = vi.spyOn(console, 'error') consoleMock.mockImplementation(() => undefined) const queryCache = new QueryCache() const queryClient = new QueryClient({ queryCache, defaultOptions: { dehydrate: { shouldDehydrateQuery: () => true, }, }, }) const promise = queryClient .prefetchQuery({ queryKey: ['error'], queryFn: () => Promise.reject(new Error('test error')), retry: false, }) .catch(() => undefined) const dehydrated = dehydrate(queryClient) expect(dehydrated.queries[0]?.promise).toBeInstanceOf(Promise) await expect(dehydrated.queries[0]?.promise).rejects.toThrow('redacted') await promise consoleMock.mockRestore() }) test('should log error in development environment when redacting errors', async () => { const originalNodeEnv = process.env.NODE_ENV process.env.NODE_ENV = 'development' const consoleMock = vi.spyOn(console, 'error') consoleMock.mockImplementation(() => undefined) const queryCache = new QueryCache() const queryClient = new QueryClient({ queryCache, defaultOptions: { dehydrate: { shouldDehydrateQuery: () => true, shouldRedactErrors: () => true, }, }, }) const testError = new Error('test error') const promise = queryClient .prefetchQuery({ queryKey: ['error'], queryFn: () => Promise.reject(testError), retry: false, }) .catch(() => undefined) const dehydrated = dehydrate(queryClient) await expect(dehydrated.queries[0]?.promise).rejects.toThrow('redacted') expect(consoleMock).toHaveBeenCalledWith( expect.stringContaining('test error'), ) await promise process.env.NODE_ENV = originalNodeEnv consoleMock.mockRestore() }) // When React hydrates promises across RSC/client boundaries, it passes // them as special ReactPromise types. There are situations where the // promise might have time to resolve before we end up hydrating it, in // which case React will have made it a special synchronous thenable where // .then() resolves immediately. // In these cases it's important we hydrate the data synchronously, or else // the data in the cache wont match the content that was rendered on the server. // What can end up happening otherwise is that the content is visible from the // server, but the client renders a Suspense fallback, only to immediately show // the data again. test('should rehydrate synchronous thenable immediately', async () => { // --- server --- const serverQueryClient = new QueryClient({ defaultOptions: { dehydrate: { shouldDehydrateQuery: () => true, }, }, }) const originalPromise = serverQueryClient.prefetchQuery({ queryKey: ['data'], queryFn: () => null, }) const dehydrated = dehydrate(serverQueryClient) // --- server end --- // Simulate a synchronous thenable // @ts-expect-error dehydrated.queries[0].promise.then = (cb) => { cb?.('server data') } // --- client --- const clientQueryClient = new QueryClient() hydrate(clientQueryClient, dehydrated) // If data is already resolved, it should end up in the cache immediately expect(clientQueryClient.getQueryData(['data'])).toBe('server data') // Need to await the original promise or else it will get a cancellation // error and test will fail await originalPromise }) }) ================================================ FILE: packages/query-core/src/__tests__/infiniteQueryBehavior.test.tsx ================================================ import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest' import { queryKey, sleep } from '@tanstack/query-test-utils' import { CancelledError, InfiniteQueryObserver, QueryClient } from '..' import type { InfiniteData, InfiniteQueryObserverResult, QueryCache } from '..' describe('InfiniteQueryBehavior', () => { let queryClient: QueryClient let queryCache: QueryCache beforeEach(() => { vi.useFakeTimers() queryClient = new QueryClient() queryCache = queryClient.getQueryCache() queryClient.mount() }) afterEach(() => { queryClient.clear() vi.useRealTimers() }) test('should throw an error if the queryFn is not defined', async () => { const key = queryKey() const observer = new InfiniteQueryObserver(queryClient, { queryKey: key, retry: false, initialPageParam: 1, getNextPageParam: () => 2, }) let observerResult: | InfiniteQueryObserverResult | undefined const unsubscribe = observer.subscribe((result) => { observerResult = result }) await vi.advanceTimersByTimeAsync(0) const query = queryCache.find({ queryKey: key })! expect(observerResult).toMatchObject({ isError: true, error: new Error(`Missing queryFn: '${query.queryHash}'`), }) unsubscribe() }) test('should apply the maxPages option to limit the number of pages', async () => { const key = queryKey() let abortSignal: AbortSignal | null = null const queryFnSpy = vi.fn().mockImplementation(({ pageParam, signal }) => { abortSignal = signal return pageParam }) const observer = new InfiniteQueryObserver(queryClient, { queryKey: key, queryFn: queryFnSpy, getNextPageParam: (lastPage) => lastPage + 1, getPreviousPageParam: (firstPage) => firstPage - 1, maxPages: 2, initialPageParam: 1, }) let observerResult: | InfiniteQueryObserverResult | undefined const unsubscribe = observer.subscribe((result) => { observerResult = result }) // Wait for the first page to be fetched await vi.advanceTimersByTimeAsync(0) expect(observerResult).toMatchObject({ isFetching: false, data: { pages: [1], pageParams: [1] }, }) expect(queryFnSpy).toHaveBeenNthCalledWith(1, { queryKey: key, client: queryClient, pageParam: 1, meta: undefined, direction: 'forward', signal: abortSignal, }) queryFnSpy.mockClear() // Fetch the second page await observer.fetchNextPage() expect(queryFnSpy).toHaveBeenNthCalledWith(1, { queryKey: key, client: queryClient, pageParam: 2, direction: 'forward', meta: undefined, signal: abortSignal, }) expect(observerResult).toMatchObject({ isFetching: false, data: { pages: [1, 2], pageParams: [1, 2] }, }) queryFnSpy.mockClear() // Fetch the page before the first page await observer.fetchPreviousPage() expect(queryFnSpy).toHaveBeenNthCalledWith(1, { queryKey: key, client: queryClient, pageParam: 0, direction: 'backward', meta: undefined, signal: abortSignal, }) // Only first two pages should be in the data expect(observerResult).toMatchObject({ isFetching: false, data: { pages: [0, 1], pageParams: [0, 1] }, }) queryFnSpy.mockClear() // Fetch the page before await observer.fetchPreviousPage() expect(queryFnSpy).toHaveBeenNthCalledWith(1, { queryKey: key, client: queryClient, pageParam: -1, meta: undefined, direction: 'backward', signal: abortSignal, }) expect(observerResult).toMatchObject({ isFetching: false, data: { pages: [-1, 0], pageParams: [-1, 0] }, }) queryFnSpy.mockClear() // Fetch the page after await observer.fetchNextPage() expect(queryFnSpy).toHaveBeenNthCalledWith(1, { queryKey: key, client: queryClient, pageParam: 1, meta: undefined, direction: 'forward', signal: abortSignal, }) expect(observerResult).toMatchObject({ isFetching: false, data: { pages: [0, 1] }, }) queryFnSpy.mockClear() // Refetch the infinite query await observer.refetch() // Only 2 pages should refetch expect(queryFnSpy).toHaveBeenCalledTimes(2) expect(queryFnSpy).toHaveBeenNthCalledWith(1, { queryKey: key, client: queryClient, pageParam: 0, meta: undefined, direction: 'forward', signal: abortSignal, }) expect(queryFnSpy).toHaveBeenNthCalledWith(2, { queryKey: key, client: queryClient, pageParam: 1, meta: undefined, direction: 'forward', signal: abortSignal, }) unsubscribe() }) test('should support query cancellation', async () => { const key = queryKey() let abortSignal: AbortSignal | null = null const queryFnSpy = vi.fn().mockImplementation(({ pageParam, signal }) => { abortSignal = signal sleep(10) return pageParam }) const observer = new InfiniteQueryObserver(queryClient, { queryKey: key, queryFn: queryFnSpy, getNextPageParam: (lastPage) => lastPage + 1, getPreviousPageParam: (firstPage) => firstPage - 1, initialPageParam: 1, }) let observerResult: | InfiniteQueryObserverResult | undefined const unsubscribe = observer.subscribe((result) => { observerResult = result }) const query = observer.getCurrentQuery() query.cancel() // Wait for the first page to be cancelled await vi.advanceTimersByTimeAsync(0) expect(observerResult).toMatchObject({ isFetching: false, isError: true, error: new CancelledError(), data: undefined, }) expect(queryFnSpy).toHaveBeenCalledTimes(1) expect(queryFnSpy).toHaveBeenNthCalledWith(1, { queryKey: key, client: queryClient, pageParam: 1, meta: undefined, direction: 'forward', signal: abortSignal, }) unsubscribe() }) test('should not refetch pages if the query is cancelled', async () => { const key = queryKey() let abortSignal: AbortSignal | null = null let queryFnSpy = vi.fn().mockImplementation(({ pageParam, signal }) => { abortSignal = signal return pageParam }) const observer = new InfiniteQueryObserver(queryClient, { queryKey: key, queryFn: queryFnSpy, getNextPageParam: (lastPage) => lastPage + 1, getPreviousPageParam: (firstPage) => firstPage - 1, initialPageParam: 1, }) let observerResult: | InfiniteQueryObserverResult | undefined const unsubscribe = observer.subscribe((result) => { observerResult = result }) // Wait for the first page to be fetched await vi.advanceTimersByTimeAsync(0) expect(observerResult).toMatchObject({ isFetching: false, data: { pages: [1], pageParams: [1] }, }) queryFnSpy.mockClear() // Fetch the second page await observer.fetchNextPage() expect(observerResult).toMatchObject({ isFetching: false, data: { pages: [1, 2], pageParams: [1, 2] }, }) expect(queryFnSpy).toHaveBeenCalledTimes(1) expect(queryFnSpy).toHaveBeenNthCalledWith(1, { queryKey: key, client: queryClient, pageParam: 2, meta: undefined, direction: 'forward', signal: abortSignal, }) queryFnSpy = vi.fn().mockImplementation(({ pageParam = 1, signal }) => { abortSignal = signal sleep(10) return pageParam }) // Refetch the query observer.refetch() expect(observerResult).toMatchObject({ isFetching: true, isError: false, }) // Cancel the query const query = observer.getCurrentQuery() await query.cancel() vi.advanceTimersByTime(10) expect(observerResult).toMatchObject({ isFetching: false, isError: true, error: new CancelledError(), data: { pages: [1, 2], pageParams: [1, 2] }, }) // Pages should not have been fetched expect(queryFnSpy).toHaveBeenCalledTimes(0) unsubscribe() }) test('should not enter an infinite loop when a page errors while retry is on #8046', async () => { let errorCount = 0 const key = queryKey() interface TestResponse { data: Array<{ id: string }> nextToken?: number } const fakeData = [ { data: [{ id: 'item-1' }], nextToken: 1 }, { data: [{ id: 'item-2' }], nextToken: 2 }, { data: [{ id: 'item-3' }], nextToken: 3 }, { data: [{ id: 'item-4' }] }, ] const fetchData = async ({ nextToken = 0 }: { nextToken?: number }) => new Promise((resolve, reject) => { setTimeout(() => { if (nextToken == 2 && errorCount < 3) { errorCount += 1 reject({ statusCode: 429 }) return } resolve(fakeData[nextToken] as TestResponse) }, 10) }) const observer = new InfiniteQueryObserver< TestResponse, Error, InfiniteData, typeof key, number >(queryClient, { retry: 5, staleTime: 0, retryDelay: 10, queryKey: key, initialPageParam: 1, getNextPageParam: (lastPage) => lastPage.nextToken, queryFn: ({ pageParam }) => fetchData({ nextToken: pageParam }), }) // Fetch Page 1 const fetchPage1Promise = observer.fetchNextPage() await vi.advanceTimersByTimeAsync(10) const page1Data = await fetchPage1Promise expect(page1Data.data?.pageParams).toEqual([1]) // Fetch Page 2, as per the queryFn, this will reject 2 times then resolves const fetchPage2Promise = observer.fetchNextPage() await vi.advanceTimersByTimeAsync(70) const page2Data = await fetchPage2Promise expect(page2Data.data?.pageParams).toEqual([1, 2]) // Fetch Page 3 const fetchPage3Promise = observer.fetchNextPage() await vi.advanceTimersByTimeAsync(10) const page3Data = await fetchPage3Promise expect(page3Data.data?.pageParams).toEqual([1, 2, 3]) // Now the real deal; re-fetching this query **should not** stamp into an // infinite loop where the retryer every time restarts from page 1 // once it reaches the page where it errors. // For this to work, we'd need to reset the error count so we actually retry errorCount = 0 const reFetchPromise = observer.fetchNextPage() await vi.advanceTimersByTimeAsync(10) const reFetchedData = await reFetchPromise expect(reFetchedData.data?.pageParams).toEqual([1, 2, 3]) }) test('should fetch even if initialPageParam is null', async () => { const key = queryKey() const observer = new InfiniteQueryObserver(queryClient, { queryKey: key, queryFn: () => 'data', getNextPageParam: () => null, initialPageParam: null, }) let observerResult: | InfiniteQueryObserverResult | undefined const unsubscribe = observer.subscribe((result) => { observerResult = result }) await vi.advanceTimersByTimeAsync(0) expect(observerResult).toMatchObject({ isFetching: false, data: { pages: ['data'], pageParams: [null] }, }) unsubscribe() }) test('should not fetch next page when getNextPageParam returns null', async () => { const key = queryKey() const observer = new InfiniteQueryObserver(queryClient, { queryKey: key, queryFn: ({ pageParam }) => sleep(0).then(() => pageParam), getNextPageParam: (lastPage) => (lastPage === 1 ? null : lastPage + 1), initialPageParam: 1, }) let observerResult: | InfiniteQueryObserverResult, Error> | undefined const unsubscribe = observer.subscribe((result) => { observerResult = result }) await vi.advanceTimersByTimeAsync(0) expect(observerResult).toMatchObject({ isFetching: false, data: { pages: [1], pageParams: [1] }, }) await observer.fetchNextPage() expect(observerResult).toMatchObject({ isFetching: false, data: { pages: [1], pageParams: [1] }, }) unsubscribe() }) test('should use persister when provided', async () => { const key = queryKey() const persisterSpy = vi.fn().mockImplementation(async (fn) => { return await fn() }) const observer = new InfiniteQueryObserver(queryClient, { queryKey: key, queryFn: ({ pageParam }) => sleep(0).then(() => pageParam), getNextPageParam: (lastPage) => lastPage + 1, initialPageParam: 1, persister: persisterSpy, }) const unsubscribe = observer.subscribe(() => {}) await vi.advanceTimersByTimeAsync(0) expect(persisterSpy).toHaveBeenCalledTimes(1) unsubscribe() }) }) ================================================ FILE: packages/query-core/src/__tests__/infiniteQueryObserver.test-d.tsx ================================================ import { afterEach, beforeEach, describe, expectTypeOf, it, vi } from 'vitest' import { queryKey } from '@tanstack/query-test-utils' import { InfiniteQueryObserver, QueryClient } from '..' import type { InfiniteData } from '..' describe('InfiniteQueryObserver', () => { let queryClient: QueryClient beforeEach(() => { queryClient = new QueryClient() queryClient.mount() }) afterEach(() => { queryClient.clear() }) it('should be inferred as a correct result type', () => { const next: number | undefined = 2 const queryFn = vi.fn(({ pageParam }) => String(pageParam)) const observer = new InfiniteQueryObserver(queryClient, { queryKey: queryKey(), queryFn, initialPageParam: 1, getNextPageParam: () => next, }) const result = observer.getCurrentResult() if (result.isPending) { expectTypeOf(result.data).toEqualTypeOf() expectTypeOf(result.error).toEqualTypeOf() expectTypeOf(result.isLoading).toEqualTypeOf() expectTypeOf(result.status).toEqualTypeOf<'pending'>() expectTypeOf(result.isPlaceholderData).toEqualTypeOf() } if (result.isLoading) { expectTypeOf(result.data).toEqualTypeOf() expectTypeOf(result.error).toEqualTypeOf() expectTypeOf(result.isPending).toEqualTypeOf() expectTypeOf(result.status).toEqualTypeOf<'pending'>() expectTypeOf(result.isPlaceholderData).toEqualTypeOf() } if (result.isLoadingError) { expectTypeOf(result.data).toEqualTypeOf() expectTypeOf(result.error).toEqualTypeOf() expectTypeOf(result.status).toEqualTypeOf<'error'>() expectTypeOf(result.isPlaceholderData).toEqualTypeOf() } if (result.isRefetchError) { expectTypeOf(result.data).toEqualTypeOf>() expectTypeOf(result.error).toEqualTypeOf() expectTypeOf(result.status).toEqualTypeOf<'error'>() expectTypeOf(result.isFetchNextPageError).toEqualTypeOf() expectTypeOf(result.isFetchPreviousPageError).toEqualTypeOf() expectTypeOf(result.isPlaceholderData).toEqualTypeOf() } if (result.isSuccess) { expectTypeOf(result.data).toEqualTypeOf>() expectTypeOf(result.error).toEqualTypeOf() expectTypeOf(result.status).toEqualTypeOf<'success'>() expectTypeOf(result.isPlaceholderData).toEqualTypeOf() } if (result.isPlaceholderData) { expectTypeOf(result.data).toEqualTypeOf>() expectTypeOf(result.error).toEqualTypeOf() expectTypeOf(result.status).toEqualTypeOf<'success'>() } }) }) ================================================ FILE: packages/query-core/src/__tests__/infiniteQueryObserver.test.tsx ================================================ import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest' import { queryKey, sleep } from '@tanstack/query-test-utils' import { InfiniteQueryObserver, QueryClient } from '..' import type { DefaultedInfiniteQueryObserverOptions, InfiniteData, } from '../types' describe('InfiniteQueryObserver', () => { let queryClient: QueryClient beforeEach(() => { vi.useFakeTimers() queryClient = new QueryClient() queryClient.mount() }) afterEach(() => { queryClient.clear() vi.useRealTimers() }) test('should be able to fetch an infinite query with selector', async () => { const key = queryKey() const observer = new InfiniteQueryObserver(queryClient, { queryKey: key, queryFn: () => sleep(10).then(() => 1), select: (data) => ({ pages: data.pages.map((x) => `${x}`), pageParams: data.pageParams, }), initialPageParam: 1, getNextPageParam: () => 2, }) let observerResult const unsubscribe = observer.subscribe((result) => { observerResult = result }) await vi.advanceTimersByTimeAsync(10) unsubscribe() expect(observerResult).toMatchObject({ data: { pages: ['1'], pageParams: [1] }, }) }) test('should pass the meta option to the queryFn', async () => { const meta = { it: 'works', } const key = queryKey() const queryFn = vi.fn(() => sleep(10).then(() => 1)) const observer = new InfiniteQueryObserver(queryClient, { meta, queryKey: key, queryFn, select: (data) => ({ pages: data.pages.map((x) => `${x}`), pageParams: data.pageParams, }), initialPageParam: 1, getNextPageParam: () => 2, }) let observerResult const unsubscribe = observer.subscribe((result) => { observerResult = result }) await vi.advanceTimersByTimeAsync(10) unsubscribe() expect(observerResult).toMatchObject({ data: { pages: ['1'], pageParams: [1] }, }) expect(queryFn).toBeCalledWith(expect.objectContaining({ meta })) }) test('should make getNextPageParam and getPreviousPageParam receive current pageParams', async () => { const key = queryKey() let single: Array = [] let all: Array = [] const observer = new InfiniteQueryObserver(queryClient, { queryKey: key, queryFn: ({ pageParam }) => sleep(10).then(() => String(pageParam)), initialPageParam: 1, getNextPageParam: (_, __, lastPageParam, allPageParams) => { single.push('next' + lastPageParam) all.push('next' + allPageParams.join(',')) return lastPageParam + 1 }, getPreviousPageParam: (_, __, firstPageParam, allPageParams) => { single.push('prev' + firstPageParam) all.push('prev' + allPageParams.join(',')) return firstPageParam - 1 }, }) await vi.advanceTimersByTimeAsync(10) observer.fetchNextPage() await vi.advanceTimersByTimeAsync(10) observer.fetchPreviousPage() await vi.advanceTimersByTimeAsync(10) expect(single).toEqual(['next1', 'prev1', 'prev1', 'next1', 'prev0']) expect(all).toEqual(['next1', 'prev1', 'prev1', 'next0,1', 'prev0,1']) single = [] all = [] observer.refetch() await vi.advanceTimersByTimeAsync(20) expect(single).toEqual(['next0', 'next1', 'prev0']) expect(all).toEqual(['next0', 'next0,1', 'prev0,1']) }) test('should not invoke getNextPageParam and getPreviousPageParam on empty pages', () => { const key = queryKey() const getNextPageParam = vi.fn() const getPreviousPageParam = vi.fn() const observer = new InfiniteQueryObserver(queryClient, { queryKey: key, queryFn: ({ pageParam }) => sleep(10).then(() => String(pageParam)), initialPageParam: 1, getNextPageParam: getNextPageParam.mockImplementation( (_, __, lastPageParam) => lastPageParam + 1, ), getPreviousPageParam: getPreviousPageParam.mockImplementation( (_, __, firstPageParam) => firstPageParam - 1, ), }) const unsubscribe = observer.subscribe(() => {}) getNextPageParam.mockClear() getPreviousPageParam.mockClear() queryClient.setQueryData(key, { pages: [], pageParams: [] }) expect(getNextPageParam).toHaveBeenCalledTimes(0) expect(getPreviousPageParam).toHaveBeenCalledTimes(0) unsubscribe() }) test('should stop refetching if undefined is returned from getNextPageParam', async () => { const key = queryKey() let next: number | undefined = 2 const queryFn = vi.fn<(...args: Array) => any>(({ pageParam }) => sleep(10).then(() => String(pageParam)), ) const observer = new InfiniteQueryObserver(queryClient, { queryKey: key, queryFn, initialPageParam: 1, getNextPageParam: () => next, }) observer.fetchNextPage() await vi.advanceTimersByTimeAsync(10) observer.fetchNextPage() await vi.advanceTimersByTimeAsync(10) expect(observer.getCurrentResult().data?.pages).toEqual(['1', '2']) expect(queryFn).toBeCalledTimes(2) expect(observer.getCurrentResult().hasNextPage).toBe(true) next = undefined observer.refetch() await vi.advanceTimersByTimeAsync(10) expect(observer.getCurrentResult().data?.pages).toEqual(['1']) expect(queryFn).toBeCalledTimes(3) expect(observer.getCurrentResult().hasNextPage).toBe(false) }) test('should stop refetching if null is returned from getNextPageParam', async () => { const key = queryKey() let next: number | null = 2 const queryFn = vi.fn<(...args: Array) => any>(({ pageParam }) => sleep(10).then(() => String(pageParam)), ) const observer = new InfiniteQueryObserver(queryClient, { queryKey: key, queryFn, initialPageParam: 1, getNextPageParam: () => next, }) observer.fetchNextPage() await vi.advanceTimersByTimeAsync(10) observer.fetchNextPage() await vi.advanceTimersByTimeAsync(10) expect(observer.getCurrentResult().data?.pages).toEqual(['1', '2']) expect(queryFn).toBeCalledTimes(2) expect(observer.getCurrentResult().hasNextPage).toBe(true) next = null observer.refetch() await vi.advanceTimersByTimeAsync(10) expect(observer.getCurrentResult().data?.pages).toEqual(['1']) expect(queryFn).toBeCalledTimes(3) expect(observer.getCurrentResult().hasNextPage).toBe(false) }) test('should set infinite query behavior via getOptimisticResult and return the initial state', () => { const key = queryKey() const observer = new InfiniteQueryObserver(queryClient, { queryKey: key, queryFn: () => sleep(10).then(() => 1), initialPageParam: 1, getNextPageParam: () => 2, }) const options: DefaultedInfiniteQueryObserverOptions< number, Error, InfiniteData, typeof key, number > = { queryKey: key, queryFn: () => sleep(10).then(() => 1), initialPageParam: 1, getNextPageParam: () => 2, throwOnError: true, refetchOnReconnect: false, queryHash: key.join(''), behavior: undefined, } const result = observer.getOptimisticResult(options) expect(options.behavior).toBeDefined() expect(options.behavior?.onFetch).toBeDefined() expect(result).toMatchObject({ data: undefined, hasNextPage: false, hasPreviousPage: false, isFetching: false, isFetchingNextPage: false, isFetchingPreviousPage: false, isError: false, isRefetchError: false, isRefetching: false, }) }) }) ================================================ FILE: packages/query-core/src/__tests__/mutationCache.test.tsx ================================================ import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest' import { queryKey, sleep } from '@tanstack/query-test-utils' import { MutationCache, MutationObserver, QueryClient } from '..' import { executeMutation } from './utils' describe('mutationCache', () => { beforeEach(() => { vi.useFakeTimers() }) afterEach(() => { vi.useRealTimers() }) describe('MutationCacheConfig error callbacks', () => { test('should call onError and onSettled when a mutation errors', async () => { const key = queryKey() const onError = vi.fn() const onSuccess = vi.fn() const onSettled = vi.fn() const testCache = new MutationCache({ onError, onSuccess, onSettled }) const testClient = new QueryClient({ mutationCache: testCache }) executeMutation( testClient, { mutationKey: key, mutationFn: () => sleep(10).then(() => Promise.reject(new Error('error'))), onMutate: () => 'result', }, 'vars', ).catch(() => undefined) await vi.advanceTimersByTimeAsync(10) const mutation = testCache.getAll()[0] expect(onError).toHaveBeenCalledTimes(1) expect(onError).toHaveBeenCalledWith( new Error('error'), 'vars', 'result', mutation, { client: testClient, meta: undefined, mutationKey: key, }, ) expect(onSuccess).not.toHaveBeenCalled() expect(onSettled).toHaveBeenCalledTimes(1) expect(onSettled).toHaveBeenCalledWith( undefined, new Error('error'), 'vars', 'result', mutation, { client: testClient, meta: undefined, mutationKey: key, }, ) }) test('should be awaited', async () => { const key = queryKey() const states: Array = [] const onError = () => sleep(10).then(() => { states.push(1) states.push(2) }) const onSettled = () => sleep(10).then(() => { states.push(5) states.push(6) }) const testCache = new MutationCache({ onError, onSettled }) const testClient = new QueryClient({ mutationCache: testCache }) executeMutation( testClient, { mutationKey: key, mutationFn: () => sleep(10).then(() => Promise.reject(new Error('error'))), onError: () => sleep(10).then(() => { states.push(3) states.push(4) }), onSettled: () => sleep(10).then(() => { states.push(7) states.push(8) }), }, 'vars', ).catch(() => undefined) await vi.advanceTimersByTimeAsync(50) expect(states).toEqual([1, 2, 3, 4, 5, 6, 7, 8]) }) }) describe('MutationCacheConfig success callbacks', () => { test('should call onSuccess and onSettled when a mutation is successful', async () => { const key = queryKey() const onError = vi.fn() const onSuccess = vi.fn() const onSettled = vi.fn() const testCache = new MutationCache({ onError, onSuccess, onSettled }) const testClient = new QueryClient({ mutationCache: testCache }) executeMutation( testClient, { mutationKey: key, mutationFn: () => sleep(10).then(() => ({ data: 5 })), onMutate: () => 'result', }, 'vars', ) await vi.advanceTimersByTimeAsync(10) const mutation = testCache.getAll()[0] expect(onSuccess).toHaveBeenCalledTimes(1) expect(onSuccess).toHaveBeenCalledWith( { data: 5 }, 'vars', 'result', mutation, { client: testClient, meta: undefined, mutationKey: key, }, ) expect(onError).not.toHaveBeenCalled() expect(onSettled).toHaveBeenCalledTimes(1) expect(onSettled).toHaveBeenCalledWith( { data: 5 }, null, 'vars', 'result', mutation, { client: testClient, meta: undefined, mutationKey: key, }, ) }) test('should be awaited', async () => { const key = queryKey() const states: Array = [] const onSuccess = () => sleep(10).then(() => { states.push(1) states.push(2) }) const onSettled = () => sleep(10).then(() => { states.push(5) states.push(6) }) const testCache = new MutationCache({ onSuccess, onSettled }) const testClient = new QueryClient({ mutationCache: testCache }) executeMutation( testClient, { mutationKey: key, mutationFn: () => sleep(10).then(() => ({ data: 5 })), onSuccess: () => sleep(10).then(() => { states.push(3) states.push(4) }), onSettled: () => sleep(10).then(() => { states.push(7) states.push(8) }), }, 'vars', ) await vi.advanceTimersByTimeAsync(50) expect(states).toEqual([1, 2, 3, 4, 5, 6, 7, 8]) }) }) describe('MutationCacheConfig.onMutate', () => { test('should be called before a mutation executes', () => { const key = queryKey() const onMutate = vi.fn() const testCache = new MutationCache({ onMutate }) const testClient = new QueryClient({ mutationCache: testCache }) executeMutation( testClient, { mutationKey: key, mutationFn: () => sleep(10).then(() => ({ data: 5 })), onMutate: () => 'result', }, 'vars', ) const mutation = testCache.getAll()[0] expect(onMutate).toHaveBeenCalledWith('vars', mutation, { client: testClient, meta: undefined, mutationKey: key, }) }) test('should be awaited', async () => { const key = queryKey() const states: Array = [] const onMutate = () => sleep(10).then(() => { states.push(1) states.push(2) }) const testCache = new MutationCache({ onMutate }) const testClient = new QueryClient({ mutationCache: testCache }) executeMutation( testClient, { mutationKey: key, mutationFn: () => sleep(10).then(() => ({ data: 5 })), onMutate: () => sleep(10).then(() => { states.push(3) states.push(4) }), }, 'vars', ) await vi.advanceTimersByTimeAsync(20) expect(states).toEqual([1, 2, 3, 4]) }) }) describe('find', () => { test('should filter correctly', () => { const testCache = new MutationCache() const testClient = new QueryClient({ mutationCache: testCache }) const key = ['mutation', 'vars'] executeMutation( testClient, { mutationKey: key, mutationFn: () => sleep(10).then(() => undefined), }, 'vars', ) const [mutation] = testCache.getAll() expect(testCache.find({ mutationKey: key })).toEqual(mutation) expect( testCache.find({ mutationKey: ['mutation'], exact: false }), ).toEqual(mutation) expect(testCache.find({ mutationKey: ['unknown'] })).toEqual(undefined) expect( testCache.find({ predicate: (m) => m.options.mutationKey?.[0] === key[0], }), ).toEqual(mutation) }) }) describe('findAll', () => { test('should filter correctly', () => { const testCache = new MutationCache() const testClient = new QueryClient({ mutationCache: testCache }) executeMutation( testClient, { mutationKey: ['a', 1], mutationFn: () => sleep(10).then(() => undefined), }, 1, ) executeMutation( testClient, { mutationKey: ['a', 2], mutationFn: () => sleep(10).then(() => undefined), }, 2, ) executeMutation( testClient, { mutationKey: ['b'], mutationFn: () => sleep(10).then(() => undefined), }, 3, ) const [mutation1, mutation2] = testCache.getAll() expect( testCache.findAll({ mutationKey: ['a'], exact: false }), ).toHaveLength(2) expect(testCache.find({ mutationKey: ['a', 1] })).toEqual(mutation1) expect( testCache.findAll({ predicate: (m) => m.options.mutationKey?.[1] === 2, }), ).toEqual([mutation2]) expect(testCache.findAll({ mutationKey: ['unknown'] })).toEqual([]) }) }) describe('garbage collection', () => { test('should remove unused mutations after gcTime has elapsed', async () => { const testCache = new MutationCache() const testClient = new QueryClient({ mutationCache: testCache }) const onSuccess = vi.fn() executeMutation( testClient, { mutationKey: ['a', 1], gcTime: 10, mutationFn: () => sleep(10).then(() => undefined), onSuccess, }, 1, ) await vi.advanceTimersByTimeAsync(10) expect(testCache.getAll()).toHaveLength(1) await vi.advanceTimersByTimeAsync(10) expect(testCache.getAll()).toHaveLength(0) expect(onSuccess).toHaveBeenCalledTimes(1) }) test('should not remove mutations if there are active observers', async () => { const queryClient = new QueryClient() const observer = new MutationObserver(queryClient, { gcTime: 10, mutationFn: (input: number) => sleep(10).then(() => input), }) const unsubscribe = observer.subscribe(() => undefined) expect(queryClient.getMutationCache().getAll()).toHaveLength(0) observer.mutate(1) expect(queryClient.getMutationCache().getAll()).toHaveLength(1) await vi.advanceTimersByTimeAsync(10) expect(queryClient.getMutationCache().getAll()).toHaveLength(1) unsubscribe() expect(queryClient.getMutationCache().getAll()).toHaveLength(1) await vi.advanceTimersByTimeAsync(10) expect(queryClient.getMutationCache().getAll()).toHaveLength(0) }) test('should be garbage collected later when unsubscribed and mutation is pending', async () => { const queryClient = new QueryClient() const onSuccess = vi.fn() const observer = new MutationObserver(queryClient, { gcTime: 10, mutationFn: () => sleep(10).then(() => 'data'), onSuccess, }) const unsubscribe = observer.subscribe(() => undefined) observer.mutate(1) unsubscribe() expect(queryClient.getMutationCache().getAll()).toHaveLength(1) await vi.advanceTimersByTimeAsync(10) // unsubscribe should not remove even though gcTime has elapsed b/c mutation is still pending expect(queryClient.getMutationCache().getAll()).toHaveLength(1) await vi.advanceTimersByTimeAsync(10) // should be removed after an additional gcTime wait expect(queryClient.getMutationCache().getAll()).toHaveLength(0) expect(onSuccess).toHaveBeenCalledTimes(1) }) test('should call callbacks even with gcTime 0 and mutation still pending', async () => { const queryClient = new QueryClient() const onSuccess = vi.fn() const observer = new MutationObserver(queryClient, { gcTime: 0, mutationFn: () => sleep(10).then(() => 'data'), onSuccess, }) const unsubscribe = observer.subscribe(() => undefined) observer.mutate(1) unsubscribe() await vi.advanceTimersByTimeAsync(10) expect(queryClient.getMutationCache().getAll()).toHaveLength(0) expect(onSuccess).toHaveBeenCalledTimes(1) }) }) }) ================================================ FILE: packages/query-core/src/__tests__/mutationObserver.test.tsx ================================================ import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest' import { queryKey, sleep } from '@tanstack/query-test-utils' import { MutationObserver, QueryClient } from '..' describe('mutationObserver', () => { let queryClient: QueryClient beforeEach(() => { vi.useFakeTimers() queryClient = new QueryClient() queryClient.mount() }) afterEach(() => { queryClient.clear() vi.useRealTimers() }) test('onUnsubscribe should not remove the current mutation observer if there is still a subscription', async () => { const mutation = new MutationObserver(queryClient, { mutationFn: (text: string) => sleep(20).then(() => text), }) const subscription1Handler = vi.fn() const subscription2Handler = vi.fn() const unsubscribe1 = mutation.subscribe(subscription1Handler) const unsubscribe2 = mutation.subscribe(subscription2Handler) mutation.mutate('input') unsubscribe1() expect(subscription1Handler).toBeCalledTimes(1) expect(subscription2Handler).toBeCalledTimes(1) await vi.advanceTimersByTimeAsync(20) expect(subscription1Handler).toBeCalledTimes(1) expect(subscription2Handler).toBeCalledTimes(2) unsubscribe2() }) test('unsubscribe should remove observer to trigger GC', async () => { const mutation = new MutationObserver(queryClient, { mutationFn: (text: string) => sleep(5).then(() => text), gcTime: 10, }) const subscriptionHandler = vi.fn() const unsubscribe = mutation.subscribe(subscriptionHandler) mutation.mutate('input') await vi.advanceTimersByTimeAsync(5) expect(queryClient.getMutationCache().findAll()).toHaveLength(1) unsubscribe() await vi.advanceTimersByTimeAsync(10) expect(queryClient.getMutationCache().findAll()).toHaveLength(0) }) test('reset should remove observer to trigger GC', async () => { const mutation = new MutationObserver(queryClient, { mutationFn: (text: string) => sleep(5).then(() => text), gcTime: 10, }) const subscriptionHandler = vi.fn() const unsubscribe = mutation.subscribe(subscriptionHandler) mutation.mutate('input') await vi.advanceTimersByTimeAsync(5) expect(queryClient.getMutationCache().findAll()).toHaveLength(1) mutation.reset() await vi.advanceTimersByTimeAsync(10) expect(queryClient.getMutationCache().findAll()).toHaveLength(0) unsubscribe() }) test('changing mutation keys should reset the observer', async () => { const key = queryKey() const mutation = new MutationObserver(queryClient, { mutationKey: [...key, '1'], mutationFn: (text: string) => sleep(5).then(() => text), }) const subscriptionHandler = vi.fn() const unsubscribe = mutation.subscribe(subscriptionHandler) mutation.mutate('input') await vi.advanceTimersByTimeAsync(5) expect(mutation.getCurrentResult()).toMatchObject({ status: 'success', data: 'input', }) mutation.setOptions({ mutationKey: [...key, '2'], }) expect(mutation.getCurrentResult()).toMatchObject({ status: 'idle', }) unsubscribe() }) test('changing mutation keys should not affect already existing mutations', async () => { const key = queryKey() const mutationObserver = new MutationObserver(queryClient, { mutationKey: [...key, '1'], mutationFn: (text: string) => sleep(5).then(() => text), }) const subscriptionHandler = vi.fn() const unsubscribe = mutationObserver.subscribe(subscriptionHandler) mutationObserver.mutate('input') await vi.advanceTimersByTimeAsync(5) expect( queryClient.getMutationCache().find({ mutationKey: [...key, '1'] }), ).toMatchObject({ options: { mutationKey: [...key, '1'] }, state: { status: 'success', data: 'input', }, }) mutationObserver.setOptions({ mutationKey: [...key, '2'], }) expect( queryClient.getMutationCache().find({ mutationKey: [...key, '1'] }), ).toMatchObject({ options: { mutationKey: [...key, '1'] }, state: { status: 'success', data: 'input', }, }) unsubscribe() }) test('changing mutation meta should not affect successful mutations', async () => { const mutationObserver = new MutationObserver(queryClient, { meta: { a: 1 }, mutationFn: (text: string) => sleep(5).then(() => text), }) const subscriptionHandler = vi.fn() const unsubscribe = mutationObserver.subscribe(subscriptionHandler) mutationObserver.mutate('input') await vi.advanceTimersByTimeAsync(5) expect(queryClient.getMutationCache().find({})).toMatchObject({ options: { meta: { a: 1 } }, state: { status: 'success', data: 'input', }, }) mutationObserver.setOptions({ meta: { a: 2 }, }) expect(queryClient.getMutationCache().find({})).toMatchObject({ options: { meta: { a: 1 } }, state: { status: 'success', data: 'input', }, }) unsubscribe() }) test('mutation cache should have different meta when updated between mutations', async () => { const mutationFn = (text: string) => sleep(5).then(() => text) const mutationObserver = new MutationObserver(queryClient, { meta: { a: 1 }, mutationFn, }) const subscriptionHandler = vi.fn() const unsubscribe = mutationObserver.subscribe(subscriptionHandler) mutationObserver.mutate('input') await vi.advanceTimersByTimeAsync(5) mutationObserver.setOptions({ meta: { a: 2 }, mutationFn, }) mutationObserver.mutate('input') await vi.advanceTimersByTimeAsync(5) const mutations = queryClient.getMutationCache().findAll() expect(mutations[0]).toMatchObject({ options: { meta: { a: 1 } }, state: { status: 'success', data: 'input', }, }) expect(mutations[1]).toMatchObject({ options: { meta: { a: 2 } }, state: { status: 'success', data: 'input', }, }) unsubscribe() }) test('changing mutation meta should not affect rejected mutations', async () => { const mutationObserver = new MutationObserver(queryClient, { meta: { a: 1 }, mutationFn: (_: string) => sleep(5).then(() => Promise.reject(new Error('err'))), }) const subscriptionHandler = vi.fn() const unsubscribe = mutationObserver.subscribe(subscriptionHandler) mutationObserver.mutate('input').catch(() => undefined) await vi.advanceTimersByTimeAsync(5) expect(queryClient.getMutationCache().find({})).toMatchObject({ options: { meta: { a: 1 } }, state: { status: 'error', }, }) mutationObserver.setOptions({ meta: { a: 2 }, }) expect(queryClient.getMutationCache().find({})).toMatchObject({ options: { meta: { a: 1 } }, state: { status: 'error', }, }) unsubscribe() }) test('changing mutation meta should affect pending mutations', async () => { const mutationObserver = new MutationObserver(queryClient, { meta: { a: 1 }, mutationFn: (text: string) => sleep(20).then(() => text), }) const subscriptionHandler = vi.fn() const unsubscribe = mutationObserver.subscribe(subscriptionHandler) mutationObserver.mutate('input') await vi.advanceTimersByTimeAsync(5) expect(queryClient.getMutationCache().find({})).toMatchObject({ options: { meta: { a: 1 } }, state: { status: 'pending', }, }) mutationObserver.setOptions({ meta: { a: 2 }, }) expect(queryClient.getMutationCache().find({})).toMatchObject({ options: { meta: { a: 2 } }, state: { status: 'pending', }, }) unsubscribe() }) test('mutation callbacks should be called in correct order with correct arguments for success case', async () => { const onSuccess = vi.fn() const onSettled = vi.fn() const mutationObserver = new MutationObserver(queryClient, { mutationFn: (text: string) => Promise.resolve(text.toUpperCase()), }) const subscriptionHandler = vi.fn() const unsubscribe = mutationObserver.subscribe(subscriptionHandler) mutationObserver.mutate('success', { onSuccess, onSettled, }) await vi.advanceTimersByTimeAsync(0) expect(onSuccess).toHaveBeenCalledTimes(1) expect(onSuccess).toHaveBeenCalledWith('SUCCESS', 'success', undefined, { client: queryClient, meta: undefined, mutationKey: undefined, }) expect(onSettled).toHaveBeenCalledTimes(1) expect(onSettled).toHaveBeenCalledWith( 'SUCCESS', null, 'success', undefined, { client: queryClient, meta: undefined, mutationKey: undefined, }, ) unsubscribe() }) test('mutation callbacks should be called in correct order with correct arguments for error case', async () => { const onError = vi.fn() const onSettled = vi.fn() const error = new Error('error') const mutationObserver = new MutationObserver(queryClient, { mutationFn: (_: string) => Promise.reject(error), }) const subscriptionHandler = vi.fn() const unsubscribe = mutationObserver.subscribe(subscriptionHandler) mutationObserver .mutate('error', { onError, onSettled, }) .catch(() => {}) await vi.advanceTimersByTimeAsync(0) expect(onError).toHaveBeenCalledTimes(1) expect(onError).toHaveBeenCalledWith(error, 'error', undefined, { client: queryClient, meta: undefined, mutationKey: undefined, }) expect(onSettled).toHaveBeenCalledTimes(1) expect(onSettled).toHaveBeenCalledWith( undefined, error, 'error', undefined, { client: queryClient, meta: undefined, mutationKey: undefined, }, ) unsubscribe() }) }) ================================================ FILE: packages/query-core/src/__tests__/mutations.test.tsx ================================================ import { queryKey, sleep } from '@tanstack/query-test-utils' import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest' import { QueryClient } from '..' import { MutationObserver } from '../mutationObserver' import { executeMutation } from './utils' import type { MutationState } from '../mutation' describe('mutations', () => { let queryClient: QueryClient beforeEach(() => { vi.useFakeTimers() queryClient = new QueryClient() queryClient.mount() }) afterEach(() => { queryClient.clear() vi.useRealTimers() }) test('mutate should accept null values', async () => { let variables const mutation = new MutationObserver(queryClient, { mutationFn: (vars: unknown) => { variables = vars return Promise.resolve(vars) }, }) mutation.mutate(null) await vi.advanceTimersByTimeAsync(0) expect(variables).toBe(null) }) test('setMutationDefaults should be able to set defaults', async () => { const key = queryKey() const fn = vi.fn() queryClient.setMutationDefaults(key, { mutationFn: fn, }) executeMutation( queryClient, { mutationKey: key, }, 'vars', ) await vi.advanceTimersByTimeAsync(0) expect(fn).toHaveBeenCalledTimes(1) expect(fn).toHaveBeenCalledWith('vars', { client: queryClient, meta: undefined, mutationKey: key, }) }) test('mutation should set correct success states', async () => { const mutation = new MutationObserver(queryClient, { mutationFn: (text: string) => sleep(10).then(() => text), onMutate: (text) => text, }) expect(mutation.getCurrentResult()).toEqual({ context: undefined, data: undefined, error: null, failureCount: 0, failureReason: null, isError: false, isIdle: true, isPending: false, isPaused: false, isSuccess: false, mutate: expect.any(Function), reset: expect.any(Function), status: 'idle', variables: undefined, submittedAt: 0, }) const states: Array> = [] mutation.subscribe((state) => { states.push(state) }) mutation.mutate('todo') await vi.advanceTimersByTimeAsync(0) expect(states[0]).toEqual({ context: undefined, data: undefined, error: null, failureCount: 0, failureReason: null, isError: false, isIdle: false, isPending: true, isPaused: false, isSuccess: false, mutate: expect.any(Function), reset: expect.any(Function), status: 'pending', variables: 'todo', submittedAt: expect.any(Number), }) await vi.advanceTimersByTimeAsync(0) expect(states[1]).toEqual({ context: 'todo', data: undefined, error: null, failureCount: 0, failureReason: null, isError: false, isIdle: false, isPending: true, isPaused: false, isSuccess: false, mutate: expect.any(Function), reset: expect.any(Function), status: 'pending', variables: 'todo', submittedAt: expect.any(Number), }) await vi.advanceTimersByTimeAsync(10) expect(states[2]).toEqual({ context: 'todo', data: 'todo', error: null, failureCount: 0, failureReason: null, isError: false, isIdle: false, isPending: false, isPaused: false, isSuccess: true, mutate: expect.any(Function), reset: expect.any(Function), status: 'success', variables: 'todo', submittedAt: expect.any(Number), }) }) test('mutation should set correct error states', async () => { const mutation = new MutationObserver(queryClient, { mutationFn: (_: string) => sleep(20).then(() => Promise.reject(new Error('err'))), onMutate: (text) => text, retry: 1, retryDelay: 10, }) const states: Array> = [] mutation.subscribe((state) => { states.push(state) }) mutation.mutate('todo').catch(() => undefined) await vi.advanceTimersByTimeAsync(0) expect(states[0]).toEqual({ context: undefined, data: undefined, error: null, failureCount: 0, failureReason: null, isError: false, isIdle: false, isPending: true, isPaused: false, isSuccess: false, mutate: expect.any(Function), reset: expect.any(Function), status: 'pending', variables: 'todo', submittedAt: expect.any(Number), }) await vi.advanceTimersByTimeAsync(0) expect(states[1]).toEqual({ context: 'todo', data: undefined, error: null, failureCount: 0, failureReason: null, isError: false, isIdle: false, isPending: true, isPaused: false, isSuccess: false, mutate: expect.any(Function), reset: expect.any(Function), status: 'pending', variables: 'todo', submittedAt: expect.any(Number), }) await vi.advanceTimersByTimeAsync(20) expect(states[2]).toEqual({ context: 'todo', data: undefined, error: null, failureCount: 1, failureReason: new Error('err'), isError: false, isIdle: false, isPending: true, isPaused: false, isSuccess: false, mutate: expect.any(Function), reset: expect.any(Function), status: 'pending', variables: 'todo', submittedAt: expect.any(Number), }) await vi.advanceTimersByTimeAsync(30) expect(states[3]).toEqual({ context: 'todo', data: undefined, error: new Error('err'), failureCount: 2, failureReason: new Error('err'), isError: true, isIdle: false, isPending: false, isPaused: false, isSuccess: false, mutate: expect.any(Function), reset: expect.any(Function), status: 'error', variables: 'todo', submittedAt: expect.any(Number), }) }) test('should be able to restore a mutation', async () => { const key = queryKey() const onMutate = vi.fn() const onSuccess = vi.fn() const onSettled = vi.fn() queryClient.setMutationDefaults(key, { mutationFn: (text: string) => sleep(10).then(() => text), onMutate, onSuccess, onSettled, }) const mutation = queryClient .getMutationCache() .build( queryClient, { mutationKey: key, }, { context: 'todo', data: undefined, error: null, failureCount: 1, failureReason: 'err', isPaused: true, status: 'pending', variables: 'todo', submittedAt: 1, }, ) expect(mutation.state).toEqual({ context: 'todo', data: undefined, error: null, failureCount: 1, failureReason: 'err', isPaused: true, status: 'pending', variables: 'todo', submittedAt: 1, }) void queryClient.resumePausedMutations() // check that the mutation is correctly resumed expect(mutation.state).toEqual({ context: 'todo', data: undefined, error: null, failureCount: 1, failureReason: 'err', isPaused: false, status: 'pending', variables: 'todo', submittedAt: 1, }) await vi.advanceTimersByTimeAsync(10) expect(mutation.state).toEqual({ context: 'todo', data: 'todo', error: null, failureCount: 0, failureReason: null, isPaused: false, status: 'success', variables: 'todo', submittedAt: 1, }) expect(onMutate).not.toHaveBeenCalled() expect(onSuccess).toHaveBeenCalled() expect(onSettled).toHaveBeenCalled() }) test('addObserver should not add an existing observer', () => { const mutationCache = queryClient.getMutationCache() const observer = new MutationObserver(queryClient, {}) const currentMutation = mutationCache.build(queryClient, {}) const fn = vi.fn() const unsubscribe = mutationCache.subscribe((event) => { fn(event.type) }) currentMutation.addObserver(observer) currentMutation.addObserver(observer) expect(fn).toHaveBeenCalledTimes(1) expect(fn).toHaveBeenCalledWith('observerAdded') unsubscribe() }) test('mutate should throw an error if no mutationFn found', async () => { const mutation = new MutationObserver(queryClient, { mutationFn: undefined, retry: false, }) let error: any try { await mutation.mutate() } catch (err) { error = err } expect(error).toEqual(new Error('No mutationFn found')) }) test('mutate update the mutation state even without an active subscription 1', async () => { const onSuccess = vi.fn() const onSettled = vi.fn() const mutation = new MutationObserver(queryClient, { mutationFn: () => { return Promise.resolve('update') }, }) mutation.mutate(undefined, { onSuccess, onSettled }) await vi.advanceTimersByTimeAsync(0) expect(mutation.getCurrentResult().data).toEqual('update') expect(onSuccess).not.toHaveBeenCalled() expect(onSettled).not.toHaveBeenCalled() }) test('mutate update the mutation state even without an active subscription 2', async () => { const onSuccess = vi.fn() const onSettled = vi.fn() const mutation = new MutationObserver(queryClient, { mutationFn: () => { return Promise.resolve('update') }, }) mutation.mutate(undefined, { onSuccess, onSettled }) await vi.advanceTimersByTimeAsync(0) expect(mutation.getCurrentResult().data).toEqual('update') expect(onSuccess).not.toHaveBeenCalled() expect(onSettled).not.toHaveBeenCalled() }) test('mutation callbacks should see updated options', async () => { const onSuccess = vi.fn() const mutation = new MutationObserver(queryClient, { mutationFn: async () => { await sleep(100) return Promise.resolve('update') }, onSuccess: () => { onSuccess(1) }, }) void mutation.mutate() mutation.setOptions({ mutationFn: async () => { await sleep(100) return Promise.resolve('update') }, onSuccess: () => { onSuccess(2) }, }) await vi.advanceTimersByTimeAsync(100) expect(onSuccess).toHaveBeenCalledTimes(1) expect(onSuccess).toHaveBeenCalledWith(2) }) describe('scoped mutations', () => { test('mutations in the same scope should run in serial', async () => { const key1 = queryKey() const key2 = queryKey() const results: Array = [] executeMutation( queryClient, { mutationKey: key1, scope: { id: 'scope', }, mutationFn: async () => { results.push('start-A') await sleep(10) results.push('finish-A') return 'a' }, }, 'vars1', ) expect( queryClient.getMutationCache().find({ mutationKey: key1 })?.state, ).toMatchObject({ status: 'pending', isPaused: false, }) executeMutation( queryClient, { mutationKey: key2, scope: { id: 'scope', }, mutationFn: async () => { results.push('start-B') await sleep(10) results.push('finish-B') return 'b' }, }, 'vars2', ) expect( queryClient.getMutationCache().find({ mutationKey: key2 })?.state, ).toMatchObject({ status: 'pending', isPaused: true, }) await vi.advanceTimersByTimeAsync(20) expect(results).toStrictEqual([ 'start-A', 'finish-A', 'start-B', 'finish-B', ]) }) }) test('mutations without scope should run in parallel', async () => { const key1 = queryKey() const key2 = queryKey() const results: Array = [] executeMutation( queryClient, { mutationKey: key1, mutationFn: async () => { results.push('start-A') await sleep(10) results.push('finish-A') return 'a' }, }, 'vars1', ) executeMutation( queryClient, { mutationKey: key2, mutationFn: async () => { results.push('start-B') await sleep(10) results.push('finish-B') return 'b' }, }, 'vars2', ) await vi.advanceTimersByTimeAsync(10) expect(results).toStrictEqual([ 'start-A', 'start-B', 'finish-A', 'finish-B', ]) }) test('each scope should run in parallel, serial within scope', async () => { const results: Array = [] executeMutation( queryClient, { scope: { id: '1', }, mutationFn: async () => { results.push('start-A1') await sleep(10) results.push('finish-A1') return 'a' }, }, 'vars1', ) executeMutation( queryClient, { scope: { id: '1', }, mutationFn: async () => { results.push('start-B1') await sleep(10) results.push('finish-B1') return 'b' }, }, 'vars2', ) executeMutation( queryClient, { scope: { id: '2', }, mutationFn: async () => { results.push('start-A2') await sleep(10) results.push('finish-A2') return 'a' }, }, 'vars1', ) executeMutation( queryClient, { scope: { id: '2', }, mutationFn: async () => { results.push('start-B2') await sleep(10) results.push('finish-B2') return 'b' }, }, 'vars2', ) await vi.advanceTimersByTimeAsync(20) expect(results).toStrictEqual([ 'start-A1', 'start-A2', 'finish-A1', 'start-B1', 'finish-A2', 'start-B2', 'finish-B1', 'finish-B2', ]) }) describe('callback return types', () => { test('should handle all sync callback patterns', async () => { const key = queryKey() const results: Array = [] executeMutation( queryClient, { mutationKey: key, mutationFn: () => Promise.resolve('success'), onMutate: () => { results.push('onMutate-sync') return { backup: 'data' } // onMutate can return a result }, onSuccess: () => { results.push('onSuccess-implicit-void') // Implicit void return }, onError: () => { results.push('onError-explicit-void') return // Explicit void return }, onSettled: () => { results.push('onSettled-return-value') return 'ignored-value' // Non-void return (should be ignored) }, }, 'vars', ) await vi.advanceTimersByTimeAsync(0) expect(results).toEqual([ 'onMutate-sync', 'onSuccess-implicit-void', 'onSettled-return-value', ]) }) test('should handle all async callback patterns', async () => { const key = queryKey() const results: Array = [] executeMutation( queryClient, { mutationKey: key, mutationFn: () => Promise.resolve('success'), onMutate: async () => { results.push('onMutate-async') await sleep(10) return { backup: 'async-data' } }, onSuccess: async () => { results.push('onSuccess-async-start') await sleep(20) results.push('onSuccess-async-end') // Implicit void return from async }, onSettled: () => { results.push('onSettled-promise') return Promise.resolve('also-ignored') // Promise (should be ignored) }, }, 'vars', ) await vi.advanceTimersByTimeAsync(30) expect(results).toEqual([ 'onMutate-async', 'onSuccess-async-start', 'onSuccess-async-end', 'onSettled-promise', ]) }) test('should handle Promise.all() and Promise.allSettled() patterns', async () => { const key = queryKey() const results: Array = [] executeMutation( queryClient, { mutationKey: key, mutationFn: () => Promise.resolve('success'), onSuccess: () => { results.push('onSuccess-start') return Promise.all([ sleep(20).then(() => results.push('invalidate-queries')), sleep(10).then(() => results.push('track-analytics')), ]) }, onSettled: () => { results.push('onSettled-start') return Promise.allSettled([ sleep(10).then(() => results.push('cleanup-1')), Promise.reject('error').catch(() => results.push('cleanup-2-failed'), ), ]) }, }, 'vars', ) await vi.advanceTimersByTimeAsync(30) expect(results).toEqual([ 'onSuccess-start', 'track-analytics', 'invalidate-queries', 'onSettled-start', 'cleanup-2-failed', 'cleanup-1', ]) }) test('should handle mixed sync/async patterns and return value isolation', async () => { const key = queryKey() const results: Array = [] const mutationPromise = executeMutation( queryClient, { mutationKey: key, mutationFn: () => Promise.resolve('actual-result'), onMutate: () => { results.push('sync-onMutate') return { rollback: 'data' } }, onSuccess: async () => { results.push('async-onSuccess') await sleep(10) return 'success-return-ignored' }, onError: () => { results.push('sync-onError') return Promise.resolve('error-return-ignored') }, onSettled: (_data, _error, _variables, onMutateResult) => { results.push(`settled-onMutateResult-${onMutateResult?.rollback}`) return Promise.all([ Promise.resolve('cleanup-1'), Promise.resolve('cleanup-2'), ]) }, }, 'vars', ) await vi.advanceTimersByTimeAsync(10) const mutationResult = await mutationPromise // Verify mutation returns its own result, not callback returns expect(mutationResult).toBe('actual-result') console.log(results) expect(results).toEqual([ 'sync-onMutate', 'async-onSuccess', 'settled-onMutateResult-data', ]) }) test('should handle error cases with all callback patterns', async () => { const key = queryKey() const results: Array = [] const newMutationError = new Error('mutation-error') let mutationError: Error | undefined executeMutation( queryClient, { mutationKey: key, mutationFn: () => Promise.reject(newMutationError), onMutate: () => { results.push('onMutate') return { backup: 'error-data' } }, onSuccess: () => { results.push('onSuccess-should-not-run') }, onError: async () => { results.push('onError-async') await sleep(10) // Test Promise.all() in error callback return Promise.all([ sleep(10).then(() => results.push('error-cleanup-1')), sleep(20).then(() => results.push('error-cleanup-2')), ]) }, onSettled: (_data, _error, _variables, onMutateResult) => { results.push(`settled-error-${onMutateResult?.backup}`) return Promise.allSettled([ Promise.resolve('settled-cleanup'), Promise.reject('settled-error'), ]) }, }, 'vars', ).catch((error) => { mutationError = error }) await vi.advanceTimersByTimeAsync(30) expect(results).toEqual([ 'onMutate', 'onError-async', 'error-cleanup-1', 'error-cleanup-2', 'settled-error-error-data', ]) expect(mutationError).toEqual(newMutationError) }) }) }) ================================================ FILE: packages/query-core/src/__tests__/notifyManager.test.tsx ================================================ import { afterEach, assertType, beforeEach, describe, expect, expectTypeOf, it, vi, } from 'vitest' import { sleep } from '@tanstack/query-test-utils' import { createNotifyManager } from '../notifyManager' describe('notifyManager', () => { beforeEach(() => { vi.useFakeTimers() }) afterEach(() => { vi.useRealTimers() }) it('should use default notifyFn', async () => { const notifyManagerTest = createNotifyManager() const callbackSpy = vi.fn() notifyManagerTest.schedule(callbackSpy) await vi.advanceTimersByTimeAsync(0) expect(callbackSpy).toHaveBeenCalled() }) it('should use default batchNotifyFn', async () => { const notifyManagerTest = createNotifyManager() const callbackScheduleSpy = vi.fn().mockImplementation(() => sleep(20)) const callbackBatchLevel2Spy = vi.fn().mockImplementation(() => { notifyManagerTest.schedule(callbackScheduleSpy) }) const callbackBatchLevel1Spy = vi.fn().mockImplementation(() => { notifyManagerTest.batch(callbackBatchLevel2Spy) }) notifyManagerTest.batch(callbackBatchLevel1Spy) await vi.advanceTimersByTimeAsync(0) expect(callbackBatchLevel1Spy).toHaveBeenCalledTimes(1) expect(callbackBatchLevel2Spy).toHaveBeenCalledTimes(1) expect(callbackScheduleSpy).toHaveBeenCalledTimes(1) }) it('should use a custom scheduler when configured', async () => { const customCallback = vi.fn((cb) => queueMicrotask(cb)) const notifyManagerTest = createNotifyManager() const notifySpy = vi.fn() notifyManagerTest.setScheduler(customCallback) notifyManagerTest.setNotifyFunction(notifySpy) notifyManagerTest.batch(() => notifyManagerTest.schedule(vi.fn)) expect(customCallback).toHaveBeenCalledOnce() await vi.advanceTimersByTimeAsync(0) expect(notifySpy).toHaveBeenCalledTimes(1) }) it('should notify if error is thrown', async () => { const notifyManagerTest = createNotifyManager() const notifySpy = vi.fn() notifyManagerTest.setNotifyFunction(notifySpy) try { notifyManagerTest.batch(() => { notifyManagerTest.schedule(vi.fn) throw new Error('Foo') }) } catch {} await vi.advanceTimersByTimeAsync(0) expect(notifySpy).toHaveBeenCalledTimes(1) }) it('typeDefs should catch proper signatures', () => { const notifyManagerTest = createNotifyManager() // we define some fn with its signature: const fn: (a: string, b: number) => string = (a, b) => a + b // now someFn expect to be called with args [a: string, b: number] const someFn = notifyManagerTest.batchCalls(fn) expectTypeOf(someFn).parameters.toEqualTypeOf>() assertType>(['im happy', 4]) assertType>([ 'im not happy', // @ts-expect-error false, ]) }) it('should use custom batch notify function', async () => { const notifyManagerTest = createNotifyManager() const batchNotifySpy = vi.fn((cb) => cb()) const callbackSpy1 = vi.fn() const callbackSpy2 = vi.fn() notifyManagerTest.setBatchNotifyFunction(batchNotifySpy) notifyManagerTest.batch(() => { notifyManagerTest.schedule(callbackSpy1) notifyManagerTest.schedule(callbackSpy2) }) await vi.advanceTimersByTimeAsync(0) expect(batchNotifySpy).toHaveBeenCalled() expect(callbackSpy1).toHaveBeenCalled() expect(callbackSpy2).toHaveBeenCalled() }) it('should batch calls correctly', async () => { const notifyManagerTest = createNotifyManager() const callbackSpy = vi.fn() const batchedFn = notifyManagerTest.batchCalls((a: number, b: string) => { callbackSpy(a, b) }) batchedFn(1, 'test') await vi.advanceTimersByTimeAsync(0) expect(callbackSpy).toHaveBeenCalledWith(1, 'test') }) }) ================================================ FILE: packages/query-core/src/__tests__/OmitKeyof.test-d.ts ================================================ import { describe, expectTypeOf, it } from 'vitest' import type { OmitKeyof } from '..' describe('OmitKeyof', () => { it("'s string key type check", () => { type A = { x: string y: number } type ExpectedType = { x: string } // Bad point // 1. original Omit can use 'z' as type parameter with no type error // 2. original Omit have no auto complete for 2nd type parameter expectTypeOf>().toEqualTypeOf() // Solution // 1. strictly expectTypeOf< OmitKeyof< A, // OmitKeyof can't use 'z' as type parameter with type error because A don't have key 'z' // @ts-expect-error Type does not satisfy the constraint keyof A 'z' | 'y' > >().toEqualTypeOf() expectTypeOf< OmitKeyof< A, // OmitKeyof can't use 'z' as type parameter with type error because A don't have key 'z' // @ts-expect-error Type does not satisfy the constraint keyof A 'z' | 'y', 'strictly' > >().toEqualTypeOf() // 2. safely expectTypeOf< OmitKeyof< A, // OmitKeyof can't use 'z' as type parameter type error with strictly parameter or default parameter // @ts-expect-error Type does not satisfy the constraint keyof A 'z' | 'y' > >().toEqualTypeOf() expectTypeOf< OmitKeyof< A, // With 'safely', OmitKeyof can use 'z' as type parameter like original Omit but This support autocomplete too yet for DX. 'z' | 'y', 'safely' > >().toEqualTypeOf() }) it("'s number key type check", () => { type A = { [1]: string [2]: number } type ExpectedType = { [1]: string } // Bad point // 1. original Omit can use 3 as type parameter with no type error // 2. original Omit have no auto complete for 2nd type parameter expectTypeOf>().toEqualTypeOf() // Solution // 1. strictly expectTypeOf< OmitKeyof< A, // OmitKeyof can't use 3 as type parameter with type error because A don't have key 3 // @ts-expect-error Type does not satisfy the constraint keyof A 3 | 2 > >().toEqualTypeOf() expectTypeOf< OmitKeyof< A, // OmitKeyof can't use 3 as type parameter with type error because A don't have key 3 // @ts-expect-error Type does not satisfy the constraint keyof A 3 | 2, 'strictly' > >().toEqualTypeOf() // 2. safely expectTypeOf< OmitKeyof< A, // OmitKeyof can't use 3 as type parameter type error with strictly parameter or default parameter // @ts-expect-error Type does not satisfy the constraint keyof A 3 | 2 > >().toEqualTypeOf() expectTypeOf< OmitKeyof< A, // With 'safely', OmitKeyof can use 3 as type parameter like original Omit but This support autocomplete too yet for DX. 3 | 2, 'safely' > >().toEqualTypeOf() }) it("'s symbol key type check", () => { const symbol1 = Symbol() const symbol2 = Symbol() const symbol3 = Symbol() type A = { [symbol1]: string [symbol2]: number } type ExpectedType = { [symbol1]: string } // Bad point // 1. original Omit can use symbol3 as type parameter with no type error // 2. original Omit have no auto complete for 2nd type parameter expectTypeOf< Omit >().toEqualTypeOf() // Solution // 1. strictly expectTypeOf< OmitKeyof< A, // OmitKeyof can't use symbol3 as type parameter with type error because A don't have key symbol3 // @ts-expect-error Type does not satisfy the constraint keyof A typeof symbol3 | typeof symbol2 > >().toEqualTypeOf() expectTypeOf< OmitKeyof< A, // OmitKeyof can't use symbol3 as type parameter with type error because A don't have key symbol3 // @ts-expect-error Type does not satisfy the constraint keyof A typeof symbol3 | typeof symbol2, 'strictly' > >().toEqualTypeOf() // 2. safely expectTypeOf< OmitKeyof< A, // OmitKeyof can't use symbol3 as type parameter type error with strictly parameter or default parameter // @ts-expect-error Type does not satisfy the constraint keyof A typeof symbol3 | typeof symbol2 > >().toEqualTypeOf() expectTypeOf< OmitKeyof< A, // With 'safely', OmitKeyof can use symbol3 as type parameter like original Omit but This support autocomplete too yet for DX. typeof symbol3 | typeof symbol2, 'safely' > >().toEqualTypeOf() }) }) ================================================ FILE: packages/query-core/src/__tests__/onlineManager.test.tsx ================================================ import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest' import { OnlineManager } from '../onlineManager' import { setIsServer } from './utils' describe('onlineManager', () => { let onlineManager: OnlineManager beforeEach(() => { vi.useFakeTimers() onlineManager = new OnlineManager() }) afterEach(() => { vi.useRealTimers() }) test('isOnline should return true if navigator is undefined', () => { const navigatorSpy = vi.spyOn(globalThis, 'navigator', 'get') // Force navigator to be undefined // @ts-expect-error navigatorSpy.mockImplementation(() => undefined) expect(onlineManager.isOnline()).toBeTruthy() navigatorSpy.mockRestore() }) test('isOnline should return true if navigator.onLine is true', () => { const navigatorSpy = vi.spyOn(navigator, 'onLine', 'get') navigatorSpy.mockImplementation(() => true) expect(onlineManager.isOnline()).toBeTruthy() navigatorSpy.mockRestore() }) test('setEventListener should use online boolean arg', () => { let count = 0 const setup = (setOnline: (online: boolean) => void) => { setTimeout(() => { count++ setOnline(false) }, 20) return () => void 0 } onlineManager.setEventListener(setup) vi.advanceTimersByTime(20) expect(count).toEqual(1) expect(onlineManager.isOnline()).toBeFalsy() }) test('setEventListener should call previous remove handler when replacing an event listener', () => { const remove1Spy = vi.fn() const remove2Spy = vi.fn() onlineManager.setEventListener(() => remove1Spy) onlineManager.setEventListener(() => remove2Spy) expect(remove1Spy).toHaveBeenCalledTimes(1) expect(remove2Spy).not.toHaveBeenCalled() }) test('cleanup (removeEventListener) should not be called if window is not defined', () => { const restoreIsServer = setIsServer(true) const removeEventListenerSpy = vi.spyOn(globalThis, 'removeEventListener') const unsubscribe = onlineManager.subscribe(() => undefined) unsubscribe() expect(removeEventListenerSpy).not.toHaveBeenCalled() restoreIsServer() }) test('cleanup (removeEventListener) should not be called if window.addEventListener is not defined', () => { const { addEventListener } = globalThis.window // @ts-expect-error globalThis.window.addEventListener = undefined const removeEventListenerSpy = vi.spyOn(globalThis, 'removeEventListener') const unsubscribe = onlineManager.subscribe(() => undefined) unsubscribe() expect(removeEventListenerSpy).not.toHaveBeenCalled() globalThis.window.addEventListener = addEventListener }) test('it should replace default window listener when a new event listener is set', () => { const addEventListenerSpy = vi.spyOn(globalThis.window, 'addEventListener') const removeEventListenerSpy = vi.spyOn( globalThis.window, 'removeEventListener', ) // Should set the default event listener with window event listeners const unsubscribe = onlineManager.subscribe(() => undefined) expect(addEventListenerSpy).toHaveBeenCalledTimes(2) // Should replace the window default event listener by a new one // and it should call window.removeEventListener twice onlineManager.setEventListener(() => { return () => void 0 }) expect(removeEventListenerSpy).toHaveBeenCalledTimes(2) unsubscribe() addEventListenerSpy.mockRestore() removeEventListenerSpy.mockRestore() }) test('should call removeEventListener when last listener unsubscribes', () => { const addEventListenerSpy = vi.spyOn(globalThis.window, 'addEventListener') const removeEventListenerSpy = vi.spyOn( globalThis.window, 'removeEventListener', ) const unsubscribe1 = onlineManager.subscribe(() => undefined) const unsubscribe2 = onlineManager.subscribe(() => undefined) expect(addEventListenerSpy).toHaveBeenCalledTimes(2) // online + offline unsubscribe1() expect(removeEventListenerSpy).toHaveBeenCalledTimes(0) unsubscribe2() expect(removeEventListenerSpy).toHaveBeenCalledTimes(2) // online + offline }) test('should keep setup function even if last listener unsubscribes', () => { const setupSpy = vi.fn().mockImplementation(() => () => undefined) onlineManager.setEventListener(setupSpy) const unsubscribe1 = onlineManager.subscribe(() => undefined) expect(setupSpy).toHaveBeenCalledTimes(1) unsubscribe1() const unsubscribe2 = onlineManager.subscribe(() => undefined) expect(setupSpy).toHaveBeenCalledTimes(2) unsubscribe2() }) test('should call listeners when setOnline is called', () => { const listener = vi.fn() onlineManager.subscribe(listener) onlineManager.setOnline(false) onlineManager.setOnline(false) expect(listener).toHaveBeenNthCalledWith(1, false) onlineManager.setOnline(true) onlineManager.setOnline(true) expect(listener).toHaveBeenCalledTimes(2) expect(listener).toHaveBeenNthCalledWith(2, true) }) }) ================================================ FILE: packages/query-core/src/__tests__/queriesObserver.test.tsx ================================================ import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest' import { queryKey, sleep } from '@tanstack/query-test-utils' import { QueriesObserver, QueryClient } from '..' import type { QueryObserverResult } from '..' describe('queriesObserver', () => { let queryClient: QueryClient beforeEach(() => { vi.useFakeTimers() queryClient = new QueryClient() queryClient.mount() }) afterEach(() => { queryClient.clear() vi.useRealTimers() }) test('should return an array with all query results', async () => { const key1 = queryKey() const key2 = queryKey() const queryFn1 = vi.fn().mockReturnValue(1) const queryFn2 = vi.fn().mockReturnValue(2) const observer = new QueriesObserver(queryClient, [ { queryKey: key1, queryFn: queryFn1 }, { queryKey: key2, queryFn: queryFn2 }, ]) let observerResult const unsubscribe = observer.subscribe((result) => { observerResult = result }) await vi.advanceTimersByTimeAsync(0) unsubscribe() expect(observerResult).toMatchObject([{ data: 1 }, { data: 2 }]) }) test('should update when a query updates', async () => { const key1 = queryKey() const key2 = queryKey() const queryFn1 = vi.fn().mockReturnValue(1) const queryFn2 = vi.fn().mockReturnValue(2) const observer = new QueriesObserver(queryClient, [ { queryKey: key1, queryFn: queryFn1 }, { queryKey: key2, queryFn: queryFn2 }, ]) const results: Array> = [] results.push(observer.getCurrentResult()) const unsubscribe = observer.subscribe((result) => { results.push(result) }) await vi.advanceTimersByTimeAsync(0) queryClient.setQueryData(key2, 3) unsubscribe() expect(results.length).toBe(6) expect(results[0]).toMatchObject([ { status: 'pending', fetchStatus: 'idle', data: undefined }, { status: 'pending', fetchStatus: 'idle', data: undefined }, ]) expect(results[1]).toMatchObject([ { status: 'pending', fetchStatus: 'fetching', data: undefined }, { status: 'pending', fetchStatus: 'idle', data: undefined }, ]) expect(results[2]).toMatchObject([ { status: 'pending', fetchStatus: 'fetching', data: undefined }, { status: 'pending', fetchStatus: 'fetching', data: undefined }, ]) expect(results[3]).toMatchObject([ { status: 'success', data: 1 }, { status: 'pending', fetchStatus: 'fetching', data: undefined }, ]) expect(results[4]).toMatchObject([ { status: 'success', data: 1 }, { status: 'success', data: 2 }, ]) expect(results[5]).toMatchObject([ { status: 'success', data: 1 }, { status: 'success', data: 3 }, ]) }) test('should update when a query is removed', async () => { const key1 = queryKey() const key2 = queryKey() const queryFn1 = vi.fn().mockReturnValue(1) const queryFn2 = vi.fn().mockReturnValue(2) const observer = new QueriesObserver(queryClient, [ { queryKey: key1, queryFn: queryFn1 }, { queryKey: key2, queryFn: queryFn2 }, ]) const results: Array> = [] results.push(observer.getCurrentResult()) const unsubscribe = observer.subscribe((result) => { results.push(result) }) await vi.advanceTimersByTimeAsync(0) observer.setQueries([{ queryKey: key2, queryFn: queryFn2 }]) const queryCache = queryClient.getQueryCache() expect(queryCache.find({ queryKey: key1, type: 'active' })).toBeUndefined() expect(queryCache.find({ queryKey: key2, type: 'active' })).toBeDefined() unsubscribe() expect(queryCache.find({ queryKey: key1, type: 'active' })).toBeUndefined() expect(queryCache.find({ queryKey: key2, type: 'active' })).toBeUndefined() expect(results.length).toBe(6) expect(results[0]).toMatchObject([ { status: 'pending', fetchStatus: 'idle', data: undefined }, { status: 'pending', fetchStatus: 'idle', data: undefined }, ]) expect(results[1]).toMatchObject([ { status: 'pending', fetchStatus: 'fetching', data: undefined }, { status: 'pending', fetchStatus: 'idle', data: undefined }, ]) expect(results[2]).toMatchObject([ { status: 'pending', fetchStatus: 'fetching', data: undefined }, { status: 'pending', fetchStatus: 'fetching', data: undefined }, ]) expect(results[3]).toMatchObject([ { status: 'success', data: 1 }, { status: 'pending', fetchStatus: 'fetching', data: undefined }, ]) expect(results[4]).toMatchObject([ { status: 'success', data: 1 }, { status: 'success', data: 2 }, ]) expect(results[5]).toMatchObject([{ status: 'success', data: 2 }]) }) test('should update when a query changed position', async () => { const key1 = queryKey() const key2 = queryKey() const queryFn1 = vi.fn().mockReturnValue(1) const queryFn2 = vi.fn().mockReturnValue(2) const observer = new QueriesObserver(queryClient, [ { queryKey: key1, queryFn: queryFn1 }, { queryKey: key2, queryFn: queryFn2 }, ]) const results: Array> = [] results.push(observer.getCurrentResult()) const unsubscribe = observer.subscribe((result) => { results.push(result) }) await vi.advanceTimersByTimeAsync(0) observer.setQueries([ { queryKey: key2, queryFn: queryFn2 }, { queryKey: key1, queryFn: queryFn1 }, ]) unsubscribe() expect(results.length).toBe(6) expect(results[0]).toMatchObject([ { status: 'pending', fetchStatus: 'idle', data: undefined }, { status: 'pending', fetchStatus: 'idle', data: undefined }, ]) expect(results[1]).toMatchObject([ { status: 'pending', fetchStatus: 'fetching', data: undefined }, { status: 'pending', fetchStatus: 'idle', data: undefined }, ]) expect(results[2]).toMatchObject([ { status: 'pending', fetchStatus: 'fetching', data: undefined }, { status: 'pending', fetchStatus: 'fetching', data: undefined }, ]) expect(results[3]).toMatchObject([ { status: 'success', data: 1 }, { status: 'pending', fetchStatus: 'fetching', data: undefined }, ]) expect(results[4]).toMatchObject([ { status: 'success', data: 1 }, { status: 'success', data: 2 }, ]) expect(results[5]).toMatchObject([ { status: 'success', data: 2 }, { status: 'success', data: 1 }, ]) }) test('should not update when nothing has changed', async () => { const key1 = queryKey() const key2 = queryKey() const queryFn1 = vi.fn().mockReturnValue(1) const queryFn2 = vi.fn().mockReturnValue(2) const observer = new QueriesObserver(queryClient, [ { queryKey: key1, queryFn: queryFn1 }, { queryKey: key2, queryFn: queryFn2 }, ]) const results: Array> = [] results.push(observer.getCurrentResult()) const unsubscribe = observer.subscribe((result) => { results.push(result) }) await vi.advanceTimersByTimeAsync(0) observer.setQueries([ { queryKey: key1, queryFn: queryFn1 }, { queryKey: key2, queryFn: queryFn2 }, ]) unsubscribe() expect(results.length).toBe(5) expect(results[0]).toMatchObject([ { status: 'pending', fetchStatus: 'idle', data: undefined }, { status: 'pending', fetchStatus: 'idle', data: undefined }, ]) expect(results[1]).toMatchObject([ { status: 'pending', fetchStatus: 'fetching', data: undefined }, { status: 'pending', fetchStatus: 'idle', data: undefined }, ]) expect(results[2]).toMatchObject([ { status: 'pending', fetchStatus: 'fetching', data: undefined }, { status: 'pending', fetchStatus: 'fetching', data: undefined }, ]) expect(results[3]).toMatchObject([ { status: 'success', data: 1 }, { status: 'pending', fetchStatus: 'fetching', data: undefined }, ]) expect(results[4]).toMatchObject([ { status: 'success', data: 1 }, { status: 'success', data: 2 }, ]) }) test('should trigger all fetches when subscribed', () => { const key1 = queryKey() const key2 = queryKey() const queryFn1 = vi.fn().mockReturnValue(1) const queryFn2 = vi.fn().mockReturnValue(2) const observer = new QueriesObserver(queryClient, [ { queryKey: key1, queryFn: queryFn1 }, { queryKey: key2, queryFn: queryFn2 }, ]) const unsubscribe = observer.subscribe(() => undefined) unsubscribe() expect(queryFn1).toHaveBeenCalledTimes(1) expect(queryFn2).toHaveBeenCalledTimes(1) }) test('should not destroy the observer if there is still a subscription', async () => { const key1 = queryKey() const observer = new QueriesObserver(queryClient, [ { queryKey: key1, queryFn: async () => { await sleep(20) return 1 }, }, ]) const subscription1Handler = vi.fn() const subscription2Handler = vi.fn() const unsubscribe1 = observer.subscribe(subscription1Handler) const unsubscribe2 = observer.subscribe(subscription2Handler) unsubscribe1() await vi.advanceTimersByTimeAsync(20) // 1 call: pending expect(subscription1Handler).toBeCalledTimes(1) // 1 call: success expect(subscription2Handler).toBeCalledTimes(1) // Clean-up unsubscribe2() }) test('should handle duplicate query keys in different positions', async () => { const key1 = queryKey() const key2 = queryKey() const queryFn1 = vi.fn().mockReturnValue(1) const queryFn2 = vi.fn().mockReturnValue(2) const observer = new QueriesObserver(queryClient, [ { queryKey: key1, queryFn: queryFn1 }, { queryKey: key2, queryFn: queryFn2 }, { queryKey: key1, queryFn: queryFn1 }, ]) const results: Array> = [] results.push( observer.getOptimisticResult( [ { queryKey: key1, queryFn: queryFn1 }, { queryKey: key2, queryFn: queryFn2 }, { queryKey: key1, queryFn: queryFn1 }, ], undefined, )[0], ) const unsubscribe = observer.subscribe((result) => { results.push(result) }) await vi.advanceTimersByTimeAsync(0) unsubscribe() expect(results.length).toBe(6) expect(results[0]).toMatchObject([ { status: 'pending', fetchStatus: 'idle', data: undefined }, { status: 'pending', fetchStatus: 'idle', data: undefined }, { status: 'pending', fetchStatus: 'idle', data: undefined }, ]) expect(results[1]).toMatchObject([ { status: 'pending', fetchStatus: 'fetching', data: undefined }, { status: 'pending', fetchStatus: 'idle', data: undefined }, { status: 'pending', fetchStatus: 'idle', data: undefined }, ]) expect(results[2]).toMatchObject([ { status: 'pending', fetchStatus: 'fetching', data: undefined }, { status: 'pending', fetchStatus: 'fetching', data: undefined }, { status: 'pending', fetchStatus: 'idle', data: undefined }, ]) expect(results[3]).toMatchObject([ { status: 'success', fetchStatus: 'idle', data: 1 }, { status: 'pending', fetchStatus: 'fetching', data: undefined }, { status: 'pending', fetchStatus: 'idle', data: undefined }, ]) expect(results[4]).toMatchObject([ { status: 'success', fetchStatus: 'idle', data: 1 }, { status: 'pending', fetchStatus: 'fetching', data: undefined }, { status: 'success', fetchStatus: 'idle', data: 1 }, ]) expect(results[5]).toMatchObject([ { status: 'success', fetchStatus: 'idle', data: 1 }, { status: 'success', fetchStatus: 'idle', data: 2 }, { status: 'success', fetchStatus: 'idle', data: 1 }, ]) // Verify that queryFn1 was only called once despite being used twice expect(queryFn1).toHaveBeenCalledTimes(1) expect(queryFn2).toHaveBeenCalledTimes(1) }) test('should notify when results change during early return', async () => { const key1 = queryKey() const key2 = queryKey() const queryFn1 = vi.fn().mockReturnValue(1) const queryFn2 = vi.fn().mockReturnValue(2) queryClient.setQueryData(key1, 1) queryClient.setQueryData(key2, 2) const observer = new QueriesObserver(queryClient, [ { queryKey: key1, queryFn: queryFn1 }, { queryKey: key2, queryFn: queryFn2 }, ]) const results: Array> = [] results.push(observer.getCurrentResult()) const onUpdate = vi.fn((result: Array) => { results.push(result) }) const unsubscribe = observer.subscribe(onUpdate) const baseline = results.length observer.setQueries([ { queryKey: key1, queryFn: queryFn1, select: (d: any) => d + 100, }, { queryKey: key2, queryFn: queryFn2, select: (d: any) => d + 100, }, ]) await vi.advanceTimersByTimeAsync(0) unsubscribe() expect(results.length).toBeGreaterThan(baseline) expect(results[results.length - 1]).toMatchObject([ { status: 'success', data: 101 }, { status: 'success', data: 102 }, ]) }) }) ================================================ FILE: packages/query-core/src/__tests__/query.test.tsx ================================================ import { afterEach, beforeEach, describe, expect, it, test, vi } from 'vitest' import { mockVisibilityState, queryKey, sleep, } from '@tanstack/query-test-utils' import { CancelledError, Query, QueryClient, QueryObserver, dehydrate, hydrate, } from '..' import { hashQueryKeyByOptions } from '../utils' import { mockOnlineManagerIsOnline, setIsServer } from './utils' import type { QueryCache, QueryFunctionContext, QueryKey, QueryObserverResult, } from '..' describe('query', () => { let queryClient: QueryClient let queryCache: QueryCache beforeEach(() => { vi.useFakeTimers() queryClient = new QueryClient() queryCache = queryClient.getQueryCache() queryClient.mount() }) afterEach(() => { queryClient.clear() vi.useRealTimers() }) test('should use the longest garbage collection time it has seen', async () => { const key = queryKey() await queryClient.prefetchQuery({ queryKey: key, queryFn: () => 'data', gcTime: 100, }) await queryClient.prefetchQuery({ queryKey: key, queryFn: () => 'data', gcTime: 200, }) await queryClient.prefetchQuery({ queryKey: key, queryFn: () => 'data', gcTime: 10, }) const query = queryCache.find({ queryKey: key })! expect(query.gcTime).toBe(200) }) it('should continue retry after focus regain and resolve all promises', async () => { const key = queryKey() // make page unfocused const visibilityMock = mockVisibilityState('hidden') let count = 0 let result const promise = queryClient.fetchQuery({ queryKey: key, queryFn: () => { count++ if (count === 3) { return `data${count}` } throw new Error(`error${count}`) }, retry: 3, retryDelay: 1, }) promise.then((data) => { result = data }) // Check if we do not have a result expect(result).toBeUndefined() // Check if the query is really paused await vi.advanceTimersByTimeAsync(50) expect(result).toBeUndefined() // Reset visibilityState to original value visibilityMock.mockRestore() window.dispatchEvent(new Event('visibilitychange')) // There should not be a result yet expect(result).toBeUndefined() // By now we should have a value await vi.advanceTimersByTimeAsync(50) expect(result).toBe('data3') }) it('should continue retry after reconnect and resolve all promises', async () => { const key = queryKey() const onlineMock = mockOnlineManagerIsOnline(false) let count = 0 let result const promise = queryClient.fetchQuery({ queryKey: key, queryFn: () => { count++ if (count === 3) { return `data${count}` } throw new Error(`error${count}`) }, retry: 3, retryDelay: 1, }) promise.then((data) => { result = data }) // Check if we do not have a result expect(result).toBeUndefined() // Check if the query is really paused await vi.advanceTimersByTimeAsync(1) expect(result).toBeUndefined() // Reset navigator to original value onlineMock.mockReturnValue(true) // trigger online event queryClient.getQueryCache().onOnline() // There should not be a result yet expect(result).toBeUndefined() // Promise should eventually be resolved await vi.advanceTimersByTimeAsync(2) expect(result).toBe('data3') onlineMock.mockRestore() }) it('should throw a CancelledError when a paused query is cancelled', async () => { const key = queryKey() // make page unfocused const visibilityMock = mockVisibilityState('hidden') let count = 0 let result: unknown const promise = queryClient.fetchQuery({ queryKey: key, queryFn: (): Promise => { count++ throw new Error(`error${count}`) }, retry: 3, retryDelay: 1, }) promise.catch((data) => { result = data }) const query = queryCache.find({ queryKey: key })! // Check if the query is really paused await vi.advanceTimersByTimeAsync(50) expect(result).toBeUndefined() // Cancel query query.cancel() // Check if the error is set to the cancelled error try { await promise expect.unreachable() } catch { expect(result).toBeInstanceOf(CancelledError) } finally { // Reset visibilityState to original value visibilityMock.mockRestore() } }) test('should not throw a CancelledError when fetchQuery is in progress and the last observer unsubscribes when AbortSignal is consumed', async () => { const key = queryKey() const observer = new QueryObserver(queryClient, { queryKey: key, queryFn: async () => { await sleep(100) return 'data' }, }) const unsubscribe = observer.subscribe(() => undefined) await vi.advanceTimersByTimeAsync(100) expect(queryCache.find({ queryKey: key })?.state.data).toBe('data') const promise = queryClient.fetchQuery({ queryKey: key, queryFn: async ({ signal }) => { await sleep(100) return 'data2' + String(signal) }, }) // Ensure the fetch is in progress await vi.advanceTimersByTimeAsync(10) // Unsubscribe while fetch is in progress unsubscribe() // await queryClient.cancelQueries() await vi.advanceTimersByTimeAsync(90) // Fetch should complete successfully without throwing a CancelledError await expect(promise).resolves.toBe('data') expect(queryCache.find({ queryKey: key })?.state.data).toBe('data') }) test('should provide context to queryFn', () => { const key = queryKey() const queryFn = vi .fn< ( context: QueryFunctionContext>, ) => Promise<'data'> >() .mockResolvedValue('data') queryClient.prefetchQuery({ queryKey: key, queryFn }) expect(queryFn).toHaveBeenCalledTimes(1) const args = queryFn.mock.calls[0]![0] expect(args).toBeDefined() expect(args.pageParam).toBeUndefined() expect(args.queryKey).toEqual(key) expect(args.signal).toBeInstanceOf(AbortSignal) expect(args.client).toEqual(queryClient) }) test('should continue if cancellation is not supported and signal is not consumed', async () => { const key = queryKey() queryClient.prefetchQuery({ queryKey: key, queryFn: () => sleep(100).then(() => 'data'), }) await vi.advanceTimersByTimeAsync(10) // Subscribe and unsubscribe to simulate cancellation because the last observer unsubscribed const observer = new QueryObserver(queryClient, { queryKey: key, enabled: false, }) const unsubscribe = observer.subscribe(() => undefined) unsubscribe() await vi.advanceTimersByTimeAsync(90) const query = queryCache.find({ queryKey: key })! expect(query.state).toMatchObject({ data: 'data', status: 'success', dataUpdateCount: 1, }) }) test('should not continue when last observer unsubscribed if the signal was consumed', async () => { const key = queryKey() queryClient.prefetchQuery({ queryKey: key, queryFn: async ({ signal }) => { await sleep(100) return signal.aborted ? 'aborted' : 'data' }, }) await vi.advanceTimersByTimeAsync(10) // Subscribe and unsubscribe to simulate cancellation because the last observer unsubscribed const observer = new QueryObserver(queryClient, { queryKey: key, enabled: false, }) const unsubscribe = observer.subscribe(() => undefined) unsubscribe() await vi.advanceTimersByTimeAsync(90) const query = queryCache.find({ queryKey: key })! expect(query.state).toMatchObject({ data: undefined, status: 'pending', fetchStatus: 'idle', }) }) test('should provide an AbortSignal to the queryFn that provides info about the cancellation state', async () => { const key = queryKey() const queryFn = vi.fn< ( context: QueryFunctionContext>, ) => Promise >() const onAbort = vi.fn() const abortListener = vi.fn() let error queryFn.mockImplementation(async ({ signal }) => { signal.onabort = onAbort signal.addEventListener('abort', abortListener) await sleep(10) signal.onabort = null signal.removeEventListener('abort', abortListener) throw new Error() }) const promise = queryClient.fetchQuery({ queryKey: key, queryFn, retry: 3, retryDelay: 10, }) promise.catch((e) => { error = e }) const query = queryCache.find({ queryKey: key })! expect(queryFn).toHaveBeenCalledTimes(1) const signal = queryFn.mock.calls[0]![0].signal expect(signal.aborted).toBe(false) expect(onAbort).not.toHaveBeenCalled() expect(abortListener).not.toHaveBeenCalled() query.cancel() await vi.advanceTimersByTimeAsync(100) expect(signal.aborted).toBe(true) expect(onAbort).toHaveBeenCalledTimes(1) expect(abortListener).toHaveBeenCalledTimes(1) expect(error).toBeInstanceOf(CancelledError) }) test('should not continue if explicitly cancelled', async () => { const key = queryKey() const queryFn = vi.fn<(...args: Array) => unknown>() queryFn.mockImplementation(async () => { await sleep(10) throw new Error() }) let error const promise = queryClient.fetchQuery({ queryKey: key, queryFn, retry: 3, retryDelay: 10, }) promise.catch((e) => { error = e }) const query = queryCache.find({ queryKey: key })! query.cancel() await vi.advanceTimersByTimeAsync(100) expect(queryFn).toHaveBeenCalledTimes(1) expect(error).toBeInstanceOf(CancelledError) }) test('should not error if reset while pending', async () => { const key = queryKey() const queryFn = vi.fn<(...args: Array) => unknown>() queryFn.mockImplementation(async () => { await sleep(10) throw new Error() }) let error queryClient .fetchQuery({ queryKey: key, queryFn, retry: 3, retryDelay: 10, }) .catch((e) => { error = e }) // Ensure the query is pending const query = queryCache.find({ queryKey: key })! expect(query.state.status).toBe('pending') // Reset the query while it is pending query.reset() await vi.advanceTimersByTimeAsync(100) // The query should expect(queryFn).toHaveBeenCalledTimes(1) // have been called, expect(query.state.error).toBe(null) // not have an error, and expect(query.state.fetchStatus).toBe('idle') // not be loading any longer expect(query.state.data).toBe(undefined) // have no data // the call to fetchQuery must reject // because it was reset and not reverted // so it would resolve with undefined otherwise expect(error).toBeInstanceOf(CancelledError) }) test('should reset to default state when created from hydration', async () => { const client = new QueryClient() await client.prefetchQuery({ queryKey: ['string'], queryFn: () => Promise.resolve('string'), }) const dehydrated = dehydrate(client) const hydrationClient = new QueryClient() hydrate(hydrationClient, dehydrated) expect(hydrationClient.getQueryData(['string'])).toBe('string') const query = hydrationClient.getQueryCache().find({ queryKey: ['string'] }) query?.reset() expect(hydrationClient.getQueryData(['string'])).toBe(undefined) }) test('should be able to refetch a cancelled query', async () => { const key = queryKey() const queryFn = vi.fn<(...args: Array) => unknown>() queryFn.mockImplementation(() => sleep(50).then(() => 'data')) queryClient.prefetchQuery({ queryKey: key, queryFn }) const query = queryCache.find({ queryKey: key })! await vi.advanceTimersByTimeAsync(10) query.cancel() await vi.advanceTimersByTimeAsync(100) expect(queryFn).toHaveBeenCalledTimes(1) expect(query.state.error).toBeInstanceOf(CancelledError) const result = query.fetch() await vi.advanceTimersByTimeAsync(50) await expect(result).resolves.toBe('data') expect(query.state.error).toBe(null) expect(queryFn).toHaveBeenCalledTimes(2) }) test('cancelling a resolved query should not have any effect', async () => { const key = queryKey() await queryClient.prefetchQuery({ queryKey: key, queryFn: () => 'data', }) const query = queryCache.find({ queryKey: key })! query.cancel() await vi.advanceTimersByTimeAsync(10) expect(query.state.data).toBe('data') }) test('cancelling a rejected query should not have any effect', async () => { const key = queryKey() const error = new Error('error') await queryClient.prefetchQuery({ queryKey: key, queryFn: () => Promise.reject(error), }) const query = queryCache.find({ queryKey: key })! query.cancel() await vi.advanceTimersByTimeAsync(10) expect(query.state.error).toBe(error) expect(query.state.error).not.toBeInstanceOf(CancelledError) }) test('the previous query status should be kept when refetching', async () => { const key = queryKey() await queryClient.prefetchQuery({ queryKey: key, queryFn: () => 'data' }) const query = queryCache.find({ queryKey: key })! expect(query.state.status).toBe('success') await queryClient.prefetchQuery({ queryKey: key, queryFn: () => Promise.reject('reject'), retry: false, }) expect(query.state.status).toBe('error') queryClient.prefetchQuery({ queryKey: key, queryFn: () => sleep(10).then(() => Promise.reject('reject')), retry: false, }) expect(query.state.status).toBe('error') await vi.advanceTimersByTimeAsync(10) expect(query.state.status).toBe('error') }) test('queries with gcTime 0 should be removed immediately after unsubscribing', async () => { const key = queryKey() let count = 0 const observer = new QueryObserver(queryClient, { queryKey: key, queryFn: () => { count++ return 'data' }, gcTime: 0, staleTime: Infinity, }) const unsubscribe1 = observer.subscribe(() => undefined) unsubscribe1() await vi.advanceTimersByTimeAsync(0) expect(queryCache.find({ queryKey: key })).toBeUndefined() const unsubscribe2 = observer.subscribe(() => undefined) unsubscribe2() await vi.advanceTimersByTimeAsync(0) expect(queryCache.find({ queryKey: key })).toBeUndefined() expect(count).toBe(1) }) test('should be garbage collected when unsubscribed to', async () => { const key = queryKey() const observer = new QueryObserver(queryClient, { queryKey: key, queryFn: () => 'data', gcTime: 0, }) expect(queryCache.find({ queryKey: key })).toBeDefined() const unsubscribe = observer.subscribe(() => undefined) expect(queryCache.find({ queryKey: key })).toBeDefined() unsubscribe() await vi.advanceTimersByTimeAsync(0) expect(queryCache.find({ queryKey: key })).toBeUndefined() }) test('should be garbage collected later when unsubscribed and query is fetching', async () => { const key = queryKey() const observer = new QueryObserver(queryClient, { queryKey: key, queryFn: () => sleep(20).then(() => 'data'), gcTime: 10, }) const unsubscribe = observer.subscribe(() => undefined) await vi.advanceTimersByTimeAsync(20) expect(queryCache.find({ queryKey: key })).toBeDefined() observer.refetch() unsubscribe() // unsubscribe should not remove even though gcTime has elapsed b/c query is still fetching expect(queryCache.find({ queryKey: key })).toBeDefined() // should be removed after an additional staleTime wait await vi.advanceTimersByTimeAsync(30) expect(queryCache.find({ queryKey: key })).toBeUndefined() }) test('should not be garbage collected unless there are no subscribers', async () => { const key = queryKey() const observer = new QueryObserver(queryClient, { queryKey: key, queryFn: () => 'data', gcTime: 0, }) expect(queryCache.find({ queryKey: key })).toBeDefined() const unsubscribe = observer.subscribe(() => undefined) await vi.advanceTimersByTimeAsync(100) expect(queryCache.find({ queryKey: key })).toBeDefined() unsubscribe() await vi.advanceTimersByTimeAsync(100) expect(queryCache.find({ queryKey: key })).toBeUndefined() queryClient.setQueryData(key, 'data') await vi.advanceTimersByTimeAsync(100) expect(queryCache.find({ queryKey: key })).toBeDefined() }) test('should return proper count of observers', () => { const key = queryKey() const options = { queryKey: key, queryFn: () => 'data' } const observer = new QueryObserver(queryClient, options) const observer2 = new QueryObserver(queryClient, options) const observer3 = new QueryObserver(queryClient, options) const query = queryCache.find({ queryKey: key }) expect(query?.getObserversCount()).toEqual(0) const unsubscribe1 = observer.subscribe(() => undefined) const unsubscribe2 = observer2.subscribe(() => undefined) const unsubscribe3 = observer3.subscribe(() => undefined) expect(query?.getObserversCount()).toEqual(3) unsubscribe3() expect(query?.getObserversCount()).toEqual(2) unsubscribe2() expect(query?.getObserversCount()).toEqual(1) unsubscribe1() expect(query?.getObserversCount()).toEqual(0) }) test('stores meta object in query', async () => { const meta = { it: 'works', } const key = queryKey() await queryClient.prefetchQuery({ queryKey: key, queryFn: () => 'data', meta, }) const query = queryCache.find({ queryKey: key })! expect(query.meta).toBe(meta) expect(query.options.meta).toBe(meta) }) test('updates meta object on change', async () => { const meta = { it: 'works', } const key = queryKey() const queryFn = () => 'data' await queryClient.prefetchQuery({ queryKey: key, queryFn, meta }) await queryClient.prefetchQuery({ queryKey: key, queryFn, meta: undefined }) const query = queryCache.find({ queryKey: key })! expect(query.meta).toBeUndefined() expect(query.options.meta).toBeUndefined() }) test('can use default meta', async () => { const meta = { it: 'works', } const key = queryKey() const queryFn = () => 'data' queryClient.setQueryDefaults(key, { meta }) await queryClient.prefetchQuery({ queryKey: key, queryFn }) const query = queryCache.find({ queryKey: key })! expect(query.meta).toBe(meta) }) test('provides meta object inside query function', async () => { const meta = { it: 'works', } const queryFn = vi.fn(() => 'data') const key = queryKey() await queryClient.prefetchQuery({ queryKey: key, queryFn, meta }) expect(queryFn).toBeCalledWith( expect.objectContaining({ meta, }), ) }) test('should refetch the observer when online method is called', () => { const key = queryKey() const observer = new QueryObserver(queryClient, { queryKey: key, queryFn: () => 'data', }) const refetchSpy = vi.spyOn(observer, 'refetch') const unsubscribe = observer.subscribe(() => undefined) queryCache.onOnline() // Should refetch the observer expect(refetchSpy).toHaveBeenCalledTimes(1) unsubscribe() refetchSpy.mockRestore() }) test('should not add an existing observer', async () => { const key = queryKey() await queryClient.prefetchQuery({ queryKey: key, queryFn: () => 'data' }) const query = queryCache.find({ queryKey: key })! expect(query.getObserversCount()).toEqual(0) const observer = new QueryObserver(queryClient, { queryKey: key, }) expect(query.getObserversCount()).toEqual(0) query.addObserver(observer) expect(query.getObserversCount()).toEqual(1) query.addObserver(observer) expect(query.getObserversCount()).toEqual(1) }) test('should not try to remove an observer that does not exist', async () => { const key = queryKey() await queryClient.prefetchQuery({ queryKey: key, queryFn: () => 'data' }) const query = queryCache.find({ queryKey: key })! const observer = new QueryObserver(queryClient, { queryKey: key, }) expect(query.getObserversCount()).toEqual(0) const notifySpy = vi.spyOn(queryCache, 'notify') expect(() => query.removeObserver(observer)).not.toThrow() expect(notifySpy).not.toHaveBeenCalled() notifySpy.mockRestore() }) test('should not change state on invalidate() if already invalidated', async () => { const key = queryKey() await queryClient.prefetchQuery({ queryKey: key, queryFn: () => 'data' }) const query = queryCache.find({ queryKey: key })! query.invalidate() expect(query.state.isInvalidated).toBeTruthy() const previousState = query.state query.invalidate() expect(query.state).toBe(previousState) }) test('fetch should not dispatch "fetch" query is already fetching', async () => { const key = queryKey() const queryFn = () => sleep(10).then(() => 'data') const updates: Array = [] queryClient.prefetchQuery({ queryKey: key, queryFn }) await vi.advanceTimersByTimeAsync(10) const query = queryCache.find({ queryKey: key })! const unsubscribe = queryClient.getQueryCache().subscribe((event) => { updates.push(event.type) }) void query.fetch({ queryKey: key, queryFn, }) query.fetch({ queryKey: key, queryFn, }) await vi.advanceTimersByTimeAsync(10) expect(updates).toEqual([ 'updated', // type: 'fetch' 'updated', // type: 'success' ]) unsubscribe() }) test('fetch should throw an error if the queryFn is not defined', async () => { const key = queryKey() const observer = new QueryObserver(queryClient, { queryKey: key, queryFn: undefined, retry: false, }) const unsubscribe = observer.subscribe(() => undefined) await vi.advanceTimersByTimeAsync(10) const query = queryCache.find({ queryKey: key })! expect(observer.getCurrentResult()).toMatchObject({ status: 'error', error: new Error(`Missing queryFn: '${query.queryHash}'`), }) unsubscribe() }) test('fetch should dispatch an error if the queryFn returns undefined', async () => { const consoleMock = vi.spyOn(console, 'error') consoleMock.mockImplementation(() => undefined) const key = queryKey() const observer = new QueryObserver(queryClient, { queryKey: key, queryFn: () => undefined, retry: false, }) let observerResult: QueryObserverResult | undefined const unsubscribe = observer.subscribe((result) => { observerResult = result }) await vi.advanceTimersByTimeAsync(10) const error = new Error(`${JSON.stringify(key)} data is undefined`) expect(observerResult).toMatchObject({ isError: true, error, }) expect(consoleMock).toHaveBeenCalledWith( `Query data cannot be undefined. Please make sure to return a value other than undefined from your query function. Affected query key: ["${key}"]`, ) unsubscribe() consoleMock.mockRestore() }) it('should not retry on the server', async () => { const resetIsServer = setIsServer(true) const key = queryKey() let count = 0 const observer = new QueryObserver(queryClient, { queryKey: key, queryFn: () => { count++ return Promise.reject(new Error('error')) }, }) await observer.refetch() expect(count).toBe(1) resetIsServer() }) test('constructor should call initialDataUpdatedAt if defined as a function', async () => { const key = queryKey() const initialDataUpdatedAtSpy = vi.fn() await queryClient.prefetchQuery({ queryKey: key, queryFn: () => 'data', initialData: 'initial', initialDataUpdatedAt: initialDataUpdatedAtSpy, }) expect(initialDataUpdatedAtSpy).toHaveBeenCalled() }) test('should work with initialDataUpdatedAt set to zero', async () => { const key = queryKey() await queryClient.prefetchQuery({ queryKey: key, queryFn: () => 'data', staleTime: Infinity, initialData: 'initial', initialDataUpdatedAt: 0, }) expect(queryCache.find({ queryKey: key })?.state).toMatchObject({ data: 'initial', status: 'success', dataUpdatedAt: 0, }) }) test('queries should be garbage collected even if they never fetched', async () => { const key = queryKey() queryClient.setQueryDefaults(key, { gcTime: 10 }) const fn = vi.fn() const unsubscribe = queryClient.getQueryCache().subscribe(fn) queryClient.setQueryData(key, 'data') await vi.advanceTimersByTimeAsync(10) expect(fn).toHaveBeenCalledWith( expect.objectContaining({ type: 'removed', }), ) expect(queryClient.getQueryCache().findAll()).toHaveLength(0) unsubscribe() }) test('should always revert to idle state (#5968)', async () => { let mockedData = [1] const key = queryKey() const queryFn = vi .fn< ( context: QueryFunctionContext>, ) => Promise >() .mockImplementation( ({ signal }) => new Promise((resolve, reject) => { const abortListener = () => { clearTimeout(timerId) reject(signal.reason) } signal.addEventListener('abort', abortListener) const timerId = setTimeout(() => { signal.removeEventListener('abort', abortListener) resolve(mockedData.join(' - ')) }, 50) }), ) const observer = new QueryObserver(queryClient, { queryKey: key, queryFn, }) const unsubscribe = observer.subscribe(() => undefined) await vi.advanceTimersByTimeAsync(50) // let it resolve expect(observer.getCurrentResult().data).toBe('1') expect(observer.getCurrentResult().fetchStatus).toBe('idle') mockedData = [1, 2] // update "server" state in the background void queryClient.invalidateQueries({ queryKey: key }) await vi.advanceTimersByTimeAsync(5) void queryClient.invalidateQueries({ queryKey: key }) await vi.advanceTimersByTimeAsync(5) unsubscribe() // unsubscribe to simulate unmount await vi.advanceTimersByTimeAsync(5) // reverted to previous data and idle fetchStatus expect(queryCache.find({ queryKey: key })?.state).toMatchObject({ status: 'success', data: '1', fetchStatus: 'idle', }) // set up a new observer to simulate a mount of new component const newObserver = new QueryObserver(queryClient, { queryKey: key, queryFn, }) const spy = vi.fn() newObserver.subscribe(({ data }) => spy(data)) await vi.advanceTimersByTimeAsync(60) // let it resolve expect(spy).toHaveBeenCalledWith('1 - 2') }) test('should not reject a promise when silently cancelled in the background', async () => { const key = queryKey() let x = 0 queryClient.setQueryData(key, 'initial') const queryFn = vi.fn().mockImplementation(async () => { await sleep(100) return 'data' + x }) const promise = queryClient.fetchQuery({ queryKey: key, queryFn, }) await vi.advanceTimersByTimeAsync(10) expect(queryFn).toHaveBeenCalledTimes(1) x = 1 // cancel ongoing re-fetches void queryClient.refetchQueries({ queryKey: key }, { cancelRefetch: true }) await vi.advanceTimersByTimeAsync(10) // The promise should not reject await vi.waitFor(() => expect(promise).resolves.toBe('data1')) expect(queryFn).toHaveBeenCalledTimes(2) }) it('should have an error log when queryFn data is not serializable', async () => { const consoleMock = vi.spyOn(console, 'error') consoleMock.mockImplementation(() => undefined) const key = queryKey() const queryFn = vi.fn() const data: Array<{ id: number name: string link: null | { id: number; name: string; link: unknown } }> = Array.from({ length: 5 }) .fill(null) .map((_, index) => ({ id: index, name: `name-${index}`, link: null, })) if (data[0] && data[1]) { data[0].link = data[1] data[1].link = data[0] } queryFn.mockImplementation(() => sleep(10).then(() => data)) queryClient.prefetchQuery({ queryKey: key, queryFn, initialData: structuredClone(data), }) await vi.advanceTimersByTimeAsync(10) const query = queryCache.find({ queryKey: key })! expect(queryFn).toHaveBeenCalledTimes(1) expect(query.state.status).toBe('error') expect( query.state.error?.message.includes('Maximum call stack size exceeded'), ).toBeTruthy() expect(consoleMock).toHaveBeenCalledWith( expect.stringContaining( 'Structural sharing requires data to be JSON serializable', ), ) consoleMock.mockRestore() }) it('should have an error status when setData has any error inside', async () => { const key = queryKey() const queryFn = vi .fn<() => Promise>() .mockImplementation(() => sleep(10).then(() => 'data')) queryClient.prefetchQuery({ queryKey: key, queryFn, structuralSharing: () => { throw Error('Any error') }, }) const query = queryCache.find({ queryKey: key })! expect(queryFn).toHaveBeenCalledTimes(1) await vi.advanceTimersByTimeAsync(10) expect(query.state.status).toBe('error') }) test('should use persister if provided', async () => { const key = queryKey() await queryClient.prefetchQuery({ queryKey: key, queryFn: () => 'data', persister: () => Promise.resolve('persisted data'), }) const query = queryCache.find({ queryKey: key })! expect(query.state.data).toBe('persisted data') }) test('should use queryFn from observer if not provided in options', async () => { const key = queryKey() const queryFn = () => Promise.resolve('data') const observer = new QueryObserver(queryClient, { queryKey: key, queryFn: queryFn, }) const query = new Query({ client: queryClient, queryKey: key, queryHash: hashQueryKeyByOptions(key), }) query.addObserver(observer) await query.fetch() const result = await query.state.data expect(result).toBe('data') expect(query.options.queryFn).toBe(queryFn) }) test('should log error when queryKey is not an array', async () => { const consoleMock = vi.spyOn(console, 'error') const key: unknown = 'string-key' await queryClient.prefetchQuery({ queryKey: key as QueryKey, queryFn: () => 'data', }) expect(consoleMock).toHaveBeenCalledWith( "As of v4, queryKey needs to be an Array. If you are using a string like 'repoData', please change it to an Array, e.g. ['repoData']", ) consoleMock.mockRestore() }) test('should call initialData function when it is a function', () => { const key = queryKey() const initialDataFn = vi.fn(() => 'initial data') const query = new Query({ client: queryClient, queryKey: key, queryHash: hashQueryKeyByOptions(key), options: { queryFn: () => 'data', initialData: initialDataFn, }, }) expect(initialDataFn).toHaveBeenCalledTimes(1) expect(query.state.data).toBe('initial data') }) test('should update initialData when Query exists without data', async () => { const key = queryKey() const queryFn = vi.fn(async () => { await sleep(100) return 'data' }) const promise = queryClient.prefetchQuery({ queryKey: key, queryFn, staleTime: 1000, }) vi.advanceTimersByTime(50) expect(queryClient.getQueryState(key)).toMatchObject({ data: undefined, status: 'pending', fetchStatus: 'fetching', }) const observer = new QueryObserver(queryClient, { queryKey: key, queryFn, staleTime: 1000, initialData: 'initialData', initialDataUpdatedAt: 10, }) const unsubscribe = observer.subscribe(vi.fn()) expect(queryClient.getQueryState(key)).toMatchObject({ data: 'initialData', dataUpdatedAt: 10, status: 'success', fetchStatus: 'fetching', }) vi.advanceTimersByTime(50) await promise expect(queryClient.getQueryState(key)).toMatchObject({ data: 'data', status: 'success', fetchStatus: 'idle', }) expect(queryFn).toHaveBeenCalledTimes(1) unsubscribe() // resetting should get us back ot 'initialData' queryClient.getQueryCache().find({ queryKey: key })!.reset() expect(queryClient.getQueryState(key)).toMatchObject({ data: 'initialData', status: 'success', fetchStatus: 'idle', }) }) test('should not override fetching state when revert happens after new observer subscribes', async () => { const key = queryKey() let count = 0 const queryFn = vi.fn(async ({ signal: _signal }) => { // Destructure `signal` to intentionally consume it so observer-removal uses revert-cancel path await sleep(50) return 'data' + count++ }) const query = new Query({ client: queryClient, queryKey: key, queryHash: hashQueryKeyByOptions(key), options: { queryFn }, }) const observer1 = new QueryObserver(queryClient, { queryKey: key, queryFn, }) query.addObserver(observer1) const promise1 = query.fetch() await vi.advanceTimersByTimeAsync(10) query.removeObserver(observer1) const observer2 = new QueryObserver(queryClient, { queryKey: key, queryFn, }) query.addObserver(observer2) query.fetch() await expect(promise1).rejects.toBeInstanceOf(CancelledError) await vi.waitFor(() => expect(query.state.fetchStatus).toBe('idle')) expect(queryFn).toHaveBeenCalledTimes(2) expect(query.state).toMatchObject({ fetchStatus: 'idle', status: 'success', data: 'data1', }) }) }) ================================================ FILE: packages/query-core/src/__tests__/queryCache.test.tsx ================================================ import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest' import { queryKey, sleep } from '@tanstack/query-test-utils' import { QueryCache, QueryClient, QueryObserver } from '..' describe('queryCache', () => { let queryClient: QueryClient let queryCache: QueryCache beforeEach(() => { vi.useFakeTimers() queryClient = new QueryClient() queryCache = queryClient.getQueryCache() }) afterEach(() => { queryClient.clear() vi.useRealTimers() }) describe('subscribe', () => { test('should pass the correct query', () => { const key = queryKey() const subscriber = vi.fn() const unsubscribe = queryCache.subscribe(subscriber) queryClient.setQueryData(key, 'foo') const query = queryCache.find({ queryKey: key }) expect(subscriber).toHaveBeenCalledWith({ query, type: 'added' }) unsubscribe() }) test('should notify listeners when new query is added', async () => { const key = queryKey() const callback = vi.fn() queryCache.subscribe(callback) queryClient.prefetchQuery({ queryKey: key, queryFn: () => sleep(100).then(() => 'data'), }) await vi.advanceTimersByTimeAsync(100) expect(callback).toHaveBeenCalled() }) test('should notify query cache when a query becomes stale', async () => { const key = queryKey() const events: Array = [] const queries: Array = [] const unsubscribe = queryCache.subscribe((event) => { events.push(event.type) queries.push(event.query) }) const observer = new QueryObserver(queryClient, { queryKey: key, queryFn: () => 'data', staleTime: 10, }) const unsubScribeObserver = observer.subscribe(vi.fn()) await vi.advanceTimersByTimeAsync(11) expect(events.length).toBe(8) expect(events).toEqual([ 'added', // 1. Query added -> loading 'observerResultsUpdated', // 2. Observer result updated -> loading 'observerAdded', // 3. Observer added 'observerResultsUpdated', // 4. Observer result updated -> fetching 'updated', // 5. Query updated -> fetching 'observerResultsUpdated', // 6. Observer result updated -> success 'updated', // 7. Query updated -> success 'observerResultsUpdated', // 8. Observer result updated -> stale ]) queries.forEach((query) => { expect(query).toBeDefined() }) unsubscribe() unsubScribeObserver() }) test('should include the queryCache and query when notifying listeners', async () => { const key = queryKey() const callback = vi.fn() queryCache.subscribe(callback) queryClient.prefetchQuery({ queryKey: key, queryFn: () => sleep(100).then(() => 'data'), }) await vi.advanceTimersByTimeAsync(100) const query = queryCache.find({ queryKey: key }) expect(callback).toHaveBeenCalledWith({ query, type: 'added' }) }) test('should notify subscribers when new query with initialData is added', async () => { const key = queryKey() const callback = vi.fn() queryCache.subscribe(callback) queryClient.prefetchQuery({ queryKey: key, queryFn: () => sleep(100).then(() => 'data'), initialData: 'initial', }) await vi.advanceTimersByTimeAsync(100) expect(callback).toHaveBeenCalled() }) test('should be able to limit cache size', async () => { const testCache = new QueryCache() const unsubscribe = testCache.subscribe((event) => { if (event.type === 'added') { if (testCache.getAll().length > 2) { testCache .findAll({ type: 'inactive', predicate: (q) => q !== event.query, }) .forEach((query) => { testCache.remove(query) }) } } }) const testClient = new QueryClient({ queryCache: testCache }) testClient.prefetchQuery({ queryKey: ['key1'], queryFn: () => sleep(100).then(() => 'data1'), }) expect(testCache.findAll().length).toBe(1) testClient.prefetchQuery({ queryKey: ['key2'], queryFn: () => sleep(100).then(() => 'data2'), }) expect(testCache.findAll().length).toBe(2) testClient.prefetchQuery({ queryKey: ['key3'], queryFn: () => sleep(100).then(() => 'data3'), }) await vi.advanceTimersByTimeAsync(100) expect(testCache.findAll().length).toBe(1) expect(testCache.findAll()[0]!.state.data).toBe('data3') unsubscribe() }) }) describe('find', () => { test('find should filter correctly', async () => { const key = queryKey() queryClient.prefetchQuery({ queryKey: key, queryFn: () => sleep(100).then(() => 'data1'), }) await vi.advanceTimersByTimeAsync(100) const query = queryCache.find({ queryKey: key })! expect(query).toBeDefined() }) test('find should filter correctly with exact set to false', async () => { const key = queryKey() queryClient.prefetchQuery({ queryKey: key, queryFn: () => sleep(100).then(() => 'data1'), }) await vi.advanceTimersByTimeAsync(100) const query = queryCache.find({ queryKey: key, exact: false })! expect(query).toBeDefined() }) }) describe('findAll', () => { test('should filter correctly', async () => { const key1 = queryKey() const key2 = queryKey() const keyFetching = queryKey() queryClient.prefetchQuery({ queryKey: key1, queryFn: () => sleep(100).then(() => 'data1'), }) queryClient.prefetchQuery({ queryKey: key2, queryFn: () => sleep(100).then(() => 'data2'), }) queryClient.prefetchQuery({ queryKey: [{ a: 'a', b: 'b' }], queryFn: () => sleep(100).then(() => 'data3'), }) queryClient.prefetchQuery({ queryKey: ['posts', 1], queryFn: () => sleep(100).then(() => 'data4'), }) await vi.advanceTimersByTimeAsync(100) queryClient.invalidateQueries({ queryKey: key2 }) const query1 = queryCache.find({ queryKey: key1 })! const query2 = queryCache.find({ queryKey: key2 })! const query3 = queryCache.find({ queryKey: [{ a: 'a', b: 'b' }] })! const query4 = queryCache.find({ queryKey: ['posts', 1] })! expect(queryCache.findAll({ queryKey: key1 })).toEqual([query1]) // wrapping in an extra array doesn't yield the same results anymore since v4 because keys need to be an array expect(queryCache.findAll({ queryKey: [key1] })).toEqual([]) expect(queryCache.findAll()).toEqual([query1, query2, query3, query4]) expect(queryCache.findAll({})).toEqual([query1, query2, query3, query4]) expect(queryCache.findAll({ queryKey: key1, type: 'inactive' })).toEqual([ query1, ]) expect(queryCache.findAll({ queryKey: key1, type: 'active' })).toEqual([]) expect(queryCache.findAll({ queryKey: key1, stale: true })).toEqual([]) expect(queryCache.findAll({ queryKey: key1, stale: false })).toEqual([ query1, ]) expect( queryCache.findAll({ queryKey: key1, stale: false, type: 'active' }), ).toEqual([]) expect( queryCache.findAll({ queryKey: key1, stale: false, type: 'inactive', }), ).toEqual([query1]) expect( queryCache.findAll({ queryKey: key1, stale: false, type: 'inactive', exact: true, }), ).toEqual([query1]) expect(queryCache.findAll({ queryKey: key2 })).toEqual([query2]) expect(queryCache.findAll({ queryKey: key2, stale: undefined })).toEqual([ query2, ]) expect(queryCache.findAll({ queryKey: key2, stale: true })).toEqual([ query2, ]) expect(queryCache.findAll({ queryKey: key2, stale: false })).toEqual([]) expect(queryCache.findAll({ queryKey: [{ b: 'b' }] })).toEqual([query3]) expect( queryCache.findAll({ queryKey: [{ a: 'a' }], exact: false }), ).toEqual([query3]) expect( queryCache.findAll({ queryKey: [{ a: 'a' }], exact: true }), ).toEqual([]) expect( queryCache.findAll({ queryKey: [{ a: 'a', b: 'b' }], exact: true }), ).toEqual([query3]) expect(queryCache.findAll({ queryKey: [{ a: 'a', b: 'b' }] })).toEqual([ query3, ]) expect( queryCache.findAll({ queryKey: [{ a: 'a', b: 'b', c: 'c' }] }), ).toEqual([]) expect( queryCache.findAll({ queryKey: [{ a: 'a' }], stale: false }), ).toEqual([query3]) expect( queryCache.findAll({ queryKey: [{ a: 'a' }], stale: true }), ).toEqual([]) expect( queryCache.findAll({ queryKey: [{ a: 'a' }], type: 'active' }), ).toEqual([]) expect( queryCache.findAll({ queryKey: [{ a: 'a' }], type: 'inactive' }), ).toEqual([query3]) expect( queryCache.findAll({ predicate: (query) => query === query3 }), ).toEqual([query3]) expect(queryCache.findAll({ queryKey: ['posts'] })).toEqual([query4]) expect(queryCache.findAll({ fetchStatus: 'idle' })).toEqual([ query1, query2, query3, query4, ]) expect( queryCache.findAll({ queryKey: key2, fetchStatus: undefined }), ).toEqual([query2]) queryClient.prefetchQuery({ queryKey: keyFetching, queryFn: () => sleep(20).then(() => 'dataFetching'), }) expect(queryCache.findAll({ fetchStatus: 'fetching' })).toEqual([ queryCache.find({ queryKey: keyFetching }), ]) await vi.advanceTimersByTimeAsync(20) expect(queryCache.findAll({ fetchStatus: 'fetching' })).toEqual([]) }) test('should return all the queries when no filters are defined', async () => { const key1 = queryKey() const key2 = queryKey() await queryClient.prefetchQuery({ queryKey: key1, queryFn: () => 'data1', }) await queryClient.prefetchQuery({ queryKey: key2, queryFn: () => 'data2', }) expect(queryCache.findAll().length).toBe(2) }) }) describe('QueryCacheConfig error callbacks', () => { test('should call onError and onSettled when a query errors', async () => { const key = queryKey() const onSuccess = vi.fn() const onSettled = vi.fn() const onError = vi.fn() const testCache = new QueryCache({ onSuccess, onError, onSettled }) const testClient = new QueryClient({ queryCache: testCache }) testClient.prefetchQuery({ queryKey: key, queryFn: () => sleep(100).then(() => Promise.reject('error')), }) await vi.advanceTimersByTimeAsync(100) const query = testCache.find({ queryKey: key }) expect(onError).toHaveBeenCalledWith('error', query) expect(onError).toHaveBeenCalledTimes(1) expect(onSuccess).not.toHaveBeenCalled() expect(onSettled).toHaveBeenCalledTimes(1) expect(onSettled).toHaveBeenCalledWith(undefined, 'error', query) }) }) describe('QueryCacheConfig success callbacks', () => { test('should call onSuccess and onSettled when a query is successful', async () => { const key = queryKey() const onSuccess = vi.fn() const onSettled = vi.fn() const onError = vi.fn() const testCache = new QueryCache({ onSuccess, onError, onSettled }) const testClient = new QueryClient({ queryCache: testCache }) testClient.prefetchQuery({ queryKey: key, queryFn: () => sleep(100).then(() => ({ data: 5 })), }) await vi.advanceTimersByTimeAsync(100) const query = testCache.find({ queryKey: key }) expect(onSuccess).toHaveBeenCalledWith({ data: 5 }, query) expect(onSuccess).toHaveBeenCalledTimes(1) expect(onError).not.toHaveBeenCalled() expect(onSettled).toHaveBeenCalledTimes(1) expect(onSettled).toHaveBeenCalledWith({ data: 5 }, null, query) }) }) describe('QueryCache.add', () => { test('should not try to add a query already added to the cache', async () => { const key = queryKey() queryClient.prefetchQuery({ queryKey: key, queryFn: () => sleep(100).then(() => 'data1'), }) await vi.advanceTimersByTimeAsync(100) const query = queryCache.findAll()[0]! const queryClone = Object.assign({}, query) queryCache.add(queryClone) expect(queryCache.getAll().length).toEqual(1) }) }) }) ================================================ FILE: packages/query-core/src/__tests__/queryClient.test-d.tsx ================================================ import { assertType, describe, expectTypeOf, it } from 'vitest' import { QueryClient } from '../queryClient' import type { MutationFilters, QueryFilters, Updater } from '../utils' import type { Mutation } from '../mutation' import type { Query, QueryState } from '../query' import type { DataTag, DefaultError, DefaultedQueryObserverOptions, EnsureQueryDataOptions, FetchInfiniteQueryOptions, InfiniteData, MutationOptions, OmitKeyof, QueryKey, QueryObserverOptions, } from '../types' describe('getQueryData', () => { it('should be typed if key is tagged', () => { const queryKey = ['key'] as DataTag, number> const queryClient = new QueryClient() const data = queryClient.getQueryData(queryKey) expectTypeOf(data).toEqualTypeOf() }) it('should infer unknown if key is not tagged', () => { const queryKey = ['key'] as const const queryClient = new QueryClient() const data = queryClient.getQueryData(queryKey) expectTypeOf(data).toEqualTypeOf() }) it('should infer passed generic if passed', () => { const queryKey = ['key'] as const const queryClient = new QueryClient() const data = queryClient.getQueryData(queryKey) expectTypeOf(data).toEqualTypeOf() }) it('should only allow Arrays to be passed', () => { assertType>([ // @ts-expect-error TS2345: Argument of type 'string' is not assignable to parameter of type 'QueryKey' { queryKey: 'key' }, ]) }) }) describe('setQueryData', () => { it('updater should be typed if key is tagged', () => { const queryKey = ['key'] as DataTag, number> const queryClient = new QueryClient() const data = queryClient.setQueryData(queryKey, (prev) => { expectTypeOf(prev).toEqualTypeOf() return prev }) expectTypeOf(data).toEqualTypeOf() }) it('value should be typed if key is tagged', () => { const queryKey = ['key'] as DataTag, number> const queryClient = new QueryClient() // @ts-expect-error value should be a number queryClient.setQueryData(queryKey, '1') // @ts-expect-error value should be a number queryClient.setQueryData(queryKey, () => '1') const data = queryClient.setQueryData(queryKey, 1) expectTypeOf(data).toEqualTypeOf() }) it('should infer unknown for updater if key is not tagged', () => { const queryKey = ['key'] as const const queryClient = new QueryClient() const data = queryClient.setQueryData(queryKey, (prev) => { expectTypeOf(prev).toEqualTypeOf() return prev }) expectTypeOf(data).toEqualTypeOf() }) it('should infer unknown for value if key is not tagged', () => { const queryKey = ['key'] as const const queryClient = new QueryClient() const data = queryClient.setQueryData(queryKey, 'foo') expectTypeOf(data).toEqualTypeOf() }) it('should infer passed generic if passed', () => { const queryKey = ['key'] as const const queryClient = new QueryClient() const data = queryClient.setQueryData(queryKey, (prev) => { expectTypeOf(prev).toEqualTypeOf() return prev }) expectTypeOf(data).toEqualTypeOf() }) it('should infer passed generic for value', () => { const queryKey = ['key'] as const const queryClient = new QueryClient() const data = queryClient.setQueryData(queryKey, 'foo') expectTypeOf(data).toEqualTypeOf() }) it('should preserve updater parameter type inference when used in functions with explicit return types', () => { const queryKey = ['key'] as DataTag, number> const queryClient = new QueryClient() // Simulate usage inside a function with explicit return type // The outer function returns 'unknown' but this shouldn't affect the updater's type inference ;(() => queryClient.setQueryData(queryKey, (data) => { expectTypeOf(data).toEqualTypeOf() return data })) satisfies () => unknown }) }) describe('getQueryState', () => { it('should be loose typed without tag', () => { const queryKey = ['key'] as const const queryClient = new QueryClient() const data = queryClient.getQueryState(queryKey) expectTypeOf(data).toEqualTypeOf | undefined>() }) it('should be typed if key is tagged', () => { const queryKey = ['key'] as DataTag, number> const queryClient = new QueryClient() const data = queryClient.getQueryState(queryKey) expectTypeOf(data).toEqualTypeOf | undefined>() }) it('should be typed including error if key is tagged', () => { type CustomError = Error & { customError: string } const queryKey = ['key'] as DataTag, number, CustomError> const queryClient = new QueryClient() const data = queryClient.getQueryState(queryKey) expectTypeOf(data).toEqualTypeOf< QueryState | undefined >() }) }) describe('fetchInfiniteQuery', () => { it('should allow passing pages', async () => { const data = await new QueryClient().fetchInfiniteQuery({ queryKey: ['key'], queryFn: () => Promise.resolve('string'), getNextPageParam: () => 1, initialPageParam: 1, pages: 5, }) expectTypeOf(data).toEqualTypeOf>() }) it('should not allow passing getNextPageParam without pages', () => { assertType>([ { queryKey: ['key'], queryFn: () => Promise.resolve('string'), initialPageParam: 1, getNextPageParam: () => 1, }, ]) }) it('should not allow passing pages without getNextPageParam', () => { assertType>([ // @ts-expect-error Property 'getNextPageParam' is missing { queryKey: ['key'], queryFn: () => Promise.resolve('string'), initialPageParam: 1, pages: 5, }, ]) }) }) describe('defaultOptions', () => { it('should have a typed QueryFunctionContext', () => { new QueryClient({ defaultOptions: { queries: { queryFn: (context) => { expectTypeOf(context).toEqualTypeOf<{ client: QueryClient queryKey: QueryKey meta: Record | undefined signal: AbortSignal pageParam?: unknown direction?: unknown }>() return Promise.resolve('data') }, }, }, }) }) }) describe('fully typed usage', () => { it('type-checks various methods with data & error included in the type', async () => { const queryClient = new QueryClient() type TData = { foo: string } type TError = DefaultError & { bar: string } // // Construct typed arguments // const queryOptions: EnsureQueryDataOptions = { queryKey: ['key'] as any, } const fetchInfiniteQueryOptions: FetchInfiniteQueryOptions = { queryKey: ['key'] as any, pages: 5, getNextPageParam: (lastPage) => { expectTypeOf(lastPage).toEqualTypeOf() return 0 }, initialPageParam: 0, } const mutationOptions: MutationOptions = {} const queryFilters: QueryFilters> = { predicate(query) { expectTypeOf(query).toEqualTypeOf< Query> >() expectTypeOf(query.state.data).toEqualTypeOf() expectTypeOf(query.state.error).toEqualTypeOf() return false }, } const queryKey = queryFilters.queryKey! const mutationFilters: MutationFilters = { predicate(mutation) { expectTypeOf(mutation).toEqualTypeOf>() expectTypeOf(mutation.state.data).toEqualTypeOf() expectTypeOf(mutation.state.error).toEqualTypeOf() return false }, } const mutationKey = mutationOptions.mutationKey! // // Method type tests // const state = queryClient.getQueryState(queryKey) expectTypeOf(state).toEqualTypeOf | undefined>() const queryData1 = queryClient.getQueryData(queryKey) expectTypeOf(queryData1).toEqualTypeOf() const queryData2 = await queryClient.ensureQueryData(queryOptions) expectTypeOf(queryData2).toEqualTypeOf() const queriesData = queryClient.getQueriesData(queryFilters) expectTypeOf(queriesData).toEqualTypeOf< Array<[ReadonlyArray, unknown]> >() const queryData3 = queryClient.setQueryData(queryKey, { foo: '' }) type SetQueryDataUpdaterArg = Parameters< typeof queryClient.setQueryData >[1] expectTypeOf().toEqualTypeOf< Updater >() expectTypeOf(queryData3).toEqualTypeOf() const queriesData2 = queryClient.setQueriesData(queryFilters, { foo: '' }) // TODO: types here are wrong and coming up undefined type SetQueriesDataUpdaterArg = Parameters< typeof queryClient.setQueriesData >[1] expectTypeOf().toEqualTypeOf< Updater >() expectTypeOf(queriesData2).toEqualTypeOf>() const queryState = queryClient.getQueryState(queryKey) expectTypeOf(queryState).toEqualTypeOf< QueryState | undefined >() const fetchedQuery = await queryClient.fetchQuery(queryOptions) expectTypeOf(fetchedQuery).toEqualTypeOf() queryClient.prefetchQuery(queryOptions) const infiniteQuery = await queryClient.fetchInfiniteQuery( fetchInfiniteQueryOptions, ) expectTypeOf(infiniteQuery).toEqualTypeOf>() const infiniteQueryData = await queryClient.ensureInfiniteQueryData( fetchInfiniteQueryOptions, ) expectTypeOf(infiniteQueryData).toEqualTypeOf< InfiniteData >() const defaultQueryOptions = queryClient.defaultQueryOptions(queryOptions) expectTypeOf(defaultQueryOptions).toEqualTypeOf< DefaultedQueryObserverOptions >() const mutationOptions2 = queryClient.defaultMutationOptions(mutationOptions) expectTypeOf(mutationOptions2).toEqualTypeOf< MutationOptions >() queryClient.setMutationDefaults(mutationKey, { onSettled(data, error, variables, context) { expectTypeOf(data).toEqualTypeOf() expectTypeOf(error).toEqualTypeOf() expectTypeOf(variables).toEqualTypeOf() expectTypeOf(context).toEqualTypeOf() }, }) const queryDefaults = queryClient.getQueryDefaults(queryKey) expectTypeOf(queryDefaults).toEqualTypeOf< OmitKeyof, 'queryKey'> >() // Voids and Untyped returns queryClient.invalidateQueries(queryFilters) queryClient.isFetching(queryFilters) queryClient.isMutating(mutationFilters) queryClient.removeQueries(queryFilters) queryClient.resetQueries(queryFilters) queryClient.cancelQueries(queryFilters) queryClient.invalidateQueries(queryFilters) queryClient.refetchQueries(queryFilters) queryClient.prefetchInfiniteQuery(fetchInfiniteQueryOptions) queryClient.setQueryDefaults(queryKey, {} as any) queryClient.getMutationDefaults(mutationKey) }) it('type-checks various methods with untyped arguments', async () => { const queryClient = new QueryClient() // // Construct typed arguments // const queryOptions: EnsureQueryDataOptions = { queryKey: ['key'] as any, } const fetchInfiniteQueryOptions: FetchInfiniteQueryOptions = { queryKey: ['key'] as any, pages: 5, getNextPageParam: (lastPage) => { expectTypeOf(lastPage).toEqualTypeOf() return 0 }, initialPageParam: 0, } const mutationOptions: MutationOptions = {} const queryFilters: QueryFilters = { predicate(query) { expectTypeOf(query).toEqualTypeOf>() expectTypeOf(query.state.data).toEqualTypeOf() expectTypeOf(query.state.error).toEqualTypeOf() return false }, } const queryKey = queryFilters.queryKey! const mutationFilters: MutationFilters = { predicate(mutation) { expectTypeOf(mutation).toEqualTypeOf() expectTypeOf(mutation.state.data).toEqualTypeOf() expectTypeOf(mutation.state.error).toEqualTypeOf() return false }, } const mutationKey = mutationOptions.mutationKey! // // Method type tests // const state = queryClient.getQueryState(queryKey) expectTypeOf(state).toEqualTypeOf< QueryState | undefined >() const queryData1 = queryClient.getQueryData(queryKey) expectTypeOf(queryData1).toEqualTypeOf() const queryData2 = await queryClient.ensureQueryData(queryOptions) expectTypeOf(queryData2).toEqualTypeOf() const queriesData = queryClient.getQueriesData(queryFilters) expectTypeOf(queriesData).toEqualTypeOf>() const queryData3 = queryClient.setQueryData(queryKey, { foo: '' }) type SetQueryDataUpdaterArg = Parameters< typeof queryClient.setQueryData >[1] expectTypeOf().toEqualTypeOf< Updater >() expectTypeOf(queryData3).toEqualTypeOf() const queriesData2 = queryClient.setQueriesData(queryFilters, { foo: '' }) // TODO: types here are wrong and coming up undefined type SetQueriesDataUpdaterArg = Parameters< typeof queryClient.setQueriesData >[1] expectTypeOf().toEqualTypeOf< Updater >() expectTypeOf(queriesData2).toEqualTypeOf>() const queryState = queryClient.getQueryState(queryKey) expectTypeOf(queryState).toEqualTypeOf< QueryState | undefined >() const fetchedQuery = await queryClient.fetchQuery(queryOptions) expectTypeOf(fetchedQuery).toEqualTypeOf() queryClient.prefetchQuery(queryOptions) const infiniteQuery = await queryClient.fetchInfiniteQuery( fetchInfiniteQueryOptions, ) expectTypeOf(infiniteQuery).toEqualTypeOf>() const infiniteQueryData = await queryClient.ensureInfiniteQueryData( fetchInfiniteQueryOptions, ) expectTypeOf(infiniteQueryData).toEqualTypeOf< InfiniteData >() const defaultQueryOptions = queryClient.defaultQueryOptions(queryOptions) expectTypeOf(defaultQueryOptions).toEqualTypeOf< DefaultedQueryObserverOptions< unknown, DefaultError, unknown, unknown, QueryKey > >() const mutationOptions2 = queryClient.defaultMutationOptions(mutationOptions) expectTypeOf(mutationOptions2).toEqualTypeOf< MutationOptions >() queryClient.setMutationDefaults(mutationKey, { onSettled(data, error, variables, context) { expectTypeOf(data).toEqualTypeOf() expectTypeOf(error).toEqualTypeOf() expectTypeOf(variables).toEqualTypeOf() expectTypeOf(context).toEqualTypeOf() }, }) const queryDefaults = queryClient.getQueryDefaults(queryKey) expectTypeOf(queryDefaults).toEqualTypeOf< OmitKeyof, 'queryKey'> >() // Voids and Untyped returns queryClient.invalidateQueries(queryFilters) queryClient.isFetching(queryFilters) queryClient.isMutating(mutationFilters) queryClient.removeQueries(queryFilters) queryClient.resetQueries(queryFilters) queryClient.cancelQueries(queryFilters) queryClient.invalidateQueries(queryFilters) queryClient.refetchQueries(queryFilters) queryClient.prefetchInfiniteQuery(fetchInfiniteQueryOptions) queryClient.setQueryDefaults(queryKey, {} as any) queryClient.getMutationDefaults(mutationKey) }) }) describe('invalidateQueries', () => { it('shows type error when queryKey is a wrong type in invalidateQueries', () => { assertType>([]) assertType>([ { queryKey: ['1'] }, ]) assertType>([ // @ts-expect-error { queryKey: '1' }, ]) }) it('needs queryKey to be an array (#8684)', () => { assertType>({ // @ts-expect-error key is not an array queryKey: { foo: true }, }) }) it('predicate should be typed if key is tagged', () => { const queryKey = ['key'] as DataTag, number> const queryClient = new QueryClient() queryClient.invalidateQueries({ queryKey, predicate: (query) => { expectTypeOf(query.state.data).toEqualTypeOf() expectTypeOf(query.queryKey).toEqualTypeOf() return true }, }) }) }) describe('cancelQueries', () => { it('predicate should be typed if key is tagged', () => { const queryKey = ['key'] as DataTag, number> const queryClient = new QueryClient() queryClient.cancelQueries({ queryKey, predicate: (query) => { expectTypeOf(query.state.data).toEqualTypeOf() expectTypeOf(query.queryKey).toEqualTypeOf() return true }, }) }) }) describe('removeQueries', () => { it('predicate should be typed if key is tagged', () => { const queryKey = ['key'] as DataTag, number> const queryClient = new QueryClient() queryClient.removeQueries({ queryKey, predicate: (query) => { expectTypeOf(query.state.data).toEqualTypeOf() expectTypeOf(query.queryKey).toEqualTypeOf() return true }, }) }) }) describe('refetchQueries', () => { it('predicate should be typed if key is tagged', () => { const queryKey = ['key'] as DataTag, number> const queryClient = new QueryClient() queryClient.refetchQueries({ queryKey, predicate: (query) => { expectTypeOf(query.state.data).toEqualTypeOf() expectTypeOf(query.queryKey).toEqualTypeOf() return true }, }) }) }) describe('resetQueries', () => { it('predicate should be typed if key is tagged', () => { const queryKey = ['key'] as DataTag, number> const queryClient = new QueryClient() queryClient.resetQueries({ queryKey, predicate: (query) => { expectTypeOf(query.state.data).toEqualTypeOf() expectTypeOf(query.queryKey).toEqualTypeOf() return true }, }) }) }) type SuccessCallback = () => unknown it('should infer types correctly with expression body arrow functions', () => { const queryKey = ['key'] as DataTag, number> const queryClient = new QueryClient() // @ts-expect-error const callbackTest: SuccessCallback = () => queryClient.setQueryData(queryKey, (data) => { expectTypeOf(data).toEqualTypeOf() return data }) }) it('should infer types correctly with block body arrow functions', () => { const queryKey = ['key'] as DataTag, number> const queryClient = new QueryClient() // @ts-expect-error const callbackTest2: SuccessCallback = () => { queryClient.setQueryData(queryKey, (data) => { expectTypeOf(data).toEqualTypeOf() return data }) } }) ================================================ FILE: packages/query-core/src/__tests__/queryClient.test.tsx ================================================ import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest' import { queryKey, sleep } from '@tanstack/query-test-utils' import { CancelledError, MutationObserver, QueryClient, QueryObserver, dehydrate, focusManager, hydrate, onlineManager, skipToken, } from '..' import { mockOnlineManagerIsOnline } from './utils' import type { QueryCache, QueryFunction, QueryObserverOptions } from '..' describe('queryClient', () => { let queryClient: QueryClient let queryCache: QueryCache beforeEach(() => { vi.useFakeTimers() queryClient = new QueryClient() queryCache = queryClient.getQueryCache() queryClient.mount() }) afterEach(() => { queryClient.clear() queryClient.unmount() vi.useRealTimers() }) describe('defaultOptions', () => { test('should merge defaultOptions', () => { const key = queryKey() const queryFn = () => 'data' const testClient = new QueryClient({ defaultOptions: { queries: { queryFn } }, }) expect(() => testClient.prefetchQuery({ queryKey: key })).not.toThrow() }) test('should merge defaultOptions when query is added to cache', async () => { const key = queryKey() const testClient = new QueryClient({ defaultOptions: { queries: { gcTime: Infinity }, }, }) const fetchData = () => Promise.resolve('data') await testClient.prefetchQuery({ queryKey: key, queryFn: fetchData }) const newQuery = testClient.getQueryCache().find({ queryKey: key }) expect(newQuery?.options.gcTime).toBe(Infinity) }) test('should get defaultOptions', () => { const queryFn = () => 'data' const defaultOptions = { queries: { queryFn } } const testClient = new QueryClient({ defaultOptions, }) expect(testClient.getDefaultOptions()).toMatchObject(defaultOptions) }) }) describe('setQueryDefaults', () => { test('should not trigger a fetch', () => { const key = queryKey() queryClient.setQueryDefaults(key, { queryFn: () => 'data' }) const data = queryClient.getQueryData(key) expect(data).toBeUndefined() }) test('should be able to override defaults', async () => { const key = queryKey() queryClient.setQueryDefaults(key, { queryFn: () => 'data' }) const observer = new QueryObserver(queryClient, { queryKey: key }) const { data } = await observer.refetch() expect(data).toBe('data') }) test('should match the query key partially', async () => { const key = queryKey() queryClient.setQueryDefaults([key], { queryFn: () => 'data' }) const observer = new QueryObserver(queryClient, { queryKey: [key, 'a'], }) const { data } = await observer.refetch() expect(data).toBe('data') }) test('should not match if the query key is a subset', async () => { const key = queryKey() queryClient.setQueryDefaults([key, 'a'], { queryFn: () => 'data', }) const observer = new QueryObserver(queryClient, { queryKey: [key], retry: false, enabled: false, }) const { status } = await observer.refetch() expect(status).toBe('error') }) test('should also set defaults for observers', () => { const key = queryKey() queryClient.setQueryDefaults(key, { queryFn: () => 'data', enabled: false, }) const observer = new QueryObserver(queryClient, { queryKey: [key], }) expect(observer.getCurrentResult().status).toBe('pending') expect(observer.getCurrentResult().fetchStatus).toBe('idle') }) test('should update existing query defaults', () => { const key = queryKey() const queryOptions1 = { queryFn: () => 'data' } const queryOptions2 = { retry: false } queryClient.setQueryDefaults(key, { ...queryOptions1 }) queryClient.setQueryDefaults(key, { ...queryOptions2 }) expect(queryClient.getQueryDefaults(key)).toMatchObject(queryOptions2) }) test('should merge defaultOptions', () => { const key = queryKey() queryClient.setQueryDefaults([...key, 'todo'], { suspense: true }) queryClient.setQueryDefaults([...key, 'todo', 'detail'], { staleTime: 5000, }) expect( queryClient.getQueryDefaults([...key, 'todo', 'detail']), ).toMatchObject({ suspense: true, staleTime: 5000 }) }) }) describe('defaultQueryOptions', () => { test('should default networkMode when persister is present', () => { expect( new QueryClient({ defaultOptions: { queries: { persister: 'ignore' as any, }, }, }).defaultQueryOptions({ queryKey: queryKey() }).networkMode, ).toBe('offlineFirst') }) test('should not default networkMode without persister', () => { expect( new QueryClient({ defaultOptions: { queries: { staleTime: 1000, }, }, }).defaultQueryOptions({ queryKey: queryKey() }).networkMode, ).toBe(undefined) }) test('should not default networkMode when already present', () => { expect( new QueryClient({ defaultOptions: { queries: { persister: 'ignore' as any, networkMode: 'always', }, }, }).defaultQueryOptions({ queryKey: queryKey() }).networkMode, ).toBe('always') }) }) describe('setQueryData', () => { test('should not crash if query could not be found', () => { const key = queryKey() const user = { userId: 1 } expect(() => { queryClient.setQueryData([key, user], (prevUser?: typeof user) => ({ ...prevUser!, name: 'James', })) }).not.toThrow() }) test('should not crash when variable is null', () => { const key = queryKey() queryClient.setQueryData([key, { userId: null }], 'Old Data') expect(() => { queryClient.setQueryData([key, { userId: null }], 'New Data') }).not.toThrow() }) test('should use default options', () => { const key = queryKey() const testClient = new QueryClient({ defaultOptions: { queries: { queryKeyHashFn: () => 'someKey' } }, }) const testCache = testClient.getQueryCache() testClient.setQueryData(key, 'data') expect(testClient.getQueryData(key)).toBe('data') expect(testCache.find({ queryKey: key })).toBe(testCache.get('someKey')) }) test('should create a new query if query was not found 1', () => { const key = queryKey() queryClient.setQueryData(key, 'bar') expect(queryClient.getQueryData(key)).toBe('bar') }) test('should create a new query if query was not found 2', () => { const key = queryKey() queryClient.setQueryData(key, 'qux') expect(queryClient.getQueryData(key)).toBe('qux') }) test('should not create a new query if query was not found and data is undefined', () => { const key = queryKey() expect(queryClient.getQueryCache().find({ queryKey: key })).toBe( undefined, ) queryClient.setQueryData(key, undefined) expect(queryClient.getQueryCache().find({ queryKey: key })).toBe( undefined, ) }) test('should not create a new query if query was not found and updater returns undefined', () => { const key = queryKey() expect(queryClient.getQueryCache().find({ queryKey: key })).toBe( undefined, ) queryClient.setQueryData(key, () => undefined) expect(queryClient.getQueryCache().find({ queryKey: key })).toBe( undefined, ) }) test('should not update query data if data is undefined', () => { const key = queryKey() queryClient.setQueryData(key, 'qux') queryClient.setQueryData(key, undefined) expect(queryClient.getQueryData(key)).toBe('qux') }) test('should not update query data if updater returns undefined', () => { const key = queryKey() queryClient.setQueryData(key, 'qux') queryClient.setQueryData(key, () => undefined) expect(queryClient.getQueryData(key)).toBe('qux') }) test('should accept an update function', () => { const key = queryKey() const updater = vi.fn((oldData) => `new data + ${oldData}`) queryClient.setQueryData(key, 'test data') queryClient.setQueryData(key, updater) expect(updater).toHaveBeenCalled() expect(queryCache.find({ queryKey: key })!.state.data).toEqual( 'new data + test data', ) }) test('should set the new data without comparison if structuralSharing is set to false', () => { const key = queryKey() queryClient.setDefaultOptions({ queries: { structuralSharing: false, }, }) const oldData = { value: true } const newData = { value: true } queryClient.setQueryData(key, oldData) queryClient.setQueryData(key, newData) expect(queryCache.find({ queryKey: key })!.state.data).toBe(newData) }) test('should apply a custom structuralSharing function when provided', () => { const key = queryKey() const queryObserverOptions = { structuralSharing: ( prevData: { value: Date } | undefined, newData: { value: Date }, ) => { if (!prevData) { return newData } return newData.value.getTime() === prevData.value.getTime() ? prevData : newData }, } as QueryObserverOptions queryClient.setDefaultOptions({ queries: queryObserverOptions }) const oldData = { value: new Date(2022, 6, 19) } const newData = { value: new Date(2022, 6, 19) } queryClient.setQueryData(key, oldData) queryClient.setQueryData(key, newData) expect(queryCache.find({ queryKey: key })!.state.data).toBe(oldData) const distinctData = { value: new Date(2021, 11, 25) } queryClient.setQueryData(key, distinctData) expect(queryCache.find({ queryKey: key })!.state.data).toBe(distinctData) }) test('should not set isFetching to false', async () => { const key = queryKey() queryClient.prefetchQuery({ queryKey: key, queryFn: () => sleep(10).then(() => 23), }) expect(queryClient.getQueryState(key)).toMatchObject({ data: undefined, fetchStatus: 'fetching', }) queryClient.setQueryData(key, 42) expect(queryClient.getQueryState(key)).toMatchObject({ data: 42, fetchStatus: 'fetching', }) await vi.advanceTimersByTimeAsync(10) expect(queryClient.getQueryState(key)).toMatchObject({ data: 23, fetchStatus: 'idle', }) }) }) describe('setQueriesData', () => { test('should update all existing, matching queries', () => { queryClient.setQueryData(['key', 1], 1) queryClient.setQueryData(['key', 2], 2) const result = queryClient.setQueriesData( { queryKey: ['key'] }, (old) => (old ? old + 5 : undefined), ) expect(result).toEqual([ [['key', 1], 6], [['key', 2], 7], ]) expect(queryClient.getQueryData(['key', 1])).toBe(6) expect(queryClient.getQueryData(['key', 2])).toBe(7) }) test('should accept queryFilters', () => { queryClient.setQueryData(['key', 1], 1) queryClient.setQueryData(['key', 2], 2) const query1 = queryCache.find({ queryKey: ['key', 1] })! const result = queryClient.setQueriesData( { predicate: (query) => query === query1 }, (old) => old! + 5, ) expect(result).toEqual([[['key', 1], 6]]) expect(queryClient.getQueryData(['key', 1])).toBe(6) expect(queryClient.getQueryData(['key', 2])).toBe(2) }) test('should not update non existing queries', () => { const result = queryClient.setQueriesData( { queryKey: ['key'] }, 'data', ) expect(result).toEqual([]) expect(queryClient.getQueryData(['key'])).toBe(undefined) }) }) describe('isFetching', () => { test('should return length of fetching queries', async () => { expect(queryClient.isFetching()).toBe(0) queryClient.prefetchQuery({ queryKey: queryKey(), queryFn: () => sleep(10).then(() => 'data'), }) expect(queryClient.isFetching()).toBe(1) queryClient.prefetchQuery({ queryKey: queryKey(), queryFn: () => sleep(5).then(() => 'data'), }) expect(queryClient.isFetching()).toBe(2) await vi.advanceTimersByTimeAsync(5) expect(queryClient.isFetching()).toEqual(1) await vi.advanceTimersByTimeAsync(5) expect(queryClient.isFetching()).toEqual(0) }) }) describe('isMutating', () => { test('should return length of mutating', async () => { expect(queryClient.isMutating()).toBe(0) new MutationObserver(queryClient, { mutationFn: () => sleep(10).then(() => 'data'), }).mutate() expect(queryClient.isMutating()).toBe(1) new MutationObserver(queryClient, { mutationFn: () => sleep(5).then(() => 'data'), }).mutate() expect(queryClient.isMutating()).toBe(2) await vi.advanceTimersByTimeAsync(5) expect(queryClient.isMutating()).toEqual(1) await vi.advanceTimersByTimeAsync(5) expect(queryClient.isMutating()).toEqual(0) }) }) describe('getQueryData', () => { test('should return the query data if the query is found', () => { const key = queryKey() queryClient.setQueryData([key, 'id'], 'bar') expect(queryClient.getQueryData([key, 'id'])).toBe('bar') }) test('should return undefined if the query is not found', () => { const key = queryKey() expect(queryClient.getQueryData(key)).toBeUndefined() }) test('should match exact by default', () => { const key = queryKey() queryClient.setQueryData([key, 'id'], 'bar') expect(queryClient.getQueryData([key])).toBeUndefined() }) }) describe('ensureQueryData', () => { test('should return the cached query data if the query is found', async () => { const key = queryKey() const queryFn = () => Promise.resolve('data') queryClient.setQueryData([key, 'id'], 'bar') await expect( queryClient.ensureQueryData({ queryKey: [key, 'id'], queryFn }), ).resolves.toEqual('bar') }) test('should return the cached query data if the query is found and cached query data is falsy', async () => { const key = queryKey() const queryFn = () => Promise.resolve(0) queryClient.setQueryData([key, 'id'], null) await expect( queryClient.ensureQueryData({ queryKey: [key, 'id'], queryFn }), ).resolves.toEqual(null) }) test('should call fetchQuery and return its results if the query is not found', async () => { const key = queryKey() const queryFn = () => Promise.resolve('data') await expect( queryClient.ensureQueryData({ queryKey: [key], queryFn }), ).resolves.toEqual('data') }) test('should return the cached query data if the query is found and preFetchQuery in the background when revalidateIfStale is set', async () => { const TIMEOUT = 10 const key = queryKey() queryClient.setQueryData([key, 'id'], 'old') const queryFn = () => new Promise((resolve) => { setTimeout(() => resolve('new'), TIMEOUT) }) await expect( queryClient.ensureQueryData({ queryKey: [key, 'id'], queryFn, revalidateIfStale: true, }), ).resolves.toEqual('old') await vi.advanceTimersByTimeAsync(TIMEOUT + 10) await expect( queryClient.ensureQueryData({ queryKey: [key, 'id'], queryFn, revalidateIfStale: true, }), ).resolves.toEqual('new') }) test('should not fetch with initialDat', async () => { const key = queryKey() const queryFn = vi.fn().mockImplementation(() => Promise.resolve('data')) await expect( queryClient.ensureQueryData({ queryKey: [key, 'id'], queryFn, initialData: 'initial', }), ).resolves.toEqual('initial') expect(queryFn).toHaveBeenCalledTimes(0) }) }) describe('ensureInfiniteQueryData', () => { test('should return the cached query data if the query is found', async () => { const key = queryKey() const queryFn = () => Promise.resolve('data') queryClient.setQueryData([key, 'id'], { pages: ['bar'], pageParams: [0] }) await expect( queryClient.ensureInfiniteQueryData({ queryKey: [key, 'id'], queryFn, initialPageParam: 1, getNextPageParam: () => undefined, }), ).resolves.toEqual({ pages: ['bar'], pageParams: [0] }) }) test('should fetch the query and return its results if the query is not found', async () => { const key = queryKey() const queryFn = () => Promise.resolve('data') await expect( queryClient.ensureInfiniteQueryData({ queryKey: [key, 'id'], queryFn, initialPageParam: 1, getNextPageParam: () => undefined, }), ).resolves.toEqual({ pages: ['data'], pageParams: [1] }) }) test('should return the cached query data if the query is found and preFetchQuery in the background when revalidateIfStale is set', async () => { const TIMEOUT = 10 const key = queryKey() queryClient.setQueryData([key, 'id'], { pages: ['old'], pageParams: [0] }) const queryFn = () => sleep(TIMEOUT).then(() => 'new') await expect( queryClient.ensureInfiniteQueryData({ queryKey: [key, 'id'], queryFn, initialPageParam: 1, getNextPageParam: () => undefined, revalidateIfStale: true, }), ).resolves.toEqual({ pages: ['old'], pageParams: [0] }) await vi.advanceTimersByTimeAsync(TIMEOUT + 10) await expect( queryClient.ensureInfiniteQueryData({ queryKey: [key, 'id'], queryFn, initialPageParam: 1, getNextPageParam: () => undefined, revalidateIfStale: true, }), ).resolves.toEqual({ pages: ['new'], pageParams: [0] }) }) }) describe('getQueriesData', () => { test('should return the query data for all matched queries', () => { const key1 = queryKey() const key2 = queryKey() queryClient.setQueryData([key1, 1], 1) queryClient.setQueryData([key1, 2], 2) queryClient.setQueryData([key2, 2], 2) expect(queryClient.getQueriesData({ queryKey: [key1] })).toEqual([ [[key1, 1], 1], [[key1, 2], 2], ]) }) test('should return empty array if queries are not found', () => { const key = queryKey() expect(queryClient.getQueriesData({ queryKey: key })).toEqual([]) }) test('should accept query filters', () => { queryClient.setQueryData(['key', 1], 1) queryClient.setQueryData(['key', 2], 2) const query1 = queryCache.find({ queryKey: ['key', 1] })! const result = queryClient.getQueriesData({ predicate: (query) => query === query1, }) expect(result).toEqual([[['key', 1], 1]]) }) }) describe('fetchQuery', () => { test('should not type-error with strict query key', async () => { type StrictData = 'data' type StrictQueryKey = ['strict', ...ReturnType] const key: StrictQueryKey = ['strict', ...queryKey()] const fetchFn: QueryFunction = () => Promise.resolve('data') await expect( queryClient.fetchQuery({ queryKey: key, queryFn: fetchFn, }), ).resolves.toEqual('data') }) // https://github.com/tannerlinsley/react-query/issues/652 test('should not retry by default', async () => { const key = queryKey() await expect( queryClient.fetchQuery({ queryKey: key, queryFn: (): Promise => { throw new Error('error') }, }), ).rejects.toEqual(new Error('error')) }) test('should return the cached data on cache hit', async () => { const key = queryKey() const fetchFn = () => Promise.resolve('data') const first = await queryClient.fetchQuery({ queryKey: key, queryFn: fetchFn, }) const second = await queryClient.fetchQuery({ queryKey: key, queryFn: fetchFn, }) expect(second).toBe(first) }) test('should read from cache with static staleTime even if invalidated', async () => { const key = queryKey() const fetchFn = vi.fn(() => Promise.resolve({ data: 'data' })) const first = await queryClient.fetchQuery({ queryKey: key, queryFn: fetchFn, staleTime: 'static', }) expect(first.data).toBe('data') expect(fetchFn).toHaveBeenCalledTimes(1) await queryClient.invalidateQueries({ queryKey: key, refetchType: 'none', }) const second = await queryClient.fetchQuery({ queryKey: key, queryFn: fetchFn, staleTime: 'static', }) expect(fetchFn).toHaveBeenCalledTimes(1) expect(second).toBe(first) }) test('should be able to fetch when garbage collection time is set to 0 and then be removed', async () => { const key1 = queryKey() const promise = queryClient.fetchQuery({ queryKey: key1, queryFn: () => sleep(10).then(() => 1), gcTime: 0, }) await vi.advanceTimersByTimeAsync(10) await expect(promise).resolves.toEqual(1) await vi.advanceTimersByTimeAsync(1) expect(queryClient.getQueryData(key1)).toEqual(undefined) }) test('should keep a query in cache if garbage collection time is Infinity', async () => { const key1 = queryKey() const promise = queryClient.fetchQuery({ queryKey: key1, queryFn: () => sleep(10).then(() => 1), gcTime: Infinity, }) await vi.advanceTimersByTimeAsync(10) const result2 = queryClient.getQueryData(key1) await expect(promise).resolves.toEqual(1) expect(result2).toEqual(1) }) test('should not force fetch', async () => { const key = queryKey() queryClient.setQueryData(key, 'og') const fetchFn = () => Promise.resolve('new') const first = await queryClient.fetchQuery({ queryKey: key, queryFn: fetchFn, initialData: 'initial', staleTime: 100, }) expect(first).toBe('og') }) test('should only fetch if the data is older then the given stale time', async () => { const key = queryKey() let count = 0 const queryFn = () => ++count queryClient.setQueryData(key, count) const firstPromise = queryClient.fetchQuery({ queryKey: key, queryFn, staleTime: 100, }) await expect(firstPromise).resolves.toBe(0) await vi.advanceTimersByTimeAsync(10) const secondPromise = queryClient.fetchQuery({ queryKey: key, queryFn, staleTime: 10, }) await expect(secondPromise).resolves.toBe(1) const thirdPromise = queryClient.fetchQuery({ queryKey: key, queryFn, staleTime: 10, }) await expect(thirdPromise).resolves.toBe(1) await vi.advanceTimersByTimeAsync(10) const fourthPromise = queryClient.fetchQuery({ queryKey: key, queryFn, staleTime: 10, }) await expect(fourthPromise).resolves.toBe(2) }) test('should allow new meta', async () => { const key = queryKey() const first = await queryClient.fetchQuery({ queryKey: key, queryFn: ({ meta }) => Promise.resolve(meta), meta: { foo: true, }, }) expect(first).toStrictEqual({ foo: true }) const second = await queryClient.fetchQuery({ queryKey: key, queryFn: ({ meta }) => Promise.resolve(meta), meta: { foo: false, }, }) expect(second).toStrictEqual({ foo: false }) }) }) describe('fetchInfiniteQuery', () => { test('should not type-error with strict query key', async () => { type StrictData = string type StrictQueryKey = ['strict', ...ReturnType] const key: StrictQueryKey = ['strict', ...queryKey()] const data = { pages: ['data'], pageParams: [0], } as const const fetchFn: QueryFunction = () => Promise.resolve(data.pages[0]) await expect( queryClient.fetchInfiniteQuery< StrictData, any, StrictData, StrictQueryKey, number >({ queryKey: key, queryFn: fetchFn, initialPageParam: 0 }), ).resolves.toEqual(data) }) test('should return infinite query data', async () => { const key = queryKey() const result = await queryClient.fetchInfiniteQuery({ queryKey: key, initialPageParam: 10, queryFn: ({ pageParam }) => Number(pageParam), }) const result2 = queryClient.getQueryData(key) const expected = { pages: [10], pageParams: [10], } expect(result).toEqual(expected) expect(result2).toEqual(expected) }) }) describe('prefetchInfiniteQuery', () => { test('should not type-error with strict query key', async () => { type StrictData = 'data' type StrictQueryKey = ['strict', ...ReturnType] const key: StrictQueryKey = ['strict', ...queryKey()] const fetchFn: QueryFunction = () => Promise.resolve('data') await queryClient.prefetchInfiniteQuery< StrictData, any, StrictData, StrictQueryKey, number >({ queryKey: key, queryFn: fetchFn, initialPageParam: 0 }) const result = queryClient.getQueryData(key) expect(result).toEqual({ pages: ['data'], pageParams: [0], }) }) test('should return infinite query data', async () => { const key = queryKey() await queryClient.prefetchInfiniteQuery({ queryKey: key, queryFn: ({ pageParam }) => Number(pageParam), initialPageParam: 10, }) const result = queryClient.getQueryData(key) expect(result).toEqual({ pages: [10], pageParams: [10], }) }) test('should prefetch multiple pages', async () => { const key = queryKey() await queryClient.prefetchInfiniteQuery({ queryKey: key, queryFn: ({ pageParam }) => String(pageParam), getNextPageParam: (_lastPage, _pages, lastPageParam) => lastPageParam + 5, initialPageParam: 10, pages: 3, }) const result = queryClient.getQueryData(key) expect(result).toEqual({ pages: ['10', '15', '20'], pageParams: [10, 15, 20], }) }) test('should stop prefetching if getNextPageParam returns undefined', async () => { const key = queryKey() let count = 0 await queryClient.prefetchInfiniteQuery({ queryKey: key, queryFn: ({ pageParam }) => String(pageParam), getNextPageParam: (_lastPage, _pages, lastPageParam) => { count++ return lastPageParam >= 20 ? undefined : lastPageParam + 5 }, initialPageParam: 10, pages: 5, }) const result = queryClient.getQueryData(key) expect(result).toEqual({ pages: ['10', '15', '20'], pageParams: [10, 15, 20], }) // this check ensures we're exiting the fetch loop early expect(count).toBe(3) }) }) describe('prefetchQuery', () => { test('should not type-error with strict query key', async () => { type StrictData = 'data' type StrictQueryKey = ['strict', ...ReturnType] const key: StrictQueryKey = ['strict', ...queryKey()] const fetchFn: QueryFunction = () => Promise.resolve('data') await queryClient.prefetchQuery< StrictData, any, StrictData, StrictQueryKey >({ queryKey: key, queryFn: fetchFn }) const result = queryClient.getQueryData(key) expect(result).toEqual('data') }) test('should return undefined when an error is thrown', async () => { const key = queryKey() const result = await queryClient.prefetchQuery({ queryKey: key, queryFn: (): Promise => { throw new Error('error') }, retry: false, }) expect(result).toBeUndefined() }) test('should be garbage collected after gcTime if unused', async () => { const key = queryKey() await queryClient.prefetchQuery({ queryKey: key, queryFn: () => 'data', gcTime: 10, }) expect(queryCache.find({ queryKey: key })).toBeDefined() await vi.advanceTimersByTimeAsync(15) expect(queryCache.find({ queryKey: key })).not.toBeDefined() }) }) describe('removeQueries', () => { test('should not crash when exact is provided', async () => { const key = queryKey() const fetchFn = () => Promise.resolve('data') // check the query was added to the cache await queryClient.prefetchQuery({ queryKey: key, queryFn: fetchFn }) expect(queryCache.find({ queryKey: key })).toBeTruthy() // check the error doesn't occur expect(() => queryClient.removeQueries({ queryKey: key, exact: true }), ).not.toThrow() // check query was successful removed expect(queryCache.find({ queryKey: key })).toBeFalsy() }) }) describe('cancelQueries', () => { test('should revert queries to their previous state', async () => { const key1 = queryKey() queryClient.setQueryData(key1, 'data') const pending = queryClient.fetchQuery({ queryKey: key1, queryFn: () => sleep(1000).then(() => 'data2'), }) await vi.advanceTimersByTimeAsync(10) await queryClient.cancelQueries() // with previous data present, imperative fetch should resolve to that data after cancel await expect(pending).resolves.toBe('data') const state1 = queryClient.getQueryState(key1) expect(state1).toMatchObject({ data: 'data', status: 'success', }) }) test('should not revert if revert option is set to false', async () => { const key1 = queryKey() await queryClient.fetchQuery({ queryKey: key1, queryFn: () => 'data', }) queryClient.prefetchQuery({ queryKey: key1, queryFn: () => sleep(1000).then(() => 'data2'), }) await vi.advanceTimersByTimeAsync(10) await queryClient.cancelQueries({ queryKey: key1 }, { revert: false }) const state1 = queryClient.getQueryState(key1) expect(state1).toMatchObject({ status: 'error', }) }) test('should throw CancelledError for imperative methods when initial fetch is cancelled', async () => { const key = queryKey() const promise = queryClient.fetchQuery({ queryKey: key, queryFn: async () => { await sleep(50) return 25 }, }) await vi.advanceTimersByTimeAsync(10) await queryClient.cancelQueries({ queryKey: key }) // we have to reject here because we can't resolve with `undefined` // the alternative would be a never-ending promise await expect(promise).rejects.toBeInstanceOf(CancelledError) // however, the query was correctly reverted to pending state expect(queryClient.getQueryState(key)).toMatchObject({ status: 'pending', fetchStatus: 'idle', data: undefined, error: null, }) }) }) describe('refetchQueries', () => { test('should not refetch if all observers are disabled', async () => { const key = queryKey() const queryFn = vi .fn<(...args: Array) => string>() .mockReturnValue('data') await queryClient.fetchQuery({ queryKey: key, queryFn }) const observer1 = new QueryObserver(queryClient, { queryKey: key, queryFn, enabled: false, }) observer1.subscribe(() => undefined) await queryClient.refetchQueries() observer1.destroy() expect(queryFn).toHaveBeenCalledTimes(1) }) test('should refetch if at least one observer is enabled', async () => { const key = queryKey() const queryFn = vi .fn<(...args: Array) => string>() .mockReturnValue('data') await queryClient.fetchQuery({ queryKey: key, queryFn }) const observer1 = new QueryObserver(queryClient, { queryKey: key, queryFn, enabled: false, }) const observer2 = new QueryObserver(queryClient, { queryKey: key, queryFn, refetchOnMount: false, }) observer1.subscribe(() => undefined) observer2.subscribe(() => undefined) await queryClient.refetchQueries() observer1.destroy() observer2.destroy() expect(queryFn).toHaveBeenCalledTimes(2) }) test('should refetch all queries when no arguments are given', async () => { const key1 = queryKey() const key2 = queryKey() const queryFn1 = vi .fn<(...args: Array) => string>() .mockReturnValue('data1') const queryFn2 = vi .fn<(...args: Array) => string>() .mockReturnValue('data2') await queryClient.fetchQuery({ queryKey: key1, queryFn: queryFn1 }) await queryClient.fetchQuery({ queryKey: key2, queryFn: queryFn2 }) const observer1 = new QueryObserver(queryClient, { queryKey: key1, queryFn: queryFn1, staleTime: Infinity, initialData: 'initial', }) const observer2 = new QueryObserver(queryClient, { queryKey: key1, queryFn: queryFn1, staleTime: Infinity, initialData: 'initial', }) observer1.subscribe(() => undefined) observer2.subscribe(() => undefined) await queryClient.refetchQueries() observer1.destroy() observer2.destroy() expect(queryFn1).toHaveBeenCalledTimes(2) expect(queryFn2).toHaveBeenCalledTimes(2) }) test('should be able to refetch all fresh queries', async () => { const key1 = queryKey() const key2 = queryKey() const queryFn1 = vi .fn<(...args: Array) => string>() .mockReturnValue('data1') const queryFn2 = vi .fn<(...args: Array) => string>() .mockReturnValue('data2') await queryClient.fetchQuery({ queryKey: key1, queryFn: queryFn1 }) await queryClient.fetchQuery({ queryKey: key2, queryFn: queryFn2 }) const observer = new QueryObserver(queryClient, { queryKey: key1, queryFn: queryFn1, staleTime: Infinity, }) const unsubscribe = observer.subscribe(() => undefined) await queryClient.refetchQueries({ type: 'active', stale: false }) unsubscribe() expect(queryFn1).toHaveBeenCalledTimes(2) expect(queryFn2).toHaveBeenCalledTimes(1) }) test('should be able to refetch all stale queries', async () => { const key1 = queryKey() const key2 = queryKey() const queryFn1 = vi .fn<(...args: Array) => string>() .mockReturnValue('data1') const queryFn2 = vi .fn<(...args: Array) => string>() .mockReturnValue('data2') await queryClient.fetchQuery({ queryKey: key1, queryFn: queryFn1 }) await queryClient.fetchQuery({ queryKey: key2, queryFn: queryFn2 }) const observer = new QueryObserver(queryClient, { queryKey: key1, queryFn: queryFn1, }) const unsubscribe = observer.subscribe(() => undefined) queryClient.invalidateQueries({ queryKey: key1 }) await queryClient.refetchQueries({ stale: true }) unsubscribe() // fetchQuery, observer mount, invalidation (cancels observer mount) and refetch expect(queryFn1).toHaveBeenCalledTimes(4) expect(queryFn2).toHaveBeenCalledTimes(1) }) test('should be able to refetch all stale and active queries', async () => { const key1 = queryKey() const key2 = queryKey() const queryFn1 = vi .fn<(...args: Array) => string>() .mockReturnValue('data1') const queryFn2 = vi .fn<(...args: Array) => string>() .mockReturnValue('data2') await queryClient.fetchQuery({ queryKey: key1, queryFn: queryFn1 }) await queryClient.fetchQuery({ queryKey: key2, queryFn: queryFn2 }) queryClient.invalidateQueries({ queryKey: key1 }) const observer = new QueryObserver(queryClient, { queryKey: key1, queryFn: queryFn1, }) const unsubscribe = observer.subscribe(() => undefined) await queryClient.refetchQueries( { type: 'active', stale: true }, { cancelRefetch: false }, ) unsubscribe() expect(queryFn1).toHaveBeenCalledTimes(2) expect(queryFn2).toHaveBeenCalledTimes(1) }) test('should be able to refetch all active and inactive queries (queryClient.refetchQueries()', async () => { const key1 = queryKey() const key2 = queryKey() const queryFn1 = vi .fn<(...args: Array) => string>() .mockReturnValue('data1') const queryFn2 = vi .fn<(...args: Array) => string>() .mockReturnValue('data2') await queryClient.fetchQuery({ queryKey: key1, queryFn: queryFn1 }) await queryClient.fetchQuery({ queryKey: key2, queryFn: queryFn2 }) const observer = new QueryObserver(queryClient, { queryKey: key1, queryFn: queryFn1, staleTime: Infinity, }) const unsubscribe = observer.subscribe(() => undefined) await queryClient.refetchQueries() unsubscribe() expect(queryFn1).toHaveBeenCalledTimes(2) expect(queryFn2).toHaveBeenCalledTimes(2) }) test('should be able to refetch all active and inactive queries (queryClient.refetchQueries({ type: "all" }))', async () => { const key1 = queryKey() const key2 = queryKey() const queryFn1 = vi .fn<(...args: Array) => string>() .mockReturnValue('data1') const queryFn2 = vi .fn<(...args: Array) => string>() .mockReturnValue('data2') await queryClient.fetchQuery({ queryKey: key1, queryFn: queryFn1 }) await queryClient.fetchQuery({ queryKey: key2, queryFn: queryFn2 }) const observer = new QueryObserver(queryClient, { queryKey: key1, queryFn: queryFn1, staleTime: Infinity, }) const unsubscribe = observer.subscribe(() => undefined) await queryClient.refetchQueries({ type: 'all' }) unsubscribe() expect(queryFn1).toHaveBeenCalledTimes(2) expect(queryFn2).toHaveBeenCalledTimes(2) }) test('should be able to refetch only active queries', async () => { const key1 = queryKey() const key2 = queryKey() const queryFn1 = vi .fn<(...args: Array) => string>() .mockReturnValue('data1') const queryFn2 = vi .fn<(...args: Array) => string>() .mockReturnValue('data2') await queryClient.fetchQuery({ queryKey: key1, queryFn: queryFn1 }) await queryClient.fetchQuery({ queryKey: key2, queryFn: queryFn2 }) const observer = new QueryObserver(queryClient, { queryKey: key1, queryFn: queryFn1, staleTime: Infinity, }) const unsubscribe = observer.subscribe(() => undefined) await queryClient.refetchQueries({ type: 'active' }) unsubscribe() expect(queryFn1).toHaveBeenCalledTimes(2) expect(queryFn2).toHaveBeenCalledTimes(1) }) test('should be able to refetch only inactive queries', async () => { const key1 = queryKey() const key2 = queryKey() const queryFn1 = vi .fn<(...args: Array) => string>() .mockReturnValue('data1') const queryFn2 = vi .fn<(...args: Array) => string>() .mockReturnValue('data2') await queryClient.fetchQuery({ queryKey: key1, queryFn: queryFn1 }) await queryClient.fetchQuery({ queryKey: key2, queryFn: queryFn2 }) const observer = new QueryObserver(queryClient, { queryKey: key1, queryFn: queryFn1, staleTime: Infinity, }) const unsubscribe = observer.subscribe(() => undefined) await queryClient.refetchQueries({ type: 'inactive' }) unsubscribe() expect(queryFn1).toHaveBeenCalledTimes(1) expect(queryFn2).toHaveBeenCalledTimes(2) }) test('should throw an error if throwOnError option is set to true', async () => { const key1 = queryKey() const queryFnError = () => Promise.reject('error') try { await queryClient.fetchQuery({ queryKey: key1, queryFn: queryFnError, retry: false, }) } catch {} let error: any try { await queryClient.refetchQueries( { queryKey: key1 }, { throwOnError: true }, ) } catch (err) { error = err } expect(error).toEqual('error') }) test('should resolve Promise immediately if query is paused', async () => { const key1 = queryKey() const queryFn1 = vi .fn<(...args: Array) => string>() .mockReturnValue('data1') await queryClient.fetchQuery({ queryKey: key1, queryFn: queryFn1 }) const onlineMock = mockOnlineManagerIsOnline(false) await queryClient.refetchQueries({ queryKey: key1 }) // if we reach this point, the test succeeds because the Promise was resolved immediately expect(queryFn1).toHaveBeenCalledTimes(1) onlineMock.mockRestore() }) test('should refetch if query we are offline but query networkMode is always', async () => { const key1 = queryKey() queryClient.setQueryDefaults(key1, { networkMode: 'always' }) const queryFn1 = vi .fn<(...args: Array) => string>() .mockReturnValue('data1') await queryClient.fetchQuery({ queryKey: key1, queryFn: queryFn1 }) const onlineMock = mockOnlineManagerIsOnline(false) await queryClient.refetchQueries({ queryKey: key1 }) // initial fetch + refetch (even though we are offline) expect(queryFn1).toHaveBeenCalledTimes(2) onlineMock.mockRestore() }) test('should not refetch static queries', async () => { const key = queryKey() const queryFn = vi.fn(() => 'data1') await queryClient.fetchQuery({ queryKey: key, queryFn: queryFn }) expect(queryFn).toHaveBeenCalledTimes(1) const observer = new QueryObserver(queryClient, { queryKey: key, queryFn, staleTime: 'static', }) const unsubscribe = observer.subscribe(() => undefined) await queryClient.refetchQueries() expect(queryFn).toHaveBeenCalledTimes(1) unsubscribe() }) }) describe('invalidateQueries', () => { test('should refetch active queries by default', async () => { const key1 = queryKey() const key2 = queryKey() const queryFn1 = vi .fn<(...args: Array) => string>() .mockReturnValue('data1') const queryFn2 = vi .fn<(...args: Array) => string>() .mockReturnValue('data2') await queryClient.fetchQuery({ queryKey: key1, queryFn: queryFn1 }) await queryClient.fetchQuery({ queryKey: key2, queryFn: queryFn2 }) const observer = new QueryObserver(queryClient, { queryKey: key1, queryFn: queryFn1, staleTime: Infinity, }) const unsubscribe = observer.subscribe(() => undefined) queryClient.invalidateQueries({ queryKey: key1 }) unsubscribe() expect(queryFn1).toHaveBeenCalledTimes(2) expect(queryFn2).toHaveBeenCalledTimes(1) }) test('should not refetch inactive queries by default', async () => { const key1 = queryKey() const key2 = queryKey() const queryFn1 = vi .fn<(...args: Array) => string>() .mockReturnValue('data1') const queryFn2 = vi .fn<(...args: Array) => string>() .mockReturnValue('data2') await queryClient.fetchQuery({ queryKey: key1, queryFn: queryFn1 }) await queryClient.fetchQuery({ queryKey: key2, queryFn: queryFn2 }) const observer = new QueryObserver(queryClient, { queryKey: key1, enabled: false, staleTime: Infinity, }) const unsubscribe = observer.subscribe(() => undefined) queryClient.invalidateQueries({ queryKey: key1 }) unsubscribe() expect(queryFn1).toHaveBeenCalledTimes(1) expect(queryFn2).toHaveBeenCalledTimes(1) }) test('should not refetch active queries when "refetch" is "none"', async () => { const key1 = queryKey() const key2 = queryKey() const queryFn1 = vi .fn<(...args: Array) => string>() .mockReturnValue('data1') const queryFn2 = vi .fn<(...args: Array) => string>() .mockReturnValue('data2') await queryClient.fetchQuery({ queryKey: key1, queryFn: queryFn1 }) await queryClient.fetchQuery({ queryKey: key2, queryFn: queryFn2 }) const observer = new QueryObserver(queryClient, { queryKey: key1, queryFn: queryFn1, staleTime: Infinity, }) const unsubscribe = observer.subscribe(() => undefined) queryClient.invalidateQueries({ queryKey: key1, refetchType: 'none', }) unsubscribe() expect(queryFn1).toHaveBeenCalledTimes(1) expect(queryFn2).toHaveBeenCalledTimes(1) }) test('should refetch inactive queries when "refetch" is "inactive"', async () => { const key1 = queryKey() const key2 = queryKey() const queryFn1 = vi .fn<(...args: Array) => string>() .mockReturnValue('data1') const queryFn2 = vi .fn<(...args: Array) => string>() .mockReturnValue('data2') await queryClient.fetchQuery({ queryKey: key1, queryFn: queryFn1 }) await queryClient.fetchQuery({ queryKey: key2, queryFn: queryFn2 }) const observer = new QueryObserver(queryClient, { queryKey: key1, queryFn: queryFn1, staleTime: Infinity, refetchOnMount: false, }) const unsubscribe = observer.subscribe(() => undefined) unsubscribe() await queryClient.invalidateQueries({ queryKey: key1, refetchType: 'inactive', }) expect(queryFn1).toHaveBeenCalledTimes(2) expect(queryFn2).toHaveBeenCalledTimes(1) }) test('should refetch active and inactive queries when "refetch" is "all"', async () => { const key1 = queryKey() const key2 = queryKey() const queryFn1 = vi .fn<(...args: Array) => string>() .mockReturnValue('data1') const queryFn2 = vi .fn<(...args: Array) => string>() .mockReturnValue('data2') await queryClient.fetchQuery({ queryKey: key1, queryFn: queryFn1 }) await queryClient.fetchQuery({ queryKey: key2, queryFn: queryFn2 }) const observer = new QueryObserver(queryClient, { queryKey: key1, queryFn: queryFn1, staleTime: Infinity, }) const unsubscribe = observer.subscribe(() => undefined) queryClient.invalidateQueries({ refetchType: 'all', }) unsubscribe() expect(queryFn1).toHaveBeenCalledTimes(2) expect(queryFn2).toHaveBeenCalledTimes(2) }) test('should not refetch disabled inactive queries even if "refetchType" is "all', async () => { const queryFn = vi .fn<(...args: Array) => string>() .mockReturnValue('data1') const observer = new QueryObserver(queryClient, { queryKey: queryKey(), queryFn: queryFn, staleTime: Infinity, enabled: false, }) const unsubscribe = observer.subscribe(() => undefined) unsubscribe() await queryClient.invalidateQueries({ refetchType: 'all', }) expect(queryFn).toHaveBeenCalledTimes(0) }) test('should not refetch inactive queries that have a skipToken queryFn even if "refetchType" is "all', async () => { const key = queryKey() const observer = new QueryObserver(queryClient, { queryKey: key, queryFn: skipToken, staleTime: Infinity, }) queryClient.setQueryData(key, 'data1') const unsubscribe = observer.subscribe(() => undefined) unsubscribe() expect(queryClient.getQueryState(key)?.dataUpdateCount).toBe(1) await queryClient.invalidateQueries({ refetchType: 'all', }) expect(queryClient.getQueryState(key)?.dataUpdateCount).toBe(1) }) test('should cancel ongoing fetches if cancelRefetch option is set (default value)', async () => { const key = queryKey() const abortFn = vi.fn() let fetchCount = 0 const observer = new QueryObserver(queryClient, { queryKey: key, queryFn: ({ signal }) => new Promise((resolve) => { fetchCount++ setTimeout(() => resolve(5), 10) signal.addEventListener('abort', abortFn) }), initialData: 1, }) observer.subscribe(() => undefined) queryClient.refetchQueries() await vi.advanceTimersByTimeAsync(10) observer.destroy() expect(abortFn).toHaveBeenCalledTimes(1) expect(fetchCount).toBe(2) }) test('should not cancel ongoing fetches if cancelRefetch option is set to false', async () => { const key = queryKey() const abortFn = vi.fn() let fetchCount = 0 const observer = new QueryObserver(queryClient, { queryKey: key, queryFn: ({ signal }) => { return new Promise((resolve) => { fetchCount++ setTimeout(() => resolve(5), 10) signal.addEventListener('abort', abortFn) }) }, initialData: 1, }) observer.subscribe(() => undefined) queryClient.refetchQueries(undefined, { cancelRefetch: false }) await vi.advanceTimersByTimeAsync(10) observer.destroy() expect(abortFn).toHaveBeenCalledTimes(0) expect(fetchCount).toBe(1) }) test('should not refetch static queries after invalidation', async () => { const key = queryKey() const queryFn = vi.fn(() => 'data1') await queryClient.fetchQuery({ queryKey: key, queryFn: queryFn }) expect(queryFn).toHaveBeenCalledTimes(1) const observer = new QueryObserver(queryClient, { queryKey: key, queryFn, staleTime: 'static', }) const unsubscribe = observer.subscribe(() => undefined) await queryClient.invalidateQueries() expect(queryFn).toHaveBeenCalledTimes(1) unsubscribe() }) }) describe('resetQueries', () => { test('should notify listeners when a query is reset', async () => { const key = queryKey() const callback = vi.fn() await queryClient.prefetchQuery({ queryKey: key, queryFn: () => 'data' }) queryCache.subscribe(callback) queryClient.resetQueries({ queryKey: key }) expect(callback).toHaveBeenCalled() }) test('should reset query', async () => { const key = queryKey() await queryClient.prefetchQuery({ queryKey: key, queryFn: () => 'data' }) let state = queryClient.getQueryState(key) expect(state?.data).toEqual('data') expect(state?.status).toEqual('success') queryClient.resetQueries({ queryKey: key }) state = queryClient.getQueryState(key) expect(state).toBeTruthy() expect(state?.data).toBeUndefined() expect(state?.status).toEqual('pending') expect(state?.fetchStatus).toEqual('idle') }) test('should reset query data to initial data if set', async () => { const key = queryKey() await queryClient.prefetchQuery({ queryKey: key, queryFn: () => 'data', initialData: 'initial', }) let state = queryClient.getQueryState(key) expect(state?.data).toEqual('data') queryClient.resetQueries({ queryKey: key }) state = queryClient.getQueryState(key) expect(state).toBeTruthy() expect(state?.data).toEqual('initial') }) test('should refetch all active queries', async () => { const key1 = queryKey() const key2 = queryKey() const key3 = queryKey() const queryFn1 = vi .fn<(...args: Array) => string>() .mockReturnValue('data1') const queryFn2 = vi .fn<(...args: Array) => string>() .mockReturnValue('data2') const observer1 = new QueryObserver(queryClient, { queryKey: key1, queryFn: queryFn1, enabled: true, }) const observer2 = new QueryObserver(queryClient, { queryKey: key2, queryFn: queryFn2, enabled: false, }) const observer3 = new QueryObserver(queryClient, { queryKey: key3, queryFn: skipToken, }) let didSkipTokenRun = false observer1.subscribe(() => undefined) observer2.subscribe(() => undefined) observer3.subscribe(() => (didSkipTokenRun = true)) await queryClient.resetQueries() observer3.destroy() observer2.destroy() observer1.destroy() expect(queryFn1).toHaveBeenCalledTimes(2) expect(queryFn2).toHaveBeenCalledTimes(0) expect(didSkipTokenRun).toBe(false) }) }) describe('focusManager and onlineManager', () => { afterEach(() => { onlineManager.setOnline(true) focusManager.setFocused(undefined) }) test('should notify queryCache and mutationCache if focused', async () => { const testClient = new QueryClient() testClient.mount() const queryCacheOnFocusSpy = vi.spyOn( testClient.getQueryCache(), 'onFocus', ) const queryCacheOnOnlineSpy = vi.spyOn( testClient.getQueryCache(), 'onOnline', ) const mutationCacheResumePausedMutationsSpy = vi.spyOn( testClient.getMutationCache(), 'resumePausedMutations', ) focusManager.setFocused(false) expect(queryCacheOnFocusSpy).not.toHaveBeenCalled() expect(mutationCacheResumePausedMutationsSpy).not.toHaveBeenCalled() focusManager.setFocused(true) await vi.advanceTimersByTimeAsync(0) expect(queryCacheOnFocusSpy).toHaveBeenCalledTimes(1) expect(mutationCacheResumePausedMutationsSpy).toHaveBeenCalledTimes(1) expect(queryCacheOnOnlineSpy).not.toHaveBeenCalled() queryCacheOnFocusSpy.mockRestore() mutationCacheResumePausedMutationsSpy.mockRestore() queryCacheOnOnlineSpy.mockRestore() }) test('should notify queryCache and mutationCache if online', async () => { const testClient = new QueryClient() testClient.mount() const queryCacheOnFocusSpy = vi.spyOn( testClient.getQueryCache(), 'onFocus', ) const queryCacheOnOnlineSpy = vi.spyOn( testClient.getQueryCache(), 'onOnline', ) const mutationCacheResumePausedMutationsSpy = vi.spyOn( testClient.getMutationCache(), 'resumePausedMutations', ) onlineManager.setOnline(false) expect(queryCacheOnOnlineSpy).not.toHaveBeenCalled() expect(mutationCacheResumePausedMutationsSpy).not.toHaveBeenCalled() onlineManager.setOnline(true) await vi.advanceTimersByTimeAsync(0) expect(queryCacheOnOnlineSpy).toHaveBeenCalledTimes(1) expect(mutationCacheResumePausedMutationsSpy).toHaveBeenCalledTimes(1) expect(queryCacheOnFocusSpy).not.toHaveBeenCalled() queryCacheOnFocusSpy.mockRestore() queryCacheOnOnlineSpy.mockRestore() mutationCacheResumePausedMutationsSpy.mockRestore() }) test('should resume paused mutations when coming online', async () => { const consoleMock = vi.spyOn(console, 'error') consoleMock.mockImplementation(() => undefined) onlineManager.setOnline(false) const observer1 = new MutationObserver(queryClient, { mutationFn: () => Promise.resolve(1), }) const observer2 = new MutationObserver(queryClient, { mutationFn: () => Promise.resolve(2), }) void observer1.mutate() void observer2.mutate() expect(observer1.getCurrentResult().isPaused).toBeTruthy() expect(observer2.getCurrentResult().isPaused).toBeTruthy() onlineManager.setOnline(true) await vi.advanceTimersByTimeAsync(0) expect(observer1.getCurrentResult().status).toBe('success') expect(observer2.getCurrentResult().status).toBe('success') }) test('should resume paused mutations in parallel', async () => { onlineManager.setOnline(false) const orders: Array = [] const observer1 = new MutationObserver(queryClient, { mutationFn: async () => { orders.push('1start') await sleep(50) orders.push('1end') return 1 }, }) const observer2 = new MutationObserver(queryClient, { mutationFn: async () => { orders.push('2start') await sleep(20) orders.push('2end') return 2 }, }) void observer1.mutate() void observer2.mutate() expect(observer1.getCurrentResult().isPaused).toBeTruthy() expect(observer2.getCurrentResult().isPaused).toBeTruthy() onlineManager.setOnline(true) await vi.advanceTimersByTimeAsync(50) expect(observer1.getCurrentResult().status).toBe('success') expect(observer2.getCurrentResult().status).toBe('success') expect(orders).toEqual(['1start', '2start', '2end', '1end']) }) test('should resume paused mutations one after the other when in the same scope when invoked manually at the same time', async () => { const consoleMock = vi.spyOn(console, 'error') consoleMock.mockImplementation(() => undefined) onlineManager.setOnline(false) const orders: Array = [] const observer1 = new MutationObserver(queryClient, { scope: { id: 'scope', }, mutationFn: async () => { orders.push('1start') await sleep(50) orders.push('1end') return 1 }, }) const observer2 = new MutationObserver(queryClient, { scope: { id: 'scope', }, mutationFn: async () => { orders.push('2start') await sleep(20) orders.push('2end') return 2 }, }) void observer1.mutate() void observer2.mutate() expect(observer1.getCurrentResult().isPaused).toBeTruthy() expect(observer2.getCurrentResult().isPaused).toBeTruthy() onlineManager.setOnline(true) void queryClient.resumePausedMutations() await vi.advanceTimersByTimeAsync(70) expect(observer1.getCurrentResult().status).toBe('success') expect(observer2.getCurrentResult().status).toBe('success') expect(orders).toEqual(['1start', '1end', '2start', '2end']) }) test('should resumePausedMutations when coming online after having called resumePausedMutations while offline', async () => { const consoleMock = vi.spyOn(console, 'error') consoleMock.mockImplementation(() => undefined) onlineManager.setOnline(false) const observer = new MutationObserver(queryClient, { mutationFn: () => Promise.resolve(1), }) void observer.mutate() expect(observer.getCurrentResult().isPaused).toBeTruthy() await queryClient.resumePausedMutations() // still paused because we are still offline expect(observer.getCurrentResult().isPaused).toBeTruthy() onlineManager.setOnline(true) await vi.advanceTimersByTimeAsync(0) expect(observer.getCurrentResult().status).toBe('success') }) test('should resumePausedMutations when coming online after having restored cache (and resumed) while offline', async () => { const consoleMock = vi.spyOn(console, 'error') consoleMock.mockImplementation(() => undefined) onlineManager.setOnline(false) const observer = new MutationObserver(queryClient, { mutationFn: () => Promise.resolve(1), }) void observer.mutate() expect(observer.getCurrentResult().isPaused).toBeTruthy() const state = dehydrate(queryClient) const newQueryClient = new QueryClient({ defaultOptions: { mutations: { mutationFn: () => Promise.resolve(1), }, }, }) newQueryClient.mount() hydrate(newQueryClient, state) // still paused because we are still offline expect( newQueryClient.getMutationCache().getAll()[0]?.state.isPaused, ).toBeTruthy() await newQueryClient.resumePausedMutations() onlineManager.setOnline(true) await vi.advanceTimersByTimeAsync(0) expect(newQueryClient.getMutationCache().getAll()[0]?.state.status).toBe( 'success', ) newQueryClient.unmount() }) test('should notify queryCache after resumePausedMutations has finished when coming online', async () => { const key = queryKey() let count = 0 const results: Array = [] const queryObserver = new QueryObserver(queryClient, { queryKey: key, queryFn: async () => { count++ results.push('data' + count) await sleep(10) return 'data' + count }, }) const unsubscribe = queryObserver.subscribe(() => undefined) await vi.advanceTimersByTimeAsync(10) expect(queryClient.getQueryData(key)).toBe('data1') onlineManager.setOnline(false) const observer = new MutationObserver(queryClient, { mutationFn: async () => { results.push('mutation1-start') await sleep(50) results.push('mutation1-end') return 1 }, }) void observer.mutate() const observer2 = new MutationObserver(queryClient, { scope: { id: 'scope', }, mutationFn: async () => { results.push('mutation2-start') await sleep(50) results.push('mutation2-end') return 2 }, }) void observer2.mutate() const observer3 = new MutationObserver(queryClient, { scope: { id: 'scope', }, mutationFn: async () => { results.push('mutation3-start') await sleep(50) results.push('mutation3-end') return 3 }, }) void observer3.mutate() expect(observer.getCurrentResult().isPaused).toBeTruthy() expect(observer2.getCurrentResult().isPaused).toBeTruthy() expect(observer3.getCurrentResult().isPaused).toBeTruthy() onlineManager.setOnline(true) await vi.advanceTimersByTimeAsync(110) expect(queryClient.getQueryData(key)).toBe('data2') // refetch from coming online should happen after mutations have finished expect(results).toStrictEqual([ 'data1', 'mutation1-start', 'mutation2-start', 'mutation1-end', 'mutation2-end', 'mutation3-start', // 3 starts after 2 because they are in the same scope 'mutation3-end', 'data2', ]) unsubscribe() }) test('should notify queryCache and mutationCache after multiple mounts and single unmount', async () => { const testClient = new QueryClient() testClient.mount() testClient.mount() testClient.unmount() const queryCacheOnFocusSpy = vi.spyOn( testClient.getQueryCache(), 'onFocus', ) const queryCacheOnOnlineSpy = vi.spyOn( testClient.getQueryCache(), 'onOnline', ) const mutationCacheResumePausedMutationsSpy = vi.spyOn( testClient.getMutationCache(), 'resumePausedMutations', ) onlineManager.setOnline(false) onlineManager.setOnline(true) await vi.advanceTimersByTimeAsync(0) expect(queryCacheOnOnlineSpy).toHaveBeenCalledTimes(1) expect(mutationCacheResumePausedMutationsSpy).toHaveBeenCalledTimes(1) focusManager.setFocused(true) await vi.advanceTimersByTimeAsync(0) expect(queryCacheOnFocusSpy).toHaveBeenCalledTimes(1) expect(mutationCacheResumePausedMutationsSpy).toHaveBeenCalledTimes(2) queryCacheOnFocusSpy.mockRestore() queryCacheOnOnlineSpy.mockRestore() mutationCacheResumePausedMutationsSpy.mockRestore() focusManager.setFocused(undefined) onlineManager.setOnline(true) }) test('should not notify queryCache and mutationCache after multiple mounts/unmounts', () => { const testClient = new QueryClient() testClient.mount() testClient.mount() testClient.unmount() testClient.unmount() const queryCacheOnFocusSpy = vi.spyOn( testClient.getQueryCache(), 'onFocus', ) const queryCacheOnOnlineSpy = vi.spyOn( testClient.getQueryCache(), 'onOnline', ) const mutationCacheResumePausedMutationsSpy = vi.spyOn( testClient.getMutationCache(), 'resumePausedMutations', ) onlineManager.setOnline(true) expect(queryCacheOnOnlineSpy).not.toHaveBeenCalled() expect(mutationCacheResumePausedMutationsSpy).not.toHaveBeenCalled() focusManager.setFocused(true) expect(queryCacheOnFocusSpy).not.toHaveBeenCalled() expect(mutationCacheResumePausedMutationsSpy).not.toHaveBeenCalled() queryCacheOnFocusSpy.mockRestore() queryCacheOnOnlineSpy.mockRestore() mutationCacheResumePausedMutationsSpy.mockRestore() focusManager.setFocused(undefined) onlineManager.setOnline(true) }) }) describe('setMutationDefaults', () => { test('should update existing mutation defaults', () => { const key = queryKey() const mutationOptions1 = { mutationFn: () => Promise.resolve('data') } const mutationOptions2 = { retry: false } queryClient.setMutationDefaults(key, mutationOptions1) queryClient.setMutationDefaults(key, mutationOptions2) expect(queryClient.getMutationDefaults(key)).toMatchObject( mutationOptions2, ) }) }) }) ================================================ FILE: packages/query-core/src/__tests__/queryObserver.test-d.tsx ================================================ import { afterEach, beforeEach, describe, expectTypeOf, it } from 'vitest' import { queryKey } from '@tanstack/query-test-utils' import { QueryClient, QueryObserver } from '..' import type { DefaultError } from '..' describe('queryObserver', () => { let queryClient: QueryClient beforeEach(() => { queryClient = new QueryClient() queryClient.mount() }) afterEach(() => { queryClient.clear() }) it('should be inferred as a correct result type', () => { const observer = new QueryObserver(queryClient, { queryKey: queryKey(), queryFn: () => Promise.resolve({ value: 'data' }), }) const result = observer.getCurrentResult() if (result.isPending) { expectTypeOf(result.data).toEqualTypeOf() expectTypeOf(result.error).toEqualTypeOf() expectTypeOf(result.isError).toEqualTypeOf() expectTypeOf(result.isPending).toEqualTypeOf() expectTypeOf(result.isLoading).toEqualTypeOf() expectTypeOf(result.isLoadingError).toEqualTypeOf() expectTypeOf(result.isRefetchError).toEqualTypeOf() expectTypeOf(result.status).toEqualTypeOf<'pending'>() expectTypeOf(result.isPlaceholderData).toEqualTypeOf() } if (result.isLoading) { expectTypeOf(result.data).toEqualTypeOf() expectTypeOf(result.error).toEqualTypeOf() expectTypeOf(result.isError).toEqualTypeOf() expectTypeOf(result.isPending).toEqualTypeOf() expectTypeOf(result.isLoading).toEqualTypeOf() expectTypeOf(result.isLoadingError).toEqualTypeOf() expectTypeOf(result.isRefetchError).toEqualTypeOf() expectTypeOf(result.isSuccess).toEqualTypeOf() expectTypeOf(result.status).toEqualTypeOf<'pending'>() expectTypeOf(result.isPlaceholderData).toEqualTypeOf() } if (result.isLoadingError) { expectTypeOf(result.data).toEqualTypeOf() expectTypeOf(result.error).toEqualTypeOf() expectTypeOf(result.isError).toEqualTypeOf() expectTypeOf(result.isPending).toEqualTypeOf() expectTypeOf(result.isLoading).toEqualTypeOf() expectTypeOf(result.isLoadingError).toEqualTypeOf() expectTypeOf(result.isRefetchError).toEqualTypeOf() expectTypeOf(result.isSuccess).toEqualTypeOf() expectTypeOf(result.status).toEqualTypeOf<'error'>() expectTypeOf(result.isPlaceholderData).toEqualTypeOf() } if (result.isRefetchError) { expectTypeOf(result.data).toEqualTypeOf<{ value: string }>() expectTypeOf(result.error).toEqualTypeOf() expectTypeOf(result.isError).toEqualTypeOf() expectTypeOf(result.isPending).toEqualTypeOf() expectTypeOf(result.isLoading).toEqualTypeOf() expectTypeOf(result.isLoadingError).toEqualTypeOf() expectTypeOf(result.isRefetchError).toEqualTypeOf() expectTypeOf(result.isSuccess).toEqualTypeOf() expectTypeOf(result.status).toEqualTypeOf<'error'>() expectTypeOf(result.isPlaceholderData).toEqualTypeOf() } if (result.isSuccess) { expectTypeOf(result.data).toEqualTypeOf<{ value: string }>() expectTypeOf(result.error).toEqualTypeOf() expectTypeOf(result.isError).toEqualTypeOf() expectTypeOf(result.isPending).toEqualTypeOf() expectTypeOf(result.isLoading).toEqualTypeOf() expectTypeOf(result.isLoadingError).toEqualTypeOf() expectTypeOf(result.isRefetchError).toEqualTypeOf() expectTypeOf(result.isSuccess).toEqualTypeOf() expectTypeOf(result.status).toEqualTypeOf<'success'>() expectTypeOf(result.isPlaceholderData).toEqualTypeOf() } if (result.isPlaceholderData) { expectTypeOf(result.data).toEqualTypeOf<{ value: string }>() expectTypeOf(result.error).toEqualTypeOf() expectTypeOf(result.isError).toEqualTypeOf() expectTypeOf(result.isPending).toEqualTypeOf() expectTypeOf(result.isLoading).toEqualTypeOf() expectTypeOf(result.isLoadingError).toEqualTypeOf() expectTypeOf(result.isRefetchError).toEqualTypeOf() expectTypeOf(result.isSuccess).toEqualTypeOf() expectTypeOf(result.status).toEqualTypeOf<'success'>() expectTypeOf(result.isPlaceholderData).toEqualTypeOf() } }) describe('placeholderData', () => { it('previousQuery should have typed queryKey', () => { const testQueryKey = ['SomeQuery', 42, { foo: 'bar' }] as const new QueryObserver(new QueryClient(), { queryKey: testQueryKey, placeholderData: (_, previousQuery) => { if (previousQuery) { expectTypeOf(previousQuery.queryKey).toEqualTypeOf< typeof testQueryKey >() } }, }) }) it('previousQuery should have typed error', () => { class CustomError extends Error { name = 'CustomError' as const } new QueryObserver(new QueryClient(), { queryKey: ['key'], placeholderData: (_, previousQuery) => { if (previousQuery) { expectTypeOf( previousQuery.state.error, ).toEqualTypeOf() } return undefined }, }) }) it('previousData should have the same type as query data', () => { const queryData = { foo: 'bar' } as const new QueryObserver(new QueryClient(), { queryKey: ['key'], queryFn: () => queryData, select: (data) => data.foo, placeholderData: (previousData) => { expectTypeOf(previousData).toEqualTypeOf< typeof queryData | undefined >() return undefined }, }) }) }) }) ================================================ FILE: packages/query-core/src/__tests__/queryObserver.test.tsx ================================================ import { afterEach, beforeEach, describe, expect, expectTypeOf, it, test, vi, } from 'vitest' import { queryKey, sleep } from '@tanstack/query-test-utils' import { QueryClient, QueryObserver, focusManager } from '..' import type { QueryObserverResult } from '..' describe('queryObserver', () => { let queryClient: QueryClient beforeEach(() => { vi.useFakeTimers() queryClient = new QueryClient({ defaultOptions: { queries: { experimental_prefetchInRender: true, }, }, }) queryClient.mount() }) afterEach(() => { queryClient.clear() vi.useRealTimers() }) test('should trigger a fetch when subscribed', () => { const key = queryKey() const queryFn = vi .fn<(...args: Array) => string>() .mockReturnValue('data') const observer = new QueryObserver(queryClient, { queryKey: key, queryFn }) const unsubscribe = observer.subscribe(() => undefined) unsubscribe() expect(queryFn).toHaveBeenCalledTimes(1) }) test('should be able to read latest data after subscribing', () => { const key = queryKey() queryClient.setQueryData(key, 'data') const observer = new QueryObserver(queryClient, { queryKey: key, enabled: false, }) const unsubscribe = observer.subscribe(vi.fn()) expect(observer.getCurrentResult()).toMatchObject({ status: 'success', data: 'data', }) unsubscribe() }) describe('enabled is a callback that initially returns false', () => { let observer: QueryObserver> let enabled: boolean let count: number let key: Array beforeEach(() => { key = queryKey() count = 0 enabled = false observer = new QueryObserver(queryClient, { queryKey: key, staleTime: Infinity, enabled: () => enabled, queryFn: async () => { await sleep(10) count++ return 'data' }, }) }) test('should not fetch on mount', () => { const unsubscribe = observer.subscribe(vi.fn()) // Has not fetched and is not fetching since its disabled expect(count).toBe(0) expect(observer.getCurrentResult()).toMatchObject({ status: 'pending', fetchStatus: 'idle', data: undefined, }) unsubscribe() }) test('should not be re-fetched when invalidated with refetchType: all', async () => { const unsubscribe = observer.subscribe(vi.fn()) queryClient.invalidateQueries({ queryKey: key, refetchType: 'all' }) // So we still expect it to not have fetched and not be fetching expect(count).toBe(0) expect(observer.getCurrentResult()).toMatchObject({ status: 'pending', fetchStatus: 'idle', data: undefined, }) await vi.advanceTimersByTimeAsync(10) expect(count).toBe(0) unsubscribe() }) test('should still trigger a fetch when refetch is called', async () => { const unsubscribe = observer.subscribe(vi.fn()) expect(enabled).toBe(false) // Not the same with explicit refetch, this will override enabled and trigger a fetch anyway observer.refetch() expect(observer.getCurrentResult()).toMatchObject({ status: 'pending', fetchStatus: 'fetching', data: undefined, }) await vi.advanceTimersByTimeAsync(10) expect(count).toBe(1) expect(observer.getCurrentResult()).toMatchObject({ status: 'success', fetchStatus: 'idle', data: 'data', }) unsubscribe() }) test('should fetch if unsubscribed, then enabled returns true, and then re-subscribed', async () => { let unsubscribe = observer.subscribe(vi.fn()) expect(observer.getCurrentResult()).toMatchObject({ status: 'pending', fetchStatus: 'idle', data: undefined, }) unsubscribe() enabled = true unsubscribe = observer.subscribe(vi.fn()) expect(observer.getCurrentResult()).toMatchObject({ status: 'pending', fetchStatus: 'fetching', data: undefined, }) await vi.advanceTimersByTimeAsync(10) expect(count).toBe(1) unsubscribe() }) test('should not be re-fetched if not subscribed to after enabled was toggled to true (fetchStatus: "idle")', () => { const unsubscribe = observer.subscribe(vi.fn()) // Toggle enabled enabled = true unsubscribe() queryClient.invalidateQueries({ queryKey: key, refetchType: 'active' }) expect(observer.getCurrentResult()).toMatchObject({ status: 'pending', fetchStatus: 'idle', data: undefined, }) expect(count).toBe(0) }) test('should not be re-fetched if not subscribed to after enabled was toggled to true (fetchStatus: "fetching")', async () => { const unsubscribe = observer.subscribe(vi.fn()) // Toggle enabled enabled = true queryClient.invalidateQueries({ queryKey: key, refetchType: 'active' }) expect(observer.getCurrentResult()).toMatchObject({ status: 'pending', fetchStatus: 'fetching', data: undefined, }) await vi.advanceTimersByTimeAsync(10) expect(count).toBe(1) unsubscribe() }) test('should handle that the enabled callback updates the return value', async () => { const unsubscribe = observer.subscribe(vi.fn()) // Toggle enabled enabled = true queryClient.invalidateQueries({ queryKey: key, refetchType: 'inactive' }) // should not refetch since it was active and we only refetch inactive await vi.advanceTimersByTimeAsync(10) expect(count).toBe(0) queryClient.invalidateQueries({ queryKey: key, refetchType: 'active' }) // should refetch since it was active and we refetch active await vi.advanceTimersByTimeAsync(10) expect(count).toBe(1) // Toggle enabled enabled = false // should not refetch since it is not active and we only refetch active queryClient.invalidateQueries({ queryKey: key, refetchType: 'active' }) await vi.advanceTimersByTimeAsync(10) expect(count).toBe(1) unsubscribe() }) }) test('should be able to read latest data when re-subscribing (but not re-fetching)', async () => { const key = queryKey() let count = 0 const observer = new QueryObserver(queryClient, { queryKey: key, staleTime: Infinity, queryFn: async () => { await sleep(10) count++ return 'data' }, }) let unsubscribe = observer.subscribe(vi.fn()) // unsubscribe before data comes in unsubscribe() expect(count).toBe(0) expect(observer.getCurrentResult()).toMatchObject({ status: 'pending', fetchStatus: 'fetching', data: undefined, }) await vi.advanceTimersByTimeAsync(10) expect(count).toBe(1) // re-subscribe after data comes in unsubscribe = observer.subscribe(vi.fn()) expect(observer.getCurrentResult()).toMatchObject({ status: 'success', data: 'data', }) unsubscribe() }) test('should notify when switching query', async () => { const key1 = queryKey() const key2 = queryKey() const results: Array = [] const observer = new QueryObserver(queryClient, { queryKey: key1, queryFn: () => 1, }) const unsubscribe = observer.subscribe((result) => { results.push(result) }) await vi.advanceTimersByTimeAsync(0) observer.setOptions({ queryKey: key2, queryFn: () => 2 }) await vi.advanceTimersByTimeAsync(0) unsubscribe() expect(results.length).toBe(4) expect(results[0]).toMatchObject({ data: undefined, status: 'pending' }) expect(results[1]).toMatchObject({ data: 1, status: 'success' }) expect(results[2]).toMatchObject({ data: undefined, status: 'pending' }) expect(results[3]).toMatchObject({ data: 2, status: 'success' }) }) test('should be able to fetch with a selector', async () => { const key = queryKey() const observer = new QueryObserver(queryClient, { queryKey: key, queryFn: () => ({ count: 1 }), select: (data) => ({ myCount: data.count }), }) let observerResult const unsubscribe = observer.subscribe((result) => { expectTypeOf(result).toEqualTypeOf< QueryObserverResult<{ myCount: number }> >() observerResult = result }) await vi.advanceTimersByTimeAsync(0) unsubscribe() expect(observerResult).toMatchObject({ data: { myCount: 1 } }) }) test('should be able to fetch with a selector using the fetch method', async () => { const key = queryKey() const observer = new QueryObserver(queryClient, { queryKey: key, queryFn: () => ({ count: 1 }), select: (data) => ({ myCount: data.count }), }) const observerResult = await observer.refetch() expectTypeOf(observerResult.data).toEqualTypeOf< { myCount: number } | undefined >() expect(observerResult.data).toMatchObject({ myCount: 1 }) }) test('should be able to fetch with a selector and object syntax', async () => { const key = queryKey() const observer = new QueryObserver(queryClient, { queryKey: key, queryFn: () => ({ count: 1 }), select: (data) => ({ myCount: data.count }), }) let observerResult const unsubscribe = observer.subscribe((result) => { observerResult = result }) await vi.advanceTimersByTimeAsync(0) unsubscribe() expect(observerResult).toMatchObject({ data: { myCount: 1 } }) }) test('should run the selector again if the data changed', async () => { const key = queryKey() let count = 0 const observer = new QueryObserver(queryClient, { queryKey: key, queryFn: () => ({ count }), select: (data) => { count++ return { myCount: data.count } }, }) const observerResult1 = await observer.refetch() const observerResult2 = await observer.refetch() expect(count).toBe(2) expect(observerResult1.data).toMatchObject({ myCount: 0 }) expect(observerResult2.data).toMatchObject({ myCount: 1 }) }) test('should run the selector again if the selector changed', async () => { const key = queryKey() let count = 0 const results: Array = [] const queryFn = () => ({ count: 1 }) const select1 = (data: ReturnType) => { count++ return { myCount: data.count } } const select2 = (_data: ReturnType) => { count++ return { myCount: 99 } } const observer = new QueryObserver(queryClient, { queryKey: key, queryFn, select: select1, }) const unsubscribe = observer.subscribe((result) => { results.push(result) }) await vi.advanceTimersByTimeAsync(0) observer.setOptions({ queryKey: key, queryFn, select: select2, }) await observer.refetch() unsubscribe() expect(count).toBe(2) expect(results.length).toBe(5) expect(results[0]).toMatchObject({ status: 'pending', isFetching: true, data: undefined, }) expect(results[1]).toMatchObject({ status: 'success', isFetching: false, data: { myCount: 1 }, }) expect(results[2]).toMatchObject({ status: 'success', isFetching: false, data: { myCount: 99 }, }) expect(results[3]).toMatchObject({ status: 'success', isFetching: true, data: { myCount: 99 }, }) expect(results[4]).toMatchObject({ status: 'success', isFetching: false, data: { myCount: 99 }, }) }) test('should not run the selector again if the data and selector did not change', async () => { const key = queryKey() let count = 0 const results: Array = [] const queryFn = () => ({ count: 1 }) const select = (data: ReturnType) => { count++ return { myCount: data.count } } const observer = new QueryObserver(queryClient, { queryKey: key, queryFn, select, }) const unsubscribe = observer.subscribe((result) => { results.push(result) }) await vi.advanceTimersByTimeAsync(0) observer.setOptions({ queryKey: key, queryFn, select, }) await observer.refetch() unsubscribe() expect(count).toBe(1) expect(results.length).toBe(4) expect(results[0]).toMatchObject({ status: 'pending', isFetching: true, data: undefined, }) expect(results[1]).toMatchObject({ status: 'success', isFetching: false, data: { myCount: 1 }, }) expect(results[2]).toMatchObject({ status: 'success', isFetching: true, data: { myCount: 1 }, }) expect(results[3]).toMatchObject({ status: 'success', isFetching: false, data: { myCount: 1 }, }) }) test('should not run the selector again if the data did not change', async () => { const key = queryKey() let count = 0 const observer = new QueryObserver(queryClient, { queryKey: key, queryFn: () => ({ count: 1 }), select: (data) => { count++ return { myCount: data.count } }, }) const observerResult1 = await observer.refetch() const observerResult2 = await observer.refetch() expect(count).toBe(1) expect(observerResult1.data).toMatchObject({ myCount: 1 }) expect(observerResult2.data).toMatchObject({ myCount: 1 }) }) test('should always run the selector again if selector throws an error and selector is not referentially stable', async () => { const key = queryKey() const results: Array = [] const queryFn = async () => { await sleep(10) return { count: 1 } } const observer = new QueryObserver(queryClient, { queryKey: key, queryFn, select: () => { throw new Error('selector error') }, }) const unsubscribe = observer.subscribe((result) => { results.push(result) }) await vi.advanceTimersByTimeAsync(10) observer.refetch() await vi.advanceTimersByTimeAsync(10) unsubscribe() expect(results[0]).toMatchObject({ status: 'pending', isFetching: true, data: undefined, }) expect(results[1]).toMatchObject({ status: 'error', isFetching: false, data: undefined, }) expect(results[2]).toMatchObject({ status: 'error', isFetching: true, data: undefined, }) expect(results[3]).toMatchObject({ status: 'error', isFetching: false, data: undefined, }) }) test('should return stale data if selector throws an error', async () => { const key = queryKey() const results: Array = [] let shouldError = false const error = new Error('select error') const observer = new QueryObserver(queryClient, { queryKey: key, retry: 0, queryFn: async () => { await sleep(10) return shouldError ? 2 : 1 }, select: (num) => { if (shouldError) { throw error } shouldError = true return String(num) }, }) const unsubscribe = observer.subscribe((result) => { results.push(result) }) await vi.advanceTimersByTimeAsync(10) observer.refetch() await vi.advanceTimersByTimeAsync(10) unsubscribe() expect(results[0]).toMatchObject({ status: 'pending', isFetching: true, data: undefined, error: null, }) expect(results[1]).toMatchObject({ status: 'success', isFetching: false, data: '1', error: null, }) expect(results[2]).toMatchObject({ status: 'success', isFetching: true, data: '1', error: null, }) expect(results[3]).toMatchObject({ status: 'error', isFetching: false, data: '1', error, }) }) test('should structurally share the selector', async () => { const key = queryKey() let count = 0 const observer = new QueryObserver(queryClient, { queryKey: key, queryFn: () => ({ count: ++count }), select: () => ({ myCount: 1 }), }) const observerResult1 = await observer.refetch() const observerResult2 = await observer.refetch() expect(count).toBe(2) expect(observerResult1.data).toBe(observerResult2.data) }) test('should not trigger a fetch when subscribed and disabled', async () => { const key = queryKey() const queryFn = vi .fn<(...args: Array) => string>() .mockReturnValue('data') const observer = new QueryObserver(queryClient, { queryKey: key, queryFn, enabled: false, }) const unsubscribe = observer.subscribe(() => undefined) await vi.advanceTimersByTimeAsync(0) unsubscribe() expect(queryFn).toHaveBeenCalledTimes(0) }) test('should not trigger a fetch when subscribed and disabled by callback', async () => { const key = queryKey() const queryFn = vi .fn<(...args: Array) => string>() .mockReturnValue('data') const observer = new QueryObserver(queryClient, { queryKey: key, queryFn, enabled: () => false, }) const unsubscribe = observer.subscribe(() => undefined) await vi.advanceTimersByTimeAsync(0) unsubscribe() expect(queryFn).toHaveBeenCalledTimes(0) }) test('should not trigger a fetch when not subscribed', async () => { const key = queryKey() const queryFn = vi .fn<(...args: Array) => string>() .mockReturnValue('data') new QueryObserver(queryClient, { queryKey: key, queryFn }) await vi.advanceTimersByTimeAsync(0) expect(queryFn).toHaveBeenCalledTimes(0) }) test('should be able to watch a query without defining a query function', async () => { const key = queryKey() const queryFn = vi .fn<(...args: Array) => string>() .mockReturnValue('data') const callback = vi.fn() const observer = new QueryObserver(queryClient, { queryKey: key, enabled: false, }) const unsubscribe = observer.subscribe(callback) await queryClient.fetchQuery({ queryKey: key, queryFn }) await vi.advanceTimersByTimeAsync(0) unsubscribe() expect(queryFn).toHaveBeenCalledTimes(1) expect(callback).toHaveBeenCalledTimes(2) }) test('should accept unresolved query config in update function', async () => { const key = queryKey() const queryFn = vi .fn<(...args: Array) => string>() .mockReturnValue('data') const observer = new QueryObserver(queryClient, { queryKey: key, enabled: false, }) const results: Array> = [] const unsubscribe = observer.subscribe((x) => { results.push(x) }) observer.setOptions({ queryKey: key, enabled: false, staleTime: 10 }) await queryClient.fetchQuery({ queryKey: key, queryFn }) await vi.advanceTimersByTimeAsync(0) unsubscribe() expect(queryFn).toHaveBeenCalledTimes(1) expect(results.length).toBe(2) expect(results[0]).toMatchObject({ isStale: false, data: undefined }) expect(results[1]).toMatchObject({ isStale: false, data: 'data' }) }) test('should be able to handle multiple subscribers', async () => { const key = queryKey() const queryFn = vi .fn<(...args: Array) => string>() .mockReturnValue('data') const observer = new QueryObserver(queryClient, { queryKey: key, enabled: false, }) const results1: Array> = [] const results2: Array> = [] const unsubscribe1 = observer.subscribe((x) => { results1.push(x) }) const unsubscribe2 = observer.subscribe((x) => { results2.push(x) }) await queryClient.fetchQuery({ queryKey: key, queryFn }) await vi.advanceTimersByTimeAsync(0) unsubscribe1() unsubscribe2() expect(queryFn).toHaveBeenCalledTimes(1) expect(results1.length).toBe(2) expect(results2.length).toBe(2) expect(results1[0]).toMatchObject({ data: undefined }) expect(results1[1]).toMatchObject({ data: 'data' }) expect(results2[0]).toMatchObject({ data: undefined }) expect(results2[1]).toMatchObject({ data: 'data' }) }) test('should stop retry when unsubscribing', async () => { const key = queryKey() let count = 0 const observer = new QueryObserver(queryClient, { queryKey: key, queryFn: () => { count++ return Promise.reject('reject') }, retry: 10, retryDelay: 50, }) const unsubscribe = observer.subscribe(() => undefined) await vi.advanceTimersByTimeAsync(50) unsubscribe() await vi.advanceTimersByTimeAsync(50) expect(count).toBe(2) }) test('should clear interval when unsubscribing to a refetchInterval query', async () => { const key = queryKey() let count = 0 const fetchData = () => { count++ return Promise.resolve('data') } const observer = new QueryObserver(queryClient, { queryKey: key, queryFn: fetchData, gcTime: 0, refetchInterval: 10, }) const unsubscribe = observer.subscribe(() => undefined) expect(count).toBe(1) await vi.advanceTimersByTimeAsync(10) expect(count).toBe(2) unsubscribe() await vi.advanceTimersByTimeAsync(10) expect(queryClient.getQueryCache().find({ queryKey: key })).toBeUndefined() expect(count).toBe(2) }) test('uses placeholderData as non-cache data when pending a query with no data', async () => { const key = queryKey() const observer = new QueryObserver(queryClient, { queryKey: key, queryFn: () => 'data', placeholderData: 'placeholder', }) expect(observer.getCurrentResult()).toMatchObject({ status: 'success', data: 'placeholder', }) const results: Array> = [] const unsubscribe = observer.subscribe((x) => { results.push(x) }) await vi.advanceTimersByTimeAsync(0) unsubscribe() expect(results.length).toBe(2) expect(results[0]).toMatchObject({ status: 'success', data: 'placeholder' }) expect(results[1]).toMatchObject({ status: 'success', data: 'data' }) }) test('should structurally share placeholder data', () => { const key = queryKey() const observer = new QueryObserver(queryClient, { queryKey: key, enabled: false, queryFn: () => 'data', placeholderData: {}, }) const firstData = observer.getCurrentResult().data observer.setOptions({ queryKey: key, placeholderData: {} }) const secondData = observer.getCurrentResult().data expect(firstData).toBe(secondData) }) test('should throw an error if enabled option type is not valid', () => { const key = queryKey() expect( () => new QueryObserver(queryClient, { queryKey: key, queryFn: () => 'data', // @ts-expect-error enabled: null, }), ).toThrowError('Expected enabled to be a boolean') }) test('getCurrentQuery should return the current query', () => { const key = queryKey() const observer = new QueryObserver(queryClient, { queryKey: key, queryFn: () => 'data', }) expect(observer.getCurrentQuery().queryKey).toEqual(key) }) test('should throw an error if throwOnError option is true', async () => { const key = queryKey() const observer = new QueryObserver(queryClient, { queryKey: key, queryFn: () => Promise.reject('error'), retry: false, }) let error: string | null = null try { await observer.refetch({ throwOnError: true }) } catch (err) { error = err as string } expect(error).toEqual('error') }) test('should not refetch in background if refetchIntervalInBackground is false', async () => { const key = queryKey() const queryFn = vi .fn<(...args: Array) => string>() .mockReturnValue('data') focusManager.setFocused(false) const observer = new QueryObserver(queryClient, { queryKey: key, queryFn, refetchIntervalInBackground: false, refetchInterval: 10, }) const unsubscribe = observer.subscribe(() => undefined) await vi.advanceTimersByTimeAsync(30) expect(queryFn).toHaveBeenCalledTimes(1) // Clean-up unsubscribe() focusManager.setFocused(true) }) test('should not use replaceEqualDeep for select value when structuralSharing option is true', async () => { const key = queryKey() const data = { value: 'data' } const selectedData = { value: 'data' } const observer = new QueryObserver(queryClient, { queryKey: key, queryFn: () => data, select: () => data, }) const unsubscribe = observer.subscribe(() => undefined) await vi.advanceTimersByTimeAsync(0) expect(observer.getCurrentResult().data).toBe(data) observer.setOptions({ queryKey: key, queryFn: () => data, structuralSharing: false, select: () => selectedData, }) await observer.refetch() expect(observer.getCurrentResult().data).toBe(selectedData) unsubscribe() }) test('should not use replaceEqualDeep for select value when structuralSharing option is true and placeholderData is defined', () => { const key = queryKey() const data = { value: 'data' } const selectedData1 = { value: 'data' } const selectedData2 = { value: 'data' } const placeholderData1 = { value: 'data' } const placeholderData2 = { value: 'data' } const observer = new QueryObserver(queryClient, { queryKey: key, queryFn: () => data, select: () => data, }) observer.setOptions({ queryKey: key, queryFn: () => data, select: () => { return selectedData1 }, placeholderData: placeholderData1, }) observer.setOptions({ queryKey: key, queryFn: () => data, select: () => { return selectedData2 }, placeholderData: placeholderData2, structuralSharing: false, }) expect(observer.getCurrentResult().data).toBe(selectedData2) }) test('should pass the correct previous queryKey (from prevQuery) to placeholderData function params with select', async () => { const results: Array = [] const keys: Array | null> = [] const key1 = queryKey() const key2 = queryKey() const data1 = { value: 'data1' } const data2 = { value: 'data2' } const observer = new QueryObserver(queryClient, { queryKey: key1, queryFn: () => data1, placeholderData: (prev, prevQuery) => { keys.push(prevQuery?.queryKey || null) return prev }, select: (data) => data.value, }) const unsubscribe = observer.subscribe((result) => { results.push(result) }) await vi.advanceTimersByTimeAsync(0) observer.setOptions({ queryKey: key2, queryFn: () => data2, placeholderData: (prev, prevQuery) => { keys.push(prevQuery?.queryKey || null) return prev }, select: (data) => data.value, }) await vi.advanceTimersByTimeAsync(0) unsubscribe() expect(results.length).toBe(4) expect(keys.length).toBe(3) expect(keys[0]).toBe(null) // First Query - status: 'pending', fetchStatus: 'idle' expect(keys[1]).toBe(null) // First Query - status: 'pending', fetchStatus: 'fetching' expect(keys[2]).toBe(key1) // Second Query - status: 'pending', fetchStatus: 'fetching' expect(results[0]).toMatchObject({ data: undefined, status: 'pending', fetchStatus: 'fetching', }) // Initial fetch expect(results[1]).toMatchObject({ data: 'data1', status: 'success', fetchStatus: 'idle', }) // Successful fetch expect(results[2]).toMatchObject({ data: 'data1', status: 'success', fetchStatus: 'fetching', }) // Fetch for new key, but using previous data as placeholder expect(results[3]).toMatchObject({ data: 'data2', status: 'success', fetchStatus: 'idle', }) // Successful fetch for new key }) test('should pass the correct previous data to placeholderData function params when select function is used in conjunction', async () => { const results: Array = [] const key1 = queryKey() const key2 = queryKey() const data1 = { value: 'data1' } const data2 = { value: 'data2' } let selectCount = 0 const observer = new QueryObserver(queryClient, { queryKey: key1, queryFn: () => data1, placeholderData: (prev) => prev, select: (data) => { selectCount++ return data.value }, }) const unsubscribe = observer.subscribe((result) => { results.push(result) }) await vi.advanceTimersByTimeAsync(0) observer.setOptions({ queryKey: key2, queryFn: () => data2, placeholderData: (prev) => prev, select: (data) => { selectCount++ return data.value }, }) await vi.advanceTimersByTimeAsync(0) unsubscribe() expect(results.length).toBe(4) expect(results[0]).toMatchObject({ data: undefined, status: 'pending', fetchStatus: 'fetching', }) // Initial fetch expect(results[1]).toMatchObject({ data: 'data1', status: 'success', fetchStatus: 'idle', }) // Successful fetch expect(results[2]).toMatchObject({ data: 'data1', status: 'success', fetchStatus: 'fetching', }) // Fetch for new key, but using previous data as placeholder expect(results[3]).toMatchObject({ data: 'data2', status: 'success', fetchStatus: 'idle', }) // Successful fetch for new key // it's 3 because select is an inline function expect(selectCount).toBe(3) }) test('should use cached selectResult when switching between queries and placeholderData returns previousData', async () => { const results: Array = [] const key1 = queryKey() const key2 = queryKey() const data1 = { value: 'data1' } const data2 = { value: 'data2' } const stableSelect = vi.fn((data: { value: string }) => data.value) const observer = new QueryObserver(queryClient, { queryKey: key1, queryFn: () => data1, placeholderData: (prev) => prev, select: stableSelect, }) const unsubscribe = observer.subscribe((result) => { results.push(result) }) await vi.advanceTimersByTimeAsync(0) observer.setOptions({ queryKey: key2, queryFn: () => data2, placeholderData: (prev) => prev, select: stableSelect, }) await vi.advanceTimersByTimeAsync(0) unsubscribe() expect(results.length).toBe(4) expect(results[0]).toMatchObject({ data: undefined, status: 'pending', fetchStatus: 'fetching', }) // Initial fetch expect(results[1]).toMatchObject({ data: 'data1', status: 'success', fetchStatus: 'idle', }) // Successful fetch expect(results[2]).toMatchObject({ data: 'data1', status: 'success', fetchStatus: 'fetching', }) // Fetch for new key, but using previous data as placeholder expect(results[3]).toMatchObject({ data: 'data2', status: 'success', fetchStatus: 'idle', }) // Successful fetch for new key expect(stableSelect).toHaveBeenCalledTimes(2) expect(stableSelect.mock.calls[0]![0]).toEqual(data1) expect(stableSelect.mock.calls[1]![0]).toEqual(data2) }) test('setOptions should notify cache listeners', () => { const key = queryKey() const observer = new QueryObserver(queryClient, { queryKey: key, enabled: false, }) const spy = vi.fn() const unsubscribe = queryClient.getQueryCache().subscribe(spy) observer.setOptions({ queryKey: key, enabled: false, refetchInterval: 10 }) expect(spy).toHaveBeenCalledTimes(1) expect(spy).toHaveBeenCalledWith( expect.objectContaining({ type: 'observerOptionsUpdated' }), ) unsubscribe() }) test('disabled observers should not be stale', () => { const key = queryKey() const observer = new QueryObserver(queryClient, { queryKey: key, enabled: false, }) const result = observer.getCurrentResult() expect(result.isStale).toBe(false) }) test('should allow staleTime as a function', async () => { const key = queryKey() const observer = new QueryObserver(queryClient, { queryKey: key, queryFn: async () => { await sleep(5) return { data: 'data', staleTime: 20, } }, staleTime: (query) => query.state.data?.staleTime ?? 0, }) const results: Array> = [] const unsubscribe = observer.subscribe((x) => { if (x.data) { results.push(x) } }) await vi.advanceTimersByTimeAsync(25) expect(results[0]?.isStale).toBe(false) await vi.advanceTimersByTimeAsync(1) expect(results[1]?.isStale).toBe(true) unsubscribe() }) test('should not see queries as stale is staleTime is Static', async () => { const key = queryKey() const observer = new QueryObserver(queryClient, { queryKey: key, queryFn: async () => { await sleep(5) return { data: 'data', } }, staleTime: 'static', }) const result = observer.getCurrentResult() expect(result.isStale).toBe(true) // no data = stale const results: Array> = [] const unsubscribe = observer.subscribe((x) => { if (x.data) { results.push(x) } }) await vi.advanceTimersByTimeAsync(5) expect(results[0]?.isStale).toBe(false) unsubscribe() }) test('should return a promise that resolves when data is present', async () => { const results: Array = [] const key = queryKey() let count = 0 const observer = new QueryObserver(queryClient, { queryKey: key, queryFn: () => { if (++count > 9) { return Promise.resolve('data') } throw new Error('rejected') }, retry: 10, retryDelay: 0, }) const unsubscribe = observer.subscribe(() => { results.push(observer.getCurrentResult()) }) await vi.advanceTimersByTimeAsync(8) expect(results.at(-1)?.data).toBe('data') const numberOfUniquePromises = new Set( results.map((result) => result.promise), ).size expect(numberOfUniquePromises).toBe(1) unsubscribe() }) test('should return a new promise after recovering from an error', async () => { const results: Array = [] const key = queryKey() let succeeds = false let idx = 0 const observer = new QueryObserver(queryClient, { queryKey: key, queryFn: () => { if (succeeds) { return Promise.resolve('data') } throw new Error(`rejected #${++idx}`) }, retry: 5, retryDelay: 0, }) const unsubscribe = observer.subscribe(() => { results.push(observer.getCurrentResult()) }) await vi.advanceTimersByTimeAsync(4) expect(results.at(-1)?.status).toBe('error') expect( results.every((result) => result.promise === results[0]!.promise), ).toBe(true) { // fail again const lengthBefore = results.length observer.refetch() await vi.advanceTimersByTimeAsync(4) expect(results.length).toBeGreaterThan(lengthBefore) expect(results.at(-1)?.status).toBe('error') const numberOfUniquePromises = new Set( results.map((result) => result.promise), ).size expect(numberOfUniquePromises).toBe(2) } { // succeed succeeds = true observer.refetch() await vi.advanceTimersByTimeAsync(0) results.at(-1)?.status === 'success' const numberOfUniquePromises = new Set( results.map((result) => result.promise), ).size expect(numberOfUniquePromises).toBe(3) } unsubscribe() }) test('shouldFetchOnWindowFocus should respect refetchOnWindowFocus option', () => { const key = queryKey() const observer1 = new QueryObserver(queryClient, { queryKey: key, queryFn: () => 'data', refetchOnWindowFocus: true, }) expect(observer1.shouldFetchOnWindowFocus()).toBe(true) const observer2 = new QueryObserver(queryClient, { queryKey: key, queryFn: () => 'data', refetchOnWindowFocus: false, }) expect(observer2.shouldFetchOnWindowFocus()).toBe(false) }) test('fetchOptimistic should fetch and return optimistic result', async () => { const key = queryKey() const observer = new QueryObserver(queryClient, { queryKey: key, queryFn: () => 'data', }) const result = await observer.fetchOptimistic({ queryKey: key, queryFn: () => 'data', }) expect(result.status).toBe('success') expect(result.data).toBe('data') }) test('should track error prop when throwOnError is true', async () => { const key = queryKey() const results: Array = [] const observer = new QueryObserver(queryClient, { queryKey: key, queryFn: () => Promise.reject('error'), retry: false, throwOnError: true, }) const trackedResult = observer.trackResult( observer.getCurrentResult(), (prop) => { if (prop === 'data' || prop === 'status') { observer.trackProp(prop) } }, ) trackedResult.data trackedResult.status const unsubscribe = observer.subscribe((result) => { results.push(result) }) await vi.advanceTimersByTimeAsync(0) const lastResult = results[results.length - 1] expect(lastResult?.status).toBe('error') expect(results.length).toBe(1) expect(results[0]).toMatchObject({ status: 'error', error: 'error', }) unsubscribe() }) test('should not refetchOnMount when set to "always" when staleTime is Static', async () => { const key = queryKey() const queryFn = vi.fn(() => 'data') queryClient.setQueryData(key, 'initial') const observer = new QueryObserver(queryClient, { queryKey: key, queryFn, staleTime: 'static', refetchOnMount: 'always', }) const unsubscribe = observer.subscribe(() => undefined) await vi.advanceTimersByTimeAsync(0) expect(queryFn).toHaveBeenCalledTimes(0) unsubscribe() }) test('should set fetchStatus to idle when _optimisticResults is isRestoring', () => { const key = queryKey() const observer = new QueryObserver(queryClient, { queryKey: key, queryFn: () => 'data', _optimisticResults: 'isRestoring', }) const result = observer.getCurrentResult() expect(result.fetchStatus).toBe('idle') }) test('should return isEnabled depending on enabled being resolved', () => { const key = queryKey() const observer = new QueryObserver(queryClient, { queryKey: key, queryFn: () => 'data', enabled: () => false, }) const result = observer.getCurrentResult() expect(result.isEnabled).toBe(false) }) test('should return isEnabled as true per default', () => { const key = queryKey() const observer = new QueryObserver(queryClient, { queryKey: key, queryFn: () => 'data', }) const result = observer.getCurrentResult() expect(result.isEnabled).toBe(true) }) describe('StrictMode behavior', () => { it('should deduplicate calls to queryFn', async () => { const key = queryKey() const queryFn = vi.fn(async () => { await sleep(50) return 'data' }) const observer = new QueryObserver(queryClient, { queryKey: key, queryFn, }) const unsubscribe1 = observer.subscribe(vi.fn()) await vi.advanceTimersByTimeAsync(5) unsubscribe1() // replicate strict mode behavior await vi.advanceTimersByTimeAsync(5) const unsubscribe2 = observer.subscribe(vi.fn()) await vi.advanceTimersByTimeAsync(40) expect(queryClient.getQueryState(key)).toMatchObject({ status: 'success', data: 'data', }) expect(queryFn).toHaveBeenCalledTimes(1) unsubscribe2() }) it('should resolve with data when signal was consumed', async () => { const key = queryKey() const queryFn = vi.fn(async ({ signal }) => { await sleep(50) return 'data' + String(signal) }) const observer = new QueryObserver(queryClient, { queryKey: key, queryFn, }) const unsubscribe1 = observer.subscribe(vi.fn()) await vi.advanceTimersByTimeAsync(5) unsubscribe1() // replicate strict mode behavior await vi.advanceTimersByTimeAsync(5) const unsubscribe2 = observer.subscribe(vi.fn()) await vi.advanceTimersByTimeAsync(50) expect(queryClient.getQueryState(key)).toMatchObject({ status: 'success', data: 'data[object AbortSignal]', }) expect(queryFn).toHaveBeenCalledTimes(2) unsubscribe2() }) }) }) ================================================ FILE: packages/query-core/src/__tests__/streamedQuery.test.tsx ================================================ import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest' import { queryKey, sleep } from '@tanstack/query-test-utils' import { streamedQuery } from '../streamedQuery' import { QueryClient, QueryObserver } from '..' describe('streamedQuery', () => { let queryClient: QueryClient beforeEach(() => { vi.useFakeTimers() queryClient = new QueryClient() queryClient.mount() }) afterEach(() => { queryClient.clear() vi.useRealTimers() }) function createAsyncNumberGenerator(amount: number, start = 0) { return { async *[Symbol.asyncIterator]() { let num = start while (num < amount + start) { await sleep(50) yield num++ } }, } } test('should stream data from an AsyncIterable', async () => { const key = queryKey() const observer = new QueryObserver(queryClient, { queryKey: key, queryFn: streamedQuery({ streamFn: () => createAsyncNumberGenerator(3), }), }) const unsubscribe = observer.subscribe(vi.fn()) expect(observer.getCurrentResult()).toMatchObject({ status: 'pending', fetchStatus: 'fetching', data: undefined, }) await vi.advanceTimersByTimeAsync(50) expect(observer.getCurrentResult()).toMatchObject({ status: 'success', fetchStatus: 'fetching', data: [0], }) await vi.advanceTimersByTimeAsync(50) expect(observer.getCurrentResult()).toMatchObject({ status: 'success', fetchStatus: 'fetching', data: [0, 1], }) await vi.advanceTimersByTimeAsync(50) expect(observer.getCurrentResult()).toMatchObject({ status: 'success', fetchStatus: 'idle', data: [0, 1, 2], }) unsubscribe() }) test('should allow Arrays to be returned from the stream', async () => { const key = queryKey() const observer = new QueryObserver(queryClient, { queryKey: key, queryFn: streamedQuery({ streamFn: async function* () { for await (const num of createAsyncNumberGenerator(3)) { yield [num, num] as const } }, }), }) const unsubscribe = observer.subscribe(vi.fn()) expect(observer.getCurrentResult()).toMatchObject({ status: 'pending', fetchStatus: 'fetching', data: undefined, }) await vi.advanceTimersByTimeAsync(50) expect(observer.getCurrentResult()).toMatchObject({ status: 'success', fetchStatus: 'fetching', data: [[0, 0]], }) await vi.advanceTimersByTimeAsync(50) expect(observer.getCurrentResult()).toMatchObject({ status: 'success', fetchStatus: 'fetching', data: [ [0, 0], [1, 1], ], }) await vi.advanceTimersByTimeAsync(50) expect(observer.getCurrentResult()).toMatchObject({ status: 'success', fetchStatus: 'idle', data: [ [0, 0], [1, 1], [2, 2], ], }) unsubscribe() }) test('should replace on refetch', async () => { const key = queryKey() const observer = new QueryObserver(queryClient, { queryKey: key, queryFn: streamedQuery({ streamFn: () => createAsyncNumberGenerator(2), }), }) const unsubscribe = observer.subscribe(vi.fn()) expect(observer.getCurrentResult()).toMatchObject({ status: 'pending', fetchStatus: 'fetching', data: undefined, }) await vi.advanceTimersByTimeAsync(100) expect(observer.getCurrentResult()).toMatchObject({ status: 'success', fetchStatus: 'idle', data: [0, 1], }) void observer.refetch() await vi.advanceTimersByTimeAsync(10) expect(observer.getCurrentResult()).toMatchObject({ status: 'pending', fetchStatus: 'fetching', data: undefined, }) await vi.advanceTimersByTimeAsync(40) expect(observer.getCurrentResult()).toMatchObject({ status: 'success', fetchStatus: 'fetching', data: [0], }) await vi.advanceTimersByTimeAsync(50) expect(observer.getCurrentResult()).toMatchObject({ status: 'success', fetchStatus: 'idle', data: [0, 1], }) unsubscribe() }) test('should support refetchMode append', async () => { const key = queryKey() const observer = new QueryObserver(queryClient, { queryKey: key, queryFn: streamedQuery({ streamFn: () => createAsyncNumberGenerator(2), refetchMode: 'append', }), }) const unsubscribe = observer.subscribe(vi.fn()) expect(observer.getCurrentResult()).toMatchObject({ status: 'pending', fetchStatus: 'fetching', data: undefined, }) await vi.advanceTimersByTimeAsync(100) expect(observer.getCurrentResult()).toMatchObject({ status: 'success', fetchStatus: 'idle', data: [0, 1], }) void observer.refetch() await vi.advanceTimersByTimeAsync(10) expect(observer.getCurrentResult()).toMatchObject({ status: 'success', fetchStatus: 'fetching', data: [0, 1], }) await vi.advanceTimersByTimeAsync(40) expect(observer.getCurrentResult()).toMatchObject({ status: 'success', fetchStatus: 'fetching', data: [0, 1, 0], }) await vi.advanceTimersByTimeAsync(50) expect(observer.getCurrentResult()).toMatchObject({ status: 'success', fetchStatus: 'idle', data: [0, 1, 0, 1], }) unsubscribe() }) test('should support refetchMode replace', async () => { const key = queryKey() let offset = 0 const observer = new QueryObserver(queryClient, { queryKey: key, queryFn: streamedQuery({ streamFn: () => createAsyncNumberGenerator(2, offset), refetchMode: 'replace', }), }) const unsubscribe = observer.subscribe(vi.fn()) expect(observer.getCurrentResult()).toMatchObject({ status: 'pending', fetchStatus: 'fetching', data: undefined, }) await vi.advanceTimersByTimeAsync(100) expect(observer.getCurrentResult()).toMatchObject({ status: 'success', fetchStatus: 'idle', data: [0, 1], }) offset = 100 void observer.refetch() await vi.advanceTimersByTimeAsync(10) expect(observer.getCurrentResult()).toMatchObject({ status: 'success', fetchStatus: 'fetching', data: [0, 1], }) await vi.advanceTimersByTimeAsync(40) expect(observer.getCurrentResult()).toMatchObject({ status: 'success', fetchStatus: 'fetching', data: [0, 1], }) await vi.advanceTimersByTimeAsync(50) expect(observer.getCurrentResult()).toMatchObject({ status: 'success', fetchStatus: 'idle', data: [100, 101], }) unsubscribe() }) test('should abort ongoing stream when refetch happens', async () => { const key = queryKey() const observer = new QueryObserver(queryClient, { queryKey: key, queryFn: streamedQuery({ streamFn: () => createAsyncNumberGenerator(3), refetchMode: 'append', }), }) const unsubscribe = observer.subscribe(vi.fn()) expect(observer.getCurrentResult()).toMatchObject({ status: 'pending', fetchStatus: 'fetching', data: undefined, }) await vi.advanceTimersByTimeAsync(100) expect(observer.getCurrentResult()).toMatchObject({ status: 'success', fetchStatus: 'fetching', data: [0, 1], }) void observer.refetch() await vi.advanceTimersByTimeAsync(10) expect(observer.getCurrentResult()).toMatchObject({ status: 'success', fetchStatus: 'fetching', data: [0, 1], }) await vi.advanceTimersByTimeAsync(40) expect(observer.getCurrentResult()).toMatchObject({ status: 'success', fetchStatus: 'fetching', data: [0, 1, 0], }) await vi.advanceTimersByTimeAsync(100) expect(observer.getCurrentResult()).toMatchObject({ status: 'success', fetchStatus: 'idle', data: [0, 1, 0, 1, 2], }) unsubscribe() }) test('should abort when unsubscribed', async () => { const key = queryKey() const observer = new QueryObserver(queryClient, { queryKey: key, queryFn: streamedQuery({ streamFn: (context) => { // just consume the signal // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition const numbers = context.signal ? 3 : 0 return createAsyncNumberGenerator(numbers) }, }), }) const unsubscribe = observer.subscribe(vi.fn()) expect(queryClient.getQueryState(key)).toMatchObject({ status: 'pending', fetchStatus: 'fetching', data: undefined, }) await vi.advanceTimersByTimeAsync(60) expect(queryClient.getQueryState(key)).toMatchObject({ status: 'success', fetchStatus: 'fetching', data: [0], }) unsubscribe() await vi.advanceTimersByTimeAsync(10) expect(queryClient.getQueryState(key)).toMatchObject({ status: 'success', fetchStatus: 'idle', data: [0], }) }) test('should support custom reducer', async () => { const key = queryKey() const observer = new QueryObserver(queryClient, { queryKey: key, queryFn: streamedQuery({ streamFn: () => createAsyncNumberGenerator(2), reducer: (acc, chunk) => ({ ...acc, [chunk]: true, }), initialValue: {} as Record, }), }) const unsubscribe = observer.subscribe(vi.fn()) expect(observer.getCurrentResult()).toMatchObject({ status: 'pending', fetchStatus: 'fetching', data: undefined, }) await vi.advanceTimersByTimeAsync(100) expect(observer.getCurrentResult()).toMatchObject({ status: 'success', fetchStatus: 'idle', data: { 0: true, 1: true, }, }) unsubscribe() }) test('should support custom reducer with initialValue', async () => { const key = queryKey() const observer = new QueryObserver(queryClient, { queryKey: key, queryFn: streamedQuery({ streamFn: () => createAsyncNumberGenerator(2), reducer: (acc, chunk) => ({ ...acc, [chunk]: true, }), initialValue: { 10: true, 11: true, } as Record, }), }) const unsubscribe = observer.subscribe(vi.fn()) expect(observer.getCurrentResult()).toMatchObject({ status: 'pending', fetchStatus: 'fetching', data: undefined, }) await vi.advanceTimersByTimeAsync(100) expect(observer.getCurrentResult()).toMatchObject({ status: 'success', fetchStatus: 'idle', data: { 10: true, 11: true, 0: true, 1: true, }, }) unsubscribe() }) }) ================================================ FILE: packages/query-core/src/__tests__/timeoutManager.test.tsx ================================================ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { TimeoutManager, defaultTimeoutProvider, systemSetTimeoutZero, timeoutManager, } from '../timeoutManager' describe('timeoutManager', () => { function createMockProvider(name: string = 'custom') { return { __TEST_ONLY__name: name, setTimeout: vi.fn(() => 123), clearTimeout: vi.fn(), setInterval: vi.fn(() => 456), clearInterval: vi.fn(), } } let consoleErrorSpy: ReturnType beforeEach(() => { consoleErrorSpy = vi.spyOn(console, 'error') }) afterEach(() => { vi.restoreAllMocks() }) describe('TimeoutManager', () => { let manager: TimeoutManager beforeEach(() => { manager = new TimeoutManager() }) it('by default proxies calls to globalThis setTimeout/clearTimeout', () => { const setTimeoutSpy = vi.spyOn(globalThis, 'setTimeout') const clearTimeoutSpy = vi.spyOn(globalThis, 'clearTimeout') const setIntervalSpy = vi.spyOn(globalThis, 'setInterval') const clearIntervalSpy = vi.spyOn(globalThis, 'clearInterval') const callback = vi.fn() const timeoutId = manager.setTimeout(callback, 100) expect(setTimeoutSpy).toHaveBeenCalledWith(callback, 100) clearTimeout(Number(timeoutId)) manager.clearTimeout(200) expect(clearTimeoutSpy).toHaveBeenCalledWith(200) const intervalId = manager.setInterval(callback, 300) expect(setIntervalSpy).toHaveBeenCalledWith(callback, 300) clearInterval(Number(intervalId)) manager.clearInterval(400) expect(clearIntervalSpy).toHaveBeenCalledWith(400) }) describe('setTimeoutProvider', () => { it('proxies calls to the configured timeout provider', () => { const customProvider = createMockProvider() manager.setTimeoutProvider(customProvider) const callback = vi.fn() manager.setTimeout(callback, 100) expect(customProvider.setTimeout).toHaveBeenCalledWith(callback, 100) manager.clearTimeout(999) expect(customProvider.clearTimeout).toHaveBeenCalledWith(999) manager.setInterval(callback, 200) expect(customProvider.setInterval).toHaveBeenCalledWith(callback, 200) manager.clearInterval(888) expect(customProvider.clearInterval).toHaveBeenCalledWith(888) }) it('warns when switching providers after making call', () => { // 1. switching before making any calls does not warn const customProvider = createMockProvider() manager.setTimeoutProvider(customProvider) expect(consoleErrorSpy).not.toHaveBeenCalled() // Make a call. The next switch should warn manager.setTimeout(vi.fn(), 100) // 2. switching after making a call should warn const customProvider2 = createMockProvider('custom2') manager.setTimeoutProvider(customProvider2) expect(consoleErrorSpy).toHaveBeenCalledWith( expect.stringMatching( /\[timeoutManager\]: Switching .* might result in unexpected behavior\..*/, ), expect.anything(), ) // 3. Switching again with no intermediate calls should not warn vi.mocked(consoleErrorSpy).mockClear() const customProvider3 = createMockProvider('custom3') manager.setTimeoutProvider(customProvider3) expect(consoleErrorSpy).not.toHaveBeenCalled() }) }) }) describe('globalThis timeoutManager instance', () => { it('should be an instance of TimeoutManager', () => { expect(timeoutManager).toBeInstanceOf(TimeoutManager) }) }) describe('exported functions', () => { let provider: ReturnType beforeEach(() => { provider = createMockProvider() timeoutManager.setTimeoutProvider(provider) }) afterEach(() => { timeoutManager.setTimeoutProvider(defaultTimeoutProvider) }) describe('systemSetTimeoutZero', () => { it('should use globalThis setTimeout with 0 delay', () => { const spy = vi.spyOn(globalThis, 'setTimeout') const callback = vi.fn() systemSetTimeoutZero(callback) expect(spy).toHaveBeenCalledWith(callback, 0) clearTimeout(spy.mock.results[0]?.value) }) }) }) }) ================================================ FILE: packages/query-core/src/__tests__/utils.test-d.tsx ================================================ import { describe, expectTypeOf, it } from 'vitest' import { QueryClient } from '../queryClient' import type { QueryFilters } from '../utils' import type { DataTag, QueryKey } from '../types' describe('QueryFilters', () => { it('should be typed unknown even if tagged generics are passed', () => { type TData = { a: number; b: string } type TError = Error & { message: string } const filters: QueryFilters> = { predicate(query) { expectTypeOf(query.setData({ a: 1, b: '1' })).toEqualTypeOf() return true }, queryKey: ['key'] as DataTag, } const queryClient = new QueryClient() const data = queryClient.getQueryData(filters.queryKey!) expectTypeOf(data).toEqualTypeOf() const error = queryClient.getQueryState(filters.queryKey!)?.error expectTypeOf(error).toEqualTypeOf() }) it('should be loose typed if generics are defaults', () => { const a: QueryFilters = { predicate(query) { expectTypeOf(query.setData({ a: 1, b: '1' })).toEqualTypeOf() return true }, queryKey: ['key'], } const queryClient = new QueryClient() const data = queryClient.getQueryData(a.queryKey!) expectTypeOf(data).toEqualTypeOf() const error = queryClient.getQueryState(a.queryKey!)?.error expectTypeOf(error).toEqualTypeOf() }) }) ================================================ FILE: packages/query-core/src/__tests__/utils.test.tsx ================================================ import { describe, expect, it, vi } from 'vitest' import { QueryClient } from '..' import { addToEnd, addToStart, hashKey, hashQueryKeyByOptions, isPlainArray, isPlainObject, keepPreviousData, matchMutation, partialMatchKey, replaceEqualDeep, shallowEqualObjects, shouldThrowError, } from '../utils' import { Mutation } from '../mutation' describe('core/utils', () => { describe('hashQueryKeyByOptions', () => { it('should use custom hash function when provided in options', () => { const queryKey = ['test', { a: 1, b: 2 }] const customHashFn = vi.fn(() => 'custom-hash') const result = hashQueryKeyByOptions(queryKey, { queryKeyHashFn: customHashFn, }) expect(customHashFn).toHaveBeenCalledWith(queryKey) expect(result).toEqual('custom-hash') }) it('should use default hash function when no options provided', () => { const queryKey = ['test', { a: 1, b: 2 }] const defaultResult = hashKey(queryKey) const result = hashQueryKeyByOptions(queryKey) expect(result).toEqual(defaultResult) }) }) describe('shallowEqualObjects', () => { it('should return `true` for shallow equal objects', () => { expect(shallowEqualObjects({ a: 1 }, { a: 1 })).toEqual(true) }) it('should return `false` for non shallow equal objects', () => { expect(shallowEqualObjects({ a: 1 }, { a: 2 })).toEqual(false) }) it('should return `false` if lengths are not equal', () => { expect(shallowEqualObjects({ a: 1 }, { a: 1, b: 2 })).toEqual(false) }) it('should return false if b is undefined', () => { expect(shallowEqualObjects({ a: 1 }, undefined)).toEqual(false) }) }) describe('isPlainObject', () => { it('should return `true` for a plain object', () => { expect(isPlainObject({})).toEqual(true) }) it('should return `false` for an array', () => { expect(isPlainObject([])).toEqual(false) }) it('should return `false` for null', () => { expect(isPlainObject(null)).toEqual(false) }) it('should return `false` for undefined', () => { expect(isPlainObject(undefined)).toEqual(false) }) it('should return `true` for object with an undefined constructor', () => { expect(isPlainObject(Object.create(null))).toBeTruthy() }) it('should return `false` if constructor does not have an Object-specific method', () => { class Foo { abc: any constructor() { this.abc = {} } } expect(isPlainObject(new Foo())).toBeFalsy() }) it('should return `false` if the object has a modified prototype', () => { function Graph(this: any) { this.vertices = [] this.edges = [] } Graph.prototype.addVertex = function (v: any) { this.vertices.push(v) } expect(isPlainObject(Object.create(Graph))).toBeFalsy() }) it('should return `false` for object with custom prototype', () => { const CustomProto = Object.create({ a: 1 }) const obj = Object.create(CustomProto) obj.b = 2 expect(isPlainObject(obj)).toBeFalsy() }) }) describe('isPlainArray', () => { it('should return `true` for plain arrays', () => { expect(isPlainArray([1, 2])).toEqual(true) }) it('should return `false` for non plain arrays', () => { expect(isPlainArray(Object.assign([1, 2], { a: 'b' }))).toEqual(false) }) }) describe('partialMatchKey', () => { it('should return `true` if a includes b', () => { const a = [{ a: { b: 'b' }, c: 'c', d: [{ d: 'd ' }] }] const b = [{ a: { b: 'b' }, c: 'c', d: [] }] expect(partialMatchKey(a, b)).toEqual(true) }) it('should return `false` if a does not include b', () => { const a = [{ a: { b: 'b' }, c: 'c', d: [] }] const b = [{ a: { b: 'b' }, c: 'c', d: [{ d: 'd ' }] }] expect(partialMatchKey(a, b)).toEqual(false) }) it('should return `true` if array a includes array b', () => { const a = [1, 2, 3] const b = [1, 2] expect(partialMatchKey(a, b)).toEqual(true) }) it('should return `false` if a is null and b is not', () => { const a = [null] const b = [{ a: { b: 'b' }, c: 'c', d: [{ d: 'd ' }] }] expect(partialMatchKey(a, b)).toEqual(false) }) it('should return `false` if a contains null and b is not', () => { const a = [{ a: null, c: 'c', d: [] }] const b = [{ a: { b: 'b' }, c: 'c', d: [{ d: 'd ' }] }] expect(partialMatchKey(a, b)).toEqual(false) }) it('should return `false` if b is null and a is not', () => { const a = [{ a: { b: 'b' }, c: 'c', d: [] }] const b = [null] expect(partialMatchKey(a, b)).toEqual(false) }) it('should return `false` if b contains null and a is not', () => { const a = [{ a: { b: 'b' }, c: 'c', d: [] }] const b = [{ a: null, c: 'c', d: [{ d: 'd ' }] }] expect(partialMatchKey(a, b)).toEqual(false) }) }) describe('replaceEqualDeep', () => { it('should return the previous value when the next value is an equal primitive', () => { expect(replaceEqualDeep(1, 1)).toBe(1) expect(replaceEqualDeep('1', '1')).toBe('1') expect(replaceEqualDeep(true, true)).toBe(true) expect(replaceEqualDeep(false, false)).toBe(false) expect(replaceEqualDeep(null, null)).toBe(null) expect(replaceEqualDeep(undefined, undefined)).toBe(undefined) }) it('should return the next value when the previous value is a different value', () => { const date1 = new Date() const date2 = new Date() expect(replaceEqualDeep(1, 0)).toBe(0) expect(replaceEqualDeep(1, 2)).toBe(2) expect(replaceEqualDeep('1', '2')).toBe('2') expect(replaceEqualDeep(true, false)).toBe(false) expect(replaceEqualDeep(false, true)).toBe(true) expect(replaceEqualDeep(date1, date2)).toBe(date2) }) it('should return the next value when the previous value is a different type', () => { const array = [1] const object = { a: 'a' } expect(replaceEqualDeep(0, undefined)).toBe(undefined) expect(replaceEqualDeep(undefined, 0)).toBe(0) expect(replaceEqualDeep(2, undefined)).toBe(undefined) expect(replaceEqualDeep(undefined, 2)).toBe(2) expect(replaceEqualDeep(undefined, null)).toBe(null) expect(replaceEqualDeep(null, undefined)).toBe(undefined) expect(replaceEqualDeep({}, undefined)).toBe(undefined) expect(replaceEqualDeep([], undefined)).toBe(undefined) expect(replaceEqualDeep(array, object)).toBe(object) expect(replaceEqualDeep(object, array)).toBe(array) }) it('should return the previous value when the next value is an equal array', () => { const prev = [1, 2] const next = [1, 2] expect(replaceEqualDeep(prev, next)).toBe(prev) }) it('should return a copy when the previous value is a different array subset', () => { const prev = [1, 2] const next = [1, 2, 3] const result = replaceEqualDeep(prev, next) expect(result).toEqual(next) expect(result).not.toBe(prev) expect(result).not.toBe(next) }) it('should return a copy when the previous value is a different array superset', () => { const prev = [1, 2, 3] const next = [1, 2] const result = replaceEqualDeep(prev, next) expect(result).toEqual(next) expect(result).not.toBe(prev) expect(result).not.toBe(next) }) it('should return the previous value when the next value is an equal empty array', () => { const prev: Array = [] const next: Array = [] expect(replaceEqualDeep(prev, next)).toBe(prev) }) it('should return the previous value when the next value is an equal empty object', () => { const prev = {} const next = {} expect(replaceEqualDeep(prev, next)).toBe(prev) }) it('should return the previous value when the next value is an equal object', () => { const prev = { a: 'a' } const next = { a: 'a' } expect(replaceEqualDeep(prev, next)).toBe(prev) }) it('should replace different values in objects', () => { const prev = { a: { b: 'b' }, c: 'c' } const next = { a: { b: 'b' }, c: 'd' } const result = replaceEqualDeep(prev, next) expect(result).toEqual(next) expect(result).not.toBe(prev) expect(result).not.toBe(next) expect(result.a).toBe(prev.a) expect(result.c).toBe(next.c) }) it('should replace different values in arrays', () => { const prev = [1, { a: 'a' }, { b: { b: 'b' } }, [1]] as const const next = [1, { a: 'a' }, { b: { b: 'c' } }, [1]] as const const result = replaceEqualDeep(prev, next) expect(result).toEqual(next) expect(result).not.toBe(prev) expect(result).not.toBe(next) expect(result[0]).toBe(prev[0]) expect(result[1]).toBe(prev[1]) expect(result[2]).not.toBe(next[2]) expect(result[2].b.b).toBe(next[2].b.b) expect(result[3]).toBe(prev[3]) }) it('should replace different values in arrays when the next value is a subset', () => { const prev = [{ a: 'a' }, { b: 'b' }, { c: 'c' }] const next = [{ a: 'a' }, { b: 'b' }] const result = replaceEqualDeep(prev, next) expect(result).toEqual(next) expect(result).not.toBe(prev) expect(result).not.toBe(next) expect(result[0]).toBe(prev[0]) expect(result[1]).toBe(prev[1]) expect(result[2]).toBeUndefined() }) it('should replace different values in arrays when the next value is a superset', () => { const prev = [{ a: 'a' }, { b: 'b' }] const next = [{ a: 'a' }, { b: 'b' }, { c: 'c' }] const result = replaceEqualDeep(prev, next) expect(result).toEqual(next) expect(result).not.toBe(prev) expect(result).not.toBe(next) expect(result[0]).toBe(prev[0]) expect(result[1]).toBe(prev[1]) expect(result[2]).toBe(next[2]) }) it('should copy objects which are not arrays or objects', () => { const prev = [{ a: 'a' }, { b: 'b' }, { c: 'c' }, 1] const next = [{ a: 'a' }, new Map(), { c: 'c' }, 2] const result = replaceEqualDeep(prev, next) expect(result).not.toBe(prev) expect(result).not.toBe(next) expect(result[0]).toBe(prev[0]) expect(result[1]).toBe(next[1]) expect(result[2]).toBe(prev[2]) expect(result[3]).toBe(next[3]) }) it('should support equal objects which are not arrays or objects', () => { const map = new Map() const prev = [map, [1]] const next = [map, [1]] const result = replaceEqualDeep(prev, next) expect(result).toBe(prev) }) it('should support non equal objects which are not arrays or objects', () => { const map1 = new Map() const map2 = new Map() const prev = [map1, [1]] const next = [map2, [1]] const result = replaceEqualDeep(prev, next) expect(result).not.toBe(prev) expect(result).not.toBe(next) expect(result[0]).toBe(next[0]) expect(result[1]).toBe(prev[1]) }) it('should support objects which are not plain arrays', () => { const prev = Object.assign([1, 2], { a: { b: 'b' }, c: 'c' }) const next = Object.assign([1, 2], { a: { b: 'b' }, c: 'c' }) const result = replaceEqualDeep(prev, next) expect(result).toBe(next) }) it('should replace all parent objects if some nested value changes', () => { const prev = { todo: { id: '1', meta: { createdAt: 0 }, state: { done: false } }, otherTodo: { id: '2', meta: { createdAt: 0 }, state: { done: true } }, } const next = { todo: { id: '1', meta: { createdAt: 0 }, state: { done: true } }, otherTodo: { id: '2', meta: { createdAt: 0 }, state: { done: true } }, } const result = replaceEqualDeep(prev, next) expect(result).toEqual(next) expect(result).not.toBe(prev) expect(result).not.toBe(next) expect(result.todo).not.toBe(prev.todo) expect(result.todo).not.toBe(next.todo) expect(result.todo.id).toBe(next.todo.id) expect(result.todo.meta).toBe(prev.todo.meta) expect(result.todo.state).not.toBe(next.todo.state) expect(result.todo.state.done).toBe(next.todo.state.done) expect(result.otherTodo).toBe(prev.otherTodo) }) it('should replace all parent arrays if some nested value changes', () => { const prev = { todos: [ { id: '1', meta: { createdAt: 0 }, state: { done: false } }, { id: '2', meta: { createdAt: 0 }, state: { done: true } }, ], } const next = { todos: [ { id: '1', meta: { createdAt: 0 }, state: { done: true } }, { id: '2', meta: { createdAt: 0 }, state: { done: true } }, ], } const result = replaceEqualDeep(prev, next) expect(result).toEqual(next) expect(result).not.toBe(prev) expect(result).not.toBe(next) expect(result.todos).not.toBe(prev.todos) expect(result.todos).not.toBe(next.todos) expect(result.todos[0]).not.toBe(prev.todos[0]) expect(result.todos[0]).not.toBe(next.todos[0]) expect(result.todos[0]?.id).toBe(next.todos[0]?.id) expect(result.todos[0]?.meta).toBe(prev.todos[0]?.meta) expect(result.todos[0]?.state).not.toBe(next.todos[0]?.state) expect(result.todos[0]?.state.done).toBe(next.todos[0]?.state.done) expect(result.todos[1]).toBe(prev.todos[1]) }) it('should correctly handle objects with the same number of properties and one property being undefined', () => { const obj1 = { a: 2, c: 123 } const obj2 = { a: 2, b: undefined } const result = replaceEqualDeep(obj1, obj2) expect(result).toStrictEqual(obj2) }) it('should be able to share values that contain undefined', () => { const current = [ { data: undefined, foo: true, }, ] const next = replaceEqualDeep(current, [ { data: undefined, foo: true, }, ]) expect(current).toBe(next) }) it('should return the previous value when both values are an array of undefined', () => { const current = [undefined] const next = replaceEqualDeep(current, [undefined]) expect(next).toBe(current) }) it('should return the previous value when both values are an array that contains undefined', () => { const current = [{ foo: 1 }, undefined] const next = replaceEqualDeep(current, [{ foo: 1 }, undefined]) expect(next).toBe(current) }) }) describe('matchMutation', () => { it('should return false if mutationKey options is undefined', () => { const filters = { mutationKey: ['key1'] } const queryClient = new QueryClient() const mutation = new Mutation({ client: queryClient, mutationId: 1, mutationCache: queryClient.getMutationCache(), options: {}, }) expect(matchMutation(filters, mutation)).toBeFalsy() }) }) describe('keepPreviousData', () => { it('should return the parameter as is', () => { const x = { a: 1, b: 2 } expect(keepPreviousData(x)).toEqual(x) }) }) describe('addToEnd', () => { it('should add item to the end of the array', () => { const items = [1, 2, 3] const newItems = addToEnd(items, 4) expect(newItems).toEqual([1, 2, 3, 4]) }) it('should not exceed max if provided', () => { const items = [1, 2, 3] const newItems = addToEnd(items, 4, 3) expect(newItems).toEqual([2, 3, 4]) }) it('should add item to the end of the array when max = 0', () => { const items = [1, 2, 3] const item = 4 const max = 0 expect(addToEnd(items, item, max)).toEqual([1, 2, 3, 4]) }) it('should add item to the end of the array when max is undefined', () => { const items = [1, 2, 3] const item = 4 const max = undefined expect(addToEnd(items, item, max)).toEqual([1, 2, 3, 4]) }) }) describe('addToStart', () => { it('should add an item to the start of the array', () => { const items = [1, 2, 3] const item = 4 const newItems = addToStart(items, item) expect(newItems).toEqual([4, 1, 2, 3]) }) it('should respect the max argument', () => { const items = [1, 2, 3] const item = 4 const max = 2 const newItems = addToStart(items, item, max) expect(newItems).toEqual([4, 1, 2]) }) it('should not remove any items if max = 0', () => { const items = [1, 2, 3] const item = 4 const max = 0 const newItems = addToStart(items, item, max) expect(newItems).toEqual([4, 1, 2, 3]) }) it('should not remove any items if max is undefined', () => { const items = [1, 2, 3] const item = 4 const max = 0 const newItems = addToStart(items, item, max) expect(newItems).toEqual([4, 1, 2, 3]) }) }) describe('hashKey', () => { it('should hash primitives correctly', () => { expect(hashKey(['test'])).toEqual(JSON.stringify(['test'])) expect(hashKey([123])).toEqual(JSON.stringify([123])) expect(hashKey([null])).toEqual(JSON.stringify([null])) }) it('should hash objects with sorted keys consistently', () => { const key1 = [{ b: 2, a: 1 }] const key2 = [{ a: 1, b: 2 }] const hash1 = hashKey(key1) const hash2 = hashKey(key2) expect(hash1).toEqual(hash2) expect(hash1).toEqual(JSON.stringify([{ a: 1, b: 2 }])) }) it('should hash arrays consistently', () => { const arr1 = [{ b: 2, a: 1 }, 'test', 123] const arr2 = [{ a: 1, b: 2 }, 'test', 123] expect(hashKey(arr1)).toEqual(hashKey(arr2)) }) it('should handle nested objects with sorted keys', () => { const nested1 = [{ a: { d: 4, c: 3 }, b: 2 }] const nested2 = [{ b: 2, a: { c: 3, d: 4 } }] expect(hashKey(nested1)).toEqual(hashKey(nested2)) }) }) describe('shouldThrowError', () => { it('should return the result of executing throwOnError if throwOnError parameter is a function', () => { const throwOnError = (error: Error) => error.message === 'test error' expect(shouldThrowError(throwOnError, [new Error('test error')])).toBe( true, ) expect(shouldThrowError(throwOnError, [new Error('other error')])).toBe( false, ) }) it('should return throwOnError parameter itself if throwOnError is not a function', () => { expect(shouldThrowError(true, [new Error('test error')])).toBe(true) expect(shouldThrowError(false, [new Error('test error')])).toBe(false) expect(shouldThrowError(undefined, [new Error('test error')])).toBe(false) }) }) }) ================================================ FILE: packages/query-core/src/__tests__/utils.ts ================================================ import { vi } from 'vitest' import { onlineManager } from '..' import * as utils from '../utils' import type { MockInstance } from 'vitest' import type { MutationOptions, QueryClient } from '..' export function mockOnlineManagerIsOnline( value: boolean, ): MockInstance<() => boolean> { return vi.spyOn(onlineManager, 'isOnline').mockReturnValue(value) } export function executeMutation( queryClient: QueryClient, options: MutationOptions, variables: TVariables, ) { return queryClient .getMutationCache() .build(queryClient, options) .execute(variables) } // This monkey-patches the isServer-value from utils, // so that we can pretend to be in a server environment export function setIsServer(isServer: boolean) { const original = utils.isServer Object.defineProperty(utils, 'isServer', { get: () => isServer, }) return () => { Object.defineProperty(utils, 'isServer', { get: () => original, }) } } ================================================ FILE: packages/query-devtools/eslint.config.js ================================================ // @ts-check import rootConfig from './root.eslint.config.js' export default [...rootConfig] ================================================ FILE: packages/query-devtools/package.json ================================================ { "name": "@tanstack/query-devtools", "version": "5.87.3", "description": "Developer tools to interact with and visualize the TanStack Query cache", "author": "tannerlinsley", "license": "MIT", "repository": { "type": "git", "url": "https://github.com/TanStack/query.git", "directory": "packages/query-devtools" }, "homepage": "https://tanstack.com/query", "funding": { "type": "github", "url": "https://github.com/sponsors/tannerlinsley" }, "scripts": { "clean": "premove ./build ./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": "publint --strict && attw --pack", "build": "tsup --tsconfig tsconfig.prod.json", "build:dev": "tsup --watch" }, "type": "module", "main": "./build/index.cjs", "module": "./build/index.js", "types": "./build/index.d.ts", "browser": {}, "exports": { "@tanstack/custom-condition": "./src/index.ts", "solid": { "development": "./build/index.js", "import": "./build/index.js" }, "development": { "import": { "types": "./build/index.d.ts", "default": "./build/dev.js" }, "require": "./build/dev.cjs" }, "import": { "types": "./build/index.d.ts", "default": "./build/index.js" }, "require": "./build/index.cjs" }, "files": [ "build", "src", "!src/__tests__" ], "devDependencies": { "@kobalte/core": "^0.13.4", "@solid-primitives/keyed": "^1.2.2", "@solid-primitives/resize-observer": "^2.0.26", "@solid-primitives/storage": "^1.3.11", "@tanstack/match-sorter-utils": "^8.19.4", "@tanstack/query-core": "workspace:*", "clsx": "^2.1.1", "goober": "^2.1.16", "npm-run-all2": "^5.0.0", "solid-js": "^1.9.7", "solid-transition-group": "^0.2.3", "superjson": "^2.2.2", "tsup-preset-solid": "^2.2.0", "vite-plugin-solid": "^2.11.6" } } ================================================ FILE: packages/query-devtools/tsconfig.json ================================================ { "extends": "../../tsconfig.json", "compilerOptions": { "outDir": "./dist-ts", "rootDir": ".", "jsx": "preserve", "jsxImportSource": "solid-js" }, "include": ["src", "*.config.*", "package.json"], "references": [{ "path": "../query-core" }] } ================================================ FILE: packages/query-devtools/tsconfig.prod.json ================================================ { "extends": "./tsconfig.json", "compilerOptions": { "incremental": false, "composite": false, "rootDir": "../../" } } ================================================ FILE: packages/query-devtools/tsup.config.ts ================================================ import { defineConfig } from 'tsup' import { generateTsupOptions, parsePresetOptions } from 'tsup-preset-solid' const preset_options = { entries: { entry: 'src/index.ts', dev_entry: true, }, cjs: true, drop_console: true, } export default defineConfig(() => { const parsed_data = parsePresetOptions(preset_options) const tsup_options = generateTsupOptions(parsed_data) tsup_options.forEach((tsup_option) => { tsup_option.outDir = 'build' }) return tsup_options }) ================================================ FILE: packages/query-devtools/vite.config.ts ================================================ import solid from 'vite-plugin-solid' import { defineConfig } from 'vitest/config' import packageJson from './package.json' export default defineConfig({ plugins: [solid()], // 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', coverage: { enabled: true, provider: 'istanbul', include: ['src/**/*'] }, typecheck: { enabled: true }, restoreMocks: true, }, }) ================================================ SYMLINK: packages/query-devtools/root.eslint.config.js -> eslint.config.js ================================================ ================================================ FILE: packages/query-devtools/src/constants.ts ================================================ import { mutationSortFns, sortFns } from './utils' import type { DevtoolsButtonPosition, DevtoolsPosition } from './contexts' export const firstBreakpoint = 1024 export const secondBreakpoint = 796 export const thirdBreakpoint = 700 export const BUTTON_POSITION: DevtoolsButtonPosition = 'bottom-right' export const POSITION: DevtoolsPosition = 'bottom' export const THEME_PREFERENCE = 'system' export const INITIAL_IS_OPEN = false export const DEFAULT_HEIGHT = 500 export const PIP_DEFAULT_HEIGHT = 500 export const DEFAULT_WIDTH = 500 export const DEFAULT_SORT_FN_NAME = Object.keys(sortFns)[0] export const DEFAULT_SORT_ORDER = 1 export const DEFAULT_MUTATION_SORT_FN_NAME = Object.keys(mutationSortFns)[0] ================================================ FILE: packages/query-devtools/src/Devtools.tsx ================================================ import { For, Show, batch, createEffect, createMemo, createSignal, on, onCleanup, onMount, } from 'solid-js' import { rankItem } from '@tanstack/match-sorter-utils' import * as goober from 'goober' import { clsx as cx } from 'clsx' import { TransitionGroup } from 'solid-transition-group' import { Key } from '@solid-primitives/keyed' import { createResizeObserver } from '@solid-primitives/resize-observer' import { DropdownMenu, RadioGroup } from '@kobalte/core' import { Portal } from 'solid-js/web' import { tokens } from './theme' import { convertRemToPixels, displayValue, getMutationStatusColor, getQueryStatusColor, getQueryStatusColorByLabel, getQueryStatusLabel, getSidedProp, mutationSortFns, sortFns, } from './utils' import { ArrowDown, ArrowLeft, ArrowRight, ArrowUp, CheckCircle, ChevronDown, LoadingCircle, Monitor, Moon, Offline, PauseCircle, PiPIcon, Search, Settings, Sun, TanstackLogo, Trash, Wifi, XCircle, } from './icons' import Explorer from './Explorer' import { usePiPWindow, useQueryDevtoolsContext, useTheme } from './contexts' import { BUTTON_POSITION, DEFAULT_HEIGHT, DEFAULT_MUTATION_SORT_FN_NAME, DEFAULT_SORT_FN_NAME, DEFAULT_SORT_ORDER, DEFAULT_WIDTH, INITIAL_IS_OPEN, POSITION, firstBreakpoint, secondBreakpoint, thirdBreakpoint, } from './constants' import type { DevtoolsErrorType, DevtoolsPosition, QueryDevtoolsProps, } from './contexts' import type { Mutation, MutationCache, Query, QueryCache, QueryCacheNotifyEvent, QueryState, } from '@tanstack/query-core' import type { StorageObject, StorageSetter } from '@solid-primitives/storage' import type { Accessor, Component, JSX, Setter } from 'solid-js' interface DevtoolsPanelProps { localStore: StorageObject setLocalStore: StorageSetter } interface ContentViewProps { localStore: StorageObject setLocalStore: StorageSetter showPanelViewOnly?: boolean onClose?: () => unknown } interface QueryStatusProps { label: string color: 'green' | 'yellow' | 'gray' | 'blue' | 'purple' | 'red' count: number } const [selectedQueryHash, setSelectedQueryHash] = createSignal( null, ) const [selectedMutationId, setSelectedMutationId] = createSignal( null, ) const [panelWidth, setPanelWidth] = createSignal(0) const [offline, setOffline] = createSignal(false) export type DevtoolsComponentType = Component & { shadowDOMTarget?: ShadowRoot } export const Devtools: Component = (props) => { const theme = useTheme() const css = useQueryDevtoolsContext().shadowDOMTarget ? goober.css.bind({ target: useQueryDevtoolsContext().shadowDOMTarget }) : goober.css const styles = createMemo(() => { return theme() === 'dark' ? darkStyles(css) : lightStyles(css) }) const onlineManager = createMemo( () => useQueryDevtoolsContext().onlineManager, ) onMount(() => { const unsubscribe = onlineManager().subscribe((online) => { setOffline(!online) }) onCleanup(() => { unsubscribe() }) }) const pip = usePiPWindow() const buttonPosition = createMemo(() => { return useQueryDevtoolsContext().buttonPosition || BUTTON_POSITION }) const isOpen = createMemo(() => { return props.localStore.open === 'true' ? true : props.localStore.open === 'false' ? false : useQueryDevtoolsContext().initialIsOpen || INITIAL_IS_OPEN }) const position = createMemo(() => { return ( props.localStore.position || useQueryDevtoolsContext().position || POSITION ) }) let transitionsContainerRef!: HTMLDivElement createEffect(() => { const root = transitionsContainerRef.parentElement as HTMLElement const height = props.localStore.height || DEFAULT_HEIGHT const width = props.localStore.width || DEFAULT_WIDTH const panelPosition = position() root.style.setProperty( '--tsqd-panel-height', `${panelPosition === 'top' ? '-' : ''}${height}px`, ) root.style.setProperty( '--tsqd-panel-width', `${panelPosition === 'left' ? '-' : ''}${width}px`, ) }) // Calculates the inherited font size of the parent and sets it as a CSS variable // All the design tokens are calculated based on this variable onMount(() => { // This is to make sure that the font size is updated when the stylesheet is updated // and the user focuses back on the window const onFocus = () => { const root = transitionsContainerRef.parentElement as HTMLElement const fontSize = getComputedStyle(root).fontSize root.style.setProperty('--tsqd-font-size', fontSize) } onFocus() window.addEventListener('focus', onFocus) onCleanup(() => { window.removeEventListener('focus', onFocus) }) }) const pip_open = createMemo( () => (props.localStore.pip_open ?? 'false') as 'true' | 'false', ) return ( <>
) } const PiPPanel: Component<{ children: JSX.Element }> = (props) => { const pip = usePiPWindow() const theme = useTheme() const css = useQueryDevtoolsContext().shadowDOMTarget ? goober.css.bind({ target: useQueryDevtoolsContext().shadowDOMTarget }) : goober.css const styles = createMemo(() => { return theme() === 'dark' ? darkStyles(css) : lightStyles(css) }) const getPanelDynamicStyles = () => { const { colors } = tokens const t = (light: string, dark: string) => theme() === 'dark' ? dark : light if (panelWidth() < secondBreakpoint) { return css` flex-direction: column; background-color: ${t(colors.gray[300], colors.gray[600])}; ` } return css` flex-direction: row; background-color: ${t(colors.gray[200], colors.darkGray[900])}; ` } createEffect(() => { const win = pip().pipWindow const resizeCB = () => { if (!win) return setPanelWidth(win.innerWidth) } if (win) { win.addEventListener('resize', resizeCB) resizeCB() } onCleanup(() => { if (win) { win.removeEventListener('resize', resizeCB) } }) }) return (
{props.children}
) } export const ParentPanel: Component<{ children: JSX.Element }> = (props) => { const theme = useTheme() const css = useQueryDevtoolsContext().shadowDOMTarget ? goober.css.bind({ target: useQueryDevtoolsContext().shadowDOMTarget }) : goober.css const styles = createMemo(() => { return theme() === 'dark' ? darkStyles(css) : lightStyles(css) }) let panelRef!: HTMLDivElement onMount(() => { createResizeObserver(panelRef, ({ width }, el) => { if (el === panelRef) { setPanelWidth(width) } }) }) const getPanelDynamicStyles = () => { const { colors } = tokens const t = (light: string, dark: string) => theme() === 'dark' ? dark : light if (panelWidth() < secondBreakpoint) { return css` flex-direction: column; background-color: ${t(colors.gray[300], colors.gray[600])}; ` } return css` flex-direction: row; background-color: ${t(colors.gray[200], colors.darkGray[900])}; ` } return (
{props.children}
) } const DraggablePanel: Component = (props) => { const theme = useTheme() const css = useQueryDevtoolsContext().shadowDOMTarget ? goober.css.bind({ target: useQueryDevtoolsContext().shadowDOMTarget }) : goober.css const styles = createMemo(() => { return theme() === 'dark' ? darkStyles(css) : lightStyles(css) }) const [isResizing, setIsResizing] = createSignal(false) const position = createMemo( () => (props.localStore.position || useQueryDevtoolsContext().position || POSITION) as DevtoolsPosition, ) const handleDragStart: JSX.EventHandler = ( event, ) => { const panelElement = event.currentTarget.parentElement if (!panelElement) return setIsResizing(true) const { height, width } = panelElement.getBoundingClientRect() const startX = event.clientX const startY = event.clientY let newSize = 0 const minHeight = convertRemToPixels(3.5) const minWidth = convertRemToPixels(12) const runDrag = (moveEvent: MouseEvent) => { moveEvent.preventDefault() if (position() === 'left' || position() === 'right') { const valToAdd = position() === 'right' ? startX - moveEvent.clientX : moveEvent.clientX - startX newSize = Math.round(width + valToAdd) if (newSize < minWidth) { newSize = minWidth } props.setLocalStore('width', String(Math.round(newSize))) const newWidth = panelElement.getBoundingClientRect().width // If the panel size didn't decrease, this means we have reached the minimum width // of the panel so we restore the original width in local storage // Restoring the width helps in smooth open/close transitions if (Number(props.localStore.width) < newWidth) { props.setLocalStore('width', String(newWidth)) } } else { const valToAdd = position() === 'bottom' ? startY - moveEvent.clientY : moveEvent.clientY - startY newSize = Math.round(height + valToAdd) // If the panel size is less than the minimum height, // we set the size to the minimum height if (newSize < minHeight) { newSize = minHeight setSelectedQueryHash(null) } props.setLocalStore('height', String(Math.round(newSize))) } } const unsubscribe = () => { if (isResizing()) { setIsResizing(false) } document.removeEventListener('mousemove', runDrag, false) document.removeEventListener('mouseUp', unsubscribe, false) } document.addEventListener('mousemove', runDrag, false) document.addEventListener('mouseup', unsubscribe, false) } let panelRef!: HTMLDivElement onMount(() => { createResizeObserver(panelRef, ({ width }, el) => { if (el === panelRef) { setPanelWidth(width) } }) }) createEffect(() => { const rootContainer = panelRef.parentElement?.parentElement?.parentElement if (!rootContainer) return const currentPosition = (props.localStore.position || POSITION) as DevtoolsPosition const styleProp = getSidedProp('padding', currentPosition) const isVertical = props.localStore.position === 'left' || props.localStore.position === 'right' const previousPaddings = (({ padding, paddingTop, paddingBottom, paddingLeft, paddingRight, }) => ({ padding, paddingTop, paddingBottom, paddingLeft, paddingRight, }))(rootContainer.style) rootContainer.style[styleProp] = `${ isVertical ? props.localStore.width : props.localStore.height }px` onCleanup(() => { Object.entries(previousPaddings).forEach(([property, previousValue]) => { rootContainer.style[property as keyof typeof previousPaddings] = previousValue }) }) }) const getPanelDynamicStyles = () => { const { colors } = tokens const t = (light: string, dark: string) => theme() === 'dark' ? dark : light if (panelWidth() < secondBreakpoint) { return css` flex-direction: column; background-color: ${t(colors.gray[300], colors.gray[600])}; ` } return css` flex-direction: row; background-color: ${t(colors.gray[200], colors.darkGray[900])}; ` } return ( ) } export const ContentView: Component = (props) => { setupQueryCacheSubscription() setupMutationCacheSubscription() let containerRef!: HTMLDivElement const theme = useTheme() const css = useQueryDevtoolsContext().shadowDOMTarget ? goober.css.bind({ target: useQueryDevtoolsContext().shadowDOMTarget }) : goober.css const styles = createMemo(() => { return theme() === 'dark' ? darkStyles(css) : lightStyles(css) }) const pip = usePiPWindow() const [selectedView, setSelectedView] = createSignal<'queries' | 'mutations'>( 'queries', ) const sort = createMemo(() => props.localStore.sort || DEFAULT_SORT_FN_NAME) const sortOrder = createMemo( () => Number(props.localStore.sortOrder) || DEFAULT_SORT_ORDER, ) as () => 1 | -1 const mutationSort = createMemo( () => props.localStore.mutationSort || DEFAULT_MUTATION_SORT_FN_NAME, ) const mutationSortOrder = createMemo( () => Number(props.localStore.mutationSortOrder) || DEFAULT_SORT_ORDER, ) as () => 1 | -1 const sortFn = createMemo(() => sortFns[sort() as string]) const mutationSortFn = createMemo( () => mutationSortFns[mutationSort() as string], ) const onlineManager = createMemo( () => useQueryDevtoolsContext().onlineManager, ) const query_cache = createMemo(() => { return useQueryDevtoolsContext().client.getQueryCache() }) const mutation_cache = createMemo(() => { return useQueryDevtoolsContext().client.getMutationCache() }) const queryCount = createSubscribeToQueryCacheBatcher((queryCache) => { return queryCache().getAll().length }, false) const queries = createMemo( on( () => [ queryCount(), props.localStore.filter, sort(), sortOrder(), props.localStore.hideDisabledQueries, ], () => { const curr = query_cache().getAll() let filtered = props.localStore.filter ? curr.filter( (item) => rankItem(item.queryHash, props.localStore.filter || '').passed, ) : [...curr] // Filter out disabled queries if hideDisabledQueries is enabled if (props.localStore.hideDisabledQueries === 'true') { filtered = filtered.filter((item) => !item.isDisabled()) } const sorted = sortFn() ? filtered.sort((a, b) => sortFn()!(a, b) * sortOrder()) : filtered return sorted }, ), ) const mutationCount = createSubscribeToMutationCacheBatcher( (mutationCache) => { return mutationCache().getAll().length }, false, ) const mutations = createMemo( on( () => [ mutationCount(), props.localStore.mutationFilter, mutationSort(), mutationSortOrder(), ], () => { const curr = mutation_cache().getAll() const filtered = props.localStore.mutationFilter ? curr.filter((item) => { const value = `${ item.options.mutationKey ? JSON.stringify(item.options.mutationKey) + ' - ' : '' }${new Date(item.state.submittedAt).toLocaleString()}` return rankItem(value, props.localStore.mutationFilter || '') .passed }) : [...curr] const sorted = mutationSortFn() ? filtered.sort( (a, b) => mutationSortFn()!(a, b) * mutationSortOrder(), ) : filtered return sorted }, ), ) const setDevtoolsPosition = (pos: DevtoolsPosition) => { props.setLocalStore('position', pos) } // Sets the Font Size variable on portal menu elements since they will be outside // the main panel container const setComputedVariables = (el: HTMLDivElement) => { const computedStyle = getComputedStyle(containerRef) const variable = computedStyle.getPropertyValue('--tsqd-font-size') el.style.setProperty('--tsqd-font-size', variable) } return ( <>
{ setSelectedView(value as 'queries' | 'mutations') setSelectedQueryHash(null) setSelectedMutationId(null) }} > Queries Mutations
{ if (selectedView() === 'queries') { props.setLocalStore('filter', e.currentTarget.value) } else { props.setLocalStore('mutationFilter', e.currentTarget.value) } }} class={cx('tsqd-query-filter-textfield')} name="tsqd-query-filter-input" value={ selectedView() === 'queries' ? props.localStore.filter || '' : props.localStore.mutationFilter || '' } />
setComputedVariables(el as HTMLDivElement)} mount={ pip().pipWindow ? pip().pipWindow!.document.body : document.body } >
Settings
Position setComputedVariables(el as HTMLDivElement)} mount={ pip().pipWindow ? pip().pipWindow!.document.body : document.body } > { setDevtoolsPosition('top') }} as="button" class={cx( styles().settingsSubButton, 'tsqd-settings-menu-position-btn', 'tsqd-settings-menu-position-btn-top', )} > Top { setDevtoolsPosition('bottom') }} as="button" class={cx( styles().settingsSubButton, 'tsqd-settings-menu-position-btn', 'tsqd-settings-menu-position-btn-bottom', )} > Bottom { setDevtoolsPosition('left') }} as="button" class={cx( styles().settingsSubButton, 'tsqd-settings-menu-position-btn', 'tsqd-settings-menu-position-btn-left', )} > Left { setDevtoolsPosition('right') }} as="button" class={cx( styles().settingsSubButton, 'tsqd-settings-menu-position-btn', 'tsqd-settings-menu-position-btn-right', )} > Right Theme setComputedVariables(el as HTMLDivElement)} mount={ pip().pipWindow ? pip().pipWindow!.document.body : document.body } > { props.setLocalStore('theme_preference', 'light') }} as="button" class={cx( styles().settingsSubButton, props.localStore.theme_preference === 'light' && styles().themeSelectedButton, 'tsqd-settings-menu-position-btn', 'tsqd-settings-menu-position-btn-top', )} > Light { props.setLocalStore('theme_preference', 'dark') }} as="button" class={cx( styles().settingsSubButton, props.localStore.theme_preference === 'dark' && styles().themeSelectedButton, 'tsqd-settings-menu-position-btn', 'tsqd-settings-menu-position-btn-bottom', )} > Dark { props.setLocalStore('theme_preference', 'system') }} as="button" class={cx( styles().settingsSubButton, props.localStore.theme_preference === 'system' && styles().themeSelectedButton, 'tsqd-settings-menu-position-btn', 'tsqd-settings-menu-position-btn-left', )} > System Disabled Queries setComputedVariables(el as HTMLDivElement)} mount={ pip().pipWindow ? pip().pipWindow!.document.body : document.body } > { props.setLocalStore('hideDisabledQueries', 'false') }} as="button" class={cx( styles().settingsSubButton, props.localStore.hideDisabledQueries !== 'true' && styles().themeSelectedButton, 'tsqd-settings-menu-position-btn', 'tsqd-settings-menu-position-btn-show', )} > Show { props.setLocalStore('hideDisabledQueries', 'true') }} as="button" class={cx( styles().settingsSubButton, props.localStore.hideDisabledQueries === 'true' && styles().themeSelectedButton, 'tsqd-settings-menu-position-btn', 'tsqd-settings-menu-position-btn-hide', )} > Hide
q.queryHash} each={queries()}> {(query) => }
m.mutationId} each={mutations()}> {(mutation) => }
) } const QueryRow: Component<{ query: Query }> = (props) => { const theme = useTheme() const css = useQueryDevtoolsContext().shadowDOMTarget ? goober.css.bind({ target: useQueryDevtoolsContext().shadowDOMTarget }) : goober.css const styles = createMemo(() => { return theme() === 'dark' ? darkStyles(css) : lightStyles(css) }) const { colors, alpha } = tokens const t = (light: string, dark: string) => (theme() === 'dark' ? dark : light) const queryState = createSubscribeToQueryCacheBatcher( (queryCache) => queryCache().find({ queryKey: props.query.queryKey, })?.state, true, (e) => e.query.queryHash === props.query.queryHash, ) const isDisabled = createSubscribeToQueryCacheBatcher( (queryCache) => queryCache() .find({ queryKey: props.query.queryKey, }) ?.isDisabled() ?? false, true, (e) => e.query.queryHash === props.query.queryHash, ) const isStatic = createSubscribeToQueryCacheBatcher( (queryCache) => queryCache() .find({ queryKey: props.query.queryKey, }) ?.isStatic() ?? false, true, (e) => e.query.queryHash === props.query.queryHash, ) const isStale = createSubscribeToQueryCacheBatcher( (queryCache) => queryCache() .find({ queryKey: props.query.queryKey, }) ?.isStale() ?? false, true, (e) => e.query.queryHash === props.query.queryHash, ) const observers = createSubscribeToQueryCacheBatcher( (queryCache) => queryCache() .find({ queryKey: props.query.queryKey, }) ?.getObserversCount() ?? 0, true, (e) => e.query.queryHash === props.query.queryHash, ) const color = createMemo(() => getQueryStatusColor({ queryState: queryState()!, observerCount: observers(), isStale: isStale(), }), ) const getObserverCountColorStyles = () => { if (color() === 'gray') { return css` background-color: ${t(colors[color()][200], colors[color()][700])}; color: ${t(colors[color()][700], colors[color()][300])}; ` } return css` background-color: ${t( colors[color()][200] + alpha[80], colors[color()][900], )}; color: ${t(colors[color()][800], colors[color()][300])}; ` } return ( ) } const MutationRow: Component<{ mutation: Mutation }> = (props) => { const theme = useTheme() const css = useQueryDevtoolsContext().shadowDOMTarget ? goober.css.bind({ target: useQueryDevtoolsContext().shadowDOMTarget }) : goober.css const styles = createMemo(() => { return theme() === 'dark' ? darkStyles(css) : lightStyles(css) }) const { colors, alpha } = tokens const t = (light: string, dark: string) => (theme() === 'dark' ? dark : light) const mutationState = createSubscribeToMutationCacheBatcher( (mutationCache) => { const mutations = mutationCache().getAll() const mutation = mutations.find( (m) => m.mutationId === props.mutation.mutationId, ) return mutation?.state }, ) const isPaused = createSubscribeToMutationCacheBatcher((mutationCache) => { const mutations = mutationCache().getAll() const mutation = mutations.find( (m) => m.mutationId === props.mutation.mutationId, ) if (!mutation) return false return mutation.state.isPaused }) const status = createSubscribeToMutationCacheBatcher((mutationCache) => { const mutations = mutationCache().getAll() const mutation = mutations.find( (m) => m.mutationId === props.mutation.mutationId, ) if (!mutation) return 'idle' return mutation.state.status }) const color = createMemo(() => getMutationStatusColor({ isPaused: isPaused(), status: status(), }), ) const getObserverCountColorStyles = () => { if (color() === 'gray') { return css` background-color: ${t(colors[color()][200], colors[color()][700])}; color: ${t(colors[color()][700], colors[color()][300])}; ` } return css` background-color: ${t( colors[color()][200] + alpha[80], colors[color()][900], )}; color: ${t(colors[color()][800], colors[color()][300])}; ` } return ( ) } const QueryStatusCount: Component = () => { const stale = createSubscribeToQueryCacheBatcher( (queryCache) => queryCache() .getAll() .filter((q) => getQueryStatusLabel(q) === 'stale').length, ) const fresh = createSubscribeToQueryCacheBatcher( (queryCache) => queryCache() .getAll() .filter((q) => getQueryStatusLabel(q) === 'fresh').length, ) const fetching = createSubscribeToQueryCacheBatcher( (queryCache) => queryCache() .getAll() .filter((q) => getQueryStatusLabel(q) === 'fetching').length, ) const paused = createSubscribeToQueryCacheBatcher( (queryCache) => queryCache() .getAll() .filter((q) => getQueryStatusLabel(q) === 'paused').length, ) const inactive = createSubscribeToQueryCacheBatcher( (queryCache) => queryCache() .getAll() .filter((q) => getQueryStatusLabel(q) === 'inactive').length, ) const theme = useTheme() const css = useQueryDevtoolsContext().shadowDOMTarget ? goober.css.bind({ target: useQueryDevtoolsContext().shadowDOMTarget }) : goober.css const styles = createMemo(() => { return theme() === 'dark' ? darkStyles(css) : lightStyles(css) }) return (
) } const MutationStatusCount: Component = () => { const success = createSubscribeToMutationCacheBatcher( (mutationCache) => mutationCache() .getAll() .filter( (m) => getMutationStatusColor({ isPaused: m.state.isPaused, status: m.state.status, }) === 'green', ).length, ) const pending = createSubscribeToMutationCacheBatcher( (mutationCache) => mutationCache() .getAll() .filter( (m) => getMutationStatusColor({ isPaused: m.state.isPaused, status: m.state.status, }) === 'yellow', ).length, ) const paused = createSubscribeToMutationCacheBatcher( (mutationCache) => mutationCache() .getAll() .filter( (m) => getMutationStatusColor({ isPaused: m.state.isPaused, status: m.state.status, }) === 'purple', ).length, ) const error = createSubscribeToMutationCacheBatcher( (mutationCache) => mutationCache() .getAll() .filter( (m) => getMutationStatusColor({ isPaused: m.state.isPaused, status: m.state.status, }) === 'red', ).length, ) const theme = useTheme() const css = useQueryDevtoolsContext().shadowDOMTarget ? goober.css.bind({ target: useQueryDevtoolsContext().shadowDOMTarget }) : goober.css const styles = createMemo(() => { return theme() === 'dark' ? darkStyles(css) : lightStyles(css) }) return (
) } const QueryStatus: Component = (props) => { const theme = useTheme() const css = useQueryDevtoolsContext().shadowDOMTarget ? goober.css.bind({ target: useQueryDevtoolsContext().shadowDOMTarget }) : goober.css const styles = createMemo(() => { return theme() === 'dark' ? darkStyles(css) : lightStyles(css) }) const { colors, alpha } = tokens const t = (light: string, dark: string) => (theme() === 'dark' ? dark : light) let tagRef!: HTMLButtonElement const [mouseOver, setMouseOver] = createSignal(false) const [focused, setFocused] = createSignal(false) const showLabel = createMemo(() => { if (selectedQueryHash()) { if (panelWidth() < firstBreakpoint && panelWidth() > secondBreakpoint) { return false } } if (panelWidth() < secondBreakpoint) { return false } return true }) return ( ) } const QueryDetails = () => { const theme = useTheme() const css = useQueryDevtoolsContext().shadowDOMTarget ? goober.css.bind({ target: useQueryDevtoolsContext().shadowDOMTarget }) : goober.css const styles = createMemo(() => { return theme() === 'dark' ? darkStyles(css) : lightStyles(css) }) const { colors } = tokens const t = (light: string, dark: string) => (theme() === 'dark' ? dark : light) const queryClient = useQueryDevtoolsContext().client const [restoringLoading, setRestoringLoading] = createSignal(false) const [dataMode, setDataMode] = createSignal<'view' | 'edit'>('view') const [dataEditError, setDataEditError] = createSignal(false) const errorTypes = createMemo(() => { return useQueryDevtoolsContext().errorTypes || [] }) const activeQuery = createSubscribeToQueryCacheBatcher( (queryCache) => queryCache() .getAll() .find((query) => query.queryHash === selectedQueryHash()), false, ) const activeQueryFresh = createSubscribeToQueryCacheBatcher((queryCache) => { return queryCache() .getAll() .find((query) => query.queryHash === selectedQueryHash()) }, false) const activeQueryState = createSubscribeToQueryCacheBatcher( (queryCache) => queryCache() .getAll() .find((query) => query.queryHash === selectedQueryHash())?.state, false, ) const activeQueryStateData = createSubscribeToQueryCacheBatcher( (queryCache) => { return queryCache() .getAll() .find((query) => query.queryHash === selectedQueryHash())?.state.data }, false, ) const statusLabel = createSubscribeToQueryCacheBatcher((queryCache) => { const query = queryCache() .getAll() .find((q) => q.queryHash === selectedQueryHash()) if (!query) return 'inactive' return getQueryStatusLabel(query) }) const queryStatus = createSubscribeToQueryCacheBatcher((queryCache) => { const query = queryCache() .getAll() .find((q) => q.queryHash === selectedQueryHash()) if (!query) return 'pending' return query.state.status }) const observerCount = createSubscribeToQueryCacheBatcher( (queryCache) => queryCache() .getAll() .find((query) => query.queryHash === selectedQueryHash()) ?.getObserversCount() ?? 0, ) const color = createMemo(() => getQueryStatusColorByLabel(statusLabel())) const handleRefetch = () => { sendDevToolsEvent({ type: 'REFETCH', queryHash: activeQuery()?.queryHash }) const promise = activeQuery()?.fetch() promise?.catch(() => {}) } const triggerError = (errorType?: DevtoolsErrorType) => { const activeQueryVal = activeQuery() if (!activeQueryVal) return sendDevToolsEvent({ type: 'TRIGGER_ERROR', queryHash: activeQueryVal.queryHash, metadata: { error: errorType?.name }, }) const error = errorType?.initializer(activeQueryVal) ?? new Error('Unknown error from devtools') const __previousQueryOptions = activeQueryVal.options activeQueryVal.setState({ status: 'error', error, fetchMeta: { ...activeQueryVal.state.fetchMeta, __previousQueryOptions, } as any, } as QueryState) } const restoreQueryAfterLoadingOrError = () => { const activeQueryVal = activeQuery() if (!activeQueryVal) return sendDevToolsEvent({ type: 'RESTORE_LOADING', queryHash: activeQueryVal.queryHash, }) const previousState = activeQueryVal.state const previousOptions = activeQueryVal.state.fetchMeta ? (activeQueryVal.state.fetchMeta as any).__previousQueryOptions : null activeQueryVal.cancel({ silent: true }) activeQueryVal.setState({ ...previousState, fetchStatus: 'idle', fetchMeta: null, }) if (previousOptions) { activeQueryVal.fetch(previousOptions) } } createEffect(() => { if (statusLabel() !== 'fetching') { setRestoringLoading(false) } }) const getQueryStatusColors = () => { if (color() === 'gray') { return css` background-color: ${t(colors[color()][200], colors[color()][700])}; color: ${t(colors[color()][700], colors[color()][300])}; border-color: ${t(colors[color()][400], colors[color()][600])}; ` } return css` background-color: ${t(colors[color()][100], colors[color()][900])}; color: ${t(colors[color()][700], colors[color()][300])}; border-color: ${t(colors[color()][400], colors[color()][600])}; ` } return (
Query Details
              {displayValue(activeQuery()!.queryKey, true)}
            
{statusLabel()}
Observers: {observerCount()}
Last Updated: {new Date(activeQueryState()!.dataUpdatedAt).toLocaleTimeString()}
Actions
Trigger Error
Data {dataMode() === 'view' ? 'Explorer' : 'Editor'}
setDataMode('edit')} activeQuery={activeQuery()} />
{ e.preventDefault() const formData = new FormData(e.currentTarget) const data = formData.get('data') as string try { const parsedData = JSON.parse(data) activeQuery()!.setState({ ...activeQuery()!.state, data: parsedData, }) setDataMode('view') } catch (error) { setDataEditError(true) } }} >
{dataEditError() ? 'Invalid Value' : ''}
Query Explorer
) } const MutationDetails = () => { const theme = useTheme() const css = useQueryDevtoolsContext().shadowDOMTarget ? goober.css.bind({ target: useQueryDevtoolsContext().shadowDOMTarget }) : goober.css const styles = createMemo(() => { return theme() === 'dark' ? darkStyles(css) : lightStyles(css) }) const { colors } = tokens const t = (light: string, dark: string) => (theme() === 'dark' ? dark : light) const isPaused = createSubscribeToMutationCacheBatcher((mutationCache) => { const mutations = mutationCache().getAll() const mutation = mutations.find( (m) => m.mutationId === selectedMutationId(), ) if (!mutation) return false return mutation.state.isPaused }) const status = createSubscribeToMutationCacheBatcher((mutationCache) => { const mutations = mutationCache().getAll() const mutation = mutations.find( (m) => m.mutationId === selectedMutationId(), ) if (!mutation) return 'idle' return mutation.state.status }) const color = createMemo(() => getMutationStatusColor({ isPaused: isPaused(), status: status(), }), ) const activeMutation = createSubscribeToMutationCacheBatcher( (mutationCache) => mutationCache() .getAll() .find((mutation) => mutation.mutationId === selectedMutationId()), false, ) const getQueryStatusColors = () => { if (color() === 'gray') { return css` background-color: ${t(colors[color()][200], colors[color()][700])}; color: ${t(colors[color()][700], colors[color()][300])}; border-color: ${t(colors[color()][400], colors[color()][600])}; ` } return css` background-color: ${t(colors[color()][100], colors[color()][900])}; color: ${t(colors[color()][700], colors[color()][300])}; border-color: ${t(colors[color()][400], colors[color()][600])}; ` } return (
Mutation Details
              
                
                  {displayValue(activeMutation()!.options.mutationKey, true)}
                
              
            
pending {status()}
Submitted At: {new Date( activeMutation()!.state.submittedAt, ).toLocaleTimeString()}
Variables Details
Context Details
Data Explorer
Mutations Explorer
) } const queryCacheMap = new Map< (q: Accessor) => any, { setter: Setter shouldUpdate: (event: QueryCacheNotifyEvent) => boolean } >() const setupQueryCacheSubscription = () => { const queryCache = createMemo(() => { const client = useQueryDevtoolsContext().client return client.getQueryCache() }) const unsubscribe = queryCache().subscribe((q) => { batch(() => { for (const [callback, value] of queryCacheMap.entries()) { if (!value.shouldUpdate(q)) continue value.setter(callback(queryCache)) } }) }) onCleanup(() => { queryCacheMap.clear() unsubscribe() }) return unsubscribe } const createSubscribeToQueryCacheBatcher = ( callback: (queryCache: Accessor) => Exclude, equalityCheck: boolean = true, shouldUpdate: (event: QueryCacheNotifyEvent) => boolean = () => true, ) => { const queryCache = createMemo(() => { const client = useQueryDevtoolsContext().client return client.getQueryCache() }) const [value, setValue] = createSignal( callback(queryCache), !equalityCheck ? { equals: false } : undefined, ) createEffect(() => { setValue(callback(queryCache)) }) queryCacheMap.set(callback, { setter: setValue, shouldUpdate: shouldUpdate, }) onCleanup(() => { queryCacheMap.delete(callback) }) return value } const mutationCacheMap = new Map< (q: Accessor) => any, Setter >() const setupMutationCacheSubscription = () => { const mutationCache = createMemo(() => { const client = useQueryDevtoolsContext().client return client.getMutationCache() }) const unsubscribe = mutationCache().subscribe(() => { for (const [callback, setter] of mutationCacheMap.entries()) { queueMicrotask(() => { setter(callback(mutationCache)) }) } }) onCleanup(() => { mutationCacheMap.clear() unsubscribe() }) return unsubscribe } const createSubscribeToMutationCacheBatcher = ( callback: (queryCache: Accessor) => Exclude, equalityCheck: boolean = true, ) => { const mutationCache = createMemo(() => { const client = useQueryDevtoolsContext().client return client.getMutationCache() }) const [value, setValue] = createSignal( callback(mutationCache), !equalityCheck ? { equals: false } : undefined, ) createEffect(() => { setValue(callback(mutationCache)) }) mutationCacheMap.set(callback, setValue) onCleanup(() => { mutationCacheMap.delete(callback) }) return value } type DevToolsActionType = | 'REFETCH' | 'INVALIDATE' | 'RESET' | 'REMOVE' | 'TRIGGER_ERROR' | 'RESTORE_ERROR' | 'TRIGGER_LOADING' | 'RESTORE_LOADING' | 'CLEAR_MUTATION_CACHE' | 'CLEAR_QUERY_CACHE' const DEV_TOOLS_EVENT = '@tanstack/query-devtools-event' const sendDevToolsEvent = ({ type, queryHash, metadata, }: { type: DevToolsActionType queryHash?: string metadata?: Record }) => { const event = new CustomEvent(DEV_TOOLS_EVENT, { detail: { type, queryHash, metadata }, bubbles: true, cancelable: true, }) window.dispatchEvent(event) } const stylesFactory = ( theme: 'light' | 'dark', css: (typeof goober)['css'], ) => { const { colors, font, size, alpha, shadow, border } = tokens const t = (light: string, dark: string) => (theme === 'light' ? light : dark) return { devtoolsBtn: css` z-index: 100000; position: fixed; padding: 4px; text-align: left; display: flex; align-items: center; justify-content: center; border-radius: 9999px; box-shadow: ${shadow.md()}; overflow: hidden; & div { position: absolute; top: -8px; left: -8px; right: -8px; bottom: -8px; border-radius: 9999px; & svg { position: absolute; width: 100%; height: 100%; } filter: blur(6px) saturate(1.2) contrast(1.1); } &:focus-within { outline-offset: 2px; outline: 3px solid ${colors.green[600]}; } & button { position: relative; z-index: 1; padding: 0; border-radius: 9999px; background-color: transparent; border: none; height: 40px; display: flex; width: 40px; overflow: hidden; cursor: pointer; outline: none; & svg { position: absolute; width: 100%; height: 100%; } } `, panel: css` position: fixed; z-index: 9999; display: flex; gap: ${tokens.size[0.5]}; & * { box-sizing: border-box; text-transform: none; } & *::-webkit-scrollbar { width: 7px; } & *::-webkit-scrollbar-track { background: transparent; } & *::-webkit-scrollbar-thumb { background: ${t(colors.gray[300], colors.darkGray[200])}; } & *::-webkit-scrollbar-thumb:hover { background: ${t(colors.gray[400], colors.darkGray[300])}; } `, parentPanel: css` z-index: 9999; display: flex; height: 100%; gap: ${tokens.size[0.5]}; & * { box-sizing: border-box; text-transform: none; } & *::-webkit-scrollbar { width: 7px; } & *::-webkit-scrollbar-track { background: transparent; } & *::-webkit-scrollbar-thumb { background: ${t(colors.gray[300], colors.darkGray[200])}; } & *::-webkit-scrollbar-thumb:hover { background: ${t(colors.gray[400], colors.darkGray[300])}; } `, 'devtoolsBtn-position-bottom-right': css` bottom: 12px; right: 12px; `, 'devtoolsBtn-position-bottom-left': css` bottom: 12px; left: 12px; `, 'devtoolsBtn-position-top-left': css` top: 12px; left: 12px; `, 'devtoolsBtn-position-top-right': css` top: 12px; right: 12px; `, 'devtoolsBtn-position-relative': css` position: relative; `, 'panel-position-top': css` top: 0; right: 0; left: 0; max-height: 90%; min-height: ${size[14]}; border-bottom: ${t(colors.gray[400], colors.darkGray[300])} 1px solid; `, 'panel-position-bottom': css` bottom: 0; right: 0; left: 0; max-height: 90%; min-height: ${size[14]}; border-top: ${t(colors.gray[400], colors.darkGray[300])} 1px solid; `, 'panel-position-right': css` bottom: 0; right: 0; top: 0; border-left: ${t(colors.gray[400], colors.darkGray[300])} 1px solid; max-width: 90%; `, 'panel-position-left': css` bottom: 0; left: 0; top: 0; border-right: ${t(colors.gray[400], colors.darkGray[300])} 1px solid; max-width: 90%; `, closeBtn: css` position: absolute; cursor: pointer; z-index: 5; display: flex; align-items: center; justify-content: center; outline: none; background-color: ${t(colors.gray[50], colors.darkGray[700])}; &:hover { background-color: ${t(colors.gray[200], colors.darkGray[500])}; } &:focus-visible { outline: 2px solid ${colors.blue[600]}; } & svg { color: ${t(colors.gray[600], colors.gray[400])}; width: ${size[2]}; height: ${size[2]}; } `, 'closeBtn-position-top': css` bottom: 0; right: ${size[2]}; transform: translate(0, 100%); border-right: ${t(colors.gray[400], colors.darkGray[300])} 1px solid; border-left: ${t(colors.gray[400], colors.darkGray[300])} 1px solid; border-top: none; border-bottom: ${t(colors.gray[400], colors.darkGray[300])} 1px solid; border-radius: 0px 0px ${border.radius.sm} ${border.radius.sm}; padding: ${size[0.5]} ${size[1.5]} ${size[1]} ${size[1.5]}; &::after { content: ' '; position: absolute; bottom: 100%; left: -${size[2.5]}; height: ${size[1.5]}; width: calc(100% + ${size[5]}); } & svg { transform: rotate(180deg); } `, 'closeBtn-position-bottom': css` top: 0; right: ${size[2]}; transform: translate(0, -100%); border-right: ${t(colors.gray[400], colors.darkGray[300])} 1px solid; border-left: ${t(colors.gray[400], colors.darkGray[300])} 1px solid; border-top: ${t(colors.gray[400], colors.darkGray[300])} 1px solid; border-bottom: none; border-radius: ${border.radius.sm} ${border.radius.sm} 0px 0px; padding: ${size[1]} ${size[1.5]} ${size[0.5]} ${size[1.5]}; &::after { content: ' '; position: absolute; top: 100%; left: -${size[2.5]}; height: ${size[1.5]}; width: calc(100% + ${size[5]}); } `, 'closeBtn-position-right': css` bottom: ${size[2]}; left: 0; transform: translate(-100%, 0); border-right: none; border-left: ${t(colors.gray[400], colors.darkGray[300])} 1px solid; border-top: ${t(colors.gray[400], colors.darkGray[300])} 1px solid; border-bottom: ${t(colors.gray[400], colors.darkGray[300])} 1px solid; border-radius: ${border.radius.sm} 0px 0px ${border.radius.sm}; padding: ${size[1.5]} ${size[0.5]} ${size[1.5]} ${size[1]}; &::after { content: ' '; position: absolute; left: 100%; height: calc(100% + ${size[5]}); width: ${size[1.5]}; } & svg { transform: rotate(-90deg); } `, 'closeBtn-position-left': css` bottom: ${size[2]}; right: 0; transform: translate(100%, 0); border-left: none; border-right: ${t(colors.gray[400], colors.darkGray[300])} 1px solid; border-top: ${t(colors.gray[400], colors.darkGray[300])} 1px solid; border-bottom: ${t(colors.gray[400], colors.darkGray[300])} 1px solid; border-radius: 0px ${border.radius.sm} ${border.radius.sm} 0px; padding: ${size[1.5]} ${size[1]} ${size[1.5]} ${size[0.5]}; &::after { content: ' '; position: absolute; right: 100%; height: calc(100% + ${size[5]}); width: ${size[1.5]}; } & svg { transform: rotate(90deg); } `, queriesContainer: css` flex: 1 1 700px; background-color: ${t(colors.gray[50], colors.darkGray[700])}; display: flex; flex-direction: column; & * { font-family: ui-sans-serif, Inter, system-ui, sans-serif, sans-serif; } `, dragHandle: css` position: absolute; transition: background-color 0.125s ease; &:hover { background-color: ${colors.purple[400]}${t('', alpha[90])}; } z-index: 4; `, 'dragHandle-position-top': css` bottom: 0; width: 100%; height: 3px; cursor: ns-resize; `, 'dragHandle-position-bottom': css` top: 0; width: 100%; height: 3px; cursor: ns-resize; `, 'dragHandle-position-right': css` left: 0; width: 3px; height: 100%; cursor: ew-resize; `, 'dragHandle-position-left': css` right: 0; width: 3px; height: 100%; cursor: ew-resize; `, row: css` display: flex; justify-content: space-between; align-items: center; padding: ${tokens.size[2]} ${tokens.size[2.5]}; gap: ${tokens.size[2.5]}; border-bottom: ${t(colors.gray[300], colors.darkGray[500])} 1px solid; align-items: center; & > button { padding: 0; background: transparent; border: none; display: flex; gap: ${size[0.5]}; flex-direction: column; } `, logoAndToggleContainer: css` display: flex; gap: ${tokens.size[3]}; align-items: center; `, logo: css` cursor: pointer; display: flex; flex-direction: column; background-color: transparent; border: none; gap: ${tokens.size[0.5]}; padding: 0px; &:hover { opacity: 0.7; } &:focus-visible { outline-offset: 4px; border-radius: ${border.radius.xs}; outline: 2px solid ${colors.blue[800]}; } `, tanstackLogo: css` font-size: ${font.size.md}; font-weight: ${font.weight.bold}; line-height: ${font.lineHeight.xs}; white-space: nowrap; color: ${t(colors.gray[600], colors.gray[300])}; `, queryFlavorLogo: css` font-weight: ${font.weight.semibold}; font-size: ${font.size.xs}; background: linear-gradient( to right, ${t('#ea4037, #ff9b11', '#dd524b, #e9a03b')} ); background-clip: text; -webkit-background-clip: text; line-height: 1; -webkit-text-fill-color: transparent; white-space: nowrap; `, queryStatusContainer: css` display: flex; gap: ${tokens.size[2]}; height: min-content; `, queryStatusTag: css` display: flex; gap: ${tokens.size[1.5]}; box-sizing: border-box; height: ${tokens.size[6.5]}; background: ${t(colors.gray[50], colors.darkGray[500])}; color: ${t(colors.gray[700], colors.gray[300])}; border-radius: ${tokens.border.radius.sm}; font-size: ${font.size.sm}; padding: ${tokens.size[1]}; padding-left: ${tokens.size[1.5]}; align-items: center; font-weight: ${font.weight.medium}; border: ${t('1px solid ' + colors.gray[300], '1px solid transparent')}; user-select: none; position: relative; &:focus-visible { outline-offset: 2px; outline: 2px solid ${colors.blue[800]}; } `, queryStatusTagLabel: css` font-size: ${font.size.xs}; `, queryStatusCount: css` font-size: ${font.size.xs}; padding: 0 5px; display: flex; align-items: center; justify-content: center; color: ${t(colors.gray[500], colors.gray[400])}; background-color: ${t(colors.gray[200], colors.darkGray[300])}; border-radius: 2px; font-variant-numeric: tabular-nums; height: ${tokens.size[4.5]}; `, statusTooltip: css` position: absolute; z-index: 1; background-color: ${t(colors.gray[50], colors.darkGray[500])}; top: 100%; left: 50%; transform: translate(-50%, calc(${tokens.size[2]})); padding: ${tokens.size[0.5]} ${tokens.size[2]}; border-radius: ${tokens.border.radius.sm}; font-size: ${font.size.xs}; border: 1px solid ${t(colors.gray[400], colors.gray[600])}; color: ${t(colors['gray'][600], colors['gray'][300])}; &::before { top: 0px; content: ' '; display: block; left: 50%; transform: translate(-50%, -100%); position: absolute; border-color: transparent transparent ${t(colors.gray[400], colors.gray[600])} transparent; border-style: solid; border-width: 7px; /* transform: rotate(180deg); */ } &::after { top: 0px; content: ' '; display: block; left: 50%; transform: translate(-50%, calc(-100% + 2px)); position: absolute; border-color: transparent transparent ${t(colors.gray[100], colors.darkGray[500])} transparent; border-style: solid; border-width: 7px; } `, filtersContainer: css` display: flex; gap: ${tokens.size[2]}; & > button { cursor: pointer; padding: ${tokens.size[0.5]} ${tokens.size[1.5]} ${tokens.size[0.5]} ${tokens.size[2]}; border-radius: ${tokens.border.radius.sm}; background-color: ${t(colors.gray[100], colors.darkGray[400])}; border: 1px solid ${t(colors.gray[300], colors.darkGray[200])}; color: ${t(colors.gray[700], colors.gray[300])}; font-size: ${font.size.xs}; display: flex; align-items: center; line-height: ${font.lineHeight.sm}; gap: ${tokens.size[1.5]}; max-width: 160px; &:focus-visible { outline-offset: 2px; border-radius: ${border.radius.xs}; outline: 2px solid ${colors.blue[800]}; } & svg { width: ${tokens.size[3]}; height: ${tokens.size[3]}; color: ${t(colors.gray[500], colors.gray[400])}; } } `, filterInput: css` padding: ${size[0.5]} ${size[2]}; border-radius: ${tokens.border.radius.sm}; background-color: ${t(colors.gray[100], colors.darkGray[400])}; display: flex; box-sizing: content-box; align-items: center; gap: ${tokens.size[1.5]}; max-width: 160px; min-width: 100px; border: 1px solid ${t(colors.gray[300], colors.darkGray[200])}; height: min-content; color: ${t(colors.gray[600], colors.gray[400])}; & > svg { width: ${size[3]}; height: ${size[3]}; } & input { font-size: ${font.size.xs}; width: 100%; background-color: ${t(colors.gray[100], colors.darkGray[400])}; border: none; padding: 0; line-height: ${font.lineHeight.sm}; color: ${t(colors.gray[700], colors.gray[300])}; &::placeholder { color: ${t(colors.gray[700], colors.gray[300])}; } &:focus { outline: none; } } &:focus-within { outline-offset: 2px; border-radius: ${border.radius.xs}; outline: 2px solid ${colors.blue[800]}; } `, filterSelect: css` padding: ${tokens.size[0.5]} ${tokens.size[2]}; border-radius: ${tokens.border.radius.sm}; background-color: ${t(colors.gray[100], colors.darkGray[400])}; display: flex; align-items: center; gap: ${tokens.size[1.5]}; box-sizing: content-box; max-width: 160px; border: 1px solid ${t(colors.gray[300], colors.darkGray[200])}; height: min-content; & > svg { color: ${t(colors.gray[600], colors.gray[400])}; width: ${tokens.size[2]}; height: ${tokens.size[2]}; } & > select { appearance: none; color: ${t(colors.gray[700], colors.gray[300])}; min-width: 100px; line-height: ${font.lineHeight.sm}; font-size: ${font.size.xs}; background-color: ${t(colors.gray[100], colors.darkGray[400])}; border: none; &:focus { outline: none; } } &:focus-within { outline-offset: 2px; border-radius: ${border.radius.xs}; outline: 2px solid ${colors.blue[800]}; } `, actionsContainer: css` display: flex; gap: ${tokens.size[2]}; `, actionsBtn: css` border-radius: ${tokens.border.radius.sm}; background-color: ${t(colors.gray[100], colors.darkGray[400])}; border: 1px solid ${t(colors.gray[300], colors.darkGray[200])}; width: ${tokens.size[6.5]}; height: ${tokens.size[6.5]}; justify-content: center; display: flex; align-items: center; gap: ${tokens.size[1.5]}; max-width: 160px; cursor: pointer; padding: 0; &:hover { background-color: ${t(colors.gray[200], colors.darkGray[500])}; } & svg { color: ${t(colors.gray[700], colors.gray[300])}; width: ${tokens.size[3]}; height: ${tokens.size[3]}; } &:focus-visible { outline-offset: 2px; border-radius: ${border.radius.xs}; outline: 2px solid ${colors.blue[800]}; } `, actionsBtnOffline: css` & svg { stroke: ${t(colors.yellow[700], colors.yellow[500])}; fill: ${t(colors.yellow[700], colors.yellow[500])}; } `, overflowQueryContainer: css` flex: 1; overflow-y: auto; & > div { display: flex; flex-direction: column; } `, queryRow: css` display: flex; align-items: center; padding: 0; border: none; cursor: pointer; color: ${t(colors.gray[700], colors.gray[300])}; background-color: ${t(colors.gray[50], colors.darkGray[700])}; line-height: 1; &:focus { outline: none; } &:focus-visible { outline-offset: -2px; border-radius: ${border.radius.xs}; outline: 2px solid ${colors.blue[800]}; } &:hover .tsqd-query-hash { background-color: ${t(colors.gray[200], colors.darkGray[600])}; } & .tsqd-query-observer-count { padding: 0 ${tokens.size[1]}; user-select: none; min-width: ${tokens.size[6.5]}; align-self: stretch; display: flex; align-items: center; justify-content: center; font-size: ${font.size.xs}; font-weight: ${font.weight.medium}; border-bottom-width: 1px; border-bottom-style: solid; border-bottom: 1px solid ${t(colors.gray[300], colors.darkGray[700])}; } & .tsqd-query-hash { user-select: text; font-size: ${font.size.xs}; display: flex; align-items: center; min-height: ${tokens.size[6]}; flex: 1; padding: ${tokens.size[1]} ${tokens.size[2]}; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace; border-bottom: 1px solid ${t(colors.gray[300], colors.darkGray[400])}; text-align: left; text-overflow: clip; word-break: break-word; } & .tsqd-query-disabled-indicator { align-self: stretch; display: flex; align-items: center; padding: 0 ${tokens.size[2]}; color: ${t(colors.gray[800], colors.gray[300])}; background-color: ${t(colors.gray[300], colors.darkGray[600])}; border-bottom: 1px solid ${t(colors.gray[300], colors.darkGray[400])}; font-size: ${font.size.xs}; } & .tsqd-query-static-indicator { align-self: stretch; display: flex; align-items: center; padding: 0 ${tokens.size[2]}; color: ${t(colors.teal[800], colors.teal[300])}; background-color: ${t(colors.teal[100], colors.teal[900])}; border-bottom: 1px solid ${t(colors.teal[300], colors.teal[700])}; font-size: ${font.size.xs}; } `, selectedQueryRow: css` background-color: ${t(colors.gray[200], colors.darkGray[500])}; `, detailsContainer: css` flex: 1 1 700px; background-color: ${t(colors.gray[50], colors.darkGray[700])}; color: ${t(colors.gray[700], colors.gray[300])}; font-family: ui-sans-serif, Inter, system-ui, sans-serif, sans-serif; display: flex; flex-direction: column; overflow-y: auto; display: flex; text-align: left; `, detailsHeader: css` font-family: ui-sans-serif, Inter, system-ui, sans-serif, sans-serif; position: sticky; top: 0; z-index: 2; background-color: ${t(colors.gray[200], colors.darkGray[600])}; padding: ${tokens.size[1.5]} ${tokens.size[2]}; font-weight: ${font.weight.medium}; font-size: ${font.size.xs}; line-height: ${font.lineHeight.xs}; text-align: left; `, detailsBody: css` margin: ${tokens.size[1.5]} 0px ${tokens.size[2]} 0px; & > div { display: flex; align-items: stretch; padding: 0 ${tokens.size[2]}; line-height: ${font.lineHeight.sm}; justify-content: space-between; & > span { font-size: ${font.size.xs}; } & > span:nth-child(2) { font-variant-numeric: tabular-nums; } } & > div:first-child { margin-bottom: ${tokens.size[1.5]}; } & code { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace; margin: 0; font-size: ${font.size.xs}; line-height: ${font.lineHeight.xs}; max-width: 100%; white-space: pre-wrap; overflow-wrap: anywhere; word-break: break-word; } & pre { margin: 0; display: flex; align-items: center; } `, queryDetailsStatus: css` border: 1px solid ${colors.darkGray[200]}; border-radius: ${tokens.border.radius.sm}; font-weight: ${font.weight.medium}; padding: ${tokens.size[1]} ${tokens.size[2.5]}; `, actionsBody: css` flex-wrap: wrap; margin: ${tokens.size[2]} 0px ${tokens.size[2]} 0px; display: flex; gap: ${tokens.size[2]}; padding: 0px ${tokens.size[2]}; & > button { font-family: ui-sans-serif, Inter, system-ui, sans-serif, sans-serif; font-size: ${font.size.xs}; padding: ${tokens.size[1]} ${tokens.size[2]}; display: flex; border-radius: ${tokens.border.radius.sm}; background-color: ${t(colors.gray[100], colors.darkGray[600])}; border: 1px solid ${t(colors.gray[300], colors.darkGray[400])}; align-items: center; gap: ${tokens.size[2]}; font-weight: ${font.weight.medium}; line-height: ${font.lineHeight.xs}; cursor: pointer; &:focus-visible { outline-offset: 2px; border-radius: ${border.radius.xs}; outline: 2px solid ${colors.blue[800]}; } &:hover { background-color: ${t(colors.gray[200], colors.darkGray[500])}; } &:disabled { opacity: 0.6; cursor: not-allowed; } & > span { width: ${size[1.5]}; height: ${size[1.5]}; border-radius: ${tokens.border.radius.full}; } } `, actionsSelect: css` font-size: ${font.size.xs}; padding: ${tokens.size[0.5]} ${tokens.size[2]}; display: flex; border-radius: ${tokens.border.radius.sm}; overflow: hidden; background-color: ${t(colors.gray[100], colors.darkGray[600])}; border: 1px solid ${t(colors.gray[300], colors.darkGray[400])}; align-items: center; gap: ${tokens.size[2]}; font-weight: ${font.weight.medium}; line-height: ${font.lineHeight.sm}; color: ${t(colors.red[500], colors.red[400])}; cursor: pointer; position: relative; &:hover { background-color: ${t(colors.gray[200], colors.darkGray[500])}; } & > span { width: ${size[1.5]}; height: ${size[1.5]}; border-radius: ${tokens.border.radius.full}; } &:focus-within { outline-offset: 2px; border-radius: ${border.radius.xs}; outline: 2px solid ${colors.blue[800]}; } & select { position: absolute; top: 0; left: 0; width: 100%; height: 100%; appearance: none; background-color: transparent; border: none; color: transparent; outline: none; } & svg path { stroke: ${tokens.colors.red[400]}; } & svg { width: ${tokens.size[2]}; height: ${tokens.size[2]}; } `, settingsMenu: css` display: flex; & * { font-family: ui-sans-serif, Inter, system-ui, sans-serif, sans-serif; } flex-direction: column; gap: ${size[0.5]}; border-radius: ${tokens.border.radius.sm}; border: 1px solid ${t(colors.gray[300], colors.gray[700])}; background-color: ${t(colors.gray[50], colors.darkGray[600])}; font-size: ${font.size.xs}; color: ${t(colors.gray[700], colors.gray[300])}; z-index: 99999; min-width: 120px; padding: ${size[0.5]}; `, settingsSubTrigger: css` display: flex; align-items: center; justify-content: space-between; border-radius: ${tokens.border.radius.xs}; padding: ${tokens.size[1]} ${tokens.size[1]}; cursor: pointer; background-color: transparent; border: none; color: ${t(colors.gray[700], colors.gray[300])}; & svg { color: ${t(colors.gray[600], colors.gray[400])}; transform: rotate(-90deg); width: ${tokens.size[2]}; height: ${tokens.size[2]}; } &:hover { background-color: ${t(colors.gray[200], colors.darkGray[500])}; } &:focus-visible { outline-offset: 2px; outline: 2px solid ${colors.blue[800]}; } &.data-disabled { opacity: 0.6; cursor: not-allowed; } `, settingsMenuHeader: css` padding: ${tokens.size[1]} ${tokens.size[1]}; font-weight: ${font.weight.medium}; border-bottom: 1px solid ${t(colors.gray[300], colors.darkGray[400])}; color: ${t(colors.gray[500], colors.gray[400])}; font-size: ${font.size['xs']}; `, settingsSubButton: css` display: flex; align-items: center; justify-content: space-between; color: ${t(colors.gray[700], colors.gray[300])}; font-size: ${font.size['xs']}; border-radius: ${tokens.border.radius.xs}; padding: ${tokens.size[1]} ${tokens.size[1]}; cursor: pointer; background-color: transparent; border: none; & svg { color: ${t(colors.gray[600], colors.gray[400])}; } &:hover { background-color: ${t(colors.gray[200], colors.darkGray[500])}; } &:focus-visible { outline-offset: 2px; outline: 2px solid ${colors.blue[800]}; } `, themeSelectedButton: css` background-color: ${t(colors.purple[100], colors.purple[900])}; color: ${t(colors.purple[700], colors.purple[300])}; & svg { color: ${t(colors.purple[700], colors.purple[300])}; } &:hover { background-color: ${t(colors.purple[100], colors.purple[900])}; } `, viewToggle: css` border-radius: ${tokens.border.radius.sm}; background-color: ${t(colors.gray[200], colors.darkGray[600])}; border: 1px solid ${t(colors.gray[300], colors.darkGray[200])}; display: flex; padding: 0; font-size: ${font.size.xs}; color: ${t(colors.gray[700], colors.gray[300])}; overflow: hidden; &:has(:focus-visible) { outline: 2px solid ${colors.blue[800]}; } & .tsqd-radio-toggle { opacity: 0.5; display: flex; & label { display: flex; align-items: center; cursor: pointer; line-height: ${font.lineHeight.md}; } & label:hover { background-color: ${t(colors.gray[100], colors.darkGray[500])}; } } & > [data-checked] { opacity: 1; background-color: ${t(colors.gray[100], colors.darkGray[400])}; & label:hover { background-color: ${t(colors.gray[100], colors.darkGray[400])}; } } & .tsqd-radio-toggle:first-child { & label { padding: 0 ${tokens.size[1.5]} 0 ${tokens.size[2]}; } border-right: 1px solid ${t(colors.gray[300], colors.darkGray[200])}; } & .tsqd-radio-toggle:nth-child(2) { & label { padding: 0 ${tokens.size[2]} 0 ${tokens.size[1.5]}; } } `, devtoolsEditForm: css` padding: ${size[2]}; & > [data-error='true'] { outline: 2px solid ${t(colors.red[200], colors.red[800])}; outline-offset: 2px; border-radius: ${border.radius.xs}; } `, devtoolsEditTextarea: css` width: 100%; max-height: 500px; font-family: 'Fira Code', monospace; font-size: ${font.size.xs}; border-radius: ${border.radius.sm}; field-sizing: content; padding: ${size[2]}; background-color: ${t(colors.gray[100], colors.darkGray[800])}; color: ${t(colors.gray[900], colors.gray[100])}; border: 1px solid ${t(colors.gray[200], colors.gray[700])}; resize: none; &:focus { outline-offset: 2px; border-radius: ${border.radius.xs}; outline: 2px solid ${t(colors.blue[200], colors.blue[800])}; } `, devtoolsEditFormActions: css` display: flex; justify-content: space-between; gap: ${size[2]}; align-items: center; padding-top: ${size[1]}; font-size: ${font.size.xs}; `, devtoolsEditFormError: css` color: ${t(colors.red[700], colors.red[500])}; `, devtoolsEditFormActionContainer: css` display: flex; gap: ${size[2]}; `, devtoolsEditFormAction: css` font-family: ui-sans-serif, Inter, system-ui, sans-serif, sans-serif; font-size: ${font.size.xs}; padding: ${size[1]} ${tokens.size[2]}; display: flex; border-radius: ${border.radius.sm}; background-color: ${t(colors.gray[100], colors.darkGray[600])}; border: 1px solid ${t(colors.gray[300], colors.darkGray[400])}; align-items: center; gap: ${size[2]}; font-weight: ${font.weight.medium}; line-height: ${font.lineHeight.xs}; cursor: pointer; &:focus-visible { outline-offset: 2px; border-radius: ${border.radius.xs}; outline: 2px solid ${colors.blue[800]}; } &:hover { background-color: ${t(colors.gray[200], colors.darkGray[500])}; } &:disabled { opacity: 0.6; cursor: not-allowed; } `, } } const lightStyles = (css: (typeof goober)['css']) => stylesFactory('light', css) const darkStyles = (css: (typeof goober)['css']) => stylesFactory('dark', css) ================================================ FILE: packages/query-devtools/src/DevtoolsComponent.tsx ================================================ import { createLocalStorage } from '@solid-primitives/storage' import { createMemo } from 'solid-js' import { Devtools } from './Devtools' import { getPreferredColorScheme } from './utils' import { THEME_PREFERENCE } from './constants' import { PiPProvider, QueryDevtoolsContext, ThemeContext } from './contexts' import type { DevtoolsComponentType } from './Devtools' const DevtoolsComponent: DevtoolsComponentType = (props) => { const [localStore, setLocalStore] = createLocalStorage({ prefix: 'TanstackQueryDevtools', }) const colorScheme = getPreferredColorScheme() const theme = createMemo(() => { const preference = (localStore.theme_preference || THEME_PREFERENCE) as | 'system' | 'dark' | 'light' if (preference !== 'system') return preference return colorScheme() }) return ( ) } export default DevtoolsComponent ================================================ FILE: packages/query-devtools/src/DevtoolsPanelComponent.tsx ================================================ import { createLocalStorage } from '@solid-primitives/storage' import { createMemo } from 'solid-js' import { ContentView, ParentPanel } from './Devtools' import { getPreferredColorScheme } from './utils' import { THEME_PREFERENCE } from './constants' import { PiPProvider, QueryDevtoolsContext, ThemeContext } from './contexts' import type { DevtoolsComponentType } from './Devtools' const DevtoolsPanelComponent: DevtoolsComponentType = (props) => { const [localStore, setLocalStore] = createLocalStorage({ prefix: 'TanstackQueryDevtools', }) const colorScheme = getPreferredColorScheme() const theme = createMemo(() => { const preference = (localStore.theme_preference || THEME_PREFERENCE) as | 'system' | 'dark' | 'light' if (preference !== 'system') return preference return colorScheme() }) return ( ) } export default DevtoolsPanelComponent ================================================ FILE: packages/query-devtools/src/Explorer.tsx ================================================ import { serialize, stringify } from 'superjson' import { clsx as cx } from 'clsx' import { Index, Match, Show, Switch, createMemo, createSignal } from 'solid-js' import { Key } from '@solid-primitives/keyed' import * as goober from 'goober' import { tokens } from './theme' import { deleteNestedDataByPath, displayValue, updateNestedDataByPath, } from './utils' import { Check, CopiedCopier, Copier, ErrorCopier, List, Pencil, Trash, } from './icons' import { useQueryDevtoolsContext, useTheme } from './contexts' import type { Query } from '@tanstack/query-core' /** * Chunk elements in the array by size * * when the array cannot be chunked evenly by size, the last chunk will be * filled with the remaining elements * * @example * chunkArray(['a','b', 'c', 'd', 'e'], 2) // returns [['a','b'], ['c', 'd'], ['e']] */ function chunkArray( array: Array, size: number, ): Array> { if (size < 1) return [] let i = 0 const result: Array> = [] while (i < array.length) { result.push(array.slice(i, i + size)) i = i + size } return result } const Expander = (props: { expanded: boolean }) => { const theme = useTheme() const css = useQueryDevtoolsContext().shadowDOMTarget ? goober.css.bind({ target: useQueryDevtoolsContext().shadowDOMTarget }) : goober.css const styles = createMemo(() => { return theme() === 'dark' ? darkStyles(css) : lightStyles(css) }) return ( ) } type CopyState = 'NoCopy' | 'SuccessCopy' | 'ErrorCopy' const CopyButton = (props: { value: unknown }) => { const theme = useTheme() const css = useQueryDevtoolsContext().shadowDOMTarget ? goober.css.bind({ target: useQueryDevtoolsContext().shadowDOMTarget }) : goober.css const styles = createMemo(() => { return theme() === 'dark' ? darkStyles(css) : lightStyles(css) }) const [copyState, setCopyState] = createSignal('NoCopy') return ( ) } const ClearArrayButton = (props: { dataPath: Array activeQuery: Query }) => { const theme = useTheme() const css = useQueryDevtoolsContext().shadowDOMTarget ? goober.css.bind({ target: useQueryDevtoolsContext().shadowDOMTarget }) : goober.css const styles = createMemo(() => { return theme() === 'dark' ? darkStyles(css) : lightStyles(css) }) const queryClient = useQueryDevtoolsContext().client return ( ) } const DeleteItemButton = (props: { dataPath: Array activeQuery: Query }) => { const theme = useTheme() const css = useQueryDevtoolsContext().shadowDOMTarget ? goober.css.bind({ target: useQueryDevtoolsContext().shadowDOMTarget }) : goober.css const styles = createMemo(() => { return theme() === 'dark' ? darkStyles(css) : lightStyles(css) }) const queryClient = useQueryDevtoolsContext().client return ( ) } const ToggleValueButton = (props: { dataPath: Array activeQuery: Query value: boolean }) => { const theme = useTheme() const css = useQueryDevtoolsContext().shadowDOMTarget ? goober.css.bind({ target: useQueryDevtoolsContext().shadowDOMTarget }) : goober.css const styles = createMemo(() => { return theme() === 'dark' ? darkStyles(css) : lightStyles(css) }) const queryClient = useQueryDevtoolsContext().client return ( ) } type ExplorerProps = { editable?: boolean label: string value: unknown defaultExpanded?: Array dataPath?: Array activeQuery?: Query itemsDeletable?: boolean onEdit?: () => void } function isIterable(x: any): x is Iterable { return Symbol.iterator in x } export default function Explorer(props: ExplorerProps) { const theme = useTheme() const css = useQueryDevtoolsContext().shadowDOMTarget ? goober.css.bind({ target: useQueryDevtoolsContext().shadowDOMTarget }) : goober.css const styles = createMemo(() => { return theme() === 'dark' ? darkStyles(css) : lightStyles(css) }) const queryClient = useQueryDevtoolsContext().client const [expanded, setExpanded] = createSignal( (props.defaultExpanded || []).includes(props.label), ) const toggleExpanded = () => setExpanded((old) => !old) const [expandedPages, setExpandedPages] = createSignal>([]) const subEntries = createMemo(() => { if (Array.isArray(props.value)) { return props.value.map((d, i) => ({ label: i.toString(), value: d, })) } else if ( props.value !== null && typeof props.value === 'object' && isIterable(props.value) && typeof props.value[Symbol.iterator] === 'function' ) { if (props.value instanceof Map) { return Array.from(props.value, ([key, val]) => ({ label: key, value: val, })) } return Array.from(props.value, (val, i) => ({ label: i.toString(), value: val, })) } else if (typeof props.value === 'object' && props.value !== null) { return Object.entries(props.value).map(([key, val]) => ({ label: key, value: val, })) } return [] }) const type = createMemo(() => { if (Array.isArray(props.value)) { return 'array' } else if ( props.value !== null && typeof props.value === 'object' && isIterable(props.value) && typeof props.value[Symbol.iterator] === 'function' ) { return 'Iterable' } else if (typeof props.value === 'object' && props.value !== null) { return 'object' } return typeof props.value }) const subEntryPages = createMemo(() => chunkArray(subEntries(), 100)) const currentDataPath = props.dataPath ?? [] return (
item.label}> {(entry) => { return ( ) }}
1}>
{(entries, index) => (
entry.label}> {(entry) => ( )}
)}
{props.label}: {displayValue(props.value)} } > { const oldData = props.activeQuery!.state.data const newData = updateNestedDataByPath( oldData, currentDataPath, type() === 'number' ? changeEvent.target.valueAsNumber : changeEvent.target.value, ) queryClient.setQueryData(props.activeQuery!.queryKey, newData) }} /> {displayValue(props.value)}
) } const stylesFactory = ( theme: 'light' | 'dark', css: (typeof goober)['css'], ) => { const { colors, font, size, border } = tokens const t = (light: string, dark: string) => (theme === 'light' ? light : dark) return { entry: css` & * { font-size: ${font.size.xs}; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace; } position: relative; outline: none; word-break: break-word; `, subEntry: css` margin: 0 0 0 0.5em; padding-left: 0.75em; border-left: 2px solid ${t(colors.gray[300], colors.darkGray[400])}; /* outline: 1px solid ${colors.teal[400]}; */ `, expander: css` & path { stroke: ${colors.gray[400]}; } & svg { width: ${size[3]}; height: ${size[3]}; } display: inline-flex; align-items: center; transition: all 0.1s ease; /* outline: 1px solid ${colors.blue[400]}; */ `, expanderButtonContainer: css` display: flex; align-items: center; line-height: ${size[4]}; min-height: ${size[4]}; gap: ${size[2]}; `, expanderButton: css` cursor: pointer; color: inherit; font: inherit; outline: inherit; height: ${size[5]}; background: transparent; border: none; padding: 0; display: inline-flex; align-items: center; gap: ${size[1]}; position: relative; /* outline: 1px solid ${colors.green[400]}; */ &:focus-visible { border-radius: ${border.radius.xs}; outline: 2px solid ${colors.blue[800]}; } & svg { position: relative; left: 1px; } `, info: css` color: ${t(colors.gray[500], colors.gray[500])}; font-size: ${font.size.xs}; margin-left: ${size[1]}; /* outline: 1px solid ${colors.yellow[400]}; */ `, label: css` color: ${t(colors.gray[700], colors.gray[300])}; white-space: nowrap; `, value: css` color: ${t(colors.purple[600], colors.purple[400])}; flex-grow: 1; `, actions: css` display: inline-flex; gap: ${size[2]}; align-items: center; `, row: css` display: inline-flex; gap: ${size[2]}; width: 100%; margin: ${size[0.25]} 0px; line-height: ${size[4.5]}; align-items: center; `, editableInput: css` border: none; padding: ${size[0.5]} ${size[1]} ${size[0.5]} ${size[1.5]}; flex-grow: 1; border-radius: ${border.radius.xs}; background-color: ${t(colors.gray[200], colors.darkGray[500])}; &:hover { background-color: ${t(colors.gray[300], colors.darkGray[600])}; } `, actionButton: css` background-color: transparent; color: ${t(colors.gray[500], colors.gray[500])}; border: none; display: inline-flex; padding: 0px; align-items: center; justify-content: center; cursor: pointer; width: ${size[3]}; height: ${size[3]}; position: relative; z-index: 1; &:hover svg { color: ${t(colors.gray[600], colors.gray[400])}; } &:focus-visible { border-radius: ${border.radius.xs}; outline: 2px solid ${colors.blue[800]}; outline-offset: 2px; } `, } } const lightStyles = (css: (typeof goober)['css']) => stylesFactory('light', css) const darkStyles = (css: (typeof goober)['css']) => stylesFactory('dark', css) ================================================ FILE: packages/query-devtools/src/index.ts ================================================ export type { DevtoolsButtonPosition, DevtoolsErrorType, DevtoolsPosition, } from './contexts' export { TanstackQueryDevtools, type TanstackQueryDevtoolsConfig, } from './TanstackQueryDevtools' export { TanstackQueryDevtoolsPanel, type TanstackQueryDevtoolsPanelConfig, } from './TanstackQueryDevtoolsPanel' ================================================ FILE: packages/query-devtools/src/TanstackQueryDevtools.tsx ================================================ import { render } from 'solid-js/web' import { createSignal, lazy } from 'solid-js' import { setupStyleSheet } from './utils' import type { QueryClient, onlineManager as TOnlineManager, } from '@tanstack/query-core' import type { DevtoolsComponentType } from './Devtools' import type { DevtoolsButtonPosition, DevtoolsErrorType, DevtoolsPosition, QueryDevtoolsProps, } from './contexts' import type { Signal } from 'solid-js' export interface TanstackQueryDevtoolsConfig extends QueryDevtoolsProps { styleNonce?: string shadowDOMTarget?: ShadowRoot } class TanstackQueryDevtools { #client: Signal #onlineManager: typeof TOnlineManager #queryFlavor: string #version: string #isMounted = false #styleNonce?: string #shadowDOMTarget?: ShadowRoot #buttonPosition: Signal #position: Signal #initialIsOpen: Signal #errorTypes: Signal | undefined> #hideDisabledQueries: Signal #Component: DevtoolsComponentType | undefined #dispose?: () => void constructor(config: TanstackQueryDevtoolsConfig) { const { client, queryFlavor, version, onlineManager, buttonPosition, position, initialIsOpen, errorTypes, styleNonce, shadowDOMTarget, hideDisabledQueries, } = config this.#client = createSignal(client) this.#queryFlavor = queryFlavor this.#version = version this.#onlineManager = onlineManager this.#styleNonce = styleNonce this.#shadowDOMTarget = shadowDOMTarget this.#buttonPosition = createSignal(buttonPosition) this.#position = createSignal(position) this.#initialIsOpen = createSignal(initialIsOpen) this.#errorTypes = createSignal(errorTypes) this.#hideDisabledQueries = createSignal(hideDisabledQueries) } setButtonPosition(position: DevtoolsButtonPosition) { this.#buttonPosition[1](position) } setPosition(position: DevtoolsPosition) { this.#position[1](position) } setInitialIsOpen(isOpen: boolean) { this.#initialIsOpen[1](isOpen) } setErrorTypes(errorTypes: Array) { this.#errorTypes[1](errorTypes) } setClient(client: QueryClient) { this.#client[1](client) } mount(el: T) { if (this.#isMounted) { throw new Error('Devtools is already mounted') } const dispose = render(() => { const [btnPosition] = this.#buttonPosition const [pos] = this.#position const [isOpen] = this.#initialIsOpen const [errors] = this.#errorTypes const [hideDisabledQueries] = this.#hideDisabledQueries const [queryClient] = this.#client let Devtools: DevtoolsComponentType if (this.#Component) { Devtools = this.#Component } else { Devtools = lazy(() => import('./DevtoolsComponent')) this.#Component = Devtools } setupStyleSheet(this.#styleNonce, this.#shadowDOMTarget) return ( ) }, el) this.#isMounted = true this.#dispose = dispose } unmount() { if (!this.#isMounted) { throw new Error('Devtools is not mounted') } this.#dispose?.() this.#isMounted = false } } export { TanstackQueryDevtools } ================================================ FILE: packages/query-devtools/src/TanstackQueryDevtoolsPanel.tsx ================================================ import { render } from 'solid-js/web' import { createSignal, lazy } from 'solid-js' import { setupStyleSheet } from './utils' import type { QueryClient, onlineManager as TOnlineManager, } from '@tanstack/query-core' import type { DevtoolsComponentType } from './Devtools' import type { DevtoolsButtonPosition, DevtoolsErrorType, DevtoolsPosition, QueryDevtoolsProps, } from './contexts' import type { Signal } from 'solid-js' export interface TanstackQueryDevtoolsPanelConfig extends QueryDevtoolsProps { styleNonce?: string shadowDOMTarget?: ShadowRoot onClose?: () => unknown } class TanstackQueryDevtoolsPanel { #client: Signal #onlineManager: typeof TOnlineManager #queryFlavor: string #version: string #isMounted = false #styleNonce?: string #shadowDOMTarget?: ShadowRoot #buttonPosition: Signal #position: Signal #initialIsOpen: Signal #errorTypes: Signal | undefined> #hideDisabledQueries: Signal #onClose: Signal<(() => unknown) | undefined> #Component: DevtoolsComponentType | undefined #dispose?: () => void constructor(config: TanstackQueryDevtoolsPanelConfig) { const { client, queryFlavor, version, onlineManager, buttonPosition, position, initialIsOpen, errorTypes, styleNonce, shadowDOMTarget, onClose, hideDisabledQueries, } = config this.#client = createSignal(client) this.#queryFlavor = queryFlavor this.#version = version this.#onlineManager = onlineManager this.#styleNonce = styleNonce this.#shadowDOMTarget = shadowDOMTarget this.#buttonPosition = createSignal(buttonPosition) this.#position = createSignal(position) this.#initialIsOpen = createSignal(initialIsOpen) this.#errorTypes = createSignal(errorTypes) this.#hideDisabledQueries = createSignal(hideDisabledQueries) this.#onClose = createSignal(onClose) } setButtonPosition(position: DevtoolsButtonPosition) { this.#buttonPosition[1](position) } setPosition(position: DevtoolsPosition) { this.#position[1](position) } setInitialIsOpen(isOpen: boolean) { this.#initialIsOpen[1](isOpen) } setErrorTypes(errorTypes: Array) { this.#errorTypes[1](errorTypes) } setClient(client: QueryClient) { this.#client[1](client) } setOnClose(onClose: () => unknown) { this.#onClose[1](() => onClose) } mount(el: T) { if (this.#isMounted) { throw new Error('Devtools is already mounted') } const dispose = render(() => { const [btnPosition] = this.#buttonPosition const [pos] = this.#position const [isOpen] = this.#initialIsOpen const [errors] = this.#errorTypes const [hideDisabledQueries] = this.#hideDisabledQueries const [queryClient] = this.#client const [onClose] = this.#onClose let Devtools: DevtoolsComponentType if (this.#Component) { Devtools = this.#Component } else { Devtools = lazy(() => import('./DevtoolsPanelComponent')) this.#Component = Devtools } setupStyleSheet(this.#styleNonce, this.#shadowDOMTarget) return ( ) }, el) this.#isMounted = true this.#dispose = dispose } unmount() { if (!this.#isMounted) { throw new Error('Devtools is not mounted') } this.#dispose?.() this.#isMounted = false } } export { TanstackQueryDevtoolsPanel } ================================================ FILE: packages/query-devtools/src/theme.ts ================================================ export const tokens = { colors: { inherit: 'inherit', current: 'currentColor', transparent: 'transparent', black: '#000000', white: '#ffffff', neutral: { 50: '#f9fafb', 100: '#f2f4f7', 200: '#eaecf0', 300: '#d0d5dd', 400: '#98a2b3', 500: '#667085', 600: '#475467', 700: '#344054', 800: '#1d2939', 900: '#101828', }, darkGray: { 50: '#525c7a', 100: '#49536e', 200: '#414962', 300: '#394056', 400: '#313749', 500: '#292e3d', 600: '#212530', 700: '#191c24', 800: '#111318', 900: '#0b0d10', }, gray: { 50: '#f9fafb', 100: '#f2f4f7', 200: '#eaecf0', 300: '#d0d5dd', 400: '#98a2b3', 500: '#667085', 600: '#475467', 700: '#344054', 800: '#1d2939', 900: '#101828', }, blue: { 25: '#F5FAFF', 50: '#EFF8FF', 100: '#D1E9FF', 200: '#B2DDFF', 300: '#84CAFF', 400: '#53B1FD', 500: '#2E90FA', 600: '#1570EF', 700: '#175CD3', 800: '#1849A9', 900: '#194185', }, green: { 25: '#F6FEF9', 50: '#ECFDF3', 100: '#D1FADF', 200: '#A6F4C5', 300: '#6CE9A6', 400: '#32D583', 500: '#12B76A', 600: '#039855', 700: '#027A48', 800: '#05603A', 900: '#054F31', }, red: { 50: '#fef2f2', 100: '#fee2e2', 200: '#fecaca', 300: '#fca5a5', 400: '#f87171', 500: '#ef4444', 600: '#dc2626', 700: '#b91c1c', 800: '#991b1b', 900: '#7f1d1d', 950: '#450a0a', }, yellow: { 25: '#FFFCF5', 50: '#FFFAEB', 100: '#FEF0C7', 200: '#FEDF89', 300: '#FEC84B', 400: '#FDB022', 500: '#F79009', 600: '#DC6803', 700: '#B54708', 800: '#93370D', 900: '#7A2E0E', }, purple: { 25: '#FAFAFF', 50: '#F4F3FF', 100: '#EBE9FE', 200: '#D9D6FE', 300: '#BDB4FE', 400: '#9B8AFB', 500: '#7A5AF8', 600: '#6938EF', 700: '#5925DC', 800: '#4A1FB8', 900: '#3E1C96', }, teal: { 25: '#F6FEFC', 50: '#F0FDF9', 100: '#CCFBEF', 200: '#99F6E0', 300: '#5FE9D0', 400: '#2ED3B7', 500: '#15B79E', 600: '#0E9384', 700: '#107569', 800: '#125D56', 900: '#134E48', }, pink: { 25: '#fdf2f8', 50: '#fce7f3', 100: '#fbcfe8', 200: '#f9a8d4', 300: '#f472b6', 400: '#ec4899', 500: '#db2777', 600: '#be185d', 700: '#9d174d', 800: '#831843', 900: '#500724', }, cyan: { 25: '#ecfeff', 50: '#cffafe', 100: '#a5f3fc', 200: '#67e8f9', 300: '#22d3ee', 400: '#06b6d4', 500: '#0891b2', 600: '#0e7490', 700: '#155e75', 800: '#164e63', 900: '#083344', }, }, alpha: { 100: 'ff', 90: 'e5', 80: 'cc', 70: 'b3', 60: '99', 50: '80', 40: '66', 30: '4d', 20: '33', 10: '1a', 0: '00', }, font: { size: { '2xs': 'calc(var(--tsqd-font-size) * 0.625)', xs: 'calc(var(--tsqd-font-size) * 0.75)', sm: 'calc(var(--tsqd-font-size) * 0.875)', md: 'var(--tsqd-font-size)', lg: 'calc(var(--tsqd-font-size) * 1.125)', xl: 'calc(var(--tsqd-font-size) * 1.25)', '2xl': 'calc(var(--tsqd-font-size) * 1.5)', '3xl': 'calc(var(--tsqd-font-size) * 1.875)', '4xl': 'calc(var(--tsqd-font-size) * 2.25)', '5xl': 'calc(var(--tsqd-font-size) * 3)', '6xl': 'calc(var(--tsqd-font-size) * 3.75)', '7xl': 'calc(var(--tsqd-font-size) * 4.5)', '8xl': 'calc(var(--tsqd-font-size) * 6)', '9xl': 'calc(var(--tsqd-font-size) * 8)', }, lineHeight: { xs: 'calc(var(--tsqd-font-size) * 1)', sm: 'calc(var(--tsqd-font-size) * 1.25)', md: 'calc(var(--tsqd-font-size) * 1.5)', lg: 'calc(var(--tsqd-font-size) * 1.75)', xl: 'calc(var(--tsqd-font-size) * 2)', '2xl': 'calc(var(--tsqd-font-size) * 2.25)', '3xl': 'calc(var(--tsqd-font-size) * 2.5)', '4xl': 'calc(var(--tsqd-font-size) * 2.75)', '5xl': 'calc(var(--tsqd-font-size) * 3)', '6xl': 'calc(var(--tsqd-font-size) * 3.25)', '7xl': 'calc(var(--tsqd-font-size) * 3.5)', '8xl': 'calc(var(--tsqd-font-size) * 3.75)', '9xl': 'calc(var(--tsqd-font-size) * 4)', }, weight: { thin: '100', extralight: '200', light: '300', normal: '400', medium: '500', semibold: '600', bold: '700', extrabold: '800', black: '900', }, }, breakpoints: { xs: '320px', sm: '640px', md: '768px', lg: '1024px', xl: '1280px', '2xl': '1536px', }, border: { radius: { none: '0px', xs: 'calc(var(--tsqd-font-size) * 0.125)', sm: 'calc(var(--tsqd-font-size) * 0.25)', md: 'calc(var(--tsqd-font-size) * 0.375)', lg: 'calc(var(--tsqd-font-size) * 0.5)', xl: 'calc(var(--tsqd-font-size) * 0.75)', '2xl': 'calc(var(--tsqd-font-size) * 1)', '3xl': 'calc(var(--tsqd-font-size) * 1.5)', full: '9999px', }, }, size: { 0: '0px', 0.25: 'calc(var(--tsqd-font-size) * 0.0625)', 0.5: 'calc(var(--tsqd-font-size) * 0.125)', 1: 'calc(var(--tsqd-font-size) * 0.25)', 1.5: 'calc(var(--tsqd-font-size) * 0.375)', 2: 'calc(var(--tsqd-font-size) * 0.5)', 2.5: 'calc(var(--tsqd-font-size) * 0.625)', 3: 'calc(var(--tsqd-font-size) * 0.75)', 3.5: 'calc(var(--tsqd-font-size) * 0.875)', 4: 'calc(var(--tsqd-font-size) * 1)', 4.5: 'calc(var(--tsqd-font-size) * 1.125)', 5: 'calc(var(--tsqd-font-size) * 1.25)', 5.5: 'calc(var(--tsqd-font-size) * 1.375)', 6: 'calc(var(--tsqd-font-size) * 1.5)', 6.5: 'calc(var(--tsqd-font-size) * 1.625)', 7: 'calc(var(--tsqd-font-size) * 1.75)', 8: 'calc(var(--tsqd-font-size) * 2)', 9: 'calc(var(--tsqd-font-size) * 2.25)', 10: 'calc(var(--tsqd-font-size) * 2.5)', 11: 'calc(var(--tsqd-font-size) * 2.75)', 12: 'calc(var(--tsqd-font-size) * 3)', 14: 'calc(var(--tsqd-font-size) * 3.5)', 16: 'calc(var(--tsqd-font-size) * 4)', 20: 'calc(var(--tsqd-font-size) * 5)', 24: 'calc(var(--tsqd-font-size) * 6)', 28: 'calc(var(--tsqd-font-size) * 7)', 32: 'calc(var(--tsqd-font-size) * 8)', 36: 'calc(var(--tsqd-font-size) * 9)', 40: 'calc(var(--tsqd-font-size) * 10)', 44: 'calc(var(--tsqd-font-size) * 11)', 48: 'calc(var(--tsqd-font-size) * 12)', 52: 'calc(var(--tsqd-font-size) * 13)', 56: 'calc(var(--tsqd-font-size) * 14)', 60: 'calc(var(--tsqd-font-size) * 15)', 64: 'calc(var(--tsqd-font-size) * 16)', 72: 'calc(var(--tsqd-font-size) * 18)', 80: 'calc(var(--tsqd-font-size) * 20)', 96: 'calc(var(--tsqd-font-size) * 24)', }, shadow: { xs: (_: string = 'rgb(0 0 0 / 0.1)') => `0 1px 2px 0 rgb(0 0 0 / 0.05)` as const, sm: (color: string = 'rgb(0 0 0 / 0.1)') => `0 1px 3px 0 ${color}, 0 1px 2px -1px ${color}` as const, md: (color: string = 'rgb(0 0 0 / 0.1)') => `0 4px 6px -1px ${color}, 0 2px 4px -2px ${color}` as const, lg: (color: string = 'rgb(0 0 0 / 0.1)') => `0 10px 15px -3px ${color}, 0 4px 6px -4px ${color}` as const, xl: (color: string = 'rgb(0 0 0 / 0.1)') => `0 20px 25px -5px ${color}, 0 8px 10px -6px ${color}` as const, '2xl': (color: string = 'rgb(0 0 0 / 0.25)') => `0 25px 50px -12px ${color}` as const, inner: (color: string = 'rgb(0 0 0 / 0.05)') => `inset 0 2px 4px 0 ${color}` as const, none: () => `none` as const, }, zIndices: { hide: -1, auto: 'auto', base: 0, docked: 10, dropdown: 1000, sticky: 1100, banner: 1200, overlay: 1300, modal: 1400, popover: 1500, skipLink: 1600, toast: 1700, tooltip: 1800, }, } as const ================================================ FILE: packages/query-devtools/src/utils.tsx ================================================ import { serialize } from 'superjson' import { createSignal, onCleanup, onMount } from 'solid-js' import type { Mutation, Query } from '@tanstack/query-core' import type { DevtoolsPosition } from './contexts' export function getQueryStatusLabel(query: Query) { return query.state.fetchStatus === 'fetching' ? 'fetching' : !query.getObserversCount() ? 'inactive' : query.state.fetchStatus === 'paused' ? 'paused' : query.isStale() ? 'stale' : 'fresh' } type QueryStatusLabel = 'fresh' | 'stale' | 'paused' | 'inactive' | 'fetching' export function getSidedProp( prop: T, side: DevtoolsPosition, ) { return `${prop}${ side.charAt(0).toUpperCase() + side.slice(1) }` as `${T}${Capitalize}` } export function getQueryStatusColor({ queryState, observerCount, isStale, }: { queryState: Query['state'] observerCount: number isStale: boolean }) { return queryState.fetchStatus === 'fetching' ? 'blue' : !observerCount ? 'gray' : queryState.fetchStatus === 'paused' ? 'purple' : isStale ? 'yellow' : 'green' } export function getMutationStatusColor({ status, isPaused, }: { status: Mutation['state']['status'] isPaused: boolean }) { return isPaused ? 'purple' : status === 'error' ? 'red' : status === 'pending' ? 'yellow' : status === 'success' ? 'green' : 'gray' } export function getQueryStatusColorByLabel(label: QueryStatusLabel) { return label === 'fresh' ? 'green' : label === 'stale' ? 'yellow' : label === 'paused' ? 'purple' : label === 'inactive' ? 'gray' : 'blue' } /** * Displays a string regardless the type of the data * @param {unknown} value Value to be stringified * @param {boolean} beautify Formats json to multiline */ export const displayValue = (value: unknown, beautify: boolean = false) => { const { json } = serialize(value) return JSON.stringify(json, null, beautify ? 2 : undefined) } // Sorting functions type SortFn = (a: Query, b: Query) => number const getStatusRank = (q: Query) => q.state.fetchStatus !== 'idle' ? 0 : !q.getObserversCount() ? 3 : q.isStale() ? 2 : 1 const queryHashSort: SortFn = (a, b) => a.queryHash.localeCompare(b.queryHash) const dateSort: SortFn = (a, b) => a.state.dataUpdatedAt < b.state.dataUpdatedAt ? 1 : -1 const statusAndDateSort: SortFn = (a, b) => { if (getStatusRank(a) === getStatusRank(b)) { return dateSort(a, b) } return getStatusRank(a) > getStatusRank(b) ? 1 : -1 } export const sortFns: Record = { status: statusAndDateSort, 'query hash': queryHashSort, 'last updated': dateSort, } type MutationSortFn = (a: Mutation, b: Mutation) => number const getMutationStatusRank = (m: Mutation) => m.state.isPaused ? 0 : m.state.status === 'error' ? 2 : m.state.status === 'pending' ? 1 : 3 const mutationDateSort: MutationSortFn = (a, b) => a.state.submittedAt < b.state.submittedAt ? 1 : -1 const mutationStatusSort: MutationSortFn = (a, b) => { if (getMutationStatusRank(a) === getMutationStatusRank(b)) { return mutationDateSort(a, b) } return getMutationStatusRank(a) > getMutationStatusRank(b) ? 1 : -1 } export const mutationSortFns: Record = { status: mutationStatusSort, 'last updated': mutationDateSort, } export const convertRemToPixels = (rem: number) => { return rem * parseFloat(getComputedStyle(document.documentElement).fontSize) } export const getPreferredColorScheme = () => { const [colorScheme, setColorScheme] = createSignal<'light' | 'dark'>('dark') onMount(() => { const query = window.matchMedia('(prefers-color-scheme: dark)') setColorScheme(query.matches ? 'dark' : 'light') const listener = (e: MediaQueryListEvent) => { setColorScheme(e.matches ? 'dark' : 'light') } query.addEventListener('change', listener) onCleanup(() => query.removeEventListener('change', listener)) }) return colorScheme } /** * updates nested data by path * * @param {unknown} oldData Data to be updated * @param {Array} updatePath Path to the data to be updated * @param {unknown} value New value */ export const updateNestedDataByPath = ( oldData: unknown, updatePath: Array, value: unknown, ): any => { if (updatePath.length === 0) { return value } if (oldData instanceof Map) { const newData = new Map(oldData) if (updatePath.length === 1) { newData.set(updatePath[0], value) return newData } const [head, ...tail] = updatePath newData.set(head, updateNestedDataByPath(newData.get(head), tail, value)) return newData } if (oldData instanceof Set) { const setAsArray = updateNestedDataByPath( Array.from(oldData), updatePath, value, ) return new Set(setAsArray) } if (Array.isArray(oldData)) { const newData = [...oldData] if (updatePath.length === 1) { // @ts-expect-error newData[updatePath[0]] = value return newData } const [head, ...tail] = updatePath // @ts-expect-error newData[head] = updateNestedDataByPath(newData[head], tail, value) return newData } if (oldData instanceof Object) { const newData = { ...oldData } if (updatePath.length === 1) { // @ts-expect-error newData[updatePath[0]] = value return newData } const [head, ...tail] = updatePath // @ts-expect-error newData[head] = updateNestedDataByPath(newData[head], tail, value) return newData } return oldData } /** * Deletes nested data by path * * @param {unknown} oldData Data to be updated * @param {Array} deletePath Path to the data to be deleted * @returns newData without the deleted items by path */ export const deleteNestedDataByPath = ( oldData: unknown, deletePath: Array, ): any => { if (oldData instanceof Map) { const newData = new Map(oldData) if (deletePath.length === 1) { newData.delete(deletePath[0]) return newData } const [head, ...tail] = deletePath newData.set(head, deleteNestedDataByPath(newData.get(head), tail)) return newData } if (oldData instanceof Set) { const setAsArray = deleteNestedDataByPath(Array.from(oldData), deletePath) return new Set(setAsArray) } if (Array.isArray(oldData)) { const newData = [...oldData] if (deletePath.length === 1) { return newData.filter((_, idx) => idx.toString() !== deletePath[0]) } const [head, ...tail] = deletePath // @ts-expect-error newData[head] = deleteNestedDataByPath(newData[head], tail) return newData } if (oldData instanceof Object) { const newData = { ...oldData } if (deletePath.length === 1) { // @ts-expect-error delete newData[deletePath[0]] return newData } const [head, ...tail] = deletePath // @ts-expect-error newData[head] = deleteNestedDataByPath(newData[head], tail) return newData } return oldData } // Sets up the goober stylesheet // Adds a nonce to the style tag if needed export const setupStyleSheet = (nonce?: string, target?: ShadowRoot) => { if (!nonce) return const styleExists = document.querySelector('#_goober') || target?.querySelector('#_goober') if (styleExists) return const styleTag = document.createElement('style') const textNode = document.createTextNode('') styleTag.appendChild(textNode) styleTag.id = '_goober' styleTag.setAttribute('nonce', nonce) if (target) { target.appendChild(styleTag) } else { document.head.appendChild(styleTag) } } ================================================ FILE: packages/query-devtools/src/__tests__/devtools.test.tsx ================================================ import { describe, expect, it } from 'vitest' describe('ReactQueryDevtools', () => { it('should be able to open and close devtools', () => { expect(1).toBe(1) }) }) ================================================ FILE: packages/query-devtools/src/__tests__/utils.test.ts ================================================ import { describe, expect, it } from 'vitest' import { deleteNestedDataByPath, updateNestedDataByPath } from '../utils' describe('Utils tests', () => { describe('updatedNestedDataByPath', () => { describe('array', () => { it('should update data correctly', () => { const oldData = ['one', 'two', 'three'] const newData = updateNestedDataByPath(oldData, ['1'], 'new') expect(newData).not.toBe(oldData) // should not be the same reference expect(oldData).toMatchInlineSnapshot(` [ "one", "two", "three", ] `) expect(newData).toMatchInlineSnapshot(` [ "one", "new", "three", ] `) }) }) describe('object', () => { it('should update data correctly', () => { const oldData = { title: 'Hello world', id: 1, createdAt: '2021-01-01' } const newData = updateNestedDataByPath( oldData, ['title'], 'Brave new world', ) expect(newData).not.toBe(oldData) // should not be the same reference expect(oldData).toMatchInlineSnapshot(` { "createdAt": "2021-01-01", "id": 1, "title": "Hello world", } `) expect(newData).toMatchInlineSnapshot(` { "createdAt": "2021-01-01", "id": 1, "title": "Brave new world", } `) }) }) describe('set', () => { it('should update data correctly', () => { const oldData = new Set([123, 321, 'hello', 'world']) const newData = updateNestedDataByPath(oldData, ['2'], 'hi') expect(newData).not.toBe(oldData) // should not be the same reference expect(oldData).toMatchInlineSnapshot(` Set { 123, 321, "hello", "world", } `) expect(newData).toMatchInlineSnapshot(` Set { 123, 321, "hi", "world", } `) }) }) describe('map', () => { it('should update data correctly', () => { const oldData = new Map([ ['en', 'hello'], ['fr', 'bonjour'], ]) /* eslint-disable cspell/spellchecker */ const newData = updateNestedDataByPath(oldData, ['fr'], 'salut') expect(newData).not.toBe(oldData) // should not be the same reference expect(oldData).toMatchInlineSnapshot(` Map { "en" => "hello", "fr" => "bonjour", } `) expect(newData).toMatchInlineSnapshot(` Map { "en" => "hello", "fr" => "salut", } `) }) /* eslint-enable */ }) describe('nested data', () => { it('should update data correctly', () => { /* eslint-disable cspell/spellchecker */ const oldData = new Map([ [ 'pumpkin-pie', { id: 1, title: 'Pumpkin pie', ingredients: new Set(['pumpkin', 'sugar', 'spices']), steps: ['mix', 'bake', 'eat'], translations: new Map([ ['en', 'Pumpkin pie'], ['fr', 'Tarte à la citrouille'], ]), }, ], [ 'spaghetti-bolonese', { id: 2, title: 'Spaghetti bolonese', ingredients: new Set([ 'spaghetti', 'tomato sauce', 'minced meat', ]), steps: ['cook', 'eat'], translations: new Map([ ['en', 'Spaghetti bolonese'], ['fr', 'Spaghetti bolonaise'], ]), }, ], ]) const updatedObject = updateNestedDataByPath( oldData, ['pumpkin-pie', 'title'], 'Pumpkin pie with whipped cream', ) const updatedArray = updateNestedDataByPath( oldData, ['spaghetti-bolonese', 'steps', '0'], 'prepare', ) const updatedSet = updateNestedDataByPath( oldData, ['pumpkin-pie', 'ingredients', '1'], 'honey', ) const updatedMap = updateNestedDataByPath( oldData, ['pumpkin-pie', 'translations', 'en'], 'Best pie ever', ) expect(oldData).toMatchInlineSnapshot(` Map { "pumpkin-pie" => { "id": 1, "ingredients": Set { "pumpkin", "sugar", "spices", }, "steps": [ "mix", "bake", "eat", ], "title": "Pumpkin pie", "translations": Map { "en" => "Pumpkin pie", "fr" => "Tarte à la citrouille", }, }, "spaghetti-bolonese" => { "id": 2, "ingredients": Set { "spaghetti", "tomato sauce", "minced meat", }, "steps": [ "cook", "eat", ], "title": "Spaghetti bolonese", "translations": Map { "en" => "Spaghetti bolonese", "fr" => "Spaghetti bolonaise", }, }, } `) expect(updatedObject).toMatchInlineSnapshot(` Map { "pumpkin-pie" => { "id": 1, "ingredients": Set { "pumpkin", "sugar", "spices", }, "steps": [ "mix", "bake", "eat", ], "title": "Pumpkin pie with whipped cream", "translations": Map { "en" => "Pumpkin pie", "fr" => "Tarte à la citrouille", }, }, "spaghetti-bolonese" => { "id": 2, "ingredients": Set { "spaghetti", "tomato sauce", "minced meat", }, "steps": [ "cook", "eat", ], "title": "Spaghetti bolonese", "translations": Map { "en" => "Spaghetti bolonese", "fr" => "Spaghetti bolonaise", }, }, } `) expect(updatedArray).toMatchInlineSnapshot(` Map { "pumpkin-pie" => { "id": 1, "ingredients": Set { "pumpkin", "sugar", "spices", }, "steps": [ "mix", "bake", "eat", ], "title": "Pumpkin pie", "translations": Map { "en" => "Pumpkin pie", "fr" => "Tarte à la citrouille", }, }, "spaghetti-bolonese" => { "id": 2, "ingredients": Set { "spaghetti", "tomato sauce", "minced meat", }, "steps": [ "prepare", "eat", ], "title": "Spaghetti bolonese", "translations": Map { "en" => "Spaghetti bolonese", "fr" => "Spaghetti bolonaise", }, }, } `) expect(updatedSet).toMatchInlineSnapshot(` Map { "pumpkin-pie" => { "id": 1, "ingredients": Set { "pumpkin", "honey", "spices", }, "steps": [ "mix", "bake", "eat", ], "title": "Pumpkin pie", "translations": Map { "en" => "Pumpkin pie", "fr" => "Tarte à la citrouille", }, }, "spaghetti-bolonese" => { "id": 2, "ingredients": Set { "spaghetti", "tomato sauce", "minced meat", }, "steps": [ "cook", "eat", ], "title": "Spaghetti bolonese", "translations": Map { "en" => "Spaghetti bolonese", "fr" => "Spaghetti bolonaise", }, }, } `) expect(updatedMap).toMatchInlineSnapshot(` Map { "pumpkin-pie" => { "id": 1, "ingredients": Set { "pumpkin", "sugar", "spices", }, "steps": [ "mix", "bake", "eat", ], "title": "Pumpkin pie", "translations": Map { "en" => "Best pie ever", "fr" => "Tarte à la citrouille", }, }, "spaghetti-bolonese" => { "id": 2, "ingredients": Set { "spaghetti", "tomato sauce", "minced meat", }, "steps": [ "cook", "eat", ], "title": "Spaghetti bolonese", "translations": Map { "en" => "Spaghetti bolonese", "fr" => "Spaghetti bolonaise", }, }, } `) /* eslint-enable */ }) }) }) describe('deleteNestedDataByPath', () => { it('should delete item from array correctly', () => { const oldData = ['one', 'two', 'three'] const newData = deleteNestedDataByPath(oldData, ['1']) expect(newData).not.toBe(oldData) // should not be the same reference expect(oldData).toMatchInlineSnapshot(` [ "one", "two", "three", ] `) expect(newData).toMatchInlineSnapshot(` [ "one", "three", ] `) }) it('should delete item from object correctly', () => { const oldData = { title: 'Hello world', id: 1, createdAt: '2021-01-01' } const newData = deleteNestedDataByPath(oldData, ['createdAt']) expect(newData).not.toBe(oldData) // should not be the same reference expect(oldData).toMatchInlineSnapshot(` { "createdAt": "2021-01-01", "id": 1, "title": "Hello world", } `) expect(newData).toMatchInlineSnapshot(` { "id": 1, "title": "Hello world", } `) }) it('should delete item from set', () => { const oldData = new Set([123, 321, false, true]) const newData = deleteNestedDataByPath(oldData, ['1']) expect(newData).not.toBe(oldData) // should not be the same reference expect(oldData).toMatchInlineSnapshot(` Set { 123, 321, false, true, } `) expect(newData).toMatchInlineSnapshot(` Set { 123, false, true, } `) }) it('should delete item from map', () => { const oldData = new Map([ ['123', 'one'], ['hello', 'two'], ['world', 'three'], ]) const newData = deleteNestedDataByPath(oldData, ['world']) expect(newData).not.toBe(oldData) // should not be the same reference expect(oldData).toMatchInlineSnapshot(` Map { "123" => "one", "hello" => "two", "world" => "three", } `) expect(newData).toMatchInlineSnapshot(` Map { "123" => "one", "hello" => "two", } `) }) describe('nested data', () => { it('should delete nested items correctly', () => { /* eslint-disable cspell/spellchecker */ const oldData = new Map([ [ 'pumpkin-pie', { id: 1, title: 'Pumpkin pie', ingredients: new Set(['pumpkin', 'sugar', 'spices']), steps: ['mix', 'bake', 'eat'], translations: new Map([ ['en', 'Pumpkin pie'], ['fr', 'Tarte à la citrouille'], ]), }, ], [ 'spaghetti-bolonese', { id: 2, title: 'Spaghetti bolonese', ingredients: new Set([ 'spaghetti', 'tomato sauce', 'minced meat', ]), steps: ['cook', 'eat'], translations: new Map([ ['en', 'Spaghetti bolonese'], ['fr', 'Spaghetti bolonaise'], ]), }, ], ]) const deletedFromSet = deleteNestedDataByPath(oldData, [ 'spaghetti-bolonese', 'ingredients', '0', ]) const deletedFromArray = deleteNestedDataByPath(oldData, [ 'pumpkin-pie', 'steps', '1', ]) const deletedFromObject = deleteNestedDataByPath(oldData, [ 'pumpkin-pie', 'title', ]) const deletedFromMap = deleteNestedDataByPath(oldData, [ 'spaghetti-bolonese', 'translations', 'fr', ]) expect(oldData).toMatchInlineSnapshot(` Map { "pumpkin-pie" => { "id": 1, "ingredients": Set { "pumpkin", "sugar", "spices", }, "steps": [ "mix", "bake", "eat", ], "title": "Pumpkin pie", "translations": Map { "en" => "Pumpkin pie", "fr" => "Tarte à la citrouille", }, }, "spaghetti-bolonese" => { "id": 2, "ingredients": Set { "spaghetti", "tomato sauce", "minced meat", }, "steps": [ "cook", "eat", ], "title": "Spaghetti bolonese", "translations": Map { "en" => "Spaghetti bolonese", "fr" => "Spaghetti bolonaise", }, }, } `) expect(deletedFromSet).toMatchInlineSnapshot(` Map { "pumpkin-pie" => { "id": 1, "ingredients": Set { "pumpkin", "sugar", "spices", }, "steps": [ "mix", "bake", "eat", ], "title": "Pumpkin pie", "translations": Map { "en" => "Pumpkin pie", "fr" => "Tarte à la citrouille", }, }, "spaghetti-bolonese" => { "id": 2, "ingredients": Set { "tomato sauce", "minced meat", }, "steps": [ "cook", "eat", ], "title": "Spaghetti bolonese", "translations": Map { "en" => "Spaghetti bolonese", "fr" => "Spaghetti bolonaise", }, }, } `) expect(deletedFromArray).toMatchInlineSnapshot(` Map { "pumpkin-pie" => { "id": 1, "ingredients": Set { "pumpkin", "sugar", "spices", }, "steps": [ "mix", "eat", ], "title": "Pumpkin pie", "translations": Map { "en" => "Pumpkin pie", "fr" => "Tarte à la citrouille", }, }, "spaghetti-bolonese" => { "id": 2, "ingredients": Set { "spaghetti", "tomato sauce", "minced meat", }, "steps": [ "cook", "eat", ], "title": "Spaghetti bolonese", "translations": Map { "en" => "Spaghetti bolonese", "fr" => "Spaghetti bolonaise", }, }, } `) expect(deletedFromObject).toMatchInlineSnapshot(` Map { "pumpkin-pie" => { "id": 1, "ingredients": Set { "pumpkin", "sugar", "spices", }, "steps": [ "mix", "bake", "eat", ], "translations": Map { "en" => "Pumpkin pie", "fr" => "Tarte à la citrouille", }, }, "spaghetti-bolonese" => { "id": 2, "ingredients": Set { "spaghetti", "tomato sauce", "minced meat", }, "steps": [ "cook", "eat", ], "title": "Spaghetti bolonese", "translations": Map { "en" => "Spaghetti bolonese", "fr" => "Spaghetti bolonaise", }, }, } `) expect(deletedFromMap).toMatchInlineSnapshot(` Map { "pumpkin-pie" => { "id": 1, "ingredients": Set { "pumpkin", "sugar", "spices", }, "steps": [ "mix", "bake", "eat", ], "title": "Pumpkin pie", "translations": Map { "en" => "Pumpkin pie", "fr" => "Tarte à la citrouille", }, }, "spaghetti-bolonese" => { "id": 2, "ingredients": Set { "spaghetti", "tomato sauce", "minced meat", }, "steps": [ "cook", "eat", ], "title": "Spaghetti bolonese", "translations": Map { "en" => "Spaghetti bolonese", }, }, } `) /* eslint-enable */ }) }) }) }) ================================================ FILE: packages/query-devtools/src/contexts/index.ts ================================================ export * from './PiPContext' export * from './QueryDevtoolsContext' export * from './ThemeContext' ================================================ FILE: packages/query-devtools/src/contexts/PiPContext.tsx ================================================ import { createContext, createEffect, createMemo, createSignal, onCleanup, useContext, } from 'solid-js' import { clearDelegatedEvents, delegateEvents } from 'solid-js/web' import { PIP_DEFAULT_HEIGHT } from '../constants' import { useQueryDevtoolsContext } from './QueryDevtoolsContext' import type { Accessor, JSX } from 'solid-js' import type { StorageObject, StorageSetter } from '@solid-primitives/storage' interface PiPProviderProps { children: JSX.Element localStore: StorageObject setLocalStore: StorageSetter disabled?: boolean } type PiPContextType = { pipWindow: Window | null requestPipWindow: (width: number, height: number) => void closePipWindow: () => void disabled: boolean } const PiPContext = createContext | undefined>( undefined, ) export const PiPProvider = (props: PiPProviderProps) => { // Expose pipWindow that is currently active const [pipWindow, setPipWindow] = createSignal(null) // Close pipWindow programmatically const closePipWindow = () => { const w = pipWindow() if (w != null) { w.close() setPipWindow(null) } } // Open new pipWindow const requestPipWindow = (width: number, height: number) => { // We don't want to allow multiple requests. if (pipWindow() != null) { return } const pip = window.open( '', 'TSQD-Devtools-Panel', `width=${width},height=${height},popup`, ) if (!pip) { throw new Error( 'Failed to open popup. Please allow popups for this site to view the devtools in picture-in-picture mode.', ) } // Remove existing styles pip.document.head.innerHTML = '' // Remove existing body pip.document.body.innerHTML = '' // Clear Delegated Events clearDelegatedEvents(pip.document) pip.document.title = 'TanStack Query Devtools' pip.document.body.style.margin = '0' // Detect when window is closed by user pip.addEventListener('pagehide', () => { props.setLocalStore('pip_open', 'false') setPipWindow(null) }) // It is important to copy all parent window styles. Otherwise, there would be no CSS available at all // https://developer.chrome.com/docs/web-platform/document-picture-in-picture/#copy-style-sheets-to-the-picture-in-picture-window ;[ ...(useQueryDevtoolsContext().shadowDOMTarget || document).styleSheets, ].forEach((styleSheet) => { try { const cssRules = [...styleSheet.cssRules] .map((rule) => rule.cssText) .join('') const style = document.createElement('style') const style_node = styleSheet.ownerNode let style_id = '' if (style_node && 'id' in style_node) { style_id = style_node.id } if (style_id) { style.setAttribute('id', style_id) } style.textContent = cssRules pip.document.head.appendChild(style) } catch (e) { const link = document.createElement('link') if (styleSheet.href == null) { return } link.rel = 'stylesheet' link.type = styleSheet.type link.media = styleSheet.media.toString() link.href = styleSheet.href pip.document.head.appendChild(link) } }) delegateEvents( [ 'focusin', 'focusout', 'pointermove', 'keydown', 'pointerdown', 'pointerup', 'click', 'mousedown', 'input', ], pip.document, ) props.setLocalStore('pip_open', 'true') setPipWindow(pip) } createEffect(() => { const pip_open = (props.localStore.pip_open ?? 'false') as 'true' | 'false' if (pip_open === 'true' && !props.disabled) { requestPipWindow( Number(window.innerWidth), Number(props.localStore.height || PIP_DEFAULT_HEIGHT), ) } }) createEffect(() => { // Setup mutation observer for goober styles with id `_goober const gooberStyles = ( useQueryDevtoolsContext().shadowDOMTarget || document ).querySelector('#_goober') const w = pipWindow() if (gooberStyles && w) { const observer = new MutationObserver(() => { const pip_style = ( useQueryDevtoolsContext().shadowDOMTarget || w.document ).querySelector('#_goober') if (pip_style) { pip_style.textContent = gooberStyles.textContent } }) observer.observe(gooberStyles, { childList: true, // observe direct children subtree: true, // and lower descendants too characterDataOldValue: true, // pass old data to callback }) onCleanup(() => { observer.disconnect() }) } }) const value = createMemo(() => ({ pipWindow: pipWindow(), requestPipWindow, closePipWindow, disabled: props.disabled ?? false, })) return ( {props.children} ) } export const usePiPWindow = () => { const context = createMemo(() => { const ctx = useContext(PiPContext) if (!ctx) { throw new Error('usePiPWindow must be used within a PiPProvider') } return ctx() }) return context } ================================================ FILE: packages/query-devtools/src/contexts/QueryDevtoolsContext.ts ================================================ import { createContext, useContext } from 'solid-js' import type { Query, QueryClient, onlineManager } from '@tanstack/query-core' type XPosition = 'left' | 'right' type YPosition = 'top' | 'bottom' export type DevtoolsPosition = XPosition | YPosition export type DevtoolsButtonPosition = `${YPosition}-${XPosition}` | 'relative' export interface DevtoolsErrorType { /** * The name of the error. */ name: string /** * How the error is initialized. */ initializer: (query: Query) => Error } export interface QueryDevtoolsProps { readonly client: QueryClient queryFlavor: string version: string onlineManager: typeof onlineManager buttonPosition?: DevtoolsButtonPosition position?: DevtoolsPosition initialIsOpen?: boolean errorTypes?: Array shadowDOMTarget?: ShadowRoot onClose?: () => unknown hideDisabledQueries?: boolean } export const QueryDevtoolsContext = createContext({ client: undefined as unknown as QueryClient, onlineManager: undefined as unknown as typeof onlineManager, queryFlavor: '', version: '', shadowDOMTarget: undefined, }) export function useQueryDevtoolsContext() { return useContext(QueryDevtoolsContext) } ================================================ FILE: packages/query-devtools/src/contexts/ThemeContext.ts ================================================ import { createContext, useContext } from 'solid-js' import type { Accessor } from 'solid-js' export const ThemeContext = createContext>( () => 'dark' as const, ) export function useTheme() { return useContext(ThemeContext) } ================================================ FILE: packages/query-devtools/src/icons/index.tsx ================================================ import { Show, createUniqueId } from 'solid-js' export function Search() { return ( ) } export function Trash() { return ( ) } export function ChevronDown() { return ( ) } export function ArrowUp() { return ( ) } export function ArrowDown() { return ( ) } export function ArrowLeft() { return ( ) } export function ArrowRight() { return ( ) } export function Sun() { return ( ) } export function Moon() { return ( ) } export function Monitor() { return ( ) } export function Wifi() { return ( ) } export function Offline() { return ( ) } export function Settings() { return ( ) } export function PiPIcon() { return ( ) } export function Copier() { return ( ) } export function Pencil() { return ( ) } export function CopiedCopier(props: { theme: 'light' | 'dark' }) { return ( ) } export function ErrorCopier() { return ( ) } export function List() { return ( ) } export function Check(props: { checked: boolean; theme: 'light' | 'dark' }) { return ( <> ) } export function CheckCircle() { return ( ) } export function LoadingCircle() { return ( ) } export function XCircle() { return ( ) } export function PauseCircle() { return ( ) } export function TanstackLogo() { const id = createUniqueId() return ( ) } ================================================ FILE: packages/query-persist-client-core/eslint.config.js ================================================ // @ts-check import rootConfig from './root.eslint.config.js' export default [...rootConfig] ================================================ FILE: packages/query-persist-client-core/package.json ================================================ { "name": "@tanstack/query-persist-client-core", "version": "5.89.0", "description": "Set of utilities for interacting with persisters, which can save your queryClient for later use", "author": "tannerlinsley", "license": "MIT", "repository": { "type": "git", "url": "https://github.com/TanStack/query.git", "directory": "packages/query-persist-client-core" }, "homepage": "https://tanstack.com/query", "funding": { "type": "github", "url": "https://github.com/sponsors/tannerlinsley" }, "scripts": { "clean": "premove ./build ./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": "publint --strict && attw --pack", "build": "tsup --tsconfig tsconfig.prod.json" }, "type": "module", "types": "build/legacy/index.d.ts", "main": "build/legacy/index.cjs", "module": "build/legacy/index.js", "react-native": "src/index.ts", "exports": { ".": { "@tanstack/custom-condition": "./src/index.ts", "import": { "types": "./build/modern/index.d.ts", "default": "./build/modern/index.js" }, "require": { "types": "./build/modern/index.d.cts", "default": "./build/modern/index.cjs" } }, "./package.json": "./package.json" }, "sideEffects": false, "files": [ "build", "src", "!src/__tests__" ], "dependencies": { "@tanstack/query-core": "workspace:*" }, "devDependencies": { "@tanstack/query-test-utils": "workspace:*", "npm-run-all2": "^5.0.0" } } ================================================ FILE: packages/query-persist-client-core/tsconfig.json ================================================ { "extends": "../../tsconfig.json", "compilerOptions": { "outDir": "./dist-ts", "rootDir": "." }, "include": ["src", "*.config.*", "package.json"], "references": [{ "path": "../query-core" }] } ================================================ FILE: packages/query-persist-client-core/tsconfig.prod.json ================================================ { "extends": "./tsconfig.json", "compilerOptions": { "incremental": false, "composite": false, "rootDir": "../../" } } ================================================ FILE: packages/query-persist-client-core/tsup.config.ts ================================================ import { defineConfig } from 'tsup' import { legacyConfig, modernConfig } from './root.tsup.config.js' export default defineConfig([ modernConfig({ entry: ['src/*.ts'] }), legacyConfig({ entry: ['src/*.ts'] }), ]) ================================================ FILE: packages/query-persist-client-core/vite.config.ts ================================================ import { defineConfig } from 'vitest/config' import packageJson from './package.json' export default 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, coverage: { enabled: true, provider: 'istanbul', include: ['src/**/*'] }, typecheck: { enabled: true }, restoreMocks: true, }, }) ================================================ SYMLINK: packages/query-persist-client-core/root.eslint.config.js -> eslint.config.js ================================================ ================================================ SYMLINK: packages/query-persist-client-core/root.tsup.config.js -> getTsupConfig.js ================================================ ================================================ FILE: packages/query-persist-client-core/src/createPersister.ts ================================================ import { hashKey, matchQuery, notifyManager, partialMatchKey, } from '@tanstack/query-core' import type { Query, QueryClient, QueryFilters, QueryFunctionContext, QueryKey, QueryState, } from '@tanstack/query-core' export interface PersistedQuery { buster: string queryHash: string queryKey: QueryKey state: QueryState } export type MaybePromise = T | Promise export interface AsyncStorage { getItem: (key: string) => MaybePromise setItem: (key: string, value: TStorageValue) => MaybePromise removeItem: (key: string) => MaybePromise entries?: () => MaybePromise> } export interface StoragePersisterOptions { /** The storage client used for setting and retrieving items from cache. * For SSR pass in `undefined`. */ storage: AsyncStorage | undefined | null /** * How to serialize the data to storage. * @default `JSON.stringify` */ serialize?: (persistedQuery: PersistedQuery) => MaybePromise /** * How to deserialize the data from storage. * @default `JSON.parse` */ deserialize?: (cachedString: TStorageValue) => MaybePromise /** * A unique string that can be used to forcefully invalidate existing caches, * if they do not share the same buster string */ buster?: string /** * The max-allowed age of the cache in milliseconds. * If a persisted cache is found that is older than this * time, it will be discarded * @default 24 hours */ maxAge?: number /** * Prefix to be used for storage key. * Storage key is a combination of prefix and query hash in a form of `prefix-queryHash`. * @default 'tanstack-query' */ prefix?: string /** * Filters to narrow down which Queries should be persisted. */ filters?: QueryFilters } export const PERSISTER_KEY_PREFIX = 'tanstack-query' /** * Warning: experimental feature. * This utility function enables fine-grained query persistence. * Simple add it as a `persister` parameter to `useQuery` or `defaultOptions` on `queryClient`. * * ``` * useQuery({ queryKey: ['myKey'], queryFn: fetcher, persister: createPersister({ storage: localStorage, }), }) ``` */ export function experimental_createQueryPersister({ storage, buster = '', maxAge = 1000 * 60 * 60 * 24, serialize = JSON.stringify as Required< StoragePersisterOptions >['serialize'], deserialize = JSON.parse as Required< StoragePersisterOptions >['deserialize'], prefix = PERSISTER_KEY_PREFIX, filters, }: StoragePersisterOptions) { function isExpiredOrBusted(persistedQuery: PersistedQuery) { if (persistedQuery.state.dataUpdatedAt) { const queryAge = Date.now() - persistedQuery.state.dataUpdatedAt const expired = queryAge > maxAge const busted = persistedQuery.buster !== buster if (expired || busted) { return true } return false } return true } async function retrieveQuery( queryHash: string, afterRestoreMacroTask?: (persistedQuery: PersistedQuery) => void, ) { if (storage != null) { const storageKey = `${prefix}-${queryHash}` try { const storedData = await storage.getItem(storageKey) if (storedData) { const persistedQuery = await deserialize(storedData) if (isExpiredOrBusted(persistedQuery)) { await storage.removeItem(storageKey) } else { if (afterRestoreMacroTask) { // Just after restoring we want to get fresh data from the server if it's stale notifyManager.schedule(() => afterRestoreMacroTask(persistedQuery), ) } // We must resolve the promise here, as otherwise we will have `loading` state in the app until `queryFn` resolves return persistedQuery.state.data as T } } } catch (err) { if (process.env.NODE_ENV === 'development') { console.error(err) console.warn( 'Encountered an error attempting to restore query cache from persisted location.', ) } await storage.removeItem(storageKey) } } return } async function persistQueryByKey( queryKey: QueryKey, queryClient: QueryClient, ) { if (storage != null) { const query = queryClient.getQueryCache().find({ queryKey }) if (query) { await persistQuery(query) } else { if (process.env.NODE_ENV === 'development') { console.warn( 'Could not find query to be persisted. QueryKey:', JSON.stringify(queryKey), ) } } } } async function persistQuery(query: Query) { if (storage != null) { const storageKey = `${prefix}-${query.queryHash}` storage.setItem( storageKey, await serialize({ state: query.state, queryKey: query.queryKey, queryHash: query.queryHash, buster: buster, }), ) } } async function persisterFn( queryFn: (context: QueryFunctionContext) => T | Promise, ctx: QueryFunctionContext, query: Query, ) { const matchesFilter = filters ? matchQuery(filters, query) : true // Try to restore only if we do not have any data in the cache and we have persister defined if (matchesFilter && query.state.data === undefined && storage != null) { const restoredData = await retrieveQuery( query.queryHash, (persistedQuery: PersistedQuery) => { // Set proper updatedAt, since resolving in the first pass overrides those values query.setState({ dataUpdatedAt: persistedQuery.state.dataUpdatedAt, errorUpdatedAt: persistedQuery.state.errorUpdatedAt, }) if (query.isStale()) { query.fetch() } }, ) if (restoredData != null) { return Promise.resolve(restoredData as T) } } // If we did not restore, or restoration failed - fetch const queryFnResult = await queryFn(ctx) if (matchesFilter && storage != null) { // Persist if we have storage defined, we use timeout to get proper state to be persisted notifyManager.schedule(() => { persistQuery(query) }) } return Promise.resolve(queryFnResult) } async function persisterGc() { if (storage?.entries) { const entries = await storage.entries() for (const [key, value] of entries) { if (key.startsWith(prefix)) { const persistedQuery = await deserialize(value) if (isExpiredOrBusted(persistedQuery)) { await storage.removeItem(key) } } } } else if (process.env.NODE_ENV === 'development') { throw new Error( 'Provided storage does not implement `entries` method. Garbage collection is not possible without ability to iterate over storage items.', ) } } async function restoreQueries( queryClient: QueryClient, filters: Pick = {}, ): Promise { const { exact, queryKey } = filters if (storage?.entries) { const entries = await storage.entries() for (const [key, value] of entries) { if (key.startsWith(prefix)) { const persistedQuery = await deserialize(value) if (isExpiredOrBusted(persistedQuery)) { await storage.removeItem(key) continue } if (queryKey) { if (exact) { if (persistedQuery.queryHash !== hashKey(queryKey)) { continue } } else if (!partialMatchKey(persistedQuery.queryKey, queryKey)) { continue } } queryClient.setQueryData( persistedQuery.queryKey, persistedQuery.state.data, { updatedAt: persistedQuery.state.dataUpdatedAt, }, ) } } } else if (process.env.NODE_ENV === 'development') { throw new Error( 'Provided storage does not implement `entries` method. Restoration of all stored entries is not possible without ability to iterate over storage items.', ) } } return { persisterFn, persistQuery, persistQueryByKey, retrieveQuery, persisterGc, restoreQueries, } } ================================================ FILE: packages/query-persist-client-core/src/index.ts ================================================ /* istanbul ignore file */ export * from './persist' export * from './retryStrategies' export * from './createPersister' ================================================ FILE: packages/query-persist-client-core/src/persist.ts ================================================ import { dehydrate, hydrate } from '@tanstack/query-core' import type { DehydrateOptions, DehydratedState, HydrateOptions, NotifyEventType, QueryClient, } from '@tanstack/query-core' export type Promisable = T | PromiseLike export interface Persister { persistClient: (persistClient: PersistedClient) => Promisable restoreClient: () => Promisable removeClient: () => Promisable } export interface PersistedClient { timestamp: number buster: string clientState: DehydratedState } export interface PersistQueryClientRootOptions { /** The QueryClient to persist */ queryClient: QueryClient /** The Persister interface for storing and restoring the cache * to/from a persisted location */ persister: Persister /** A unique string that can be used to forcefully * invalidate existing caches if they do not share the same buster string */ buster?: string } export interface PersistedQueryClientRestoreOptions extends PersistQueryClientRootOptions { /** The max-allowed age of the cache in milliseconds. * If a persisted cache is found that is older than this * time, it will be discarded */ maxAge?: number /** The options passed to the hydrate function */ hydrateOptions?: HydrateOptions } export interface PersistedQueryClientSaveOptions extends PersistQueryClientRootOptions { /** The options passed to the dehydrate function */ dehydrateOptions?: DehydrateOptions } export interface PersistQueryClientOptions extends PersistedQueryClientRestoreOptions, PersistedQueryClientSaveOptions, PersistQueryClientRootOptions {} /** * Checks if emitted event is about cache change and not about observers. * Useful for persist, where we only want to trigger save when cache is changed. */ const cacheEventTypes: Array = ['added', 'removed', 'updated'] function isCacheEventType(eventType: NotifyEventType) { return cacheEventTypes.includes(eventType) } /** * Restores persisted data to the QueryCache * - data obtained from persister.restoreClient * - data is hydrated using hydrateOptions * If data is expired, busted, empty, or throws, it runs persister.removeClient */ export async function persistQueryClientRestore({ queryClient, persister, maxAge = 1000 * 60 * 60 * 24, buster = '', hydrateOptions, }: PersistedQueryClientRestoreOptions) { try { const persistedClient = await persister.restoreClient() if (persistedClient) { if (persistedClient.timestamp) { const expired = Date.now() - persistedClient.timestamp > maxAge const busted = persistedClient.buster !== buster if (expired || busted) { return persister.removeClient() } else { hydrate(queryClient, persistedClient.clientState, hydrateOptions) } } else { return persister.removeClient() } } } catch (err) { if (process.env.NODE_ENV !== 'production') { console.error(err) console.warn( 'Encountered an error attempting to restore client cache from persisted location. As a precaution, the persisted cache will be discarded.', ) } await persister.removeClient() throw err } } /** * Persists data from the QueryCache * - data dehydrated using dehydrateOptions * - data is persisted using persister.persistClient */ export async function persistQueryClientSave({ queryClient, persister, buster = '', dehydrateOptions, }: PersistedQueryClientSaveOptions) { const persistClient: PersistedClient = { buster, timestamp: Date.now(), clientState: dehydrate(queryClient, dehydrateOptions), } await persister.persistClient(persistClient) } /** * Subscribe to QueryCache and MutationCache updates (for persisting) * @returns an unsubscribe function (to discontinue monitoring) */ export function persistQueryClientSubscribe( props: PersistedQueryClientSaveOptions, ) { const unsubscribeQueryCache = props.queryClient .getQueryCache() .subscribe((event) => { if (isCacheEventType(event.type)) { persistQueryClientSave(props) } }) const unsubscribeMutationCache = props.queryClient .getMutationCache() .subscribe((event) => { if (isCacheEventType(event.type)) { persistQueryClientSave(props) } }) return () => { unsubscribeQueryCache() unsubscribeMutationCache() } } /** * Restores persisted data to QueryCache and persists further changes. */ export function persistQueryClient( props: PersistQueryClientOptions, ): [() => void, Promise] { let hasUnsubscribed = false let persistQueryClientUnsubscribe: (() => void) | undefined const unsubscribe = () => { hasUnsubscribed = true persistQueryClientUnsubscribe?.() } // Attempt restore const restorePromise = persistQueryClientRestore(props).then(() => { if (!hasUnsubscribed) { // Subscribe to changes in the query cache to trigger the save persistQueryClientUnsubscribe = persistQueryClientSubscribe(props) } }) return [unsubscribe, restorePromise] } ================================================ FILE: packages/query-persist-client-core/src/retryStrategies.ts ================================================ import type { PersistedClient } from './persist' export type PersistRetryer = (props: { persistedClient: PersistedClient error: Error errorCount: number }) => PersistedClient | undefined export const removeOldestQuery: PersistRetryer = ({ persistedClient }) => { const mutations = [...persistedClient.clientState.mutations] const queries = [...persistedClient.clientState.queries] const client: PersistedClient = { ...persistedClient, clientState: { mutations, queries }, } // sort queries by dataUpdatedAt (oldest first) const sortedQueries = [...queries].sort( (a, b) => a.state.dataUpdatedAt - b.state.dataUpdatedAt, ) // clean oldest query if (sortedQueries.length > 0) { const oldestData = sortedQueries.shift() client.clientState.queries = queries.filter((q) => q !== oldestData) return client } return undefined } ================================================ FILE: packages/query-persist-client-core/src/__tests__/createPersister.test.ts ================================================ import { afterAll, beforeAll, describe, expect, test, vi } from 'vitest' import { Query, QueryClient, hashKey } from '@tanstack/query-core' import { PERSISTER_KEY_PREFIX, experimental_createQueryPersister, } from '../createPersister' import type { QueryFunctionContext, QueryKey } from '@tanstack/query-core' import type { StoragePersisterOptions } from '../createPersister' function getFreshStorage() { const storage = new Map() return { getItem: (key: string) => Promise.resolve(storage.get(key)), setItem: (key: string, value: unknown) => { storage.set(key, value) return Promise.resolve() }, removeItem: (key: string) => { storage.delete(key) return Promise.resolve() }, entries: () => { return Promise.resolve(Array.from(storage.entries())) }, } } function setupPersister( queryKey: QueryKey, persisterOptions: StoragePersisterOptions, ) { const client = new QueryClient() const context = { meta: { foo: 'bar' }, client, queryKey, // @ts-expect-error signal: undefined as AbortSignal, } satisfies QueryFunctionContext const queryHash = hashKey(queryKey) const storageKey = `${PERSISTER_KEY_PREFIX}-${queryHash}` const queryFn = vi.fn() const persister = experimental_createQueryPersister(persisterOptions) const query = new Query({ client, queryHash, queryKey, }) return { client, context, persister, query, queryFn, queryHash, queryKey, storageKey, } } describe('createPersister', () => { beforeAll(() => { vi.useFakeTimers() }) afterAll(() => { vi.useRealTimers() }) test('should fetch if storage is not provided', async () => { const { context, persister, query, queryFn } = setupPersister(['foo'], { storage: undefined, }) await persister.persisterFn(queryFn, context, query) expect(queryFn).toHaveBeenCalledOnce() expect(queryFn).toHaveBeenCalledWith(context) }) test('should fetch if there is no stored data', async () => { const storage = getFreshStorage() const { context, persister, query, queryFn } = setupPersister(['foo'], { storage, }) await persister.persisterFn(queryFn, context, query) expect(queryFn).toHaveBeenCalledOnce() expect(queryFn).toHaveBeenCalledWith(context) }) test('should fetch if query already has data', async () => { const storage = getFreshStorage() const { context, persister, query, queryFn } = setupPersister(['foo'], { storage, }) query.state.data = 'baz' await persister.persisterFn(queryFn, context, query) expect(queryFn).toHaveBeenCalledOnce() expect(queryFn).toHaveBeenCalledWith(context) }) test('should fetch if deserialization fails', async () => { const storage = getFreshStorage() const { context, persister, query, queryFn, storageKey } = setupPersister( ['foo'], { storage, }, ) await storage.setItem(storageKey, '{invalid[item') await persister.persisterFn(queryFn, context, query) expect(await storage.getItem(storageKey)).toBeUndefined() expect(queryFn).toHaveBeenCalledOnce() expect(queryFn).toHaveBeenCalledWith(context) }) test('should remove stored item if `dataUpdatedAt` is empty', async () => { const storage = getFreshStorage() const { context, persister, query, queryFn, storageKey } = setupPersister( ['foo'], { storage, }, ) await storage.setItem( storageKey, JSON.stringify({ buster: '', state: { dataUpdatedAt: undefined }, }), ) await persister.persisterFn(queryFn, context, query) expect(await storage.getItem(storageKey)).toBeUndefined() expect(queryFn).toHaveBeenCalledOnce() expect(queryFn).toHaveBeenCalledWith(context) }) test('should remove stored item if its expired', async () => { const storage = getFreshStorage() const { context, persister, query, queryFn, storageKey } = setupPersister( ['foo'], { storage, maxAge: 100, }, ) await storage.setItem( storageKey, JSON.stringify({ buster: '', state: { dataUpdatedAt: Date.now() - 200 }, }), ) await persister.persisterFn(queryFn, context, query) expect(await storage.getItem(storageKey)).toBeUndefined() expect(queryFn).toHaveBeenCalledOnce() expect(queryFn).toHaveBeenCalledWith(context) }) test('should remove stored item if its busted', async () => { const storage = getFreshStorage() const { context, persister, query, queryFn, storageKey } = setupPersister( ['foo'], { storage, }, ) await storage.setItem( storageKey, JSON.stringify({ buster: 'bust', state: { dataUpdatedAt: Date.now() }, }), ) await persister.persisterFn(queryFn, context, query) expect(await storage.getItem(storageKey)).toBeUndefined() expect(queryFn).toHaveBeenCalledOnce() expect(queryFn).toHaveBeenCalledWith(context) }) test('should restore item from the storage and set proper `updatedAt` values', async () => { const storage = getFreshStorage() const { context, persister, query, queryFn, storageKey } = setupPersister( ['foo'], { storage, }, ) const dataUpdatedAt = Date.now() await storage.setItem( storageKey, JSON.stringify({ buster: '', state: { dataUpdatedAt, data: '' }, }), ) await persister.persisterFn(queryFn, context, query) query.state.data = 'data0' query.fetch = vi.fn() expect(query.state.dataUpdatedAt).toEqual(0) await vi.advanceTimersByTimeAsync(0) expect(queryFn).toHaveBeenCalledTimes(0) expect(query.fetch).toHaveBeenCalledTimes(0) expect(query.state.dataUpdatedAt).toEqual(dataUpdatedAt) }) test('should restore item from the storage and refetch when `stale`', async () => { const storage = getFreshStorage() const { context, persister, query, queryFn, storageKey } = setupPersister( ['foo'], { storage, }, ) await storage.setItem( storageKey, JSON.stringify({ buster: '', state: { dataUpdatedAt: Date.now(), data: '' }, }), ) await persister.persisterFn(queryFn, context, query) query.state.isInvalidated = true query.fetch = vi.fn() await vi.advanceTimersByTimeAsync(0) expect(queryFn).toHaveBeenCalledTimes(0) expect(query.fetch).toHaveBeenCalledTimes(1) }) test('should store item after successful fetch', async () => { const storage = getFreshStorage() const { context, persister, query, queryFn, queryHash, queryKey, storageKey, } = setupPersister(['foo'], { storage, }) await persister.persisterFn(queryFn, context, query) query.setData('baz') await vi.advanceTimersByTimeAsync(0) expect(queryFn).toHaveBeenCalledOnce() expect(queryFn).toHaveBeenCalledWith(context) expect(JSON.parse(await storage.getItem(storageKey))).toMatchObject({ buster: '', queryHash, queryKey, state: { data: 'baz', }, }) }) test('should skip stored item if not matched by filters', async () => { const storage = getFreshStorage() const { context, persister, query, queryFn, storageKey } = setupPersister( ['foo'], { storage, filters: { predicate: () => { return false }, }, }, ) const dataUpdatedAt = Date.now() await storage.setItem( storageKey, JSON.stringify({ buster: '', state: { dataUpdatedAt }, }), ) await persister.persisterFn(queryFn, context, query) query.fetch = vi.fn() await vi.advanceTimersByTimeAsync(0) expect(queryFn).toHaveBeenCalledTimes(1) expect(query.fetch).toHaveBeenCalledTimes(0) }) test('should restore item from the storage with async deserializer', async () => { const storage = getFreshStorage() const { context, persister, query, queryFn, storageKey } = setupPersister( ['foo'], { storage, deserialize: (cachedString: string) => new Promise((resolve) => resolve(JSON.parse(cachedString))), }, ) await storage.setItem( storageKey, JSON.stringify({ buster: '', state: { dataUpdatedAt: Date.now(), data: '' }, }), ) await persister.persisterFn(queryFn, context, query) query.state.isInvalidated = true query.fetch = vi.fn() await vi.advanceTimersByTimeAsync(0) expect(queryFn).toHaveBeenCalledTimes(0) expect(query.fetch).toHaveBeenCalledTimes(1) }) test('should store item after successful fetch with async serializer', async () => { const storage = getFreshStorage() const { context, persister, query, queryFn, queryHash, queryKey, storageKey, } = setupPersister(['foo'], { storage, serialize: (persistedQuery) => new Promise((resolve) => resolve(JSON.stringify(persistedQuery))), }) await persister.persisterFn(queryFn, context, query) query.setData('baz') await vi.advanceTimersByTimeAsync(0) expect(queryFn).toHaveBeenCalledOnce() expect(queryFn).toHaveBeenCalledWith(context) expect(JSON.parse(await storage.getItem(storageKey))).toMatchObject({ buster: '', queryHash, queryKey, state: { data: 'baz', }, }) }) describe('persistQuery', () => { test('Should properly persiste basic query', async () => { const storage = getFreshStorage() const { persister, query, queryHash, queryKey, storageKey } = setupPersister(['foo'], { storage, }) query.setData('baz') await persister.persistQuery(query) expect(JSON.parse(await storage.getItem(storageKey))).toMatchObject({ buster: '', queryHash, queryKey, state: { dataUpdateCount: 1, data: 'baz', status: 'success', }, }) }) test('Should skip persistance if storage is not provided', async () => { const serializeMock = vi.fn() const { persister, query } = setupPersister(['foo'], { storage: null, serialize: serializeMock, }) query.setData('baz') await persister.persistQuery(query) expect(serializeMock).toHaveBeenCalledTimes(0) }) }) describe('persistQueryByKey', () => { test('Should skip persistance if storage is not provided', async () => { const serializeMock = vi.fn() const { persister, client, queryKey } = setupPersister(['foo'], { storage: null, serialize: serializeMock, }) client.setQueryData(queryKey, 'baz') await persister.persistQueryByKey(queryKey, client) expect(serializeMock).toHaveBeenCalledTimes(0) }) test('should skip persistance if query was not found', async () => { const serializeMock = vi.fn() const storage = getFreshStorage() const { client, persister, queryKey } = setupPersister(['foo'], { storage, serialize: serializeMock, }) client.setQueryData(queryKey, 'baz') await persister.persistQueryByKey(['foo2'], client) expect(serializeMock).toHaveBeenCalledTimes(0) }) test('Should properly persiste basic query', async () => { const storage = getFreshStorage() const { persister, client, queryHash, queryKey, storageKey } = setupPersister(['foo'], { storage, }) client.setQueryData(queryKey, 'baz') await persister.persistQueryByKey(queryKey, client) expect(JSON.parse(await storage.getItem(storageKey))).toMatchObject({ buster: '', queryHash, queryKey, state: { dataUpdateCount: 1, data: 'baz', status: 'success', }, }) }) }) describe('persisterGc', () => { test('should properly clean storage from busted entries', async () => { const storage = getFreshStorage() const { persister, client, query, queryKey } = setupPersister(['foo'], { storage, }) query.setState({ dataUpdatedAt: 1, data: 'f', }) client.getQueryCache().add(query) await persister.persistQueryByKey(queryKey, client) expect(await storage.entries()).toHaveLength(1) await persister.persisterGc() expect(await storage.entries()).toHaveLength(0) }) }) describe('restoreQueries', () => { test('should properly clean storage from busted entries', async () => { const storage = getFreshStorage() const { persister, client, query, queryKey } = setupPersister(['foo'], { storage, }) query.setState({ dataUpdatedAt: 1, data: 'f', }) client.getQueryCache().add(query) await persister.persistQueryByKey(queryKey, client) expect(await storage.entries()).toHaveLength(1) await persister.restoreQueries(client) expect(await storage.entries()).toHaveLength(0) }) test('should properly restore queries from cache without filters', async () => { const storage = getFreshStorage() const { persister, client, queryKey } = setupPersister(['foo'], { storage, }) client.setQueryData(queryKey, 'foo') await persister.persistQueryByKey(queryKey, client) expect(await storage.entries()).toHaveLength(1) client.clear() expect(client.getQueryCache().getAll()).toHaveLength(0) await persister.restoreQueries(client) expect(client.getQueryCache().getAll()).toHaveLength(1) expect(client.getQueryData(queryKey)).toEqual('foo') }) test('should properly restore queries from cache', async () => { const storage = getFreshStorage() const { persister, client, queryKey } = setupPersister(['foo', 'bar'], { storage, }) client.setQueryData(queryKey, 'foo') await persister.persistQueryByKey(queryKey, client) expect(await storage.entries()).toHaveLength(1) client.clear() expect(client.getQueryCache().getAll()).toHaveLength(0) await persister.restoreQueries(client, { queryKey }) expect(client.getQueryCache().getAll()).toHaveLength(1) expect(client.getQueryData(queryKey)).toEqual('foo') }) test('should not restore queries from cache if there is no match', async () => { const storage = getFreshStorage() const { persister, client, queryKey } = setupPersister(['foo', 'bar'], { storage, }) client.setQueryData(queryKey, 'foo') await persister.persistQueryByKey(queryKey, client) expect(await storage.entries()).toHaveLength(1) client.clear() expect(client.getQueryCache().getAll()).toHaveLength(0) await persister.restoreQueries(client, { queryKey: ['bar'] }) expect(client.getQueryCache().getAll()).toHaveLength(0) }) test('should properly restore queries from cache with partial match', async () => { const storage = getFreshStorage() const { persister, client, queryKey } = setupPersister(['foo', 'bar'], { storage, }) client.setQueryData(queryKey, 'foo') await persister.persistQueryByKey(queryKey, client) expect(await storage.entries()).toHaveLength(1) client.clear() expect(client.getQueryCache().getAll()).toHaveLength(0) await persister.restoreQueries(client, { queryKey: ['foo'] }) expect(client.getQueryCache().getAll()).toHaveLength(1) expect(client.getQueryData(queryKey)).toEqual('foo') }) test('should not restore queries from cache with exact match if there is no match', async () => { const storage = getFreshStorage() const { persister, client, queryKey } = setupPersister(['foo', 'bar'], { storage, }) client.setQueryData(queryKey, 'foo') await persister.persistQueryByKey(queryKey, client) expect(await storage.entries()).toHaveLength(1) client.clear() expect(client.getQueryCache().getAll()).toHaveLength(0) await persister.restoreQueries(client, { queryKey: ['foo'], exact: true }) expect(client.getQueryCache().getAll()).toHaveLength(0) }) test('should restore queries from cache with exact match', async () => { const storage = getFreshStorage() const { persister, client, queryKey } = setupPersister(['foo', 'bar'], { storage, }) client.setQueryData(queryKey, 'foo') await persister.persistQueryByKey(queryKey, client) expect(await storage.entries()).toHaveLength(1) client.clear() expect(client.getQueryCache().getAll()).toHaveLength(0) await persister.restoreQueries(client, { queryKey: queryKey, exact: true, }) expect(client.getQueryCache().getAll()).toHaveLength(1) }) }) }) ================================================ FILE: packages/query-persist-client-core/src/__tests__/persist.test.ts ================================================ import { describe, expect, test, vi } from 'vitest' import { QueriesObserver, QueryClient } from '@tanstack/query-core' import { persistQueryClientRestore, persistQueryClientSubscribe, } from '../persist' import { createMockPersister, createSpyPersister } from './utils' describe('persistQueryClientSubscribe', () => { test('should persist mutations', async () => { const queryClient = new QueryClient() const persister = createMockPersister() const unsubscribe = persistQueryClientSubscribe({ queryClient, persister, dehydrateOptions: { shouldDehydrateMutation: () => true }, }) queryClient.getMutationCache().build(queryClient, { mutationFn: (text: string) => Promise.resolve(text), }) const result = await persister.restoreClient() expect(result?.clientState.mutations).toHaveLength(1) unsubscribe() }) }) describe('persistQueryClientSave', () => { test('should not be triggered on observer type events', () => { const queryClient = new QueryClient() const persister = createSpyPersister() const unsubscribe = persistQueryClientSubscribe({ queryClient, persister, }) const queryKey = ['test'] const queryFn = vi.fn().mockReturnValue(1) const observer = new QueriesObserver(queryClient, [{ queryKey, queryFn }]) const unsubscribeObserver = observer.subscribe(vi.fn()) observer .getObservers()[0] ?.setOptions({ queryKey, refetchOnWindowFocus: false }) unsubscribeObserver() queryClient.setQueryData(queryKey, 2) // persistClient should be called 3 times: // 1. When query is added // 2. When queryFn is resolved // 3. When setQueryData is called // All events fired by manipulating observers are ignored expect(persister.persistClient).toHaveBeenCalledTimes(3) unsubscribe() }) }) describe('persistQueryClientRestore', () => { test('should rethrow exceptions in `restoreClient`', async () => { const consoleMock = vi .spyOn(console, 'error') .mockImplementation(() => undefined) const consoleWarn = vi .spyOn(console, 'warn') .mockImplementation(() => undefined) const queryClient = new QueryClient() const restoreError = new Error('Error restoring client') const persister = createSpyPersister() persister.restoreClient = () => Promise.reject(restoreError) await expect( persistQueryClientRestore({ queryClient, persister, }), ).rejects.toBe(restoreError) expect(consoleMock).toHaveBeenCalledTimes(1) expect(consoleWarn).toHaveBeenCalledTimes(1) expect(consoleMock).toHaveBeenNthCalledWith(1, restoreError) consoleMock.mockRestore() consoleWarn.mockRestore() }) test('should rethrow exceptions in `removeClient` before `restoreClient`', async () => { const consoleMock = vi .spyOn(console, 'error') .mockImplementation(() => undefined) const consoleWarn = vi .spyOn(console, 'warn') .mockImplementation(() => undefined) const queryClient = new QueryClient() const restoreError = new Error('Error restoring client') const removeError = new Error('Error removing client') const persister = createSpyPersister() persister.restoreClient = () => Promise.reject(restoreError) persister.removeClient = () => Promise.reject(removeError) await expect( persistQueryClientRestore({ queryClient, persister, }), ).rejects.toBe(removeError) expect(consoleMock).toHaveBeenCalledTimes(1) expect(consoleWarn).toHaveBeenCalledTimes(1) expect(consoleMock).toHaveBeenNthCalledWith(1, restoreError) consoleMock.mockRestore() consoleWarn.mockRestore() }) test('should rethrow error in `removeClient`', async () => { const queryClient = new QueryClient() const persister = createSpyPersister() const removeError = new Error('Error removing client') persister.removeClient = () => Promise.reject(removeError) persister.restoreClient = () => { return Promise.resolve({ buster: 'random-buster', clientState: { mutations: [], queries: [], }, timestamp: new Date().getTime(), }) } await expect( persistQueryClientRestore({ queryClient, persister, }), ).rejects.toBe(removeError) }) }) ================================================ FILE: packages/query-persist-client-core/src/__tests__/utils.ts ================================================ import { vi } from 'vitest' import { sleep } from '@tanstack/query-test-utils' import type { PersistedClient, Persister } from '../persist' export function createMockPersister(): Persister { let storedState: PersistedClient | undefined return { persistClient(persistClient: PersistedClient) { storedState = persistClient }, async restoreClient() { await sleep(10) return storedState }, removeClient() { storedState = undefined }, } } export function createSpyPersister(): Persister { return { persistClient: vi.fn(), restoreClient: vi.fn(), removeClient: vi.fn(), } } ================================================ FILE: packages/query-sync-storage-persister/eslint.config.js ================================================ // @ts-check import rootConfig from './root.eslint.config.js' export default [...rootConfig] ================================================ FILE: packages/query-sync-storage-persister/package.json ================================================ { "name": "@tanstack/query-sync-storage-persister", "version": "5.89.0", "description": "A persister for synchronous storages, to be used with TanStack/Query", "author": "tannerlinsley", "license": "MIT", "repository": { "type": "git", "url": "https://github.com/TanStack/query.git", "directory": "packages/query-sync-storage-persister" }, "homepage": "https://tanstack.com/query", "funding": { "type": "github", "url": "https://github.com/sponsors/tannerlinsley" }, "scripts": { "clean": "premove ./build ./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": "publint --strict && attw --pack", "build": "tsup --tsconfig tsconfig.prod.json" }, "type": "module", "types": "build/legacy/index.d.ts", "main": "build/legacy/index.cjs", "module": "build/legacy/index.js", "react-native": "src/index.ts", "exports": { ".": { "@tanstack/custom-condition": "./src/index.ts", "import": { "types": "./build/modern/index.d.ts", "default": "./build/modern/index.js" }, "require": { "types": "./build/modern/index.d.cts", "default": "./build/modern/index.cjs" } }, "./package.json": "./package.json" }, "sideEffects": false, "files": [ "build", "src", "!src/__tests__" ], "dependencies": { "@tanstack/query-core": "workspace:*", "@tanstack/query-persist-client-core": "workspace:*" }, "devDependencies": { "@tanstack/query-test-utils": "workspace:*", "npm-run-all2": "^5.0.0" } } ================================================ FILE: packages/query-sync-storage-persister/tsconfig.json ================================================ { "extends": "../../tsconfig.json", "compilerOptions": { "outDir": "./dist-ts", "rootDir": "." }, "include": ["src", "*.config.*", "package.json"], "references": [ { "path": "../query-core" }, { "path": "../query-persist-client-core" } ] } ================================================ FILE: packages/query-sync-storage-persister/tsconfig.prod.json ================================================ { "extends": "./tsconfig.json", "compilerOptions": { "incremental": false, "composite": false, "rootDir": "../../" } } ================================================ FILE: packages/query-sync-storage-persister/tsup.config.ts ================================================ import { defineConfig } from 'tsup' import { legacyConfig, modernConfig } from './root.tsup.config.js' export default defineConfig([ modernConfig({ entry: ['src/*.ts'] }), legacyConfig({ entry: ['src/*.ts'] }), ]) ================================================ FILE: packages/query-sync-storage-persister/vite.config.ts ================================================ import { defineConfig } from 'vitest/config' import packageJson from './package.json' export default 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, coverage: { enabled: true, provider: 'istanbul', include: ['src/**/*'] }, typecheck: { enabled: true }, restoreMocks: true, }, }) ================================================ SYMLINK: packages/query-sync-storage-persister/root.eslint.config.js -> eslint.config.js ================================================ ================================================ SYMLINK: packages/query-sync-storage-persister/root.tsup.config.js -> getTsupConfig.js ================================================ ================================================ FILE: packages/query-sync-storage-persister/src/index.ts ================================================ import { timeoutManager } from '@tanstack/query-core' import { noop } from './utils' import type { ManagedTimerId } from '@tanstack/query-core' import type { PersistRetryer, PersistedClient, Persister, } from '@tanstack/query-persist-client-core' interface Storage { getItem: (key: string) => string | null setItem: (key: string, value: string) => void removeItem: (key: string) => void } interface CreateSyncStoragePersisterOptions { /** The storage client used for setting and retrieving items from cache. * For SSR pass in `undefined`. Note that window.localStorage can be * `null` in Android WebViews depending on how they are configured. */ storage: Storage | undefined | null /** The key to use when storing the cache */ key?: string /** To avoid spamming, * pass a time in ms to throttle saving the cache to disk */ throttleTime?: number /** * How to serialize the data to storage. * @default `JSON.stringify` */ serialize?: (client: PersistedClient) => string /** * How to deserialize the data from storage. * @default `JSON.parse` */ deserialize?: (cachedString: string) => PersistedClient retry?: PersistRetryer } /** * @deprecated use `createAsyncStoragePersister` from `@tanstack/query-async-storage-persister` instead. */ export function createSyncStoragePersister({ storage, key = `REACT_QUERY_OFFLINE_CACHE`, throttleTime = 1000, serialize = JSON.stringify, deserialize = JSON.parse, retry, }: CreateSyncStoragePersisterOptions): Persister { if (storage) { const trySave = (persistedClient: PersistedClient): Error | undefined => { try { storage.setItem(key, serialize(persistedClient)) return } catch (error) { return error as Error } } return { persistClient: throttle((persistedClient) => { let client: PersistedClient | undefined = persistedClient let error = trySave(client) let errorCount = 0 while (error && client) { errorCount++ client = retry?.({ persistedClient: client, error, errorCount, }) if (client) { error = trySave(client) } } }, throttleTime), restoreClient: () => { const cacheString = storage.getItem(key) if (!cacheString) { return } return deserialize(cacheString) }, removeClient: () => { storage.removeItem(key) }, } } return { persistClient: noop, restoreClient: noop, removeClient: noop, } } function throttle>( func: (...args: TArgs) => any, wait = 100, ) { let timer: ManagedTimerId | null = null let params: TArgs return function (...args: TArgs) { params = args if (timer === null) { timer = timeoutManager.setTimeout(() => { func(...params) timer = null }, wait) } } } ================================================ FILE: packages/query-sync-storage-persister/src/utils.ts ================================================ export function noop(): void export function noop(): undefined export function noop() {} ================================================ FILE: packages/query-sync-storage-persister/src/__tests__/storageIsFull.test.ts ================================================ import { describe, expect, test } from 'vitest' import { MutationCache, QueryCache, QueryClient, dehydrate, } from '@tanstack/query-core' import { removeOldestQuery } from '@tanstack/query-persist-client-core' import { sleep } from '@tanstack/query-test-utils' import { createSyncStoragePersister } from '../index' function getMockStorage(limitSize?: number) { const dataSet = new Map() return { getItem(key: string): string | null { const value = dataSet.get(key) return value === undefined ? null : value }, setItem(key: string, value: string) { if (limitSize !== undefined) { const currentSize = Array.from(dataSet.entries()).reduce( (n, d) => d[0].length + d[1].length + n, 0, ) if ( currentSize - (dataSet.get(key)?.length || 0) + value.length > limitSize ) { throw Error( `Failed to execute 'setItem' on 'Storage': Setting the value of '${key}' exceeded the quota.`, ) } } return dataSet.set(key, value) }, removeItem(key: string) { return dataSet.delete(key) }, } as any as Storage } describe('create persister', () => { test('basic store and recover', async () => { const queryCache = new QueryCache() const mutationCache = new MutationCache() const queryClient = new QueryClient({ queryCache, mutationCache }) const storage = getMockStorage() const persister = createSyncStoragePersister({ throttleTime: 0, storage, }) await queryClient.prefetchQuery({ queryKey: ['string'], queryFn: () => Promise.resolve('string'), }) await queryClient.prefetchQuery({ queryKey: ['number'], queryFn: () => Promise.resolve(1), }) await queryClient.prefetchQuery({ queryKey: ['boolean'], queryFn: () => Promise.resolve(true), }) await queryClient.prefetchQuery({ queryKey: ['null'], queryFn: () => Promise.resolve(null), }) await queryClient.prefetchQuery({ queryKey: ['array'], queryFn: () => Promise.resolve(['string', 0]), }) const persistClient = { buster: 'test-buster', timestamp: Date.now(), clientState: dehydrate(queryClient), } persister.persistClient(persistClient) await sleep(1) const restoredClient = await persister.restoreClient() expect(restoredClient).toEqual(persistClient) }) test('should clean the old queries when storage full', async () => { const queryCache = new QueryCache() const mutationCache = new MutationCache() const queryClient = new QueryClient({ queryCache, mutationCache }) const N = 2000 const storage = getMockStorage(N * 5) // can save 4 items; const persister = createSyncStoragePersister({ throttleTime: 0, storage, retry: removeOldestQuery, }) await queryClient.prefetchQuery({ queryKey: ['A'], queryFn: () => Promise.resolve('A'.repeat(N)), }) await sleep(1) await queryClient.prefetchQuery({ queryKey: ['B'], queryFn: () => Promise.resolve('B'.repeat(N)), }) await sleep(1) await queryClient.prefetchQuery({ queryKey: ['C'], queryFn: () => Promise.resolve('C'.repeat(N)), }) await sleep(1) await queryClient.prefetchQuery({ queryKey: ['D'], queryFn: () => Promise.resolve('D'.repeat(N)), }) await sleep(1) await queryClient.prefetchQuery({ queryKey: ['E'], queryFn: () => Promise.resolve('E'.repeat(N)), }) const persistClient = { buster: 'test-limit', timestamp: Date.now(), clientState: dehydrate(queryClient), } persister.persistClient(persistClient) await sleep(10) const restoredClient = await persister.restoreClient() expect(restoredClient?.clientState.queries.length).toEqual(4) expect( restoredClient?.clientState.queries.find((q) => q.queryKey[0] === 'A'), ).toBeUndefined() expect( restoredClient?.clientState.queries.find((q) => q.queryKey[0] === 'B'), ).not.toBeUndefined() // update query Data await queryClient.prefetchQuery({ queryKey: ['A'], queryFn: () => Promise.resolve('a'.repeat(N)), }) const updatedPersistClient = { buster: 'test-limit', timestamp: Date.now(), clientState: dehydrate(queryClient), } persister.persistClient(updatedPersistClient) await sleep(10) const restoredClient2 = await persister.restoreClient() expect(restoredClient2?.clientState.queries.length).toEqual(4) expect( restoredClient2?.clientState.queries.find((q) => q.queryKey[0] === 'A'), ).toHaveProperty('state.data', 'a'.repeat(N)) expect( restoredClient2?.clientState.queries.find((q) => q.queryKey[0] === 'B'), ).toBeUndefined() }) test('should clear storage as default error handling', async () => { const queryCache = new QueryCache() const mutationCache = new MutationCache() const queryClient = new QueryClient({ queryCache, mutationCache }) const N = 2000 const storage = getMockStorage(0) const persister = createSyncStoragePersister({ throttleTime: 0, storage, retry: removeOldestQuery, }) await queryClient.prefetchQuery({ queryKey: ['A'], queryFn: () => Promise.resolve('A'.repeat(N)), }) await sleep(1) const persistClient = { buster: 'test-limit', timestamp: Date.now(), clientState: dehydrate(queryClient), } persister.persistClient(persistClient) await sleep(10) const restoredClient = await persister.restoreClient() expect(restoredClient).toEqual(undefined) }) }) ================================================ FILE: packages/query-test-utils/eslint.config.js ================================================ // @ts-check import rootConfig from './root.eslint.config.js' export default [...rootConfig] ================================================ FILE: packages/query-test-utils/package.json ================================================ { "name": "@tanstack/query-test-utils", "version": "0.0.0", "description": "Internal test utilities for TanStack Query", "author": "Jonghyeon Ko ", "private": true, "license": "MIT", "repository": { "type": "git", "url": "https://github.com/TanStack/query.git", "directory": "packages/query-test-utils" }, "homepage": "https://tanstack.com/query", "funding": { "type": "github", "url": "https://github.com/sponsors/tannerlinsley" }, "main": "src/index.ts", "types": "src/index.ts", "module": "src/index.ts", "exports": { ".": { "types": "./src/index.ts", "default": "./src/index.ts" }, "./package.json": "./package.json" }, "scripts": { "clean": "premove ./build ./coverage ./dist-ts", "test:eslint": "eslint --concurrency=auto ./src", "test:types": "npm-run-all --serial test:types:*", "test:types:tscurrent": "tsc --build", "test:lib": "vitest", "test:lib:dev": "pnpm run test:lib --watch" }, "type": "module", "devDependencies": { "npm-run-all2": "^5.0.0" } } ================================================ FILE: packages/query-test-utils/tsconfig.json ================================================ { "extends": "../../tsconfig.json", "compilerOptions": { "outDir": "./dist-ts", "rootDir": "." }, "include": ["src", "*.config.*", "package.json"] } ================================================ FILE: packages/query-test-utils/vite.config.ts ================================================ import { defineConfig } from 'vitest/config' import packageJson from './package.json' export default 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, coverage: { enabled: true, provider: 'istanbul', include: ['src/**/*'] }, typecheck: { enabled: true }, restoreMocks: true, }, }) ================================================ SYMLINK: packages/query-test-utils/root.eslint.config.js -> eslint.config.js ================================================ ================================================ FILE: packages/query-test-utils/src/index.ts ================================================ export { sleep } from './sleep' export { queryKey } from './queryKey' export { mockVisibilityState } from './mockVisibilityState' ================================================ FILE: packages/query-test-utils/src/mockVisibilityState.ts ================================================ import { vi } from 'vitest' import type { MockInstance } from 'vitest' export const mockVisibilityState = ( value: DocumentVisibilityState, ): MockInstance<() => DocumentVisibilityState> => vi.spyOn(document, 'visibilityState', 'get').mockReturnValue(value) ================================================ FILE: packages/query-test-utils/src/queryKey.ts ================================================ let queryKeyCount = 0 export const queryKey = (): Array => { queryKeyCount++ return [`query_${queryKeyCount}`] } ================================================ FILE: packages/query-test-utils/src/sleep.ts ================================================ export const sleep = (ms: number): Promise => new Promise((resolve) => { setTimeout(resolve, ms) }) ================================================ FILE: packages/query-test-utils/src/__test__/queryKey.test.ts ================================================ import { describe, expect, it } from 'vitest' import { queryKey } from '../queryKey' describe('queryKey', () => { it('should return a query key', () => { const key = queryKey() expect(key).toEqual(['query_1']) }) it('should return a new query key each time', () => { expect(queryKey()).not.toEqual(queryKey()) }) }) ================================================ FILE: packages/query-test-utils/src/__test__/sleep.test.ts ================================================ import { describe, expect, it } from 'vitest' import { sleep } from '../sleep' describe('sleep', () => { it('should sleep for the given amount of time', async () => { const start = Date.now() await sleep(100) const end = Date.now() expect(end - start).toBeGreaterThanOrEqual(100) }) }) ================================================ FILE: packages/react-query/README.md ================================================ ![TanStack Query Header](https://github.com/TanStack/query/raw/main/media/repo-header.png) Hooks for fetching, caching and updating asynchronous data in React #TanStack semantic-release Join the discussion on Github Best of JS Gitpod Ready-to-Code Enjoy this library? Try the entire [TanStack](https://tanstack.com)! [TanStack Table](https://github.com/TanStack/table), [TanStack Router](https://github.com/tanstack/router), [TanStack Virtual](https://github.com/tanstack/virtual), [React Charts](https://github.com/TanStack/react-charts), [React Ranger](https://github.com/TanStack/ranger) ## Visit [tanstack.com/query](https://tanstack.com/query) for docs, guides, API and more! ## 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 - [React Suspense](https://react.dev/reference/react/Suspense) + Fetch-As-You-Render Query Prefetching - Dedicated Devtools ### [Become a Sponsor!](https://github.com/sponsors/tannerlinsley/) ================================================ FILE: packages/react-query/eslint.config.js ================================================ // @ts-check import pluginReact from '@eslint-react/eslint-plugin' import * as reactHooks from 'eslint-plugin-react-hooks' import rootConfig from './root.eslint.config.js' export default [ ...rootConfig, reactHooks.configs.recommended, { files: ['**/*.{ts,tsx}'], ...pluginReact.configs.recommended, rules: { '@eslint-react/no-context-provider': 'off', // We need to be React 18 compatible }, }, { rules: { '@eslint-react/dom/no-missing-button-type': 'off', 'react-hooks/react-compiler': 'error', 'react-hooks/exhaustive-deps': 'error', 'react-hooks/rules-of-hooks': 'error', }, }, { files: ['**/__tests__/**'], rules: { '@typescript-eslint/no-unnecessary-condition': 'off', 'react-hooks/react-compiler': 'off', }, }, ] ================================================ FILE: packages/react-query/package.json ================================================ { "name": "@tanstack/react-query", "version": "5.89.0", "description": "Hooks for managing, caching and syncing asynchronous and remote data in React", "author": "tannerlinsley", "license": "MIT", "repository": { "type": "git", "url": "https://github.com/TanStack/query.git", "directory": "packages/react-query" }, "homepage": "https://tanstack.com/query", "funding": { "type": "github", "url": "https://github.com/sponsors/tannerlinsley" }, "scripts": { "clean": "premove ./build ./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 tsconfig.legacy.json", "test:types:ts51": "node ../../node_modules/typescript51/lib/tsc.js --build tsconfig.legacy.json", "test:types:ts52": "node ../../node_modules/typescript52/lib/tsc.js --build tsconfig.legacy.json", "test:types:ts53": "node ../../node_modules/typescript53/lib/tsc.js --build tsconfig.legacy.json", "test:types:ts54": "node ../../node_modules/typescript54/lib/tsc.js --build tsconfig.legacy.json", "test:types:ts55": "node ../../node_modules/typescript55/lib/tsc.js --build tsconfig.legacy.json", "test:types:ts56": "node ../../node_modules/typescript56/lib/tsc.js --build tsconfig.legacy.json", "test:types:ts57": "node ../../node_modules/typescript57/lib/tsc.js --build tsconfig.legacy.json", "test:types:tscurrent": "tsc --build", "test:lib": "vitest", "test:lib:dev": "pnpm run test:lib --watch", "test:build": "publint --strict && attw --pack", "build": "pnpm build:tsup && pnpm build:codemods", "build:tsup": "tsup --tsconfig tsconfig.prod.json", "build:codemods": "cpy ../query-codemods/* ./build/codemods" }, "type": "module", "types": "build/legacy/index.d.ts", "main": "build/legacy/index.cjs", "module": "build/legacy/index.js", "react-native": "src/index.ts", "exports": { ".": { "@tanstack/custom-condition": "./src/index.ts", "import": { "types": "./build/modern/index.d.ts", "default": "./build/modern/index.js" }, "require": { "types": "./build/modern/index.d.cts", "default": "./build/modern/index.cjs" } }, "./package.json": "./package.json" }, "sideEffects": false, "files": [ "build", "src", "!src/__tests__", "!build/codemods/node_modules", "!build/codemods/vite.config.ts", "!build/codemods/**/__testfixtures__", "!build/codemods/**/__tests__" ], "dependencies": { "@tanstack/query-core": "workspace:*" }, "devDependencies": { "@tanstack/query-persist-client-core": "workspace:*", "@tanstack/query-test-utils": "workspace:*", "@testing-library/react": "^16.1.0", "@testing-library/react-render-stream": "^2.0.0", "@types/react": "^19.0.1", "@types/react-dom": "^19.0.2", "@vitejs/plugin-react": "^4.3.4", "cpy-cli": "^5.0.0", "npm-run-all2": "^5.0.0", "react": "^19.0.0", "react-dom": "^19.0.0", "react-error-boundary": "^4.1.2" }, "peerDependencies": { "react": "^18 || ^19" } } ================================================ FILE: packages/react-query/test-setup.ts ================================================ import '@testing-library/jest-dom/vitest' import { act, cleanup as cleanupRTL } from '@testing-library/react' import { cleanup as cleanupRRS } from '@testing-library/react-render-stream' import { afterEach } from 'vitest' import { notifyManager } from '@tanstack/query-core' // https://testing-library.com/docs/react-testing-library/api#cleanup afterEach(() => { cleanupRTL() cleanupRRS() }) // Wrap notifications with act to make sure React knows about React Query updates notifyManager.setNotifyFunction((fn) => { act(fn) }) ================================================ FILE: packages/react-query/tsconfig.json ================================================ { "extends": "../../tsconfig.json", "compilerOptions": { "outDir": "./dist-ts", "rootDir": ".", "jsx": "react-jsx" }, "include": ["src", "test-setup.ts", "*.config.*", "package.json"] } ================================================ FILE: packages/react-query/tsconfig.legacy.json ================================================ { "extends": "./tsconfig.json", "compilerOptions": { "jsx": "react-jsx", "outDir": "./dist-ts/legacy" }, "include": ["src"], "exclude": ["src/__tests__"], "references": [{ "path": "../query-persist-client-core" }] } ================================================ FILE: packages/react-query/tsconfig.prod.json ================================================ { "extends": "./tsconfig.json", "compilerOptions": { "incremental": false, "composite": false, "rootDir": "../../" } } ================================================ FILE: packages/react-query/tsup.config.ts ================================================ import { defineConfig } from 'tsup' import { legacyConfig, modernConfig } from './root.tsup.config.js' export default defineConfig([ modernConfig({ entry: ['src/*.ts', 'src/*.tsx'] }), legacyConfig({ entry: ['src/*.ts', 'src/*.tsx'] }), ]) ================================================ FILE: packages/react-query/vite.config.ts ================================================ import { defineConfig } from 'vitest/config' import react from '@vitejs/plugin-react' import packageJson from './package.json' export default defineConfig({ plugins: [react()], // 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 }, restoreMocks: true, retry: process.env.CI ? 3 : 0, }, }) ================================================ SYMLINK: packages/react-query/root.eslint.config.js -> eslint.config.js ================================================ ================================================ SYMLINK: packages/react-query/root.tsup.config.js -> getTsupConfig.js ================================================ ================================================ FILE: packages/react-query/src/errorBoundaryUtils.ts ================================================ 'use client' import * as React from 'react' import { shouldThrowError } from '@tanstack/query-core' import type { DefaultedQueryObserverOptions, Query, QueryKey, QueryObserverResult, ThrowOnError, } from '@tanstack/query-core' import type { QueryErrorResetBoundaryValue } from './QueryErrorResetBoundary' export const ensurePreventErrorBoundaryRetry = < TQueryFnData, TError, TData, TQueryData, TQueryKey extends QueryKey, >( options: DefaultedQueryObserverOptions< TQueryFnData, TError, TData, TQueryData, TQueryKey >, errorResetBoundary: QueryErrorResetBoundaryValue, ) => { if ( options.suspense || options.throwOnError || options.experimental_prefetchInRender ) { // Prevent retrying failed query if the error boundary has not been reset yet if (!errorResetBoundary.isReset()) { options.retryOnMount = false } } } export const useClearResetErrorBoundary = ( errorResetBoundary: QueryErrorResetBoundaryValue, ) => { React.useEffect(() => { errorResetBoundary.clearReset() }, [errorResetBoundary]) } export const getHasError = < TData, TError, TQueryFnData, TQueryData, TQueryKey extends QueryKey, >({ result, errorResetBoundary, throwOnError, query, suspense, }: { result: QueryObserverResult errorResetBoundary: QueryErrorResetBoundaryValue throwOnError: ThrowOnError query: Query | undefined suspense: boolean | undefined }) => { return ( result.isError && !errorResetBoundary.isReset() && !result.isFetching && query && ((suspense && result.data === undefined) || shouldThrowError(throwOnError, [result.error, query])) ) } ================================================ FILE: packages/react-query/src/HydrationBoundary.tsx ================================================ 'use client' import * as React from 'react' import { hydrate } from '@tanstack/query-core' import { useQueryClient } from './QueryClientProvider' import type { DehydratedState, HydrateOptions, OmitKeyof, QueryClient, } from '@tanstack/query-core' export interface HydrationBoundaryProps { state: DehydratedState | null | undefined options?: OmitKeyof & { defaultOptions?: OmitKeyof< Exclude, 'mutations' > } children?: React.ReactNode queryClient?: QueryClient } export const HydrationBoundary = ({ children, options = {}, state, queryClient, }: HydrationBoundaryProps) => { const client = useQueryClient(queryClient) const optionsRef = React.useRef(options) optionsRef.current = options // This useMemo is for performance reasons only, everything inside it must // be safe to run in every render and code here should be read as "in render". // // This code needs to happen during the render phase, because after initial // SSR, hydration needs to happen _before_ children render. Also, if hydrating // during a transition, we want to hydrate as much as is safe in render so // we can prerender as much as possible. // // For any queries that already exist in the cache, we want to hold back on // hydrating until _after_ the render phase. The reason for this is that during // transitions, we don't want the existing queries and observers to update to // the new data on the current page, only _after_ the transition is committed. // If the transition is aborted, we will have hydrated any _new_ queries, but // we throw away the fresh data for any existing ones to avoid unexpectedly // updating the UI. const hydrationQueue: DehydratedState['queries'] | undefined = React.useMemo(() => { if (state) { if (typeof state !== 'object') { return } const queryCache = client.getQueryCache() // State is supplied from the outside and we might as well fail // gracefully if it has the wrong shape, so while we type `queries` // as required, we still provide a fallback. // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition const queries = state.queries || [] const newQueries: DehydratedState['queries'] = [] const existingQueries: DehydratedState['queries'] = [] for (const dehydratedQuery of queries) { const existingQuery = queryCache.get(dehydratedQuery.queryHash) if (!existingQuery) { newQueries.push(dehydratedQuery) } else { const hydrationIsNewer = dehydratedQuery.state.dataUpdatedAt > existingQuery.state.dataUpdatedAt || (dehydratedQuery.promise && existingQuery.state.status !== 'pending' && existingQuery.state.fetchStatus !== 'fetching' && dehydratedQuery.dehydratedAt !== undefined && dehydratedQuery.dehydratedAt > existingQuery.state.dataUpdatedAt) if (hydrationIsNewer) { existingQueries.push(dehydratedQuery) } } } if (newQueries.length > 0) { // It's actually fine to call this with queries/state that already exists // in the cache, or is older. hydrate() is idempotent for queries. hydrate(client, { queries: newQueries }, optionsRef.current) } if (existingQueries.length > 0) { return existingQueries } } return undefined }, [client, state]) React.useEffect(() => { if (hydrationQueue) { hydrate(client, { queries: hydrationQueue }, optionsRef.current) } }, [client, hydrationQueue]) return children as React.ReactElement } ================================================ FILE: packages/react-query/src/index.ts ================================================ /* istanbul ignore file */ // Re-export core export * from '@tanstack/query-core' // React Query export * from './types' export { useQueries } from './useQueries' export type { QueriesResults, QueriesOptions } from './useQueries' export { useQuery } from './useQuery' export { useSuspenseQuery } from './useSuspenseQuery' export { useSuspenseInfiniteQuery } from './useSuspenseInfiniteQuery' export { useSuspenseQueries } from './useSuspenseQueries' export type { SuspenseQueriesResults, SuspenseQueriesOptions, } from './useSuspenseQueries' export { usePrefetchQuery } from './usePrefetchQuery' export { usePrefetchInfiniteQuery } from './usePrefetchInfiniteQuery' export { queryOptions } from './queryOptions' export type { DefinedInitialDataOptions, UndefinedInitialDataOptions, UnusedSkipTokenOptions, } from './queryOptions' export { infiniteQueryOptions } from './infiniteQueryOptions' export type { DefinedInitialDataInfiniteOptions, UndefinedInitialDataInfiniteOptions, UnusedSkipTokenInfiniteOptions, } from './infiniteQueryOptions' export { QueryClientContext, QueryClientProvider, useQueryClient, } from './QueryClientProvider' export type { QueryClientProviderProps } from './QueryClientProvider' export type { QueryErrorResetBoundaryProps } from './QueryErrorResetBoundary' export { HydrationBoundary } from './HydrationBoundary' export type { HydrationBoundaryProps } from './HydrationBoundary' export type { QueryErrorClearResetFunction, QueryErrorIsResetFunction, QueryErrorResetBoundaryFunction, QueryErrorResetFunction, } from './QueryErrorResetBoundary' export { QueryErrorResetBoundary, useQueryErrorResetBoundary, } from './QueryErrorResetBoundary' export { useIsFetching } from './useIsFetching' export { useIsMutating, useMutationState } from './useMutationState' export { useMutation } from './useMutation' export { mutationOptions } from './mutationOptions' export { useInfiniteQuery } from './useInfiniteQuery' export { useIsRestoring, IsRestoringProvider } from './IsRestoringProvider' ================================================ FILE: packages/react-query/src/infiniteQueryOptions.ts ================================================ import type { DataTag, DefaultError, InfiniteData, InitialDataFunction, NonUndefinedGuard, OmitKeyof, QueryKey, SkipToken, } from '@tanstack/query-core' import type { UseInfiniteQueryOptions } from './types' export type UndefinedInitialDataInfiniteOptions< TQueryFnData, TError = DefaultError, TData = InfiniteData, TQueryKey extends QueryKey = QueryKey, TPageParam = unknown, > = UseInfiniteQueryOptions< 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< UseInfiniteQueryOptions, 'queryFn' > & { queryFn?: Exclude< UseInfiniteQueryOptions< TQueryFnData, TError, TData, TQueryKey, TPageParam >['queryFn'], SkipToken | undefined > } export type DefinedInitialDataInfiniteOptions< TQueryFnData, TError = DefaultError, TData = InfiniteData, TQueryKey extends QueryKey = QueryKey, TPageParam = unknown, > = UseInfiniteQueryOptions< TQueryFnData, TError, TData, TQueryKey, TPageParam > & { initialData: | NonUndefinedGuard> | (() => NonUndefinedGuard>) | undefined } 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> } 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> } 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> } export function infiniteQueryOptions(options: unknown) { return options } ================================================ FILE: packages/react-query/src/IsRestoringProvider.ts ================================================ 'use client' import * as React from 'react' const IsRestoringContext = React.createContext(false) export const useIsRestoring = () => React.useContext(IsRestoringContext) export const IsRestoringProvider = IsRestoringContext.Provider ================================================ FILE: packages/react-query/src/mutationOptions.ts ================================================ import type { DefaultError, WithRequired } from '@tanstack/query-core' import type { UseMutationOptions } from './types' export function mutationOptions< TData = unknown, TError = DefaultError, TVariables = void, TOnMutateResult = unknown, >( options: WithRequired< UseMutationOptions, 'mutationKey' >, ): WithRequired< UseMutationOptions, 'mutationKey' > export function mutationOptions< TData = unknown, TError = DefaultError, TVariables = void, TOnMutateResult = unknown, >( options: Omit< UseMutationOptions, 'mutationKey' >, ): Omit< UseMutationOptions, 'mutationKey' > export function mutationOptions< TData = unknown, TError = DefaultError, TVariables = void, TOnMutateResult = unknown, >( options: UseMutationOptions, ): UseMutationOptions { return options } ================================================ FILE: packages/react-query/src/QueryClientProvider.tsx ================================================ 'use client' import * as React from 'react' import type { QueryClient } from '@tanstack/query-core' export const QueryClientContext = React.createContext( undefined, ) export const useQueryClient = (queryClient?: QueryClient) => { const client = React.useContext(QueryClientContext) if (queryClient) { return queryClient } if (!client) { throw new Error('No QueryClient set, use QueryClientProvider to set one') } return client } export type QueryClientProviderProps = { client: QueryClient children?: React.ReactNode } export const QueryClientProvider = ({ client, children, }: QueryClientProviderProps): React.JSX.Element => { React.useEffect(() => { client.mount() return () => { client.unmount() } }, [client]) return ( {children} ) } ================================================ FILE: packages/react-query/src/QueryErrorResetBoundary.tsx ================================================ 'use client' import * as React from 'react' // CONTEXT export type QueryErrorResetFunction = () => void export type QueryErrorIsResetFunction = () => boolean export type QueryErrorClearResetFunction = () => void export interface QueryErrorResetBoundaryValue { clearReset: QueryErrorClearResetFunction isReset: QueryErrorIsResetFunction reset: QueryErrorResetFunction } function createValue(): QueryErrorResetBoundaryValue { let isReset = false return { clearReset: () => { isReset = false }, reset: () => { isReset = true }, isReset: () => { return isReset }, } } const QueryErrorResetBoundaryContext = React.createContext(createValue()) // HOOK export const useQueryErrorResetBoundary = () => React.useContext(QueryErrorResetBoundaryContext) // COMPONENT export type QueryErrorResetBoundaryFunction = ( value: QueryErrorResetBoundaryValue, ) => React.ReactNode export interface QueryErrorResetBoundaryProps { children: QueryErrorResetBoundaryFunction | React.ReactNode } export const QueryErrorResetBoundary = ({ children, }: QueryErrorResetBoundaryProps) => { const [value] = React.useState(() => createValue()) return ( {typeof children === 'function' ? children(value) : children} ) } ================================================ FILE: packages/react-query/src/queryOptions.ts ================================================ import type { DataTag, DefaultError, InitialDataFunction, NonUndefinedGuard, OmitKeyof, QueryFunction, QueryKey, SkipToken, } from '@tanstack/query-core' import type { UseQueryOptions } from './types' export type UndefinedInitialDataOptions< TQueryFnData = unknown, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, > = UseQueryOptions & { initialData?: | undefined | InitialDataFunction> | NonUndefinedGuard } export type UnusedSkipTokenOptions< TQueryFnData = unknown, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, > = OmitKeyof< UseQueryOptions, 'queryFn' > & { queryFn?: Exclude< UseQueryOptions['queryFn'], SkipToken | undefined > } export type DefinedInitialDataOptions< TQueryFnData = unknown, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, > = Omit, 'queryFn'> & { initialData: | NonUndefinedGuard | (() => NonUndefinedGuard) queryFn?: QueryFunction } export function queryOptions< TQueryFnData = unknown, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, >( options: DefinedInitialDataOptions, ): DefinedInitialDataOptions & { queryKey: DataTag } export function queryOptions< TQueryFnData = unknown, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, >( options: UnusedSkipTokenOptions, ): UnusedSkipTokenOptions & { queryKey: DataTag } export function queryOptions< TQueryFnData = unknown, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, >( options: UndefinedInitialDataOptions, ): UndefinedInitialDataOptions & { queryKey: DataTag } export function queryOptions(options: unknown) { return options } ================================================ FILE: packages/react-query/src/suspense.ts ================================================ import type { DefaultError, DefaultedQueryObserverOptions, Query, QueryKey, QueryObserver, QueryObserverResult, } from '@tanstack/query-core' import type { QueryErrorResetBoundaryValue } from './QueryErrorResetBoundary' export const defaultThrowOnError = < TQueryFnData = unknown, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, >( _error: TError, query: Query, ) => query.state.data === undefined export const ensureSuspenseTimers = ( defaultedOptions: DefaultedQueryObserverOptions, ) => { if (defaultedOptions.suspense) { // Handle staleTime to ensure minimum 1000ms in Suspense mode // This prevents unnecessary refetching when components remount after suspending const MIN_SUSPENSE_TIME_MS = 1000 const clamp = (value: number | 'static' | undefined) => value === 'static' ? value : Math.max(value ?? MIN_SUSPENSE_TIME_MS, MIN_SUSPENSE_TIME_MS) const originalStaleTime = defaultedOptions.staleTime defaultedOptions.staleTime = typeof originalStaleTime === 'function' ? (...args) => clamp(originalStaleTime(...args)) : clamp(originalStaleTime) if (typeof defaultedOptions.gcTime === 'number') { defaultedOptions.gcTime = Math.max( defaultedOptions.gcTime, MIN_SUSPENSE_TIME_MS, ) } } } export const willFetch = ( result: QueryObserverResult, isRestoring: boolean, ) => result.isLoading && result.isFetching && !isRestoring export const shouldSuspend = ( defaultedOptions: | DefaultedQueryObserverOptions | undefined, result: QueryObserverResult, ) => defaultedOptions?.suspense && result.isPending export const fetchOptimistic = < TQueryFnData, TError, TData, TQueryData, TQueryKey extends QueryKey, >( defaultedOptions: DefaultedQueryObserverOptions< TQueryFnData, TError, TData, TQueryData, TQueryKey >, observer: QueryObserver, errorResetBoundary: QueryErrorResetBoundaryValue, ) => observer.fetchOptimistic(defaultedOptions).catch(() => { errorResetBoundary.clearReset() }) ================================================ FILE: packages/react-query/src/types.ts ================================================ /* istanbul ignore file */ import type { DefaultError, DefinedInfiniteQueryObserverResult, DefinedQueryObserverResult, DistributiveOmit, FetchQueryOptions, InfiniteQueryObserverOptions, InfiniteQueryObserverResult, MutateFunction, MutationObserverOptions, MutationObserverResult, OmitKeyof, Override, QueryKey, QueryObserverOptions, QueryObserverResult, SkipToken, } from '@tanstack/query-core' export type AnyUseBaseQueryOptions = UseBaseQueryOptions< any, any, any, any, any > export interface UseBaseQueryOptions< TQueryFnData = unknown, TError = DefaultError, TData = TQueryFnData, TQueryData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, > extends QueryObserverOptions< TQueryFnData, TError, TData, TQueryData, TQueryKey > { /** * Set this to `false` to unsubscribe this observer from updates to the query cache. * Defaults to `true`. */ subscribed?: boolean } export interface UsePrefetchQueryOptions< TQueryFnData = unknown, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, > extends OmitKeyof< FetchQueryOptions, 'queryFn' > { queryFn?: Exclude< FetchQueryOptions['queryFn'], SkipToken > } export type AnyUseQueryOptions = UseQueryOptions export interface UseQueryOptions< TQueryFnData = unknown, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, > extends OmitKeyof< UseBaseQueryOptions, 'suspense' > {} export type AnyUseSuspenseQueryOptions = UseSuspenseQueryOptions< any, any, any, any > export interface UseSuspenseQueryOptions< TQueryFnData = unknown, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, > extends OmitKeyof< UseQueryOptions, 'queryFn' | 'enabled' | 'throwOnError' | 'placeholderData' > { queryFn?: Exclude< UseQueryOptions['queryFn'], SkipToken > } export type AnyUseInfiniteQueryOptions = UseInfiniteQueryOptions< any, any, any, any, any > export interface UseInfiniteQueryOptions< TQueryFnData = unknown, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, TPageParam = unknown, > extends OmitKeyof< InfiniteQueryObserverOptions< TQueryFnData, TError, TData, TQueryKey, TPageParam >, 'suspense' > { /** * Set this to `false` to unsubscribe this observer from updates to the query cache. * Defaults to `true`. */ subscribed?: boolean } export type AnyUseSuspenseInfiniteQueryOptions = UseSuspenseInfiniteQueryOptions export interface UseSuspenseInfiniteQueryOptions< TQueryFnData = unknown, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, TPageParam = unknown, > extends OmitKeyof< UseInfiniteQueryOptions, 'queryFn' | 'enabled' | 'throwOnError' | 'placeholderData' > { queryFn?: Exclude< UseInfiniteQueryOptions< TQueryFnData, TError, TData, TQueryKey, TPageParam >['queryFn'], SkipToken > } export type UseBaseQueryResult< TData = unknown, TError = DefaultError, > = QueryObserverResult export type UseQueryResult< TData = unknown, TError = DefaultError, > = UseBaseQueryResult export type UseSuspenseQueryResult< TData = unknown, TError = DefaultError, > = DistributiveOmit< DefinedQueryObserverResult, 'isPlaceholderData' | 'promise' > export type DefinedUseQueryResult< TData = unknown, TError = DefaultError, > = DefinedQueryObserverResult export type UseInfiniteQueryResult< TData = unknown, TError = DefaultError, > = InfiniteQueryObserverResult export type DefinedUseInfiniteQueryResult< TData = unknown, TError = DefaultError, > = DefinedInfiniteQueryObserverResult export type UseSuspenseInfiniteQueryResult< TData = unknown, TError = DefaultError, > = OmitKeyof< DefinedInfiniteQueryObserverResult, 'isPlaceholderData' | 'promise' > export type AnyUseMutationOptions = UseMutationOptions export interface UseMutationOptions< TData = unknown, TError = DefaultError, TVariables = void, TOnMutateResult = unknown, > extends OmitKeyof< MutationObserverOptions, '_defaulted' > {} export type UseMutateFunction< TData = unknown, TError = DefaultError, TVariables = void, TOnMutateResult = unknown, > = ( ...args: Parameters< MutateFunction > ) => void export type UseMutateAsyncFunction< TData = unknown, TError = DefaultError, TVariables = void, TOnMutateResult = unknown, > = MutateFunction export type UseBaseMutationResult< TData = unknown, TError = DefaultError, TVariables = unknown, TOnMutateResult = unknown, > = Override< MutationObserverResult, { mutate: UseMutateFunction } > & { mutateAsync: UseMutateAsyncFunction< TData, TError, TVariables, TOnMutateResult > } export type UseMutationResult< TData = unknown, TError = DefaultError, TVariables = unknown, TOnMutateResult = unknown, > = UseBaseMutationResult ================================================ FILE: packages/react-query/src/useBaseQuery.ts ================================================ 'use client' import * as React from 'react' import { isServer, noop, notifyManager } from '@tanstack/query-core' import { useQueryClient } from './QueryClientProvider' import { useQueryErrorResetBoundary } from './QueryErrorResetBoundary' import { ensurePreventErrorBoundaryRetry, getHasError, useClearResetErrorBoundary, } from './errorBoundaryUtils' import { useIsRestoring } from './IsRestoringProvider' import { ensureSuspenseTimers, fetchOptimistic, shouldSuspend, willFetch, } from './suspense' import type { QueryClient, QueryKey, QueryObserver, QueryObserverResult, } from '@tanstack/query-core' import type { UseBaseQueryOptions } from './types' export function useBaseQuery< TQueryFnData, TError, TData, TQueryData, TQueryKey extends QueryKey, >( options: UseBaseQueryOptions< TQueryFnData, TError, TData, TQueryData, TQueryKey >, Observer: typeof QueryObserver, queryClient?: QueryClient, ): QueryObserverResult { if (process.env.NODE_ENV !== 'production') { if (typeof options !== 'object' || Array.isArray(options)) { throw new Error( 'Bad argument type. Starting with v5, only the "Object" form is allowed when calling query related functions. Please use the error stack to find the culprit call. More info here: https://tanstack.com/query/latest/docs/react/guides/migrating-to-v5#supports-a-single-signature-one-object', ) } } const isRestoring = useIsRestoring() const errorResetBoundary = useQueryErrorResetBoundary() const client = useQueryClient(queryClient) const defaultedOptions = client.defaultQueryOptions(options) ;(client.getDefaultOptions().queries as any)?._experimental_beforeQuery?.( defaultedOptions, ) if (process.env.NODE_ENV !== 'production') { if (!defaultedOptions.queryFn) { console.error( `[${defaultedOptions.queryHash}]: No queryFn was passed as an option, and no default queryFn was found. The queryFn parameter is only optional when using a default queryFn. More info here: https://tanstack.com/query/latest/docs/framework/react/guides/default-query-function`, ) } } // Make sure results are optimistically set in fetching state before subscribing or updating options defaultedOptions._optimisticResults = isRestoring ? 'isRestoring' : 'optimistic' ensureSuspenseTimers(defaultedOptions) ensurePreventErrorBoundaryRetry(defaultedOptions, errorResetBoundary) useClearResetErrorBoundary(errorResetBoundary) // this needs to be invoked before creating the Observer because that can create a cache entry const isNewCacheEntry = !client .getQueryCache() .get(defaultedOptions.queryHash) const [observer] = React.useState( () => new Observer( client, defaultedOptions, ), ) // note: this must be called before useSyncExternalStore const result = observer.getOptimisticResult(defaultedOptions) const shouldSubscribe = !isRestoring && options.subscribed !== false React.useSyncExternalStore( React.useCallback( (onStoreChange) => { const unsubscribe = shouldSubscribe ? observer.subscribe(notifyManager.batchCalls(onStoreChange)) : noop // Update result to make sure we did not miss any query updates // between creating the observer and subscribing to it. observer.updateResult() return unsubscribe }, [observer, shouldSubscribe], ), () => observer.getCurrentResult(), () => observer.getCurrentResult(), ) React.useEffect(() => { observer.setOptions(defaultedOptions) }, [defaultedOptions, observer]) // Handle suspense if (shouldSuspend(defaultedOptions, result)) { throw fetchOptimistic(defaultedOptions, observer, errorResetBoundary) } // Handle error boundary if ( getHasError({ result, errorResetBoundary, throwOnError: defaultedOptions.throwOnError, query: client .getQueryCache() .get< TQueryFnData, TError, TQueryData, TQueryKey >(defaultedOptions.queryHash), suspense: defaultedOptions.suspense, }) ) { throw result.error } ;(client.getDefaultOptions().queries as any)?._experimental_afterQuery?.( defaultedOptions, result, ) if ( defaultedOptions.experimental_prefetchInRender && !isServer && willFetch(result, isRestoring) ) { const promise = isNewCacheEntry ? // Fetch immediately on render in order to ensure `.promise` is resolved even if the component is unmounted fetchOptimistic(defaultedOptions, observer, errorResetBoundary) : // subscribe to the "cache promise" so that we can finalize the currentThenable once data comes in client.getQueryCache().get(defaultedOptions.queryHash)?.promise promise?.catch(noop).finally(() => { // `.updateResult()` will trigger `.#currentThenable` to finalize observer.updateResult() }) } // Handle result property usage tracking return !defaultedOptions.notifyOnChangeProps ? observer.trackResult(result) : result } ================================================ FILE: packages/react-query/src/useInfiniteQuery.ts ================================================ 'use client' import { InfiniteQueryObserver } from '@tanstack/query-core' import { useBaseQuery } from './useBaseQuery' import type { DefaultError, InfiniteData, QueryClient, QueryKey, QueryObserver, } from '@tanstack/query-core' import type { DefinedUseInfiniteQueryResult, UseInfiniteQueryOptions, UseInfiniteQueryResult, } from './types' import type { DefinedInitialDataInfiniteOptions, UndefinedInitialDataInfiniteOptions, } from './infiniteQueryOptions' export function useInfiniteQuery< TQueryFnData, TError = DefaultError, TData = InfiniteData, TQueryKey extends QueryKey = QueryKey, TPageParam = unknown, >( options: DefinedInitialDataInfiniteOptions< TQueryFnData, TError, TData, TQueryKey, TPageParam >, queryClient?: QueryClient, ): DefinedUseInfiniteQueryResult export function useInfiniteQuery< TQueryFnData, TError = DefaultError, TData = InfiniteData, TQueryKey extends QueryKey = QueryKey, TPageParam = unknown, >( options: UndefinedInitialDataInfiniteOptions< TQueryFnData, TError, TData, TQueryKey, TPageParam >, queryClient?: QueryClient, ): UseInfiniteQueryResult export function useInfiniteQuery< TQueryFnData, TError = DefaultError, TData = InfiniteData, TQueryKey extends QueryKey = QueryKey, TPageParam = unknown, >( options: UseInfiniteQueryOptions< TQueryFnData, TError, TData, TQueryKey, TPageParam >, queryClient?: QueryClient, ): UseInfiniteQueryResult export function useInfiniteQuery( options: UseInfiniteQueryOptions, queryClient?: QueryClient, ) { return useBaseQuery( options, InfiniteQueryObserver as typeof QueryObserver, queryClient, ) } ================================================ FILE: packages/react-query/src/useIsFetching.ts ================================================ 'use client' import * as React from 'react' import { notifyManager } from '@tanstack/query-core' import { useQueryClient } from './QueryClientProvider' import type { QueryClient, QueryFilters } from '@tanstack/query-core' export function useIsFetching( filters?: QueryFilters, queryClient?: QueryClient, ): number { const client = useQueryClient(queryClient) const queryCache = client.getQueryCache() return React.useSyncExternalStore( React.useCallback( (onStoreChange) => queryCache.subscribe(notifyManager.batchCalls(onStoreChange)), [queryCache], ), () => client.isFetching(filters), () => client.isFetching(filters), ) } ================================================ FILE: packages/react-query/src/useMutation.ts ================================================ 'use client' import * as React from 'react' import { MutationObserver, noop, notifyManager, shouldThrowError, } from '@tanstack/query-core' import { useQueryClient } from './QueryClientProvider' import type { UseMutateFunction, UseMutationOptions, UseMutationResult, } from './types' import type { DefaultError, QueryClient } from '@tanstack/query-core' // HOOK export function useMutation< TData = unknown, TError = DefaultError, TVariables = void, TOnMutateResult = unknown, >( options: UseMutationOptions, queryClient?: QueryClient, ): UseMutationResult { const client = useQueryClient(queryClient) const [observer] = React.useState( () => new MutationObserver( client, options, ), ) React.useEffect(() => { observer.setOptions(options) }, [observer, options]) const result = React.useSyncExternalStore( React.useCallback( (onStoreChange) => observer.subscribe(notifyManager.batchCalls(onStoreChange)), [observer], ), () => observer.getCurrentResult(), () => observer.getCurrentResult(), ) const mutate = React.useCallback< UseMutateFunction >( (variables, mutateOptions) => { observer.mutate(variables, mutateOptions).catch(noop) }, [observer], ) if ( result.error && shouldThrowError(observer.options.throwOnError, [result.error]) ) { throw result.error } return { ...result, mutate, mutateAsync: result.mutate } } ================================================ FILE: packages/react-query/src/useMutationState.ts ================================================ 'use client' import * as React from 'react' import { notifyManager, replaceEqualDeep } from '@tanstack/query-core' import { useQueryClient } from './QueryClientProvider' import type { Mutation, MutationCache, MutationFilters, MutationState, QueryClient, } from '@tanstack/query-core' export function useIsMutating( filters?: MutationFilters, queryClient?: QueryClient, ): number { const client = useQueryClient(queryClient) return useMutationState( { filters: { ...filters, status: 'pending' } }, client, ).length } type MutationStateOptions = { filters?: MutationFilters select?: (mutation: Mutation) => TResult } 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 function useMutationState( options: MutationStateOptions = {}, queryClient?: QueryClient, ): Array { const mutationCache = useQueryClient(queryClient).getMutationCache() const optionsRef = React.useRef(options) const result = React.useRef>(null) if (!result.current) { result.current = getResult(mutationCache, options) } React.useEffect(() => { optionsRef.current = options }) return React.useSyncExternalStore( React.useCallback( (onStoreChange) => mutationCache.subscribe(() => { const nextResult = replaceEqualDeep( result.current, getResult(mutationCache, optionsRef.current), ) if (result.current !== nextResult) { result.current = nextResult notifyManager.schedule(onStoreChange) } }), [mutationCache], ), () => result.current, () => result.current, )! } ================================================ FILE: packages/react-query/src/usePrefetchInfiniteQuery.tsx ================================================ import { useQueryClient } from './QueryClientProvider' import type { DefaultError, FetchInfiniteQueryOptions, QueryClient, QueryKey, } from '@tanstack/query-core' export function usePrefetchInfiniteQuery< TQueryFnData = unknown, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, TPageParam = unknown, >( options: FetchInfiniteQueryOptions< TQueryFnData, TError, TData, TQueryKey, TPageParam >, queryClient?: QueryClient, ) { const client = useQueryClient(queryClient) if (!client.getQueryState(options.queryKey)) { client.prefetchInfiniteQuery(options) } } ================================================ FILE: packages/react-query/src/usePrefetchQuery.tsx ================================================ import { useQueryClient } from './QueryClientProvider' import type { DefaultError, QueryClient, QueryKey } from '@tanstack/query-core' import type { UsePrefetchQueryOptions } from './types' export function usePrefetchQuery< TQueryFnData = unknown, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, >( options: UsePrefetchQueryOptions, queryClient?: QueryClient, ) { const client = useQueryClient(queryClient) if (!client.getQueryState(options.queryKey)) { client.prefetchQuery(options) } } ================================================ FILE: packages/react-query/src/useQueries.ts ================================================ 'use client' import * as React from 'react' import { QueriesObserver, QueryObserver, noop, notifyManager, } from '@tanstack/query-core' import { useQueryClient } from './QueryClientProvider' import { useIsRestoring } from './IsRestoringProvider' import { useQueryErrorResetBoundary } from './QueryErrorResetBoundary' import { ensurePreventErrorBoundaryRetry, getHasError, useClearResetErrorBoundary, } from './errorBoundaryUtils' import { ensureSuspenseTimers, fetchOptimistic, shouldSuspend, willFetch, } from './suspense' import type { DefinedUseQueryResult, UseQueryOptions, UseQueryResult, } from './types' import type { DefaultError, OmitKeyof, QueriesObserverOptions, QueriesPlaceholderDataFunction, QueryClient, QueryFunction, QueryKey, QueryObserverOptions, ThrowOnError, } from '@tanstack/query-core' // This defines the `UseQueryOptions` that are accepted in `QueriesOptions` & `GetOptions`. // `placeholderData` function always gets undefined passed type UseQueryOptionsForUseQueries< TQueryFnData = unknown, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, > = OmitKeyof< UseQueryOptions, 'placeholderData' | 'subscribed' > & { 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 SkipTokenForUseQueries = symbol type GetUseQueryOptionsForUseQueries = // 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 } ? UseQueryOptionsForUseQueries : T extends { queryFnData: infer TQueryFnData; error?: infer TError } ? UseQueryOptionsForUseQueries : T extends { data: infer TData; error?: infer TError } ? UseQueryOptionsForUseQueries : // Part 2: responsible for applying explicit type parameter to function arguments, if tuple [TQueryFnData, TError, TData] T extends [infer TQueryFnData, infer TError, infer TData] ? UseQueryOptionsForUseQueries : T extends [infer TQueryFnData, infer TError] ? UseQueryOptionsForUseQueries : T extends [infer TQueryFnData] ? UseQueryOptionsForUseQueries : // Part 3: responsible for inferring and enforcing type if no explicit parameter was provided T extends { queryFn?: | QueryFunction | SkipTokenForUseQueries select?: (data: any) => infer TData throwOnError?: ThrowOnError } ? UseQueryOptionsForUseQueries< TQueryFnData, unknown extends TError ? DefaultError : TError, unknown extends TData ? TQueryFnData : TData, TQueryKey > : // Fallback UseQueryOptionsForUseQueries // A defined initialData setting should return a DefinedUseQueryResult rather than UseQueryResult type GetDefinedOrUndefinedQueryResult = T extends { initialData?: infer TInitialData } ? unknown extends TInitialData ? UseQueryResult : TInitialData extends TData ? DefinedUseQueryResult : TInitialData extends () => infer TInitialDataResult ? unknown extends TInitialDataResult ? UseQueryResult : TInitialDataResult extends TData ? DefinedUseQueryResult : UseQueryResult : UseQueryResult : UseQueryResult type GetUseQueryResult = // 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 | SkipTokenForUseQueries select?: (data: any) => infer TData throwOnError?: ThrowOnError } ? GetDefinedOrUndefinedQueryResult< T, unknown extends TData ? TQueryFnData : TData, unknown extends TError ? DefaultError : TError > : // Fallback UseQueryResult /** * 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, GetUseQueryOptionsForUseQueries] : T extends [infer Head, ...infer Tails] ? QueriesOptions< [...Tails], [...TResults, GetUseQueryOptionsForUseQueries], [...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< UseQueryOptionsForUseQueries< infer TQueryFnData, infer TError, infer TData, infer TQueryKey > > ? Array< UseQueryOptionsForUseQueries< 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, GetUseQueryResult] : T extends [infer Head, ...infer Tails] ? QueriesResults< [...Tails], [...TResults, GetUseQueryResult], [...TDepth, 1] > : { [K in keyof T]: GetUseQueryResult } export function useQueries< T extends Array, TCombinedResult = QueriesResults, >( { queries, ...options }: { queries: | readonly [...QueriesOptions] | readonly [...{ [K in keyof T]: GetUseQueryOptionsForUseQueries }] combine?: (result: QueriesResults) => TCombinedResult subscribed?: boolean }, queryClient?: QueryClient, ): TCombinedResult { const client = useQueryClient(queryClient) const isRestoring = useIsRestoring() const errorResetBoundary = useQueryErrorResetBoundary() const defaultedQueries = React.useMemo( () => queries.map((opts) => { const defaultedOptions = client.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 }), [queries, client, isRestoring], ) defaultedQueries.forEach((query) => { ensureSuspenseTimers(query) ensurePreventErrorBoundaryRetry(query, errorResetBoundary) }) useClearResetErrorBoundary(errorResetBoundary) const [observer] = React.useState( () => new QueriesObserver( client, defaultedQueries, options as QueriesObserverOptions, ), ) // note: this must be called before useSyncExternalStore const [optimisticResult, getCombinedResult, trackResult] = observer.getOptimisticResult( defaultedQueries, (options as QueriesObserverOptions).combine, ) const shouldSubscribe = !isRestoring && options.subscribed !== false React.useSyncExternalStore( React.useCallback( (onStoreChange) => shouldSubscribe ? observer.subscribe(notifyManager.batchCalls(onStoreChange)) : noop, [observer, shouldSubscribe], ), () => observer.getCurrentResult(), () => observer.getCurrentResult(), ) React.useEffect(() => { observer.setQueries( defaultedQueries, options as QueriesObserverOptions, ) }, [defaultedQueries, options, observer]) const shouldAtLeastOneSuspend = optimisticResult.some((result, index) => shouldSuspend(defaultedQueries[index], result), ) const suspensePromises = shouldAtLeastOneSuspend ? optimisticResult.flatMap((result, index) => { const opts = defaultedQueries[index] if (opts) { const queryObserver = new QueryObserver(client, opts) if (shouldSuspend(opts, result)) { return fetchOptimistic(opts, queryObserver, errorResetBoundary) } else if (willFetch(result, isRestoring)) { void fetchOptimistic(opts, queryObserver, errorResetBoundary) } } return [] }) : [] if (suspensePromises.length > 0) { throw Promise.all(suspensePromises) } const firstSingleResultWhichShouldThrow = optimisticResult.find( (result, index) => { const query = defaultedQueries[index] return ( query && getHasError({ result, errorResetBoundary, throwOnError: query.throwOnError, query: client.getQueryCache().get(query.queryHash), suspense: query.suspense, }) ) }, ) if (firstSingleResultWhichShouldThrow?.error) { throw firstSingleResultWhichShouldThrow.error } return getCombinedResult(trackResult()) } ================================================ FILE: packages/react-query/src/useQuery.ts ================================================ 'use client' import { QueryObserver } from '@tanstack/query-core' import { useBaseQuery } from './useBaseQuery' import type { DefaultError, NoInfer, QueryClient, QueryKey, } from '@tanstack/query-core' import type { DefinedUseQueryResult, UseQueryOptions, UseQueryResult, } from './types' import type { DefinedInitialDataOptions, UndefinedInitialDataOptions, } from './queryOptions' export function useQuery< TQueryFnData = unknown, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, >( options: DefinedInitialDataOptions, queryClient?: QueryClient, ): DefinedUseQueryResult, TError> export function useQuery< TQueryFnData = unknown, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, >( options: UndefinedInitialDataOptions, queryClient?: QueryClient, ): UseQueryResult, TError> export function useQuery< TQueryFnData = unknown, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, >( options: UseQueryOptions, queryClient?: QueryClient, ): UseQueryResult, TError> export function useQuery(options: UseQueryOptions, queryClient?: QueryClient) { return useBaseQuery(options, QueryObserver, queryClient) } ================================================ FILE: packages/react-query/src/useSuspenseInfiniteQuery.ts ================================================ 'use client' import { InfiniteQueryObserver, skipToken } from '@tanstack/query-core' import { useBaseQuery } from './useBaseQuery' import { defaultThrowOnError } from './suspense' import type { DefaultError, InfiniteData, InfiniteQueryObserverSuccessResult, QueryClient, QueryKey, QueryObserver, } from '@tanstack/query-core' import type { UseSuspenseInfiniteQueryOptions, UseSuspenseInfiniteQueryResult, } from './types' export function useSuspenseInfiniteQuery< TQueryFnData, TError = DefaultError, TData = InfiniteData, TQueryKey extends QueryKey = QueryKey, TPageParam = unknown, >( options: UseSuspenseInfiniteQueryOptions< TQueryFnData, TError, TData, TQueryKey, TPageParam >, queryClient?: QueryClient, ): UseSuspenseInfiniteQueryResult { if (process.env.NODE_ENV !== 'production') { if ((options.queryFn as any) === skipToken) { console.error('skipToken is not allowed for useSuspenseInfiniteQuery') } } return useBaseQuery( { ...options, enabled: true, suspense: true, throwOnError: defaultThrowOnError, }, InfiniteQueryObserver as typeof QueryObserver, queryClient, ) as InfiniteQueryObserverSuccessResult } ================================================ FILE: packages/react-query/src/useSuspenseQueries.ts ================================================ 'use client' import { skipToken } from '@tanstack/query-core' import { useQueries } from './useQueries' import { defaultThrowOnError } from './suspense' import type { UseSuspenseQueryOptions, UseSuspenseQueryResult } from './types' import type { DefaultError, QueryClient, QueryFunction, ThrowOnError, } from '@tanstack/query-core' // 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 SkipTokenForUseQueries = symbol type GetUseSuspenseQueryOptions = // 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 } ? UseSuspenseQueryOptions : T extends { queryFnData: infer TQueryFnData; error?: infer TError } ? UseSuspenseQueryOptions : T extends { data: infer TData; error?: infer TError } ? UseSuspenseQueryOptions : // Part 2: responsible for applying explicit type parameter to function arguments, if tuple [TQueryFnData, TError, TData] T extends [infer TQueryFnData, infer TError, infer TData] ? UseSuspenseQueryOptions : T extends [infer TQueryFnData, infer TError] ? UseSuspenseQueryOptions : T extends [infer TQueryFnData] ? UseSuspenseQueryOptions : // Part 3: responsible for inferring and enforcing type if no explicit parameter was provided T extends { queryFn?: | QueryFunction | SkipTokenForUseQueries select?: (data: any) => infer TData throwOnError?: ThrowOnError } ? UseSuspenseQueryOptions< TQueryFnData, TError, TData, TQueryKey > : T extends { queryFn?: | QueryFunction | SkipTokenForUseQueries throwOnError?: ThrowOnError } ? UseSuspenseQueryOptions< TQueryFnData, TError, TQueryFnData, TQueryKey > : // Fallback UseSuspenseQueryOptions type GetUseSuspenseQueryResult = // Part 1: responsible for mapping explicit type parameter to function result, if object T extends { queryFnData: any; error?: infer TError; data: infer TData } ? UseSuspenseQueryResult : T extends { queryFnData: infer TQueryFnData; error?: infer TError } ? UseSuspenseQueryResult : T extends { data: infer TData; error?: infer TError } ? UseSuspenseQueryResult : // Part 2: responsible for mapping explicit type parameter to function result, if tuple T extends [any, infer TError, infer TData] ? UseSuspenseQueryResult : T extends [infer TQueryFnData, infer TError] ? UseSuspenseQueryResult : T extends [infer TQueryFnData] ? UseSuspenseQueryResult : // Part 3: responsible for mapping inferred type to results, if no explicit parameter was provided T extends { queryFn?: | QueryFunction | SkipTokenForUseQueries select?: (data: any) => infer TData throwOnError?: ThrowOnError } ? UseSuspenseQueryResult< unknown extends TData ? TQueryFnData : TData, unknown extends TError ? DefaultError : TError > : T extends { queryFn?: | QueryFunction | SkipTokenForUseQueries throwOnError?: ThrowOnError } ? UseSuspenseQueryResult< TQueryFnData, unknown extends TError ? DefaultError : TError > : // Fallback UseSuspenseQueryResult /** * SuspenseQueriesOptions reducer recursively unwraps function arguments to infer/enforce type param */ export type SuspenseQueriesOptions< T extends Array, TResults extends Array = [], TDepth extends ReadonlyArray = [], > = TDepth['length'] extends MAXIMUM_DEPTH ? Array : T extends [] ? [] : T extends [infer Head] ? [...TResults, GetUseSuspenseQueryOptions] : T extends [infer Head, ...infer Tails] ? SuspenseQueriesOptions< [...Tails], [...TResults, GetUseSuspenseQueryOptions], [...TDepth, 1] > : Array 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< UseSuspenseQueryOptions< infer TQueryFnData, infer TError, infer TData, infer TQueryKey > > ? Array< UseSuspenseQueryOptions > : // Fallback Array /** * SuspenseQueriesResults reducer recursively maps type param to results */ export type SuspenseQueriesResults< T extends Array, TResults extends Array = [], TDepth extends ReadonlyArray = [], > = TDepth['length'] extends MAXIMUM_DEPTH ? Array : T extends [] ? [] : T extends [infer Head] ? [...TResults, GetUseSuspenseQueryResult] : T extends [infer Head, ...infer Tails] ? SuspenseQueriesResults< [...Tails], [...TResults, GetUseSuspenseQueryResult], [...TDepth, 1] > : { [K in keyof T]: GetUseSuspenseQueryResult } export function useSuspenseQueries< T extends Array, TCombinedResult = SuspenseQueriesResults, >( options: { queries: | readonly [...SuspenseQueriesOptions] | readonly [...{ [K in keyof T]: GetUseSuspenseQueryOptions }] combine?: (result: SuspenseQueriesResults) => TCombinedResult }, queryClient?: QueryClient, ): TCombinedResult export function useSuspenseQueries< T extends Array, TCombinedResult = SuspenseQueriesResults, >( options: { queries: readonly [...SuspenseQueriesOptions] combine?: (result: SuspenseQueriesResults) => TCombinedResult }, queryClient?: QueryClient, ): TCombinedResult export function useSuspenseQueries(options: any, queryClient?: QueryClient) { return useQueries( { ...options, queries: options.queries.map((query: any) => { if (process.env.NODE_ENV !== 'production') { if (query.queryFn === skipToken) { console.error('skipToken is not allowed for useSuspenseQueries') } } return { ...query, suspense: true, throwOnError: defaultThrowOnError, enabled: true, placeholderData: undefined, } }), }, queryClient, ) } ================================================ FILE: packages/react-query/src/useSuspenseQuery.ts ================================================ 'use client' import { QueryObserver, skipToken } from '@tanstack/query-core' import { useBaseQuery } from './useBaseQuery' import { defaultThrowOnError } from './suspense' import type { UseSuspenseQueryOptions, UseSuspenseQueryResult } from './types' import type { DefaultError, QueryClient, QueryKey } from '@tanstack/query-core' export function useSuspenseQuery< TQueryFnData = unknown, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, >( options: UseSuspenseQueryOptions, queryClient?: QueryClient, ): UseSuspenseQueryResult { if (process.env.NODE_ENV !== 'production') { if ((options.queryFn as any) === skipToken) { console.error('skipToken is not allowed for useSuspenseQuery') } } return useBaseQuery( { ...options, enabled: true, suspense: true, throwOnError: defaultThrowOnError, placeholderData: undefined, }, QueryObserver, queryClient, ) as UseSuspenseQueryResult } ================================================ FILE: packages/react-query/src/__tests__/fine-grained-persister.test.tsx ================================================ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import * as React from 'react' import { PERSISTER_KEY_PREFIX, experimental_createQueryPersister, } from '@tanstack/query-persist-client-core' import { queryKey, sleep } from '@tanstack/query-test-utils' import { QueryCache, QueryClient, hashKey, useQuery } from '..' import { renderWithClient } from './utils' describe('fine grained persister', () => { beforeEach(() => { vi.useFakeTimers() }) afterEach(() => { vi.useRealTimers() }) const queryCache = new QueryCache() const queryClient = new QueryClient({ queryCache }) it('should restore query state from persister and not refetch', async () => { const key = queryKey() const hash = hashKey(key) const spy = vi.fn(() => Promise.resolve('Works from queryFn')) const mapStorage = new Map() const storage = { getItem: (itemKey: string) => Promise.resolve(mapStorage.get(itemKey)), setItem: (itemKey: string, value: unknown) => { mapStorage.set(itemKey, value) return Promise.resolve() }, removeItem: (itemKey: string) => { mapStorage.delete(itemKey) return Promise.resolve() }, } await storage.setItem( `${PERSISTER_KEY_PREFIX}-${hash}`, JSON.stringify({ buster: '', queryHash: hash, queryKey: key, state: { dataUpdatedAt: Date.now(), data: 'Works from persister', }, }), ) function Test() { const [_, setRef] = React.useState() const { data } = useQuery({ queryKey: key, queryFn: spy, persister: experimental_createQueryPersister({ storage, }).persisterFn, staleTime: 5000, }) return
setRef(value)}>{data}
} const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(0) expect(rendered.getByText('Works from persister')).toBeInTheDocument() expect(spy).not.toHaveBeenCalled() }) it('should restore query state from persister and refetch', async () => { const key = queryKey() const hash = hashKey(key) const spy = vi.fn(async () => { await sleep(5) return 'Works from queryFn' }) const mapStorage = new Map() const storage = { getItem: (itemKey: string) => Promise.resolve(mapStorage.get(itemKey)), setItem: (itemKey: string, value: unknown) => { mapStorage.set(itemKey, value) return Promise.resolve() }, removeItem: (itemKey: string) => { mapStorage.delete(itemKey) return Promise.resolve() }, } await storage.setItem( `${PERSISTER_KEY_PREFIX}-${hash}`, JSON.stringify({ buster: '', queryHash: hash, queryKey: key, state: { dataUpdatedAt: Date.now(), data: 'Works from persister', }, }), ) function Test() { const [_, setRef] = React.useState() const { data } = useQuery({ queryKey: key, queryFn: spy, persister: experimental_createQueryPersister({ storage, }).persisterFn, }) return
setRef(value)}>{data}
} const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(0) expect(rendered.getByText('Works from persister')).toBeInTheDocument() await vi.advanceTimersByTimeAsync(6) expect(rendered.getByText('Works from queryFn')).toBeInTheDocument() expect(spy).toHaveBeenCalledTimes(1) }) it('should store query state to persister after fetch', async () => { const key = queryKey() const hash = hashKey(key) const spy = vi.fn(() => Promise.resolve('Works from queryFn')) const mapStorage = new Map() const storage = { getItem: (itemKey: string) => Promise.resolve(mapStorage.get(itemKey)), setItem: (itemKey: string, value: unknown) => { mapStorage.set(itemKey, value) return Promise.resolve() }, removeItem: (itemKey: string) => { mapStorage.delete(itemKey) return Promise.resolve() }, } function Test() { const [_, setRef] = React.useState() const { data } = useQuery({ queryKey: key, queryFn: spy, persister: experimental_createQueryPersister({ storage, }).persisterFn, }) return
setRef(value)}>{data}
} const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(0) expect(rendered.getByText('Works from queryFn')).toBeInTheDocument() expect(spy).toHaveBeenCalledTimes(1) const storedItem = await storage.getItem(`${PERSISTER_KEY_PREFIX}-${hash}`) expect(JSON.parse(storedItem)).toMatchObject({ state: { data: 'Works from queryFn', }, }) }) }) ================================================ FILE: packages/react-query/src/__tests__/HydrationBoundary.test.tsx ================================================ import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest' import * as React from 'react' import { render } from '@testing-library/react' import * as coreModule from '@tanstack/query-core' import { sleep } from '@tanstack/query-test-utils' import { HydrationBoundary, QueryClient, QueryClientProvider, dehydrate, useQuery, } from '..' import type { hydrate } from '@tanstack/query-core' describe('React hydration', () => { let stringifiedState: string beforeEach(async () => { vi.useFakeTimers() const queryClient = new QueryClient() queryClient.prefetchQuery({ queryKey: ['string'], queryFn: () => sleep(10).then(() => ['stringCached']), }) await vi.advanceTimersByTimeAsync(10) const dehydrated = dehydrate(queryClient) stringifiedState = JSON.stringify(dehydrated) queryClient.clear() }) afterEach(() => { vi.useRealTimers() }) test('should hydrate queries to the cache on context', async () => { const dehydratedState = JSON.parse(stringifiedState) const queryClient = new QueryClient() function Page() { const { data } = useQuery({ queryKey: ['string'], queryFn: () => sleep(20).then(() => ['string']), }) return (

{data}

) } const rendered = render( , ) expect(rendered.getByText('stringCached')).toBeInTheDocument() await vi.advanceTimersByTimeAsync(21) expect(rendered.getByText('string')).toBeInTheDocument() queryClient.clear() }) test('should hydrate queries to the cache on custom context', async () => { const queryClientInner = new QueryClient() const queryClientOuter = new QueryClient() const dehydratedState = JSON.parse(stringifiedState) function Page() { const { data } = useQuery({ queryKey: ['string'], queryFn: () => sleep(20).then(() => ['string']), }) return (

{data}

) } const rendered = render( , ) expect(rendered.getByText('stringCached')).toBeInTheDocument() await vi.advanceTimersByTimeAsync(21) expect(rendered.getByText('string')).toBeInTheDocument() queryClientInner.clear() queryClientOuter.clear() }) describe('ReactQueryCacheProvider with hydration support', () => { test('should hydrate new queries if queries change', async () => { const dehydratedState = JSON.parse(stringifiedState) const queryClient = new QueryClient() function Page({ queryKey }: { queryKey: [string] }) { const { data } = useQuery({ queryKey, queryFn: () => sleep(20).then(() => queryKey), }) return (

{data}

) } const rendered = render( , ) expect(rendered.getByText('stringCached')).toBeInTheDocument() await vi.advanceTimersByTimeAsync(21) expect(rendered.getByText('string')).toBeInTheDocument() const intermediateClient = new QueryClient() intermediateClient.prefetchQuery({ queryKey: ['string'], queryFn: () => sleep(20).then(() => ['should change']), }) intermediateClient.prefetchQuery({ queryKey: ['added'], queryFn: () => sleep(20).then(() => ['added']), }) await vi.advanceTimersByTimeAsync(20) const dehydrated = dehydrate(intermediateClient) intermediateClient.clear() rendered.rerender( , ) // Existing observer should not have updated at this point, // as that would indicate a side effect in the render phase expect(rendered.getByText('string')).toBeInTheDocument() // New query data should be available immediately expect(rendered.getByText('added')).toBeInTheDocument() await vi.advanceTimersByTimeAsync(0) // After effects phase has had time to run, the observer should have updated expect(rendered.queryByText('string')).not.toBeInTheDocument() expect(rendered.getByText('should change')).toBeInTheDocument() queryClient.clear() }) // When we hydrate in transitions that are later aborted, it could be // confusing to both developers and users if we suddenly updated existing // state on the screen (why did this update when it was not stale, nothing // remounted, I didn't change tabs etc?). // Any queries that does not exist in the cache yet can still be hydrated // since they don't have any observers on the current page that would update. test('should hydrate new but not existing queries if transition is aborted', async () => { const initialDehydratedState = JSON.parse(stringifiedState) const queryClient = new QueryClient() function Page({ queryKey }: { queryKey: [string] }) { const { data } = useQuery({ queryKey, queryFn: () => sleep(20).then(() => queryKey), }) return (

{data}

) } const rendered = render( , ) expect(rendered.getByText('stringCached')).toBeInTheDocument() await vi.advanceTimersByTimeAsync(21) expect(rendered.getByText('string')).toBeInTheDocument() const intermediateClient = new QueryClient() intermediateClient.prefetchQuery({ queryKey: ['string'], queryFn: () => sleep(20).then(() => ['should not change']), }) intermediateClient.prefetchQuery({ queryKey: ['added'], queryFn: () => sleep(20).then(() => ['added']), }) await vi.advanceTimersByTimeAsync(20) const newDehydratedState = dehydrate(intermediateClient) intermediateClient.clear() function Thrower() { throw new Promise(() => { // Never resolve }) // @ts-expect-error return null } React.startTransition(() => { rendered.rerender( , ) expect(rendered.getByText('loading')).toBeInTheDocument() }) React.startTransition(() => { rendered.rerender( , ) // This query existed before the transition so it should stay the same expect(rendered.getByText('string')).toBeInTheDocument() expect( rendered.queryByText('should not change'), ).not.toBeInTheDocument() // New query data should be available immediately because it was // hydrated in the previous transition, even though the new dehydrated // state did not contain it expect(rendered.getByText('added')).toBeInTheDocument() }) await vi.advanceTimersByTimeAsync(20) // It should stay the same even after effects have had a chance to run expect(rendered.getByText('string')).toBeInTheDocument() expect(rendered.queryByText('should not change')).not.toBeInTheDocument() queryClient.clear() }) test('should hydrate queries to new cache if cache changes', async () => { const dehydratedState = JSON.parse(stringifiedState) const queryClient = new QueryClient() function Page() { const { data } = useQuery({ queryKey: ['string'], queryFn: () => sleep(20).then(() => ['string']), }) return (

{data}

) } const rendered = render( , ) expect(rendered.getByText('stringCached')).toBeInTheDocument() await vi.advanceTimersByTimeAsync(21) expect(rendered.getByText('string')).toBeInTheDocument() const newClientQueryClient = new QueryClient() rendered.rerender( , ) await vi.advanceTimersByTimeAsync(20) expect(rendered.getByText('string')).toBeInTheDocument() queryClient.clear() newClientQueryClient.clear() }) }) test('should not hydrate queries if state is null', async () => { const queryClient = new QueryClient() const hydrateSpy = vi.spyOn(coreModule, 'hydrate') function Page() { return null } render( , ) await Promise.all( Array.from({ length: 1000 }).map(async (_, index) => { await vi.advanceTimersByTimeAsync(index) expect(hydrateSpy).toHaveBeenCalledTimes(0) }), ) hydrateSpy.mockRestore() queryClient.clear() }) test('should not hydrate queries if state is undefined', async () => { const queryClient = new QueryClient() const hydrateSpy = vi.spyOn(coreModule, 'hydrate') function Page() { return null } render( , ) await vi.advanceTimersByTimeAsync(0) expect(hydrateSpy).toHaveBeenCalledTimes(0) hydrateSpy.mockRestore() queryClient.clear() }) test('should not hydrate queries if state is not an object', async () => { const queryClient = new QueryClient() const hydrateSpy = vi.spyOn(coreModule, 'hydrate') function Page() { return null } render( , ) await vi.advanceTimersByTimeAsync(0) expect(hydrateSpy).toHaveBeenCalledTimes(0) hydrateSpy.mockRestore() queryClient.clear() }) test('should handle state without queries property gracefully', async () => { const queryClient = new QueryClient() const hydrateSpy = vi.spyOn(coreModule, 'hydrate') function Page() { return null } render( , ) await vi.advanceTimersByTimeAsync(0) expect(hydrateSpy).toHaveBeenCalledTimes(0) hydrateSpy.mockRestore() queryClient.clear() }) // https://github.com/TanStack/query/issues/8677 test('should not infinite loop when hydrating promises that resolve to errors', async () => { const originalHydrate = coreModule.hydrate const hydrateSpy = vi.spyOn(coreModule, 'hydrate') let hydrationCount = 0 hydrateSpy.mockImplementation((...args: Parameters) => { hydrationCount++ // Arbitrary number if (hydrationCount > 10) { // This is a rough way to detect it. Calling hydrate multiple times with // the same data is usually fine, but in this case it indicates the // logic in HydrationBoundary is not working as expected. throw new Error('Too many hydrations detected') } return originalHydrate(...args) }) // For the bug to trigger, there needs to already be a query in the cache, // with a dataUpdatedAt earlier than the dehydratedAt of the next query const clientQueryClient = new QueryClient() clientQueryClient.prefetchQuery({ queryKey: ['promise'], queryFn: () => sleep(20).then(() => 'existing'), }) await vi.advanceTimersByTimeAsync(20) const prefetchQueryClient = new QueryClient({ defaultOptions: { dehydrate: { shouldDehydrateQuery: () => true, }, }, }) prefetchQueryClient.prefetchQuery({ queryKey: ['promise'], queryFn: () => sleep(10).then(() => Promise.reject(new Error('Query failed'))), }) const dehydratedState = dehydrate(prefetchQueryClient) function ignore() { // Ignore redacted unhandled rejection } process.addListener('unhandledRejection', ignore) // Mimic what React/our synchronous thenable does for already rejected promises // @ts-expect-error dehydratedState.queries[0].promise.status = 'failure' function Page() { const { data } = useQuery({ queryKey: ['promise'], queryFn: () => sleep(20).then(() => ['new']), }) return (

{data}

) } const rendered = render( , ) expect(rendered.getByText('existing')).toBeInTheDocument() await vi.advanceTimersByTimeAsync(21) expect(rendered.getByText('new')).toBeInTheDocument() process.removeListener('unhandledRejection', ignore) hydrateSpy.mockRestore() prefetchQueryClient.clear() clientQueryClient.clear() }) }) ================================================ FILE: packages/react-query/src/__tests__/infiniteQueryOptions.test-d.tsx ================================================ import { assertType, describe, expectTypeOf, it, test } from 'vitest' import { QueryClient, dataTagSymbol, skipToken } from '@tanstack/query-core' import { infiniteQueryOptions } from '../infiniteQueryOptions' import { useInfiniteQuery } from '../useInfiniteQuery' import { useSuspenseInfiniteQuery } from '../useSuspenseInfiniteQuery' import { useQuery } from '../useQuery' 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 } = useInfiniteQuery(options) // known issue: type of pageParams is unknown when returned from useInfiniteQuery expectTypeOf(data).toEqualTypeOf< InfiniteData | undefined >() }) it('should work when passed to useSuspenseInfiniteQuery', () => { const options = infiniteQueryOptions({ queryKey: ['key'], queryFn: () => Promise.resolve('string'), getNextPageParam: () => 1, initialPageParam: 1, }) const { data } = useSuspenseInfiniteQuery(options) expectTypeOf(data).toEqualTypeOf>() }) 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 >() }) it('should throw a type error when using queryFn with skipToken in a suspense query', () => { const options = infiniteQueryOptions({ queryKey: ['key'], queryFn: Math.random() > 0.5 ? skipToken : () => Promise.resolve('string'), getNextPageParam: () => 1, initialPageParam: 1, }) // @ts-expect-error TS2345 const { data } = useSuspenseInfiniteQuery(options) expectTypeOf(data).toEqualTypeOf>() }) 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 useQuery(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, Error & { myMessage: string }> >() }) }) ================================================ FILE: packages/react-query/src/__tests__/infiniteQueryOptions.test.tsx ================================================ import { describe, expect, it } from 'vitest' import { infiniteQueryOptions } from '../infiniteQueryOptions' import type { UseInfiniteQueryOptions } from '../types' describe('infiniteQueryOptions', () => { it('should return the object received as a parameter without any modification.', () => { const object: UseInfiniteQueryOptions = { queryKey: ['key'], queryFn: () => Promise.resolve(5), getNextPageParam: () => null, initialPageParam: null, } expect(infiniteQueryOptions(object)).toStrictEqual(object) }) }) ================================================ FILE: packages/react-query/src/__tests__/mutationOptions.test-d.tsx ================================================ import { assertType, describe, expectTypeOf, it } from 'vitest' import { QueryClient } from '@tanstack/query-core' import { useIsMutating, useMutation, useMutationState } from '..' import { mutationOptions } from '../mutationOptions' import type { DefaultError, MutationFunctionContext, MutationState, WithRequired, } from '@tanstack/query-core' import type { UseMutationOptions, UseMutationResult } from '../types' describe('mutationOptions', () => { it('should not allow excess properties', () => { // @ts-expect-error this is a good error, because onMutates does not exist! mutationOptions({ mutationFn: () => Promise.resolve(5), mutationKey: ['key'], onMutates: 1000, onSuccess: (data) => { expectTypeOf(data).toEqualTypeOf() }, }) }) it('should infer types for callbacks', () => { mutationOptions({ mutationFn: () => Promise.resolve(5), mutationKey: ['key'], onSuccess: (data) => { expectTypeOf(data).toEqualTypeOf() }, }) }) it('should infer types for onError callback', () => { mutationOptions({ mutationFn: () => { throw new Error('fail') }, mutationKey: ['key'], onError: (error) => { expectTypeOf(error).toEqualTypeOf() }, }) }) it('should infer types for variables', () => { mutationOptions({ mutationFn: (vars) => { expectTypeOf(vars).toEqualTypeOf<{ id: string }>() return Promise.resolve(5) }, mutationKey: ['with-vars'], }) }) it('should infer result type correctly', () => { mutationOptions({ mutationFn: () => Promise.resolve(5), mutationKey: ['key'], onMutate: () => { return { name: 'onMutateResult' } }, onSuccess: (_data, _variables, onMutateResult) => { expectTypeOf(onMutateResult).toEqualTypeOf< { name: string } | undefined >() }, }) }) it('should infer context type correctly', () => { mutationOptions({ mutationFn: (_variables, context) => { expectTypeOf(context).toEqualTypeOf() return Promise.resolve(5) }, mutationKey: ['key'], onMutate: (_variables, context) => { expectTypeOf(context).toEqualTypeOf() }, onSuccess: (_data, _variables, _onMutateResult, context) => { expectTypeOf(context).toEqualTypeOf() }, onError: (_error, _variables, _onMutateResult, context) => { expectTypeOf(context).toEqualTypeOf() }, onSettled: (_data, _error, _variables, _onMutateResult, context) => { expectTypeOf(context).toEqualTypeOf() }, }) }) it('should error if mutationFn return type mismatches TData', () => { assertType( mutationOptions({ // @ts-expect-error this is a good error, because return type is string, not number mutationFn: async () => Promise.resolve('wrong return'), }), ) }) it('should allow mutationKey to be omitted', () => { return mutationOptions({ mutationFn: () => Promise.resolve(123), onSuccess: (data) => { expectTypeOf(data).toEqualTypeOf() }, }) }) it('should infer all types when not explicitly provided', () => { expectTypeOf( mutationOptions({ mutationFn: (id: string) => Promise.resolve(id.length), mutationKey: ['key'], onSuccess: (data) => { expectTypeOf(data).toEqualTypeOf() }, }), ).toEqualTypeOf< WithRequired< UseMutationOptions, 'mutationKey' > >() expectTypeOf( mutationOptions({ mutationFn: (id: string) => Promise.resolve(id.length), onSuccess: (data) => { expectTypeOf(data).toEqualTypeOf() }, }), ).toEqualTypeOf< Omit, 'mutationKey'> >() }) it('should infer types when used with useMutation', () => { const mutation = useMutation( mutationOptions({ mutationKey: ['key'], mutationFn: () => Promise.resolve('data'), onSuccess: (data) => { expectTypeOf(data).toEqualTypeOf() }, }), ) expectTypeOf(mutation).toEqualTypeOf< UseMutationResult >() useMutation( // should allow when used with useMutation without mutationKey mutationOptions({ mutationFn: () => Promise.resolve('data'), onSuccess: (data) => { expectTypeOf(data).toEqualTypeOf() }, }), ) }) it('should infer types when used with useIsMutating', () => { const isMutating = useIsMutating( mutationOptions({ mutationKey: ['key'], mutationFn: () => Promise.resolve(5), }), ) expectTypeOf(isMutating).toEqualTypeOf() useIsMutating( // @ts-expect-error filters should have mutationKey mutationOptions({ mutationFn: () => Promise.resolve(5), }), ) }) it('should infer types when used with queryClient.isMutating', () => { const queryClient = new QueryClient() const isMutating = queryClient.isMutating( mutationOptions({ mutationKey: ['key'], mutationFn: () => Promise.resolve(5), }), ) expectTypeOf(isMutating).toEqualTypeOf() queryClient.isMutating( // @ts-expect-error filters should have mutationKey mutationOptions({ mutationFn: () => Promise.resolve(5), }), ) }) it('should infer types when used with useMutationState', () => { const mutationState = useMutationState({ filters: mutationOptions({ mutationKey: ['key'], mutationFn: () => Promise.resolve(5), }), }) expectTypeOf(mutationState).toEqualTypeOf< Array> >() useMutationState({ // @ts-expect-error filters should have mutationKey filters: mutationOptions({ mutationFn: () => Promise.resolve(5), }), }) }) }) ================================================ FILE: packages/react-query/src/__tests__/mutationOptions.test.tsx ================================================ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { QueryClient } from '@tanstack/query-core' import { sleep } from '@tanstack/query-test-utils' import { fireEvent } from '@testing-library/react' import { mutationOptions } from '../mutationOptions' import { useIsMutating, useMutation, useMutationState } from '..' import { renderWithClient } from './utils' import type { MutationState } from '@tanstack/query-core' describe('mutationOptions', () => { beforeEach(() => { vi.useFakeTimers() }) afterEach(() => { vi.useRealTimers() }) it('should return the object received as a parameter without any modification (with mutationKey in mutationOptions)', () => { const object = { mutationKey: ['key'], mutationFn: () => sleep(10).then(() => 5), } as const expect(mutationOptions(object)).toStrictEqual(object) }) it('should return the object received as a parameter without any modification (without mutationKey in mutationOptions)', () => { const object = { mutationFn: () => sleep(10).then(() => 5), } as const expect(mutationOptions(object)).toStrictEqual(object) }) it('should return the number of fetching mutations when used with useIsMutating (with mutationKey in mutationOptions)', async () => { const isMutatingArray: Array = [] const queryClient = new QueryClient() const mutationOpts = mutationOptions({ mutationKey: ['key'], mutationFn: () => sleep(50).then(() => 'data'), }) function IsMutating() { const isMutating = useIsMutating() isMutatingArray.push(isMutating) return null } function Mutation() { const { mutate } = useMutation(mutationOpts) return (
) } function Page() { return (
) } const rendered = renderWithClient(queryClient, ) fireEvent.click(rendered.getByRole('button', { name: /mutate/i })) expect(isMutatingArray[0]).toEqual(0) await vi.advanceTimersByTimeAsync(0) expect(isMutatingArray[1]).toEqual(1) await vi.advanceTimersByTimeAsync(51) expect(isMutatingArray[2]).toEqual(0) expect(isMutatingArray[isMutatingArray.length - 1]).toEqual(0) }) it('should return the number of fetching mutations when used with useIsMutating (without mutationKey in mutationOptions)', async () => { const isMutatingArray: Array = [] const queryClient = new QueryClient() const mutationOpts = mutationOptions({ mutationFn: () => sleep(50).then(() => 'data'), }) function IsMutating() { const isMutating = useIsMutating() isMutatingArray.push(isMutating) return null } function Mutation() { const { mutate } = useMutation(mutationOpts) return (
) } function Page() { return (
) } const rendered = renderWithClient(queryClient, ) fireEvent.click(rendered.getByRole('button', { name: /mutate/i })) expect(isMutatingArray[0]).toEqual(0) await vi.advanceTimersByTimeAsync(0) expect(isMutatingArray[1]).toEqual(1) await vi.advanceTimersByTimeAsync(51) expect(isMutatingArray[2]).toEqual(0) expect(isMutatingArray[isMutatingArray.length - 1]).toEqual(0) }) it('should return the number of fetching mutations when used with useIsMutating', async () => { const isMutatingArray: Array = [] const queryClient = new QueryClient() const mutationOpts1 = mutationOptions({ mutationKey: ['key'], mutationFn: () => sleep(50).then(() => 'data1'), }) const mutationOpts2 = mutationOptions({ mutationFn: () => sleep(50).then(() => 'data2'), }) function IsMutating() { const isMutating = useIsMutating() isMutatingArray.push(isMutating) return null } function Mutation() { const { mutate: mutate1 } = useMutation(mutationOpts1) const { mutate: mutate2 } = useMutation(mutationOpts2) return (
) } function Page() { return (
) } const rendered = renderWithClient(queryClient, ) fireEvent.click(rendered.getByRole('button', { name: /mutate1/i })) fireEvent.click(rendered.getByRole('button', { name: /mutate2/i })) expect(isMutatingArray[0]).toEqual(0) await vi.advanceTimersByTimeAsync(0) expect(isMutatingArray[1]).toEqual(2) await vi.advanceTimersByTimeAsync(51) expect(isMutatingArray[2]).toEqual(0) expect(isMutatingArray[isMutatingArray.length - 1]).toEqual(0) }) it('should return the number of fetching mutations when used with useIsMutating (filter mutationOpts1.mutationKey)', async () => { const isMutatingArray: Array = [] const queryClient = new QueryClient() const mutationOpts1 = mutationOptions({ mutationKey: ['key'], mutationFn: () => sleep(50).then(() => 'data1'), }) const mutationOpts2 = mutationOptions({ mutationFn: () => sleep(50).then(() => 'data2'), }) function IsMutating() { const isMutating = useIsMutating({ mutationKey: mutationOpts1.mutationKey, }) isMutatingArray.push(isMutating) return null } function Mutation() { const { mutate: mutate1 } = useMutation(mutationOpts1) const { mutate: mutate2 } = useMutation(mutationOpts2) return (
) } function Page() { return (
) } const rendered = renderWithClient(queryClient, ) fireEvent.click(rendered.getByRole('button', { name: /mutate1/i })) fireEvent.click(rendered.getByRole('button', { name: /mutate2/i })) expect(isMutatingArray[0]).toEqual(0) await vi.advanceTimersByTimeAsync(0) expect(isMutatingArray[1]).toEqual(1) await vi.advanceTimersByTimeAsync(51) expect(isMutatingArray[2]).toEqual(0) expect(isMutatingArray[isMutatingArray.length - 1]).toEqual(0) }) it('should return the number of fetching mutations when used with queryClient.isMutating (with mutationKey in mutationOptions)', async () => { const isMutatingArray: Array = [] const queryClient = new QueryClient() const mutationOpts = mutationOptions({ mutationKey: ['mutation'], mutationFn: () => sleep(500).then(() => 'data'), }) function Mutation() { const isMutating = queryClient.isMutating(mutationOpts) const { mutate } = useMutation(mutationOpts) isMutatingArray.push(isMutating) return (
) } const rendered = renderWithClient(queryClient, ) fireEvent.click(rendered.getByRole('button', { name: /mutate/i })) expect(isMutatingArray[0]).toEqual(0) await vi.advanceTimersByTimeAsync(0) expect(isMutatingArray[1]).toEqual(1) await vi.advanceTimersByTimeAsync(501) expect(isMutatingArray[2]).toEqual(0) expect(isMutatingArray[isMutatingArray.length - 1]).toEqual(0) }) it('should return the number of fetching mutations when used with queryClient.isMutating (without mutationKey in mutationOptions)', async () => { const isMutatingArray: Array = [] const queryClient = new QueryClient() const mutationOpts = mutationOptions({ mutationFn: () => sleep(500).then(() => 'data'), }) function Mutation() { const isMutating = queryClient.isMutating() const { mutate } = useMutation(mutationOpts) isMutatingArray.push(isMutating) return (
) } const rendered = renderWithClient(queryClient, ) fireEvent.click(rendered.getByRole('button', { name: /mutate/i })) expect(isMutatingArray[0]).toEqual(0) await vi.advanceTimersByTimeAsync(0) expect(isMutatingArray[1]).toEqual(1) await vi.advanceTimersByTimeAsync(501) expect(isMutatingArray[2]).toEqual(0) expect(isMutatingArray[isMutatingArray.length - 1]).toEqual(0) }) it('should return the number of fetching mutations when used with queryClient.isMutating', async () => { const isMutatingArray: Array = [] const queryClient = new QueryClient() const mutationOpts1 = mutationOptions({ mutationKey: ['mutation'], mutationFn: () => sleep(500).then(() => 'data1'), }) const mutationOpts2 = mutationOptions({ mutationFn: () => sleep(500).then(() => 'data2'), }) function Mutation() { const isMutating = queryClient.isMutating() const { mutate: mutate1 } = useMutation(mutationOpts1) const { mutate: mutate2 } = useMutation(mutationOpts2) isMutatingArray.push(isMutating) return (
) } const rendered = renderWithClient(queryClient, ) fireEvent.click(rendered.getByRole('button', { name: /mutate1/i })) fireEvent.click(rendered.getByRole('button', { name: /mutate2/i })) expect(isMutatingArray[0]).toEqual(0) await vi.advanceTimersByTimeAsync(0) expect(isMutatingArray[1]).toEqual(2) await vi.advanceTimersByTimeAsync(501) expect(isMutatingArray[2]).toEqual(0) expect(isMutatingArray[isMutatingArray.length - 1]).toEqual(0) }) it('should return the number of fetching mutations when used with queryClient.isMutating (filter mutationOpt1.mutationKey)', async () => { const isMutatingArray: Array = [] const queryClient = new QueryClient() const mutationOpts1 = mutationOptions({ mutationKey: ['mutation'], mutationFn: () => sleep(500).then(() => 'data1'), }) const mutationOpts2 = mutationOptions({ mutationFn: () => sleep(500).then(() => 'data2'), }) function Mutation() { const isMutating = queryClient.isMutating({ mutationKey: mutationOpts1.mutationKey, }) const { mutate: mutate1 } = useMutation(mutationOpts1) const { mutate: mutate2 } = useMutation(mutationOpts2) isMutatingArray.push(isMutating) return (
) } const rendered = renderWithClient(queryClient, ) fireEvent.click(rendered.getByRole('button', { name: /mutate1/i })) fireEvent.click(rendered.getByRole('button', { name: /mutate2/i })) expect(isMutatingArray[0]).toEqual(0) await vi.advanceTimersByTimeAsync(0) expect(isMutatingArray[1]).toEqual(1) await vi.advanceTimersByTimeAsync(501) expect(isMutatingArray[2]).toEqual(0) expect(isMutatingArray[isMutatingArray.length - 1]).toEqual(0) }) it('should return the number of fetching mutations when used with useMutationState (with mutationKey in mutationOptions)', async () => { const mutationStateArray: Array< MutationState > = [] const queryClient = new QueryClient() const mutationOpts = mutationOptions({ mutationKey: ['mutation'], mutationFn: () => sleep(10).then(() => 'data'), }) function Mutation() { const { mutate } = useMutation(mutationOpts) const data = useMutationState({ filters: { mutationKey: mutationOpts.mutationKey, status: 'success' }, }) mutationStateArray.push(...data) return (
) } const rendered = renderWithClient(queryClient, ) expect(mutationStateArray.length).toEqual(0) fireEvent.click(rendered.getByRole('button', { name: /mutate/i })) await vi.advanceTimersByTimeAsync(11) expect(mutationStateArray.length).toEqual(1) expect(mutationStateArray[0]?.data).toEqual('data') }) it('should return the number of fetching mutations when used with useMutationState (without mutationKey in mutationOptions)', async () => { const mutationStateArray: Array< MutationState > = [] const queryClient = new QueryClient() const mutationOpts = mutationOptions({ mutationFn: () => sleep(10).then(() => 'data'), }) function Mutation() { const { mutate } = useMutation(mutationOpts) const data = useMutationState({ filters: { status: 'success' }, }) mutationStateArray.push(...data) return (
) } const rendered = renderWithClient(queryClient, ) expect(mutationStateArray.length).toEqual(0) fireEvent.click(rendered.getByRole('button', { name: /mutate/i })) await vi.advanceTimersByTimeAsync(11) expect(mutationStateArray.length).toEqual(1) expect(mutationStateArray[0]?.data).toEqual('data') }) it('should return the number of fetching mutations when used with useMutationState', async () => { const mutationStateArray: Array< MutationState > = [] const queryClient = new QueryClient() const mutationOpts1 = mutationOptions({ mutationKey: ['mutation'], mutationFn: () => sleep(10).then(() => 'data1'), }) const mutationOpts2 = mutationOptions({ mutationFn: () => sleep(10).then(() => 'data2'), }) function Mutation() { const { mutate: mutate1 } = useMutation(mutationOpts1) const { mutate: mutate2 } = useMutation(mutationOpts2) const data = useMutationState({ filters: { status: 'success' }, }) mutationStateArray.push(...data) return (
) } const rendered = renderWithClient(queryClient, ) expect(mutationStateArray.length).toEqual(0) fireEvent.click(rendered.getByRole('button', { name: /mutate1/i })) fireEvent.click(rendered.getByRole('button', { name: /mutate2/i })) await vi.advanceTimersByTimeAsync(11) expect(mutationStateArray.length).toEqual(2) expect(mutationStateArray[0]?.data).toEqual('data1') expect(mutationStateArray[1]?.data).toEqual('data2') }) it('should return the number of fetching mutations when used with useMutationState (filter mutationOpt1.mutationKey)', async () => { const mutationStateArray: Array< MutationState > = [] const queryClient = new QueryClient() const mutationOpts1 = mutationOptions({ mutationKey: ['mutation'], mutationFn: () => sleep(10).then(() => 'data1'), }) const mutationOpts2 = mutationOptions({ mutationFn: () => sleep(10).then(() => 'data2'), }) function Mutation() { const { mutate: mutate1 } = useMutation(mutationOpts1) const { mutate: mutate2 } = useMutation(mutationOpts2) const data = useMutationState({ filters: { mutationKey: mutationOpts1.mutationKey, status: 'success' }, }) mutationStateArray.push(...data) return (
) } const rendered = renderWithClient(queryClient, ) expect(mutationStateArray.length).toEqual(0) fireEvent.click(rendered.getByRole('button', { name: /mutate1/i })) fireEvent.click(rendered.getByRole('button', { name: /mutate2/i })) await vi.advanceTimersByTimeAsync(11) expect(mutationStateArray.length).toEqual(1) expect(mutationStateArray[0]?.data).toEqual('data1') expect(mutationStateArray[1]).toBeFalsy() }) }) ================================================ FILE: packages/react-query/src/__tests__/QueryClientProvider.test.tsx ================================================ import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest' import { render } from '@testing-library/react' import { queryKey, sleep } from '@tanstack/query-test-utils' import { QueryCache, QueryClient, QueryClientProvider, useQuery, useQueryClient, } from '..' describe('QueryClientProvider', () => { beforeEach(() => { vi.useFakeTimers() }) afterEach(() => { vi.useRealTimers() }) test('sets a specific cache for all queries to use', async () => { const key = queryKey() const queryCache = new QueryCache() const queryClient = new QueryClient({ queryCache }) function Page() { const { data } = useQuery({ queryKey: key, queryFn: () => sleep(10).then(() => 'test'), }) return (

{data}

) } const rendered = render( , ) await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('test')).toBeInTheDocument() expect(queryCache.find({ queryKey: key })).toBeDefined() }) test('allows multiple caches to be partitioned', async () => { const key1 = queryKey() const key2 = queryKey() const queryCache1 = new QueryCache() const queryCache2 = new QueryCache() const queryClient1 = new QueryClient({ queryCache: queryCache1 }) const queryClient2 = new QueryClient({ queryCache: queryCache2 }) function Page1() { const { data } = useQuery({ queryKey: key1, queryFn: () => sleep(10).then(() => 'test1'), }) return (

{data}

) } function Page2() { const { data } = useQuery({ queryKey: key2, queryFn: () => sleep(10).then(() => 'test2'), }) return (

{data}

) } const rendered = render( <> , ) await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('test1')).toBeInTheDocument() expect(rendered.getByText('test2')).toBeInTheDocument() expect(queryCache1.find({ queryKey: key1 })).toBeDefined() expect(queryCache1.find({ queryKey: key2 })).not.toBeDefined() expect(queryCache2.find({ queryKey: key1 })).not.toBeDefined() expect(queryCache2.find({ queryKey: key2 })).toBeDefined() }) test("uses defaultOptions for queries when they don't provide their own config", async () => { const key = queryKey() const queryCache = new QueryCache() const queryClient = new QueryClient({ queryCache, defaultOptions: { queries: { gcTime: Infinity, }, }, }) function Page() { const { data } = useQuery({ queryKey: key, queryFn: () => sleep(10).then(() => 'test'), }) return (

{data}

) } const rendered = render( , ) await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('test')).toBeInTheDocument() expect(queryCache.find({ queryKey: key })).toBeDefined() expect(queryCache.find({ queryKey: key })?.options.gcTime).toBe(Infinity) }) describe('useQueryClient', () => { test('should throw an error if no query client has been set', () => { const consoleMock = vi .spyOn(console, 'error') .mockImplementation(() => undefined) function Page() { useQueryClient() return null } expect(() => render()).toThrow( 'No QueryClient set, use QueryClientProvider to set one', ) consoleMock.mockRestore() }) }) }) ================================================ FILE: packages/react-query/src/__tests__/queryOptions.test-d.tsx ================================================ import { assertType, describe, expectTypeOf, it } from 'vitest' import { QueriesObserver, QueryClient, dataTagSymbol, skipToken, } from '@tanstack/query-core' import { queryOptions } from '../queryOptions' import { useQuery } from '../useQuery' import { useQueries } from '../useQueries' import { useSuspenseQuery } from '../useSuspenseQuery' import type { AnyUseQueryOptions } from '../types' import type { DataTag, InitialDataFunction, QueryObserverResult, } from '@tanstack/query-core' describe('queryOptions', () => { it('should not allow excess properties', () => { assertType( queryOptions({ queryKey: ['key'], queryFn: () => Promise.resolve(5), // @ts-expect-error this is a good error, because stallTime does not exist! stallTime: 1000, }), ) }) it('should infer types for callbacks', () => { queryOptions({ queryKey: ['key'], queryFn: () => Promise.resolve(5), staleTime: 1000, select: (data) => { expectTypeOf(data).toEqualTypeOf() }, }) }) it('should work when passed to useQuery', () => { const options = queryOptions({ queryKey: ['key'], queryFn: () => Promise.resolve(5), }) const { data } = useQuery(options) expectTypeOf(data).toEqualTypeOf() }) it('should work when passed to useSuspenseQuery', () => { const options = queryOptions({ queryKey: ['key'], queryFn: () => Promise.resolve(5), }) const { data } = useSuspenseQuery(options) expectTypeOf(data).toEqualTypeOf() }) it('should work when passed to fetchQuery', async () => { const options = queryOptions({ queryKey: ['key'], queryFn: () => Promise.resolve(5), }) const data = await new QueryClient().fetchQuery(options) expectTypeOf(data).toEqualTypeOf() }) it('should work when passed to useQueries', () => { const options = queryOptions({ queryKey: ['key'], queryFn: () => Promise.resolve(5), }) const [{ data }] = useQueries({ queries: [options], }) expectTypeOf(data).toEqualTypeOf() }) it('should tag the queryKey with the result type of the QueryFn', () => { const { queryKey } = queryOptions({ queryKey: ['key'], queryFn: () => Promise.resolve(5), }) expectTypeOf(queryKey[dataTagSymbol]).toEqualTypeOf() }) it('should tag the queryKey even if no promise is returned', () => { const { queryKey } = queryOptions({ queryKey: ['key'], queryFn: () => 5, }) expectTypeOf(queryKey[dataTagSymbol]).toEqualTypeOf() }) it('should tag the queryKey with unknown if there is no queryFn', () => { const { queryKey } = queryOptions({ queryKey: ['key'], }) expectTypeOf(queryKey[dataTagSymbol]).toEqualTypeOf() }) it('should tag the queryKey with the result type of the QueryFn if select is used', () => { const { queryKey } = queryOptions({ queryKey: ['key'], queryFn: () => Promise.resolve(5), select: (data) => data.toString(), }) expectTypeOf(queryKey[dataTagSymbol]).toEqualTypeOf() }) it('should return the proper type when passed to getQueryData', () => { const { queryKey } = queryOptions({ queryKey: ['key'], queryFn: () => Promise.resolve(5), }) const queryClient = new QueryClient() const data = queryClient.getQueryData(queryKey) expectTypeOf(data).toEqualTypeOf() }) it('should return the proper type when passed to getQueryState', () => { const { queryKey } = queryOptions({ queryKey: ['key'], queryFn: () => Promise.resolve(5), }) const queryClient = new QueryClient() const state = queryClient.getQueryState(queryKey) expectTypeOf(state?.data).toEqualTypeOf() }) it('should properly type updaterFn when passed to setQueryData', () => { const { queryKey } = queryOptions({ queryKey: ['key'], queryFn: () => Promise.resolve(5), }) const queryClient = new QueryClient() const data = queryClient.setQueryData(queryKey, (prev) => { expectTypeOf(prev).toEqualTypeOf() return prev }) expectTypeOf(data).toEqualTypeOf() }) it('should properly type value when passed to setQueryData', () => { const { queryKey } = queryOptions({ queryKey: ['key'], queryFn: () => Promise.resolve(5), }) const queryClient = new QueryClient() // @ts-expect-error value should be a number queryClient.setQueryData(queryKey, '5') // @ts-expect-error value should be a number queryClient.setQueryData(queryKey, () => '5') const data = queryClient.setQueryData(queryKey, 5) expectTypeOf(data).toEqualTypeOf() }) it('should infer even if there is a conditional skipToken', () => { const options = queryOptions({ queryKey: ['key'], queryFn: Math.random() > 0.5 ? skipToken : () => Promise.resolve(5), }) const queryClient = new QueryClient() const data = queryClient.getQueryData(options.queryKey) expectTypeOf(data).toEqualTypeOf() }) it('should infer to unknown if we disable a query with just a skipToken', () => { const options = queryOptions({ queryKey: ['key'], queryFn: skipToken, }) const queryClient = new QueryClient() const data = queryClient.getQueryData(options.queryKey) expectTypeOf(data).toEqualTypeOf() }) it('should throw a type error when using queryFn with skipToken in a suspense query', () => { const options = queryOptions({ queryKey: ['key'], queryFn: Math.random() > 0.5 ? skipToken : () => Promise.resolve(5), }) // @ts-expect-error TS2345 const { data } = useSuspenseQuery(options) expectTypeOf(data).toEqualTypeOf() }) it('should return the proper type when passed to QueriesObserver', () => { const options = queryOptions({ queryKey: ['key'], queryFn: () => Promise.resolve(5), }) const queryClient = new QueryClient() const queriesObserver = new QueriesObserver(queryClient, [options]) expectTypeOf(queriesObserver).toEqualTypeOf< QueriesObserver> >() }) it('should allow undefined response in initialData', () => { assertType((id: string | null) => queryOptions({ queryKey: ['todo', id], queryFn: () => Promise.resolve({ id: '1', title: 'Do Laundry', }), initialData: () => !id ? undefined : { id, title: 'Initial Data', }, }), ) }) it('should allow optional initialData object', () => { const testFn = (id?: string) => { const options = queryOptions({ queryKey: ['test'], queryFn: () => Promise.resolve('something string'), initialData: id ? 'initial string' : undefined, }) expectTypeOf(options.initialData).toMatchTypeOf< InitialDataFunction | string | undefined >() } testFn('id') testFn() }) it('should be passable to UseQueryOptions', () => { function somethingWithQueryOptions( options: TQueryOpts, ) { return options.queryKey } const options = queryOptions({ queryKey: ['key'], queryFn: () => Promise.resolve(1), }) assertType(somethingWithQueryOptions(options)) }) it('should return a custom query key type', () => { type MyQueryKey = [Array, { type: 'foo' }] const options = queryOptions({ queryKey: [['key'], { type: 'foo' }] as MyQueryKey, queryFn: () => Promise.resolve(1), }) expectTypeOf(options.queryKey).toEqualTypeOf< DataTag >() }) it('should return a custom query key type with datatag', () => { type MyQueryKey = DataTag< [Array, { type: 'foo' }], number, Error & { myMessage: string } > const options = queryOptions({ queryKey: [['key'], { type: 'foo' }] as MyQueryKey, queryFn: () => Promise.resolve(1), }) expectTypeOf(options.queryKey).toEqualTypeOf< DataTag >() }) }) ================================================ FILE: packages/react-query/src/__tests__/queryOptions.test.tsx ================================================ import { describe, expect, it } from 'vitest' import { queryOptions } from '../queryOptions' import type { UseQueryOptions } from '../types' describe('queryOptions', () => { it('should return the object received as a parameter without any modification.', () => { const object: UseQueryOptions = { queryKey: ['key'], queryFn: () => Promise.resolve(5), } as const expect(queryOptions(object)).toStrictEqual(object) }) }) ================================================ FILE: packages/react-query/src/__tests__/QueryResetErrorBoundary.test.tsx ================================================ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { act, fireEvent } from '@testing-library/react' import { ErrorBoundary } from 'react-error-boundary' import * as React from 'react' import { queryKey, sleep } from '@tanstack/query-test-utils' import { QueryCache, QueryClient, QueryErrorResetBoundary, useQueries, useQuery, useSuspenseQueries, useSuspenseQuery, } from '..' import { renderWithClient } from './utils' describe('QueryErrorResetBoundary', () => { beforeEach(() => { vi.useFakeTimers() }) afterEach(() => { vi.useRealTimers() }) const queryCache = new QueryCache() const queryClient = new QueryClient({ queryCache }) describe('useQuery', () => { it('should retry fetch if the reset error boundary has been reset', async () => { const consoleMock = vi .spyOn(console, 'error') .mockImplementation(() => undefined) const key = queryKey() let succeed = false function Page() { const { data } = useQuery({ queryKey: key, queryFn: () => sleep(10).then(() => { if (!succeed) throw new Error('Error') return 'data' }), retry: false, throwOnError: true, }) return
{data}
} const rendered = renderWithClient( queryClient, {({ reset }) => ( (
error boundary
)} >
)}
, ) await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('error boundary')).toBeInTheDocument() expect(rendered.getByText('retry')).toBeInTheDocument() succeed = true fireEvent.click(rendered.getByText('retry')) await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('data')).toBeInTheDocument() consoleMock.mockRestore() }) it('should not throw error if query is disabled', async () => { const consoleMock = vi .spyOn(console, 'error') .mockImplementation(() => undefined) const key = queryKey() let succeed = false function Page() { const { data, status } = useQuery({ queryKey: key, queryFn: () => sleep(10).then(() => { if (!succeed) throw new Error('Error') return 'data' }), retry: false, enabled: !succeed, throwOnError: true, }) return (
status: {status}
{data}
) } const rendered = renderWithClient( queryClient, {({ reset }) => ( (
error boundary
)} >
)}
, ) await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('error boundary')).toBeInTheDocument() expect(rendered.getByText('retry')).toBeInTheDocument() succeed = true fireEvent.click(rendered.getByText('retry')) await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('status: error')).toBeInTheDocument() consoleMock.mockRestore() }) it('should not throw error if query is disabled, and refetch if query becomes enabled again', async () => { const consoleMock = vi .spyOn(console, 'error') .mockImplementation(() => undefined) const key = queryKey() let succeed = false function Page() { const [enabled, setEnabled] = React.useState(false) const { data } = useQuery({ queryKey: key, queryFn: () => sleep(10).then(() => { if (!succeed) throw new Error('Error') return 'data' }), retry: false, enabled, throwOnError: true, }) React.useEffect(() => { setEnabled(true) }, []) return
{data}
} const rendered = renderWithClient( queryClient, {({ reset }) => ( (
error boundary
)} >
)}
, ) await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('error boundary')).toBeInTheDocument() expect(rendered.getByText('retry')).toBeInTheDocument() succeed = true fireEvent.click(rendered.getByText('retry')) await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('data')).toBeInTheDocument() consoleMock.mockRestore() }) it('should throw error if query is disabled and manually refetch', async () => { const consoleMock = vi .spyOn(console, 'error') .mockImplementation(() => undefined) const key = queryKey() function Page() { const { data, refetch, status, fetchStatus } = useQuery({ queryKey: key, queryFn: () => sleep(10).then(() => Promise.reject(new Error('Error'))), retry: false, enabled: false, throwOnError: true, }) return (
status: {status}, fetchStatus: {fetchStatus}
{data}
) } const rendered = renderWithClient( queryClient, {({ reset }) => ( (
error boundary
)} >
)}
, ) expect( rendered.getByText('status: pending, fetchStatus: idle'), ).toBeInTheDocument() await vi.advanceTimersByTimeAsync(11) expect( rendered.getByText('status: pending, fetchStatus: idle'), ).toBeInTheDocument() fireEvent.click(rendered.getByRole('button', { name: /refetch/i })) await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('error boundary')).toBeInTheDocument() consoleMock.mockRestore() }) it('should not retry fetch if the reset error boundary has not been reset', async () => { const consoleMock = vi .spyOn(console, 'error') .mockImplementation(() => undefined) const key = queryKey() let succeed = false function Page() { const { data } = useQuery({ queryKey: key, queryFn: () => sleep(10).then(() => { if (!succeed) throw new Error('Error') return 'data' }), retry: false, throwOnError: true, }) return
{data}
} const rendered = renderWithClient( queryClient, {() => ( (
error boundary
)} >
)}
, ) await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('error boundary')).toBeInTheDocument() expect(rendered.getByText('retry')).toBeInTheDocument() succeed = true fireEvent.click(rendered.getByText('retry')) await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('error boundary')).toBeInTheDocument() consoleMock.mockRestore() }) it('should retry fetch if the reset error boundary has been reset and the query contains data from a previous fetch', async () => { const consoleMock = vi .spyOn(console, 'error') .mockImplementation(() => undefined) const key = queryKey() let succeed = false function Page() { const { data } = useQuery({ queryKey: key, queryFn: () => sleep(10).then(() => { if (!succeed) throw new Error('Error') return 'data' }), retry: false, throwOnError: true, initialData: 'initial', }) return
{data}
} const rendered = renderWithClient( queryClient, {({ reset }) => ( (
error boundary
)} >
)}
, ) expect(rendered.getByText('initial')).toBeInTheDocument() await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('error boundary')).toBeInTheDocument() expect(rendered.getByText('retry')).toBeInTheDocument() succeed = true fireEvent.click(rendered.getByText('retry')) await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('data')).toBeInTheDocument() consoleMock.mockRestore() }) it('should not retry fetch if the reset error boundary has not been reset after a previous reset', async () => { const consoleMock = vi .spyOn(console, 'error') .mockImplementation(() => undefined) const key = queryKey() let succeed = false let shouldReset = false function Page() { const { data } = useQuery({ queryKey: key, queryFn: () => sleep(10).then(() => { if (!succeed) throw new Error('Error') return 'data' }), retry: false, throwOnError: true, }) return
{data}
} const rendered = renderWithClient( queryClient, {({ reset }) => ( { if (shouldReset) { reset() } }} fallbackRender={({ resetErrorBoundary }) => (
error boundary
)} >
)}
, ) await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('error boundary')).toBeInTheDocument() expect(rendered.getByText('retry')).toBeInTheDocument() succeed = false shouldReset = true await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('error boundary')).toBeInTheDocument() expect(rendered.getByText('retry')).toBeInTheDocument() succeed = true shouldReset = false fireEvent.click(rendered.getByText('retry')) await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('error boundary')).toBeInTheDocument() succeed = true shouldReset = true fireEvent.click(rendered.getByText('retry')) await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('data')).toBeInTheDocument() consoleMock.mockRestore() }) it('should throw again on error after the reset error boundary has been reset', async () => { const consoleMock = vi .spyOn(console, 'error') .mockImplementation(() => undefined) const key = queryKey() let fetchCount = 0 function Page() { const { data } = useQuery({ queryKey: key, queryFn: () => sleep(10).then(() => { fetchCount++ throw new Error('Error') }), retry: false, throwOnError: true, }) return
{data}
} const rendered = renderWithClient( queryClient, {({ reset }) => ( (
error boundary
)} >
)}
, ) await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('error boundary')).toBeInTheDocument() expect(rendered.getByText('retry')).toBeInTheDocument() fireEvent.click(rendered.getByText('retry')) await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('error boundary')).toBeInTheDocument() expect(rendered.getByText('retry')).toBeInTheDocument() fireEvent.click(rendered.getByText('retry')) await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('error boundary')).toBeInTheDocument() expect(fetchCount).toBe(3) consoleMock.mockRestore() }) it('should never render the component while the query is in error state', async () => { const consoleMock = vi .spyOn(console, 'error') .mockImplementation(() => undefined) const key = queryKey() let fetchCount = 0 let renders = 0 function Page() { const { data } = useSuspenseQuery({ queryKey: key, queryFn: () => sleep(10).then(() => { fetchCount++ if (fetchCount > 2) return 'data' throw new Error('Error') }), retry: false, }) renders++ return
{data}
} const rendered = renderWithClient( queryClient, {({ reset }) => ( (
error boundary
)} > loading}>
)}
, ) expect(rendered.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(10)) expect(rendered.getByText('error boundary')).toBeInTheDocument() expect(rendered.getByText('retry')).toBeInTheDocument() fireEvent.click(rendered.getByText('retry')) expect(rendered.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(10)) expect(rendered.getByText('error boundary')).toBeInTheDocument() expect(rendered.getByText('retry')).toBeInTheDocument() fireEvent.click(rendered.getByText('retry')) expect(rendered.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(10)) expect(rendered.getByText('data')).toBeInTheDocument() expect(fetchCount).toBe(3) expect(renders).toBe(1) consoleMock.mockRestore() }) it('should render children', () => { const consoleMock = vi .spyOn(console, 'error') .mockImplementation(() => undefined) function Page() { return (
page
) } const rendered = renderWithClient( queryClient, , ) expect(rendered.queryByText('page')).not.toBeNull() consoleMock.mockRestore() }) it('should show error boundary when using tracked queries even though we do not track the error field', async () => { const consoleMock = vi .spyOn(console, 'error') .mockImplementation(() => undefined) const key = queryKey() let succeed = false function Page() { const { data } = useQuery({ queryKey: key, queryFn: () => sleep(10).then(() => { if (!succeed) throw new Error('Error') return 'data' }), retry: false, throwOnError: true, }) return
{data}
} const rendered = renderWithClient( queryClient, {({ reset }) => ( (
error boundary
)} >
)}
, ) await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('error boundary')).toBeInTheDocument() expect(rendered.getByText('retry')).toBeInTheDocument() succeed = true fireEvent.click(rendered.getByText('retry')) await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('data')).toBeInTheDocument() consoleMock.mockRestore() }) }) describe('useQueries', () => { it('should retry fetch if the reset error boundary has been reset', async () => { const consoleMock = vi .spyOn(console, 'error') .mockImplementation(() => undefined) const key = queryKey() let succeed = false function Page() { const [{ data }] = useQueries({ queries: [ { queryKey: key, queryFn: () => sleep(10).then(() => { if (!succeed) throw new Error('Error') return 'data' }), retry: false, throwOnError: true, retryOnMount: true, }, ], }) return
{data}
} const rendered = renderWithClient( queryClient, {({ reset }) => ( (
error boundary
)} >
)}
, ) await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('error boundary')).toBeInTheDocument() expect(rendered.getByText('retry')).toBeInTheDocument() succeed = true fireEvent.click(rendered.getByText('retry')) await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('data')).toBeInTheDocument() consoleMock.mockRestore() }) it('with suspense should retry fetch if the reset error boundary has been reset', async () => { const key = queryKey() const consoleMock = vi .spyOn(console, 'error') .mockImplementation(() => undefined) let succeed = false function Page() { const [{ data }] = useSuspenseQueries({ queries: [ { queryKey: key, queryFn: () => sleep(10).then(() => { if (!succeed) throw new Error('Error') return 'data' }), retry: false, retryOnMount: true, }, ], }) return
{data}
} const rendered = renderWithClient( queryClient, {({ reset }) => ( (
error boundary
)} >
)}
, ) expect(rendered.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(10)) expect(rendered.getByText('error boundary')).toBeInTheDocument() expect(rendered.getByText('retry')).toBeInTheDocument() succeed = true fireEvent.click(rendered.getByText('retry')) expect(rendered.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(10)) expect(rendered.getByText('data')).toBeInTheDocument() consoleMock.mockRestore() }) }) }) ================================================ FILE: packages/react-query/src/__tests__/ssr-hydration.test.tsx ================================================ import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest' import { hydrateRoot } from 'react-dom/client' import { act } from 'react' import * as ReactDOMServer from 'react-dom/server' import { QueryCache, QueryClient, QueryClientProvider, dehydrate, hydrate, useQuery, } from '..' import { setIsServer } from './utils' const ReactHydrate = (element: React.ReactElement, container: Element) => { let root: any act(() => { root = hydrateRoot(container, element) }) return () => { root.unmount() } } async function fetchData(value: TData, ms?: number): Promise { await vi.advanceTimersByTimeAsync(ms || 1) return value } function PrintStateComponent({ componentName, result }: any): any { return `${componentName} - status:${result.status} fetching:${result.isFetching} data:${result.data}` } describe('Server side rendering with de/rehydration', () => { let previousIsReactActEnvironment: unknown beforeAll(() => { // @ts-expect-error we expect IS_REACT_ACT_ENVIRONMENT to exist previousIsReactActEnvironment = globalThis.IS_REACT_ACT_ENVIRONMENT = true vi.useFakeTimers() }) afterAll(() => { // @ts-expect-error we expect IS_REACT_ACT_ENVIRONMENT to exist globalThis.IS_REACT_ACT_ENVIRONMENT = previousIsReactActEnvironment vi.useRealTimers() }) it('should not mismatch on success', async () => { const consoleMock = vi.spyOn(console, 'error') consoleMock.mockImplementation(() => undefined) const fetchDataSuccess = vi.fn(fetchData) // -- Shared part -- function SuccessComponent() { const result = useQuery({ queryKey: ['success'], queryFn: () => fetchDataSuccess('success!'), }) return ( ) } // -- Server part -- setIsServer(true) const prefetchCache = new QueryCache() const prefetchClient = new QueryClient({ queryCache: prefetchCache, }) await prefetchClient.prefetchQuery({ queryKey: ['success'], queryFn: () => fetchDataSuccess('success'), }) const dehydratedStateServer = dehydrate(prefetchClient) const renderCache = new QueryCache() const renderClient = new QueryClient({ queryCache: renderCache, }) hydrate(renderClient, dehydratedStateServer) const markup = ReactDOMServer.renderToString( , ) const stringifiedState = JSON.stringify(dehydratedStateServer) renderClient.clear() setIsServer(false) const expectedMarkup = 'SuccessComponent - status:success fetching:true data:success' expect(markup).toBe(expectedMarkup) expect(fetchDataSuccess).toHaveBeenCalledTimes(1) // -- Client part -- const el = document.createElement('div') el.innerHTML = markup const queryCache = new QueryCache() const queryClient = new QueryClient({ queryCache }) hydrate(queryClient, JSON.parse(stringifiedState)) const unmount = ReactHydrate( , el, ) // Check that we have no React hydration mismatches expect(consoleMock).toHaveBeenCalledTimes(0) expect(fetchDataSuccess).toHaveBeenCalledTimes(2) expect(el.innerHTML).toBe(expectedMarkup) unmount() queryClient.clear() consoleMock.mockRestore() }) it('should not mismatch on error', async () => { const consoleMock = vi.spyOn(console, 'error') consoleMock.mockImplementation(() => undefined) const fetchDataError = vi.fn(() => { throw new Error('fetchDataError') }) // -- Shared part -- function ErrorComponent() { const result = useQuery({ queryKey: ['error'], queryFn: () => fetchDataError(), retry: false, }) return ( ) } // -- Server part -- setIsServer(true) const prefetchCache = new QueryCache() const prefetchClient = new QueryClient({ queryCache: prefetchCache, }) await prefetchClient.prefetchQuery({ queryKey: ['error'], queryFn: () => fetchDataError(), }) const dehydratedStateServer = dehydrate(prefetchClient) const renderCache = new QueryCache() const renderClient = new QueryClient({ queryCache: renderCache, }) hydrate(renderClient, dehydratedStateServer) const markup = ReactDOMServer.renderToString( , ) const stringifiedState = JSON.stringify(dehydratedStateServer) renderClient.clear() setIsServer(false) const expectedMarkup = 'ErrorComponent - status:pending fetching:true data:undefined' expect(markup).toBe(expectedMarkup) // -- Client part -- const el = document.createElement('div') el.innerHTML = markup const queryCache = new QueryCache() const queryClient = new QueryClient({ queryCache }) hydrate(queryClient, JSON.parse(stringifiedState)) const unmount = ReactHydrate( , el, ) expect(consoleMock).toHaveBeenCalledTimes(0) expect(fetchDataError).toHaveBeenCalledTimes(2) expect(el.innerHTML).toBe(expectedMarkup) await vi.advanceTimersByTimeAsync(50) expect(fetchDataError).toHaveBeenCalledTimes(2) expect(el.innerHTML).toBe( 'ErrorComponent - status:error fetching:false data:undefined', ) unmount() queryClient.clear() consoleMock.mockRestore() }) it('should not mismatch on queries that were not prefetched', async () => { const consoleMock = vi.spyOn(console, 'error') consoleMock.mockImplementation(() => undefined) const fetchDataSuccess = vi.fn(fetchData) // -- Shared part -- function SuccessComponent() { const result = useQuery({ queryKey: ['success'], queryFn: () => fetchDataSuccess('success!'), }) return ( ) } // -- Server part -- setIsServer(true) const prefetchClient = new QueryClient() const dehydratedStateServer = dehydrate(prefetchClient) const renderClient = new QueryClient() hydrate(renderClient, dehydratedStateServer) const markup = ReactDOMServer.renderToString( , ) const stringifiedState = JSON.stringify(dehydratedStateServer) renderClient.clear() setIsServer(false) const expectedMarkup = 'SuccessComponent - status:pending fetching:true data:undefined' expect(markup).toBe(expectedMarkup) // -- Client part -- const el = document.createElement('div') el.innerHTML = markup const queryCache = new QueryCache() const queryClient = new QueryClient({ queryCache }) hydrate(queryClient, JSON.parse(stringifiedState)) const unmount = ReactHydrate( , el, ) // Check that we have no React hydration mismatches expect(consoleMock).toHaveBeenCalledTimes(0) expect(fetchDataSuccess).toHaveBeenCalledTimes(1) expect(el.innerHTML).toBe(expectedMarkup) await vi.advanceTimersByTimeAsync(50) expect(fetchDataSuccess).toHaveBeenCalledTimes(1) expect(el.innerHTML).toBe( 'SuccessComponent - status:success fetching:false data:success!', ) unmount() queryClient.clear() consoleMock.mockRestore() }) }) ================================================ FILE: packages/react-query/src/__tests__/ssr.test.tsx ================================================ import * as React from 'react' import { renderToString } from 'react-dom/server' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { queryKey, sleep } from '@tanstack/query-test-utils' import { QueryCache, QueryClient, QueryClientProvider, useInfiniteQuery, useQuery, } from '..' import { setIsServer } from './utils' describe('Server Side Rendering', () => { setIsServer(true) let queryCache: QueryCache let queryClient: QueryClient beforeEach(() => { vi.useFakeTimers() queryCache = new QueryCache() queryClient = new QueryClient({ queryCache }) }) afterEach(() => { vi.useRealTimers() }) it('should not trigger fetch', () => { const key = queryKey() const queryFn = vi.fn(() => sleep(10).then(() => 'data')) function Page() { const query = useQuery({ queryKey: key, queryFn }) const content = `status ${query.status}` return (
{content}
) } const markup = renderToString( , ) expect(markup).toContain('status pending') expect(queryFn).toHaveBeenCalledTimes(0) queryCache.clear() }) it('should add prefetched data to cache', async () => { const key = queryKey() const promise = queryClient.fetchQuery({ queryKey: key, queryFn: () => sleep(10).then(() => 'data'), }) await vi.advanceTimersByTimeAsync(10) const data = await promise expect(data).toBe('data') expect(queryCache.find({ queryKey: key })?.state.data).toBe('data') queryCache.clear() }) it('should return existing data from the cache', async () => { const key = queryKey() const queryFn = vi.fn(() => sleep(10).then(() => 'data')) function Page() { const query = useQuery({ queryKey: key, queryFn }) const content = `status ${query.status}` return (
{content}
) } queryClient.prefetchQuery({ queryKey: key, queryFn }) await vi.advanceTimersByTimeAsync(10) const markup = renderToString( , ) expect(markup).toContain('status success') expect(queryFn).toHaveBeenCalledTimes(1) queryCache.clear() }) it('should add initialData to the cache', () => { const key = queryKey() function Page() { const [page, setPage] = React.useState(1) const { data } = useQuery({ queryKey: [key, page], queryFn: () => sleep(10).then(() => page), initialData: 1, }) return (

{data}

) } renderToString( , ) const keys = queryCache.getAll().map((query) => query.queryKey) expect(keys).toEqual([[key, 1]]) queryCache.clear() }) it('useInfiniteQuery should return the correct state', async () => { const key = queryKey() const queryFn = vi.fn(() => sleep(10).then(() => 'page 1')) function Page() { const query = useInfiniteQuery({ queryKey: key, queryFn, getNextPageParam: () => undefined, initialPageParam: 0, }) return (
    {query.data?.pages.map((page) => (
  • {page}
  • ))}
) } queryClient.prefetchInfiniteQuery({ queryKey: key, queryFn, initialPageParam: 0, }) await vi.advanceTimersByTimeAsync(10) const markup = renderToString( , ) expect(markup).toContain('page 1') expect(queryFn).toHaveBeenCalledTimes(1) queryCache.clear() }) }) ================================================ FILE: packages/react-query/src/__tests__/suspense.test.tsx ================================================ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { act, render } from '@testing-library/react' import { Suspense } from 'react' import { queryKey, sleep } from '@tanstack/query-test-utils' import { QueryClient, QueryClientProvider, useSuspenseQuery } from '..' import type { QueryKey } from '..' function renderWithSuspense(client: QueryClient, ui: React.ReactNode) { return render( {ui} , ) } function createTestQuery(options: { fetchCount: { count: number } queryKey: QueryKey staleTime?: number | (() => number) }) { return function TestComponent() { const { data } = useSuspenseQuery({ queryKey: options.queryKey, queryFn: () => sleep(10).then(() => { options.fetchCount.count++ return 'data' }), staleTime: options.staleTime, }) return
data: {data}
} } describe('Suspense Timer Tests', () => { let queryClient: QueryClient let fetchCount: { count: number } beforeEach(() => { vi.useFakeTimers() queryClient = new QueryClient({ defaultOptions: { queries: { retry: false, }, }, }) fetchCount = { count: 0 } }) afterEach(() => { vi.useRealTimers() }) it('should enforce minimum staleTime of 1000ms when using suspense with number', async () => { const TestComponent = createTestQuery({ fetchCount, queryKey: ['test'], staleTime: 10, }) const rendered = renderWithSuspense(queryClient, ) expect(rendered.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(10)) expect(rendered.getByText('data: data')).toBeInTheDocument() rendered.rerender( , ) await act(() => vi.advanceTimersByTimeAsync(100)) expect(fetchCount.count).toBe(1) }) it('should enforce minimum staleTime of 1000ms when using suspense with function', async () => { const TestComponent = createTestQuery({ fetchCount, queryKey: ['test-func'], staleTime: () => 10, }) const rendered = renderWithSuspense(queryClient, ) expect(rendered.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(10)) expect(rendered.getByText('data: data')).toBeInTheDocument() rendered.rerender( , ) await act(() => vi.advanceTimersByTimeAsync(100)) expect(fetchCount.count).toBe(1) }) it('should respect staleTime when value is greater than 1000ms', async () => { const TestComponent = createTestQuery({ fetchCount, queryKey: queryKey(), staleTime: 2000, }) const rendered = renderWithSuspense(queryClient, ) expect(rendered.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(10)) expect(rendered.getByText('data: data')).toBeInTheDocument() rendered.rerender( , ) await act(() => vi.advanceTimersByTimeAsync(1500)) expect(fetchCount.count).toBe(1) }) it('should enforce minimum staleTime when undefined is provided', async () => { const TestComponent = createTestQuery({ fetchCount, queryKey: queryKey(), staleTime: undefined, }) const rendered = renderWithSuspense(queryClient, ) expect(rendered.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(10)) expect(rendered.getByText('data: data')).toBeInTheDocument() rendered.rerender( , ) await act(() => vi.advanceTimersByTimeAsync(500)) expect(fetchCount.count).toBe(1) }) it('should respect staleTime when function returns value greater than 1000ms', async () => { const TestComponent = createTestQuery({ fetchCount, queryKey: queryKey(), staleTime: () => 3000, }) const rendered = renderWithSuspense(queryClient, ) expect(rendered.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(10)) expect(rendered.getByText('data: data')).toBeInTheDocument() rendered.rerender( , ) await act(() => vi.advanceTimersByTimeAsync(2000)) expect(fetchCount.count).toBe(1) }) }) ================================================ FILE: packages/react-query/src/__tests__/useInfiniteQuery.test-d.tsx ================================================ import { describe, expectTypeOf, it } from 'vitest' import { QueryClient } from '@tanstack/query-core' import { useInfiniteQuery } from '../useInfiniteQuery' import type { InfiniteData } from '@tanstack/query-core' describe('pageParam', () => { it('initialPageParam should define type of param passed to queryFunctionContext', () => { useInfiniteQuery({ queryKey: ['key'], queryFn: ({ pageParam }) => { expectTypeOf(pageParam).toEqualTypeOf() }, initialPageParam: 1, getNextPageParam: () => undefined, }) }) it('direction should be passed to queryFn of useInfiniteQuery', () => { useInfiniteQuery({ queryKey: ['key'], queryFn: ({ direction }) => { expectTypeOf(direction).toEqualTypeOf<'forward' | 'backward'>() }, initialPageParam: 1, getNextPageParam: () => undefined, }) }) it('initialPageParam should define type of param passed to queryFunctionContext for fetchInfiniteQuery', () => { const queryClient = new QueryClient() queryClient.fetchInfiniteQuery({ queryKey: ['key'], queryFn: ({ pageParam }) => { expectTypeOf(pageParam).toEqualTypeOf() }, initialPageParam: 1, }) }) it('initialPageParam should define type of param passed to queryFunctionContext for prefetchInfiniteQuery', () => { const queryClient = new QueryClient() queryClient.prefetchInfiniteQuery({ queryKey: ['key'], queryFn: ({ pageParam }) => { expectTypeOf(pageParam).toEqualTypeOf() }, initialPageParam: 1, }) }) }) describe('select', () => { it('should still return paginated data if no select result', () => { const infiniteQuery = useInfiniteQuery({ queryKey: ['key'], queryFn: ({ pageParam }) => { return pageParam * 5 }, initialPageParam: 1, getNextPageParam: () => undefined, }) // TODO: Order of generics prevents pageParams to be typed correctly. Using `unknown` for now expectTypeOf(infiniteQuery.data).toEqualTypeOf< InfiniteData | undefined >() }) it('should be able to transform data to arbitrary result', () => { const infiniteQuery = useInfiniteQuery({ queryKey: ['key'], queryFn: ({ pageParam }) => { return pageParam * 5 }, initialPageParam: 1, getNextPageParam: () => undefined, select: (data) => { expectTypeOf(data).toEqualTypeOf>() return 'selected' as const }, }) expectTypeOf(infiniteQuery.data).toEqualTypeOf<'selected' | undefined>() }) }) describe('getNextPageParam / getPreviousPageParam', () => { it('should get typed params', () => { const infiniteQuery = useInfiniteQuery({ queryKey: ['key'], queryFn: ({ pageParam }) => { return String(pageParam) }, initialPageParam: 1, getNextPageParam: (lastPage, allPages, lastPageParam, allPageParams) => { expectTypeOf(lastPage).toEqualTypeOf() expectTypeOf(allPages).toEqualTypeOf>() expectTypeOf(lastPageParam).toEqualTypeOf() expectTypeOf(allPageParams).toEqualTypeOf>() return undefined }, getPreviousPageParam: ( firstPage, allPages, firstPageParam, allPageParams, ) => { expectTypeOf(firstPage).toEqualTypeOf() expectTypeOf(allPages).toEqualTypeOf>() expectTypeOf(firstPageParam).toEqualTypeOf() expectTypeOf(allPageParams).toEqualTypeOf>() return undefined }, }) // TODO: Order of generics prevents pageParams to be typed correctly. Using `unknown` for now expectTypeOf(infiniteQuery.data).toEqualTypeOf< InfiniteData | undefined >() }) }) describe('error booleans', () => { it('should not be permanently `false`', () => { const { isFetchNextPageError, isFetchPreviousPageError, isLoadingError, isRefetchError, } = useInfiniteQuery({ queryKey: ['key'], queryFn: ({ pageParam }) => { return pageParam * 5 }, initialPageParam: 1, getNextPageParam: () => undefined, }) expectTypeOf(isFetchNextPageError).toEqualTypeOf() expectTypeOf(isFetchPreviousPageError).toEqualTypeOf() expectTypeOf(isLoadingError).toEqualTypeOf() expectTypeOf(isRefetchError).toEqualTypeOf() }) }) ================================================ FILE: packages/react-query/src/__tests__/useInfiniteQuery.test.tsx ================================================ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { fireEvent, render } from '@testing-library/react' import * as React from 'react' import { createRenderStream, useTrackRenders, } from '@testing-library/react-render-stream' import { queryKey, sleep } from '@tanstack/query-test-utils' import { QueryCache, QueryClient, QueryClientProvider, keepPreviousData, useInfiniteQuery, } from '..' import { renderWithClient, setActTimeout } from './utils' import type { InfiniteData, QueryFunctionContext, UseInfiniteQueryResult, } from '..' import type { Mock } from 'vitest' interface Result { items: Array nextId?: number prevId?: number ts: number } const pageSize = 10 const fetchItems = async ( page: number, ts: number, noNext?: boolean, noPrev?: boolean, ): Promise => { await sleep(10) return { items: [...new Array(10)].fill(null).map((_, d) => page * pageSize + d), nextId: noNext ? undefined : page + 1, prevId: noPrev ? undefined : page - 1, ts, } } describe('useInfiniteQuery', () => { beforeEach(() => { vi.useFakeTimers() }) afterEach(() => { vi.useRealTimers() }) const queryCache = new QueryCache() const queryClient = new QueryClient({ queryCache, defaultOptions: { queries: { experimental_prefetchInRender: true, }, }, }) it('should return the correct states for a successful query', async () => { const key = queryKey() const states: Array>> = [] function Page() { const state = useInfiniteQuery({ queryKey: key, queryFn: ({ pageParam }) => sleep(10).then(() => pageParam), getNextPageParam: (lastPage) => lastPage + 1, initialPageParam: 0, }) states.push(state) return null } renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(11) expect(states.length).toBe(2) expect(states[0]).toEqual({ data: undefined, dataUpdatedAt: 0, error: null, errorUpdatedAt: 0, failureCount: 0, failureReason: null, errorUpdateCount: 0, fetchNextPage: expect.any(Function), fetchPreviousPage: expect.any(Function), hasNextPage: false, hasPreviousPage: false, isError: false, isFetched: false, isFetchedAfterMount: false, isFetching: true, isPaused: false, isFetchNextPageError: false, isFetchingNextPage: false, isFetchPreviousPageError: false, isFetchingPreviousPage: false, isLoading: true, isPending: true, isInitialLoading: true, isLoadingError: false, isPlaceholderData: false, isRefetchError: false, isRefetching: false, isStale: true, isSuccess: false, isEnabled: true, refetch: expect.any(Function), status: 'pending', fetchStatus: 'fetching', promise: expect.any(Promise), }) expect(states[1]).toEqual({ data: { pages: [0], pageParams: [0] }, dataUpdatedAt: expect.any(Number), error: null, errorUpdatedAt: 0, failureCount: 0, failureReason: null, errorUpdateCount: 0, fetchNextPage: expect.any(Function), fetchPreviousPage: expect.any(Function), hasNextPage: true, hasPreviousPage: false, isError: false, isFetched: true, isFetchedAfterMount: true, isFetching: false, isPaused: false, isFetchNextPageError: false, isFetchingNextPage: false, isFetchPreviousPageError: false, isFetchingPreviousPage: false, isLoading: false, isPending: false, isInitialLoading: false, isLoadingError: false, isPlaceholderData: false, isRefetchError: false, isRefetching: false, isStale: true, isSuccess: true, isEnabled: true, refetch: expect.any(Function), status: 'success', fetchStatus: 'idle', promise: expect.any(Promise), }) }) it('should not throw when fetchNextPage returns an error', async () => { const key = queryKey() let noThrow = false function Page() { const start = 1 const state = useInfiniteQuery({ queryKey: key, queryFn: ({ pageParam }) => sleep(10).then(() => { if (pageParam === 2) throw new Error('error') return pageParam }), retry: 1, retryDelay: 10, getNextPageParam: (lastPage) => lastPage + 1, initialPageParam: start, }) const { fetchNextPage } = state React.useEffect(() => { setActTimeout(() => { fetchNextPage() .then(() => { noThrow = true }) .catch(() => undefined) }, 20) }, [fetchNextPage]) return null } renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(50) expect(noThrow).toBe(true) }) it('should keep the previous data when placeholderData is set', async () => { const key = queryKey() const states: Array>> = [] function Page() { const [order, setOrder] = React.useState('desc') const state = useInfiniteQuery({ queryKey: [key, order], queryFn: ({ pageParam }) => sleep(10).then(() => `${pageParam}-${order}`), getNextPageParam: () => 1, initialPageParam: 0, placeholderData: keepPreviousData, notifyOnChangeProps: 'all', }) states.push(state) return (
data: {state.data?.pages.join(',') ?? 'null'}
isFetching: {String(state.isFetching)}
) } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('data: 0-desc')).toBeInTheDocument() fireEvent.click(rendered.getByRole('button', { name: /fetchNextPage/i })) await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('data: 0-desc,1-desc')).toBeInTheDocument() fireEvent.click(rendered.getByRole('button', { name: /order/i })) await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('data: 0-asc')).toBeInTheDocument() expect(rendered.getByText('isFetching: false')).toBeInTheDocument() expect(states.length).toBe(6) expect(states[0]).toMatchObject({ data: undefined, isFetching: true, isFetchingNextPage: false, isSuccess: false, isPlaceholderData: false, }) expect(states[1]).toMatchObject({ data: { pages: ['0-desc'] }, isFetching: false, isFetchingNextPage: false, isSuccess: true, isPlaceholderData: false, }) expect(states[2]).toMatchObject({ data: { pages: ['0-desc'] }, isFetching: true, isFetchingNextPage: true, isSuccess: true, isPlaceholderData: false, }) expect(states[3]).toMatchObject({ data: { pages: ['0-desc', '1-desc'] }, isFetching: false, isFetchingNextPage: false, isSuccess: true, isPlaceholderData: false, }) // Set state expect(states[4]).toMatchObject({ data: { pages: ['0-desc', '1-desc'] }, isFetching: true, isFetchingNextPage: false, isSuccess: true, isPlaceholderData: true, }) expect(states[5]).toMatchObject({ data: { pages: ['0-asc'] }, isFetching: false, isFetchingNextPage: false, isSuccess: true, isPlaceholderData: false, }) }) it('should be able to select a part of the data', async () => { const key = queryKey() const states: Array>> = [] function Page() { const state = useInfiniteQuery({ queryKey: key, queryFn: () => sleep(10).then(() => ({ count: 1 })), select: (data) => ({ pages: data.pages.map((x) => `count: ${x.count}`), pageParams: data.pageParams, }), getNextPageParam: () => undefined, initialPageParam: 0, }) states.push(state) return
{state.data?.pages.join(',')}
} const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('count: 1')).toBeInTheDocument() expect(states.length).toBe(2) expect(states[0]).toMatchObject({ data: undefined, isSuccess: false, }) expect(states[1]).toMatchObject({ data: { pages: ['count: 1'] }, isSuccess: true, }) }) it('should be able to select a new result and not cause infinite renders', async () => { const key = queryKey() const states: Array< UseInfiniteQueryResult> > = [] let selectCalled = 0 function Page() { const state = useInfiniteQuery({ queryKey: key, queryFn: () => sleep(10).then(() => ({ count: 1 })), select: React.useCallback((data: InfiniteData<{ count: number }>) => { selectCalled++ return { pages: data.pages.map((x) => ({ ...x, id: Math.random() })), pageParams: data.pageParams, } }, []), getNextPageParam: () => undefined, initialPageParam: 0, }) states.push(state) return (
{state.data?.pages.map((page) => (
count: {page.count}
))}
) } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('count: 1')).toBeInTheDocument() expect(states.length).toBe(2) expect(selectCalled).toBe(1) expect(states[0]).toMatchObject({ data: undefined, isSuccess: false, }) expect(states[1]).toMatchObject({ data: { pages: [{ count: 1 }] }, isSuccess: true, }) }) it('should be able to reverse the data', async () => { const key = queryKey() const states: Array>> = [] function Page() { const state = useInfiniteQuery({ queryKey: key, queryFn: ({ pageParam }) => sleep(10).then(() => pageParam), select: (data) => ({ pages: [...data.pages].reverse(), pageParams: [...data.pageParams].reverse(), }), notifyOnChangeProps: 'all', getNextPageParam: () => 1, initialPageParam: 0, }) states.push(state) return (
data: {state.data?.pages.join(',') ?? 'null'}
isFetching: {state.isFetching}
) } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('data: 0')).toBeInTheDocument() fireEvent.click(rendered.getByRole('button', { name: /fetchNextPage/i })) await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('data: 1,0')).toBeInTheDocument() expect(states.length).toBe(4) expect(states[0]).toMatchObject({ data: undefined, isSuccess: false, }) expect(states[1]).toMatchObject({ data: { pages: [0] }, isSuccess: true, }) expect(states[2]).toMatchObject({ data: { pages: [0] }, isSuccess: true, }) expect(states[3]).toMatchObject({ data: { pages: [1, 0] }, isSuccess: true, }) }) it('should be able to fetch a previous page', async () => { const key = queryKey() const states: Array>> = [] function Page() { const start = 10 const state = useInfiniteQuery({ queryKey: key, queryFn: ({ pageParam }) => sleep(10).then(() => pageParam), initialPageParam: start, getNextPageParam: (lastPage) => lastPage + 1, getPreviousPageParam: (firstPage) => firstPage - 1, notifyOnChangeProps: 'all', }) states.push(state) return (
data: {state.data?.pages.join(',') ?? null}
) } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('data: 10')).toBeInTheDocument() fireEvent.click( rendered.getByRole('button', { name: /fetch previous page/i }), ) await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('data: 9,10')).toBeInTheDocument() expect(states.length).toBe(4) expect(states[0]).toMatchObject({ data: undefined, hasNextPage: false, hasPreviousPage: false, isFetching: true, isFetchingNextPage: false, isFetchingPreviousPage: false, isSuccess: false, }) expect(states[1]).toMatchObject({ data: { pages: [10] }, hasNextPage: true, hasPreviousPage: true, isFetching: false, isFetchingNextPage: false, isFetchingPreviousPage: false, isSuccess: true, }) expect(states[2]).toMatchObject({ data: { pages: [10] }, hasNextPage: true, hasPreviousPage: true, isFetching: true, isFetchingNextPage: false, isFetchingPreviousPage: true, isSuccess: true, }) expect(states[3]).toMatchObject({ data: { pages: [9, 10] }, hasNextPage: true, hasPreviousPage: true, isFetching: false, isFetchingNextPage: false, isFetchingPreviousPage: false, isSuccess: true, }) }) it('should be able to refetch when providing page params automatically', async () => { const key = queryKey() const states: Array>> = [] function Page() { const state = useInfiniteQuery({ queryKey: key, queryFn: ({ pageParam }) => sleep(10).then(() => pageParam), initialPageParam: 10, getPreviousPageParam: (firstPage) => firstPage - 1, getNextPageParam: (lastPage) => lastPage + 1, notifyOnChangeProps: 'all', }) states.push(state) return (
data: {state.data?.pages.join(',') ?? 'null'}
isFetching: {String(state.isFetching)}
) } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('data: 10')).toBeInTheDocument() fireEvent.click(rendered.getByRole('button', { name: /fetchNextPage/i })) await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('data: 10,11')).toBeInTheDocument() fireEvent.click( rendered.getByRole('button', { name: /fetchPreviousPage/i }), ) await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('data: 9,10,11')).toBeInTheDocument() fireEvent.click(rendered.getByRole('button', { name: /refetch/i })) expect(rendered.getByText('isFetching: false')).toBeInTheDocument() await vi.advanceTimersByTimeAsync(31) expect(states.length).toBe(8) // Initial fetch expect(states[0]).toMatchObject({ data: undefined, isFetching: true, isFetchingNextPage: false, isRefetching: false, }) // Initial fetch done expect(states[1]).toMatchObject({ data: { pages: [10] }, isFetching: false, isFetchingNextPage: false, isRefetching: false, }) // Fetch next page expect(states[2]).toMatchObject({ data: { pages: [10] }, isFetching: true, isFetchingNextPage: true, isRefetching: false, }) // Fetch next page done expect(states[3]).toMatchObject({ data: { pages: [10, 11] }, isFetching: false, isFetchingNextPage: false, isRefetching: false, }) // Fetch previous page expect(states[4]).toMatchObject({ data: { pages: [10, 11] }, isFetching: true, isFetchingNextPage: false, isFetchingPreviousPage: true, isRefetching: false, }) // Fetch previous page done expect(states[5]).toMatchObject({ data: { pages: [9, 10, 11] }, isFetching: false, isFetchingNextPage: false, isFetchingPreviousPage: false, isRefetching: false, }) // Refetch expect(states[6]).toMatchObject({ data: { pages: [9, 10, 11] }, isFetching: true, isFetchingNextPage: false, isFetchingPreviousPage: false, isRefetching: true, }) // Refetch done expect(states[7]).toMatchObject({ data: { pages: [9, 10, 11] }, isFetching: false, isFetchingNextPage: false, isFetchingPreviousPage: false, isRefetching: false, }) }) it('should return the correct states when refetch fails', async () => { const key = queryKey() const states: Array>> = [] let isRefetch = false function Page() { const state = useInfiniteQuery({ queryKey: key, queryFn: ({ pageParam }) => sleep(10).then(() => { if (isRefetch) throw new Error() return pageParam }), initialPageParam: 10, getPreviousPageParam: (firstPage) => firstPage - 1, getNextPageParam: (lastPage) => lastPage + 1, notifyOnChangeProps: 'all', retry: false, }) states.push(state) return (
data: {state.data?.pages.join(',') ?? 'null'}
isFetching: {String(state.isFetching)}
) } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('data: 10')).toBeInTheDocument() fireEvent.click(rendered.getByRole('button', { name: /refetch/i })) expect(rendered.getByText('isFetching: false')).toBeInTheDocument() await vi.advanceTimersByTimeAsync(11) expect(states.length).toBe(4) // Initial fetch expect(states[0]).toMatchObject({ data: undefined, isFetching: true, isFetchNextPageError: false, isFetchingNextPage: false, isFetchPreviousPageError: false, isFetchingPreviousPage: false, isRefetchError: false, isRefetching: false, }) // Initial fetch done expect(states[1]).toMatchObject({ data: { pages: [10] }, isFetching: false, isFetchNextPageError: false, isFetchingNextPage: false, isFetchPreviousPageError: false, isFetchingPreviousPage: false, isRefetchError: false, isRefetching: false, }) // Refetch expect(states[2]).toMatchObject({ data: { pages: [10] }, isFetching: true, isFetchNextPageError: false, isFetchingNextPage: false, isFetchPreviousPageError: false, isFetchingPreviousPage: false, isRefetchError: false, isRefetching: true, }) // Refetch failed expect(states[3]).toMatchObject({ data: { pages: [10] }, isFetching: false, isFetchNextPageError: false, isFetchingNextPage: false, isFetchPreviousPageError: false, isFetchingPreviousPage: false, isRefetchError: true, isRefetching: false, }) }) it('should return the correct states when fetchNextPage fails', async () => { const key = queryKey() const states: Array>> = [] function Page() { const state = useInfiniteQuery({ queryKey: key, queryFn: ({ pageParam }) => sleep(10).then(() => { if (pageParam !== 10) throw new Error() return pageParam }), initialPageParam: 10, getPreviousPageParam: (firstPage) => firstPage - 1, getNextPageParam: (lastPage) => lastPage + 1, notifyOnChangeProps: 'all', retry: false, }) states.push(state) return (
data: {state.data?.pages.join(',') ?? 'null'}
isFetching: {String(state.isFetching)}
) } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('data: 10')).toBeInTheDocument() fireEvent.click(rendered.getByRole('button', { name: /fetchNextPage/i })) expect(rendered.getByText('isFetching: false')).toBeInTheDocument() await vi.advanceTimersByTimeAsync(11) expect(states.length).toBe(4) // Initial fetch expect(states[0]).toMatchObject({ data: undefined, isFetching: true, isFetchNextPageError: false, isFetchingNextPage: false, isFetchPreviousPageError: false, isFetchingPreviousPage: false, isRefetchError: false, isRefetching: false, }) // Initial fetch done expect(states[1]).toMatchObject({ data: { pages: [10] }, isFetching: false, isFetchNextPageError: false, isFetchingNextPage: false, isFetchPreviousPageError: false, isFetchingPreviousPage: false, isRefetchError: false, isRefetching: false, }) // Fetch next page expect(states[2]).toMatchObject({ data: { pages: [10] }, isFetching: true, isFetchNextPageError: false, isFetchingNextPage: true, isFetchPreviousPageError: false, isFetchingPreviousPage: false, isRefetchError: false, isRefetching: false, }) // Fetch next page failed expect(states[3]).toMatchObject({ data: { pages: [10] }, isFetching: false, isFetchNextPageError: true, isFetchingNextPage: false, isFetchPreviousPageError: false, isFetchingPreviousPage: false, isRefetchError: false, isRefetching: false, }) }) it('should return the correct states when fetchPreviousPage fails', async () => { const key = queryKey() const states: Array>> = [] function Page() { const state = useInfiniteQuery({ queryKey: key, queryFn: ({ pageParam }) => sleep(10).then(() => { if (pageParam !== 10) throw new Error() return pageParam }), initialPageParam: 10, getPreviousPageParam: (firstPage) => firstPage - 1, getNextPageParam: (lastPage) => lastPage + 1, notifyOnChangeProps: 'all', retry: false, }) states.push(state) return (
data: {state.data?.pages.join(',') ?? 'null'}
isFetching: {String(state.isFetching)}
) } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('data: 10')).toBeInTheDocument() fireEvent.click( rendered.getByRole('button', { name: /fetchPreviousPage/i }), ) expect(rendered.getByText('isFetching: false')).toBeInTheDocument() await vi.advanceTimersByTimeAsync(11) expect(states.length).toBe(4) // Initial fetch expect(states[0]).toMatchObject({ data: undefined, isFetching: true, isFetchNextPageError: false, isFetchingNextPage: false, isFetchPreviousPageError: false, isFetchingPreviousPage: false, isRefetchError: false, isRefetching: false, }) // Initial fetch done expect(states[1]).toMatchObject({ data: { pages: [10] }, isFetching: false, isFetchNextPageError: false, isFetchingNextPage: false, isFetchPreviousPageError: false, isFetchingPreviousPage: false, isRefetchError: false, isRefetching: false, }) // Fetch previous page expect(states[2]).toMatchObject({ data: { pages: [10] }, isFetching: true, isFetchNextPageError: false, isFetchingNextPage: false, isFetchPreviousPageError: false, isFetchingPreviousPage: true, isRefetchError: false, isRefetching: false, }) // Fetch previous page failed expect(states[3]).toMatchObject({ data: { pages: [10] }, isFetching: false, isFetchNextPageError: false, isFetchingNextPage: false, isFetchPreviousPageError: true, isFetchingPreviousPage: false, isRefetchError: false, isRefetching: false, }) }) it('should silently cancel any ongoing fetch when fetching more', async () => { const key = queryKey() function Page() { const start = 10 const { data, fetchNextPage, refetch, status, fetchStatus } = useInfiniteQuery({ queryKey: key, queryFn: ({ pageParam }) => sleep(50).then(() => pageParam), initialPageParam: start, getNextPageParam: (lastPage) => lastPage + 1, }) return (
data: {JSON.stringify(data)}
status: {status}, {fetchStatus}
) } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(51) expect(rendered.getByText('status: success, idle')).toBeInTheDocument() expect( rendered.getByText('data: {"pages":[10],"pageParams":[10]}'), ).toBeInTheDocument() fireEvent.click(rendered.getByRole('button', { name: /refetch/i })) await vi.advanceTimersByTimeAsync(0) expect(rendered.getByText('status: success, fetching')).toBeInTheDocument() fireEvent.click(rendered.getByRole('button', { name: /fetchNextPage/i })) await vi.advanceTimersByTimeAsync(51) expect(rendered.getByText('status: success, idle')).toBeInTheDocument() expect( rendered.getByText('data: {"pages":[10,11],"pageParams":[10,11]}'), ).toBeInTheDocument() }) it('should silently cancel an ongoing fetchNextPage request when another fetchNextPage is invoked', async () => { const key = queryKey() const start = 10 const onAborts: Array) => any>> = [] const abortListeners: Array) => any>> = [] const fetchPage = vi.fn< (context: QueryFunctionContext) => Promise >(async ({ pageParam, signal }) => { const onAbort = vi.fn() const abortListener = vi.fn() onAborts.push(onAbort) abortListeners.push(abortListener) signal.onabort = onAbort signal.addEventListener('abort', abortListener) await sleep(50) return pageParam }) function Page() { const { fetchNextPage } = useInfiniteQuery({ queryKey: key, queryFn: fetchPage, initialPageParam: start, getNextPageParam: (lastPage) => lastPage + 1, }) React.useEffect(() => { setActTimeout(() => { fetchNextPage() }, 100) setActTimeout(() => { fetchNextPage() }, 110) }, [fetchNextPage]) return null } renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(160) const expectedCallCount = 3 expect(fetchPage).toBeCalledTimes(expectedCallCount) expect(onAborts).toHaveLength(expectedCallCount) expect(abortListeners).toHaveLength(expectedCallCount) let callIndex = 0 const firstCtx = fetchPage.mock.calls[callIndex]![0] expect(firstCtx.pageParam).toEqual(start) expect(firstCtx.queryKey).toEqual(key) expect(firstCtx.signal).toBeInstanceOf(AbortSignal) expect(firstCtx.signal.aborted).toBe(false) expect(onAborts[callIndex]).not.toHaveBeenCalled() expect(abortListeners[callIndex]).not.toHaveBeenCalled() callIndex = 1 const secondCtx = fetchPage.mock.calls[callIndex]![0] expect(secondCtx.pageParam).toBe(11) expect(secondCtx.queryKey).toEqual(key) expect(secondCtx.signal).toBeInstanceOf(AbortSignal) expect(secondCtx.signal.aborted).toBe(true) expect(onAborts[callIndex]).toHaveBeenCalledTimes(1) expect(abortListeners[callIndex]).toHaveBeenCalledTimes(1) callIndex = 2 const thirdCtx = fetchPage.mock.calls[callIndex]![0] expect(thirdCtx.pageParam).toBe(11) expect(thirdCtx.queryKey).toEqual(key) expect(thirdCtx.signal).toBeInstanceOf(AbortSignal) expect(thirdCtx.signal.aborted).toBe(false) expect(onAborts[callIndex]).not.toHaveBeenCalled() expect(abortListeners[callIndex]).not.toHaveBeenCalled() }) it('should not cancel an ongoing fetchNextPage request when another fetchNextPage is invoked if `cancelRefetch: false` is used', async () => { const key = queryKey() const start = 10 const onAborts: Array) => any>> = [] const abortListeners: Array) => any>> = [] const fetchPage = vi.fn< (context: QueryFunctionContext) => Promise >(async ({ pageParam, signal }) => { const onAbort = vi.fn() const abortListener = vi.fn() onAborts.push(onAbort) abortListeners.push(abortListener) signal.onabort = onAbort signal.addEventListener('abort', abortListener) await sleep(50) return pageParam }) function Page() { const { fetchNextPage } = useInfiniteQuery({ queryKey: key, queryFn: fetchPage, initialPageParam: start, getNextPageParam: (lastPage) => lastPage + 1, }) React.useEffect(() => { setActTimeout(() => { fetchNextPage() }, 100) setActTimeout(() => { fetchNextPage({ cancelRefetch: false }) }, 110) }, [fetchNextPage]) return null } renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(160) const expectedCallCount = 2 expect(fetchPage).toBeCalledTimes(expectedCallCount) expect(onAborts).toHaveLength(expectedCallCount) expect(abortListeners).toHaveLength(expectedCallCount) let callIndex = 0 const firstCtx = fetchPage.mock.calls[callIndex]![0] expect(firstCtx.pageParam).toEqual(start) expect(firstCtx.queryKey).toEqual(key) expect(firstCtx.signal).toBeInstanceOf(AbortSignal) expect(firstCtx.signal.aborted).toBe(false) expect(onAborts[callIndex]).not.toHaveBeenCalled() expect(abortListeners[callIndex]).not.toHaveBeenCalled() callIndex = 1 const secondCtx = fetchPage.mock.calls[callIndex]![0] expect(secondCtx.pageParam).toBe(11) expect(secondCtx.queryKey).toEqual(key) expect(secondCtx.signal).toBeInstanceOf(AbortSignal) expect(secondCtx.signal.aborted).toBe(false) expect(onAborts[callIndex]).not.toHaveBeenCalled() expect(abortListeners[callIndex]).not.toHaveBeenCalled() }) it('should keep fetching first page when not loaded yet and triggering fetch more', async () => { const key = queryKey() const states: Array>> = [] function Page() { const start = 10 const state = useInfiniteQuery({ queryKey: key, queryFn: ({ pageParam }) => sleep(50).then(() => pageParam), initialPageParam: start, getNextPageParam: (lastPage) => lastPage + 1, notifyOnChangeProps: 'all', }) states.push(state) const { fetchNextPage } = state React.useEffect(() => { setActTimeout(() => { fetchNextPage() }, 10) }, [fetchNextPage]) return null } renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(60) expect(states.length).toBe(2) expect(states[0]).toMatchObject({ hasNextPage: false, data: undefined, isFetching: true, isFetchingNextPage: false, isSuccess: false, }) expect(states[1]).toMatchObject({ hasNextPage: true, data: { pages: [10] }, isFetching: false, isFetchingNextPage: false, isSuccess: true, }) }) it('should stop fetching additional pages when the component is unmounted and AbortSignal is consumed', async () => { const key = queryKey() let fetches = 0 const initialData = { pages: [1, 2, 3, 4], pageParams: [0, 1, 2, 3] } function List() { useInfiniteQuery({ queryKey: key, queryFn: ({ pageParam }) => sleep(50).then(() => { fetches++ return pageParam * 10 }), initialData, initialPageParam: 0, getNextPageParam: (_, allPages) => { return allPages.length === 4 ? undefined : allPages.length }, }) return null } function Page() { const [show, setShow] = React.useState(true) React.useEffect(() => { setActTimeout(() => { setShow(false) }, 75) }, []) return show ? : null } renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(125) expect(fetches).toBe(2) expect(queryClient.getQueryState(key)).toMatchObject({ data: initialData, status: 'success', error: null, }) }) it('should be able to set new pages with the query client', async () => { const key = queryKey() let multiplier = 1 function Page() { const [firstPage, setFirstPage] = React.useState(0) const state = useInfiniteQuery({ queryKey: key, queryFn: ({ pageParam }) => sleep(10).then(() => multiplier * pageParam), getNextPageParam: (lastPage) => lastPage + 1, initialPageParam: firstPage, }) return (
data: {JSON.stringify(state.data)}
) } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(11) expect( rendered.getByText('data: {"pages":[0],"pageParams":[0]}'), ).toBeInTheDocument() fireEvent.click(rendered.getByRole('button', { name: /setPages/i })) await vi.advanceTimersByTimeAsync(11) expect( rendered.getByText('data: {"pages":[7,8],"pageParams":[7,8]}'), ).toBeInTheDocument() multiplier = 2 fireEvent.click(rendered.getByRole('button', { name: /refetch/i })) await vi.advanceTimersByTimeAsync(21) expect( rendered.getByText('data: {"pages":[14,30],"pageParams":[7,15]}'), ).toBeInTheDocument() }) it('should only refetch the first page when initialData is provided', async () => { vi.useRealTimers() const key = queryKey() const renderStream = createRenderStream>>() function Page() { const state = useInfiniteQuery({ queryKey: key, queryFn: ({ pageParam }) => sleep(10).then(() => pageParam), initialData: { pages: [1], pageParams: [1] }, getNextPageParam: (lastPage) => lastPage + 1, initialPageParam: 0, notifyOnChangeProps: 'all', }) renderStream.replaceSnapshot(state) return ( ) } const rendered = await renderStream.render( , ) { const { snapshot } = await renderStream.takeRender() expect(snapshot).toMatchObject({ data: { pages: [1] }, hasNextPage: true, isFetching: true, isFetchingNextPage: false, isSuccess: true, }) } { const { snapshot } = await renderStream.takeRender() expect(snapshot).toMatchObject({ data: { pages: [1] }, hasNextPage: true, isFetching: false, isFetchingNextPage: false, isSuccess: true, }) } fireEvent.click(rendered.getByText('fetchNextPage')) { const { snapshot } = await renderStream.takeRender() expect(snapshot).toMatchObject({ data: { pages: [1] }, hasNextPage: true, isFetching: true, isFetchingNextPage: true, isSuccess: true, }) } { const { snapshot } = await renderStream.takeRender() expect(snapshot).toMatchObject({ data: { pages: [1, 2] }, hasNextPage: true, isFetching: false, isFetchingNextPage: false, isSuccess: true, }) } }) it('should set hasNextPage to false if getNextPageParam returns undefined', async () => { const key = queryKey() const states: Array>> = [] function Page() { const state = useInfiniteQuery({ queryKey: key, queryFn: ({ pageParam }) => sleep(10).then(() => pageParam), getNextPageParam: () => undefined, initialPageParam: 1, }) states.push(state) return null } renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(11) expect(states.length).toBe(2) expect(states[0]).toMatchObject({ data: undefined, hasNextPage: false, isFetching: true, isFetchingNextPage: false, isSuccess: false, }) expect(states[1]).toMatchObject({ data: { pages: [1] }, hasNextPage: false, isFetching: false, isFetchingNextPage: false, isSuccess: true, }) }) it('should compute hasNextPage correctly using initialData', async () => { const key = queryKey() const states: Array>> = [] function Page() { const state = useInfiniteQuery({ queryKey: key, queryFn: ({ pageParam }) => sleep(10).then(() => pageParam), initialData: { pages: [10], pageParams: [10] }, getNextPageParam: (lastPage) => (lastPage === 10 ? 11 : undefined), initialPageParam: 10, }) states.push(state) return null } renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(11) expect(states.length).toBe(2) expect(states[0]).toMatchObject({ data: { pages: [10] }, hasNextPage: true, isFetching: true, isFetchingNextPage: false, isSuccess: true, }) expect(states[1]).toMatchObject({ data: { pages: [10] }, hasNextPage: true, isFetching: false, isFetchingNextPage: false, isSuccess: true, }) }) it('should compute hasNextPage correctly for falsy getFetchMore return value using initialData', async () => { const key = queryKey() const states: Array>> = [] function Page() { const state = useInfiniteQuery({ queryKey: key, queryFn: ({ pageParam }) => sleep(10).then(() => pageParam), initialPageParam: 10, initialData: { pages: [10], pageParams: [10] }, getNextPageParam: () => undefined, }) states.push(state) return null } renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(11) expect(states.length).toBe(2) expect(states[0]).toMatchObject({ data: { pages: [10] }, hasNextPage: false, isFetching: true, isFetchingNextPage: false, isSuccess: true, }) expect(states[1]).toMatchObject({ data: { pages: [10] }, hasNextPage: false, isFetching: false, isFetchingNextPage: false, isSuccess: true, }) }) it('should not use selected data when computing hasNextPage', async () => { const key = queryKey() const states: Array>> = [] function Page() { const state = useInfiniteQuery({ queryKey: key, queryFn: ({ pageParam }) => sleep(10).then(() => pageParam), getNextPageParam: (lastPage) => (lastPage === 1 ? 2 : undefined), select: (data) => ({ pages: data.pages.map((x) => x.toString()), pageParams: data.pageParams, }), initialPageParam: 1, }) states.push(state) return null } renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(11) expect(states.length).toBe(2) expect(states[0]).toMatchObject({ data: undefined, hasNextPage: false, isFetching: true, isFetchingNextPage: false, isSuccess: false, }) expect(states[1]).toMatchObject({ data: { pages: ['1'] }, hasNextPage: true, isFetching: false, isFetchingNextPage: false, isSuccess: true, }) }) it('should build fresh cursors on refetch', async () => { const key = queryKey() const genItems = (size: number) => [...new Array(size)].fill(null).map((_, d) => d) const items = genItems(15) const limit = 3 const fetchItemsWithLimit = (cursor = 0, ts: number) => sleep(10).then(() => ({ nextId: cursor + limit, items: items.slice(cursor, cursor + limit), ts, })) function Page() { const fetchCountRef = React.useRef(0) const { status, data, error, isFetchingNextPage, fetchNextPage, hasNextPage, refetch, } = useInfiniteQuery({ queryKey: key, queryFn: ({ pageParam }) => fetchItemsWithLimit(pageParam, fetchCountRef.current++), initialPageParam: 0, getNextPageParam: (lastPage) => lastPage.nextId, }) return (

Pagination

{status === 'pending' ? ( 'Loading...' ) : status === 'error' ? ( Error: {error.message} ) : ( <>
Data:
{data.pages.map((page, i) => (
Page {i}: {page.ts}
{page.items.map((item) => (

Item: {item}

))}
))}
{!isFetchingNextPage ? 'Background Updating...' : null}
)}
) } const rendered = renderWithClient(queryClient, ) expect(rendered.getByText('Loading...')).toBeInTheDocument() await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('Item: 2')).toBeInTheDocument() expect(rendered.getByText('Page 0: 0')).toBeInTheDocument() fireEvent.click(rendered.getByText('Load More')) await vi.advanceTimersByTimeAsync(0) expect(rendered.getByText('Loading more...')).toBeInTheDocument() await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('Item: 5')).toBeInTheDocument() expect(rendered.getByText('Page 0: 0')).toBeInTheDocument() expect(rendered.getByText('Page 1: 1')).toBeInTheDocument() fireEvent.click(rendered.getByText('Load More')) await vi.advanceTimersByTimeAsync(0) expect(rendered.getByText('Loading more...')).toBeInTheDocument() await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('Item: 8')).toBeInTheDocument() expect(rendered.getByText('Page 0: 0')).toBeInTheDocument() expect(rendered.getByText('Page 1: 1')).toBeInTheDocument() expect(rendered.getByText('Page 2: 2')).toBeInTheDocument() fireEvent.click(rendered.getByText('Refetch')) await vi.advanceTimersByTimeAsync(0) expect(rendered.getByText('Background Updating...')).toBeInTheDocument() await vi.advanceTimersByTimeAsync(31) expect(rendered.getByText('Item: 8')).toBeInTheDocument() expect(rendered.getByText('Page 0: 3')).toBeInTheDocument() expect(rendered.getByText('Page 1: 4')).toBeInTheDocument() expect(rendered.getByText('Page 2: 5')).toBeInTheDocument() // ensure that Item: 4 is rendered before removing it expect(rendered.queryAllByText('Item: 4')).toHaveLength(1) // remove Item: 4 fireEvent.click(rendered.getByText('Remove item')) await vi.advanceTimersByTimeAsync(0) expect(rendered.getByText('Background Updating...')).toBeInTheDocument() // ensure that an additional item is rendered (it means that cursors were properly rebuilt) await vi.advanceTimersByTimeAsync(31) expect(rendered.getByText('Item: 9')).toBeInTheDocument() expect(rendered.getByText('Page 0: 6')).toBeInTheDocument() expect(rendered.getByText('Page 1: 7')).toBeInTheDocument() expect(rendered.getByText('Page 2: 8')).toBeInTheDocument() // ensure that Item: 4 is no longer rendered expect(rendered.queryAllByText('Item: 4')).toHaveLength(0) }) it('should compute hasNextPage correctly for falsy getFetchMore return value on refetching', async () => { const key = queryKey() const MAX = 2 function Page() { const fetchCountRef = React.useRef(0) const [isRemovedLastPage, setIsRemovedLastPage] = React.useState(false) const { status, data, error, isFetching, isFetchingNextPage, fetchNextPage, hasNextPage, refetch, } = useInfiniteQuery({ queryKey: key, queryFn: ({ pageParam }) => fetchItems( pageParam, fetchCountRef.current++, pageParam === MAX || (pageParam === MAX - 1 && isRemovedLastPage), ), getNextPageParam: (lastPage) => lastPage.nextId, initialPageParam: 0, }) return (

Pagination

{status === 'pending' ? ( 'Loading...' ) : status === 'error' ? ( Error: {error.message} ) : ( <>
Data:
{data.pages.map((page, i) => (
Page {i}: {page.ts}
{page.items.map((item) => (

Item: {item}

))}
))}
{isFetching && !isFetchingNextPage ? 'Background Updating...' : null}
)}
) } const rendered = renderWithClient(queryClient, ) expect(rendered.getByText('Loading...')).toBeInTheDocument() await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('Item: 9')).toBeInTheDocument() expect(rendered.getByText('Page 0: 0')).toBeInTheDocument() fireEvent.click(rendered.getByText('Load More')) await vi.advanceTimersByTimeAsync(0) expect(rendered.getByText('Loading more...')).toBeInTheDocument() await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('Item: 19')).toBeInTheDocument() expect(rendered.getByText('Page 0: 0')).toBeInTheDocument() expect(rendered.getByText('Page 1: 1')).toBeInTheDocument() fireEvent.click(rendered.getByText('Load More')) await vi.advanceTimersByTimeAsync(0) expect(rendered.getByText('Loading more...')).toBeInTheDocument() await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('Item: 29')).toBeInTheDocument() expect(rendered.getByText('Page 0: 0')).toBeInTheDocument() expect(rendered.getByText('Page 1: 1')).toBeInTheDocument() expect(rendered.getByText('Page 2: 2')).toBeInTheDocument() expect(rendered.getByText('Nothing more to load')).toBeInTheDocument() fireEvent.click(rendered.getByText('Remove Last Page')) fireEvent.click(rendered.getByText('Refetch')) await vi.advanceTimersByTimeAsync(0) expect(rendered.getByText('Background Updating...')).toBeInTheDocument() await vi.advanceTimersByTimeAsync(21) expect(rendered.queryByText('Item: 29')).not.toBeInTheDocument() expect(rendered.getByText('Page 0: 3')).toBeInTheDocument() expect(rendered.getByText('Page 1: 4')).toBeInTheDocument() expect(rendered.queryByText('Page 2: 5')).not.toBeInTheDocument() expect(rendered.getByText('Nothing more to load')).toBeInTheDocument() }) it('should cancel the query function when there are no more subscriptions', () => { const key = queryKey() let cancelFn: Mock = vi.fn() const queryFn = ({ signal }: { signal?: AbortSignal }) => { const promise = new Promise((resolve, reject) => { cancelFn = vi.fn(() => reject('Cancelled')) signal?.addEventListener('abort', cancelFn) sleep(1000).then(() => resolve('OK')) }) return promise } function Inner() { const state = useInfiniteQuery({ queryKey: key, queryFn, getNextPageParam: () => undefined, initialPageParam: 0, }) return (

Status: {state.status}

) } function Page() { const [isVisible, setIsVisible] = React.useState(true) return ( <> {isVisible && }
{isVisible ? 'visible' : 'hidden'}
) } const rendered = renderWithClient(queryClient, ) expect(rendered.getByText('visible')).toBeInTheDocument() fireEvent.click(rendered.getByRole('button', { name: 'hide' })) expect(rendered.getByText('hidden')).toBeInTheDocument() expect(cancelFn).toHaveBeenCalled() }) it('should use provided custom queryClient', async () => { const key = queryKey() const queryFn = () => sleep(10).then(() => 'custom client') function Page() { const { data } = useInfiniteQuery( { queryKey: key, queryFn, getNextPageParam: () => undefined, initialPageParam: 0, }, queryClient, ) return
data: {data?.pages[0]}
} const rendered = render() await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('data: custom client')).toBeInTheDocument() }) it('should work with React.use()', async () => { vi.useRealTimers() const key = queryKey() const renderStream = createRenderStream({ snapshotDOM: true }) function Loading() { useTrackRenders() return <>loading... } function MyComponent() { useTrackRenders() const fetchCountRef = React.useRef(0) const query = useInfiniteQuery({ queryFn: ({ pageParam }) => fetchItems(pageParam, fetchCountRef.current++), getNextPageParam: (lastPage) => lastPage.nextId, initialPageParam: 0, queryKey: key, }) const data = React.use(query.promise) return ( <> {data.pages.map((page, index) => (
Page: {index + 1}
{page.items.map((item) => (

Item: {item}

))}
))} ) } function Page() { useTrackRenders() return ( }> ) } const rendered = await renderStream.render( , ) { const { renderedComponents, withinDOM } = await renderStream.takeRender() withinDOM().getByText('loading...') expect(renderedComponents).toEqual([Page, Loading]) } { const { renderedComponents, withinDOM } = await renderStream.takeRender() withinDOM().getByText('Page: 1') withinDOM().getByText('Item: 1') expect(renderedComponents).toEqual([MyComponent]) } // click button rendered.getByRole('button', { name: 'fetchNextPage' }).click() { const { renderedComponents, withinDOM } = await renderStream.takeRender() withinDOM().getByText('Page: 1') expect(renderedComponents).toEqual([MyComponent]) } }) }) ================================================ FILE: packages/react-query/src/__tests__/useIsFetching.test.tsx ================================================ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { fireEvent, render } from '@testing-library/react' import * as React from 'react' import { queryKey, sleep } from '@tanstack/query-test-utils' import { QueryCache, QueryClient, useIsFetching, useQuery } from '..' import { renderWithClient, setActTimeout } from './utils' describe('useIsFetching', () => { beforeEach(() => { vi.useFakeTimers() }) afterEach(() => { vi.useRealTimers() }) // See https://github.com/tannerlinsley/react-query/issues/105 it('should update as queries start and stop fetching', async () => { const queryClient = new QueryClient() const key = queryKey() function IsFetching() { const isFetching = useIsFetching() return
isFetching: {isFetching}
} function Query() { const [ready, setReady] = React.useState(false) useQuery({ queryKey: key, queryFn: () => sleep(50).then(() => 'test'), enabled: ready, }) return } function Page() { return (
) } const rendered = renderWithClient(queryClient, ) expect(rendered.getByText('isFetching: 0')).toBeInTheDocument() fireEvent.click(rendered.getByRole('button', { name: /setReady/i })) await vi.advanceTimersByTimeAsync(0) expect(rendered.getByText('isFetching: 1')).toBeInTheDocument() await vi.advanceTimersByTimeAsync(51) expect(rendered.getByText('isFetching: 0')).toBeInTheDocument() }) it('should not update state while rendering', async () => { const queryClient = new QueryClient() const key1 = queryKey() const key2 = queryKey() const isFetchingArray: Array = [] function IsFetching() { const isFetching = useIsFetching() isFetchingArray.push(isFetching) return null } function FirstQuery() { useQuery({ queryKey: key1, queryFn: () => sleep(100).then(() => 'data1'), }) return null } function SecondQuery() { useQuery({ queryKey: key2, queryFn: () => sleep(100).then(() => 'data2'), }) return null } function Page() { const [renderSecond, setRenderSecond] = React.useState(false) React.useEffect(() => { setActTimeout(() => { setRenderSecond(true) }, 50) }, []) return ( <> {renderSecond && } ) } renderWithClient(queryClient, ) expect(isFetchingArray[0]).toEqual(0) await vi.advanceTimersByTimeAsync(0) expect(isFetchingArray[1]).toEqual(1) await vi.advanceTimersByTimeAsync(50) expect(isFetchingArray[2]).toEqual(1) await vi.advanceTimersByTimeAsync(1) expect(isFetchingArray[3]).toEqual(2) await vi.advanceTimersByTimeAsync(50) expect(isFetchingArray[4]).toEqual(1) await vi.advanceTimersByTimeAsync(50) expect(isFetchingArray[5]).toEqual(0) expect(isFetchingArray).toEqual([0, 1, 1, 2, 1, 0]) }) it('should be able to filter', async () => { const queryClient = new QueryClient() const key1 = queryKey() const key2 = queryKey() const isFetchingArray: Array = [] function One() { useQuery({ queryKey: key1, queryFn: () => sleep(10).then(() => 'test1'), }) return null } function Two() { useQuery({ queryKey: key2, queryFn: () => sleep(20).then(() => 'test2'), }) return null } function Page() { const [started, setStarted] = React.useState(false) const isFetching = useIsFetching({ queryKey: key1 }) isFetchingArray.push(isFetching) return (
isFetching: {isFetching}
{started ? ( <> ) : null}
) } const rendered = renderWithClient(queryClient, ) expect(rendered.getByText('isFetching: 0')).toBeInTheDocument() fireEvent.click(rendered.getByRole('button', { name: /setStarted/i })) await vi.advanceTimersByTimeAsync(0) expect(rendered.getByText('isFetching: 1')).toBeInTheDocument() await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('isFetching: 0')).toBeInTheDocument() // at no point should we have isFetching: 2 expect(isFetchingArray).toEqual(expect.not.arrayContaining([2])) }) it('should show the correct fetching state when mounted after a query', async () => { const queryClient = new QueryClient() const key = queryKey() function Page() { useQuery({ queryKey: key, queryFn: () => sleep(10).then(() => 'test'), }) const isFetching = useIsFetching() return (
isFetching: {isFetching}
) } const rendered = renderWithClient(queryClient, ) expect(rendered.getByText('isFetching: 1')).toBeInTheDocument() await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('isFetching: 0')).toBeInTheDocument() }) it('should use provided custom queryClient', async () => { const onSuccess = vi.fn() const queryCache = new QueryCache({ onSuccess }) const queryClient = new QueryClient({ queryCache }) const key = queryKey() function Page() { useQuery( { queryKey: key, queryFn: () => sleep(10).then(() => 'test'), }, queryClient, ) const isFetching = useIsFetching({}, queryClient) return (
isFetching: {isFetching}
) } const rendered = render() expect(rendered.getByText('isFetching: 1')).toBeInTheDocument() await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('isFetching: 0')).toBeInTheDocument() expect(onSuccess).toHaveBeenCalledOnce() }) }) ================================================ FILE: packages/react-query/src/__tests__/useMutation.test.tsx ================================================ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { fireEvent, render } from '@testing-library/react' import * as React from 'react' import { ErrorBoundary } from 'react-error-boundary' import { queryKey, sleep } from '@tanstack/query-test-utils' import { MutationCache, QueryCache, QueryClient, useMutation } from '..' import { mockOnlineManagerIsOnline, renderWithClient, setActTimeout, } from './utils' import type { UseMutationResult } from '../types' describe('useMutation', () => { let queryCache: QueryCache let mutationCache: MutationCache let queryClient: QueryClient beforeEach(() => { queryCache = new QueryCache() mutationCache = new MutationCache() queryClient = new QueryClient({ queryCache, mutationCache, }) vi.useFakeTimers() }) afterEach(() => { vi.useRealTimers() }) it('should be able to reset `data`', async () => { function Page() { const { mutate, data = 'empty', reset, } = useMutation({ mutationFn: () => Promise.resolve('mutation') }) return (

{data}

) } const { getByRole } = renderWithClient(queryClient, ) expect(getByRole('heading').textContent).toBe('empty') fireEvent.click(getByRole('button', { name: /mutate/i })) await vi.advanceTimersByTimeAsync(0) expect(getByRole('heading').textContent).toBe('mutation') fireEvent.click(getByRole('button', { name: /reset/i })) await vi.advanceTimersByTimeAsync(0) expect(getByRole('heading').textContent).toBe('empty') }) it('should be able to reset `error`', async () => { function Page() { const { mutate, error, reset } = useMutation({ mutationFn: () => { const err = new Error('Expected mock error. All is well!') err.stack = '' return Promise.reject(err) }, }) return (
{error &&

{error.message}

}
) } const { getByRole, queryByRole } = renderWithClient(queryClient, ) expect(queryByRole('heading')).toBeNull() fireEvent.click(getByRole('button', { name: /mutate/i })) await vi.advanceTimersByTimeAsync(0) expect(getByRole('heading').textContent).toBe( 'Expected mock error. All is well!', ) fireEvent.click(getByRole('button', { name: /reset/i })) await vi.advanceTimersByTimeAsync(0) expect(queryByRole('heading')).toBeNull() }) it('should be able to call `onSuccess` and `onSettled` after each successful mutate', async () => { let count = 0 const onSuccessMock = vi.fn() const onSettledMock = vi.fn() function Page() { const { mutate } = useMutation({ mutationFn: (vars: { count: number }) => Promise.resolve(vars.count), onSuccess: (data) => { onSuccessMock(data) }, onSettled: (data) => { onSettledMock(data) }, }) return (

{count}

) } const { getByRole } = renderWithClient(queryClient, ) expect(getByRole('heading').textContent).toBe('0') fireEvent.click(getByRole('button', { name: /mutate/i })) fireEvent.click(getByRole('button', { name: /mutate/i })) fireEvent.click(getByRole('button', { name: /mutate/i })) await vi.advanceTimersByTimeAsync(0) expect(getByRole('heading').textContent).toBe('3') expect(onSuccessMock).toHaveBeenCalledTimes(3) expect(onSuccessMock).toHaveBeenCalledWith(1) expect(onSuccessMock).toHaveBeenCalledWith(2) expect(onSuccessMock).toHaveBeenCalledWith(3) expect(onSettledMock).toHaveBeenCalledTimes(3) expect(onSettledMock).toHaveBeenCalledWith(1) expect(onSettledMock).toHaveBeenCalledWith(2) expect(onSettledMock).toHaveBeenCalledWith(3) }) it('should set correct values for `failureReason` and `failureCount` on multiple mutate calls', async () => { let count = 0 type Value = { count: number } const mutateFn = vi.fn<(value: Value) => Promise>() mutateFn.mockImplementationOnce(() => { return Promise.reject(new Error('Error test Jonas')) }) mutateFn.mockImplementation(async (value) => { await sleep(10) return Promise.resolve(value) }) function Page() { const { mutate, failureCount, failureReason, data, status } = useMutation( { mutationFn: mutateFn }, ) return (

Data {data?.count}

Status {status}

Failed {failureCount} times

Failed because {failureReason?.message ?? 'null'}

) } const rendered = renderWithClient(queryClient, ) expect(rendered.getByText('Data')).toBeInTheDocument() fireEvent.click(rendered.getByRole('button', { name: /mutate/i })) await vi.advanceTimersByTimeAsync(0) expect(rendered.getByText('Status error')).toBeInTheDocument() expect(rendered.getByText('Failed 1 times')).toBeInTheDocument() expect( rendered.getByText('Failed because Error test Jonas'), ).toBeInTheDocument() fireEvent.click(rendered.getByRole('button', { name: /mutate/i })) await vi.advanceTimersByTimeAsync(0) expect(rendered.getByText('Status pending')).toBeInTheDocument() await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('Status success')).toBeInTheDocument() expect(rendered.getByText('Data 2')).toBeInTheDocument() expect(rendered.getByText('Failed 0 times')).toBeInTheDocument() expect(rendered.getByText('Failed because null')).toBeInTheDocument() }) it('should be able to call `onError` and `onSettled` after each failed mutate', async () => { const onErrorMock = vi.fn() const onSettledMock = vi.fn() let count = 0 function Page() { const { mutate } = useMutation({ mutationFn: (vars: { count: number }) => { const error = new Error( `Expected mock error. All is well! ${vars.count}`, ) error.stack = '' return Promise.reject(error) }, onError: (error: Error) => { onErrorMock(error.message) }, onSettled: (_data, error) => { onSettledMock(error?.message) }, }) return (

{count}

) } const { getByRole } = renderWithClient(queryClient, ) expect(getByRole('heading').textContent).toBe('0') fireEvent.click(getByRole('button', { name: /mutate/i })) fireEvent.click(getByRole('button', { name: /mutate/i })) fireEvent.click(getByRole('button', { name: /mutate/i })) await vi.advanceTimersByTimeAsync(0) expect(getByRole('heading').textContent).toBe('3') expect(onErrorMock).toHaveBeenCalledTimes(3) expect(onErrorMock).toHaveBeenCalledWith( 'Expected mock error. All is well! 1', ) expect(onErrorMock).toHaveBeenCalledWith( 'Expected mock error. All is well! 2', ) expect(onErrorMock).toHaveBeenCalledWith( 'Expected mock error. All is well! 3', ) expect(onSettledMock).toHaveBeenCalledTimes(3) expect(onSettledMock).toHaveBeenCalledWith( 'Expected mock error. All is well! 1', ) expect(onSettledMock).toHaveBeenCalledWith( 'Expected mock error. All is well! 2', ) expect(onSettledMock).toHaveBeenCalledWith( 'Expected mock error. All is well! 3', ) }) it('should be able to override the useMutation success callbacks', async () => { const callbacks: Array = [] function Page() { const { mutateAsync } = useMutation({ mutationFn: (text: string) => Promise.resolve(text), onSuccess: () => { callbacks.push('useMutation.onSuccess') return Promise.resolve() }, onSettled: () => { callbacks.push('useMutation.onSettled') return Promise.resolve() }, }) React.useEffect(() => { setActTimeout(async () => { try { const result = await mutateAsync('todo', { onSuccess: () => { callbacks.push('mutateAsync.onSuccess') return Promise.resolve() }, onSettled: () => { callbacks.push('mutateAsync.onSettled') return Promise.resolve() }, }) callbacks.push(`mutateAsync.result:${result}`) } catch {} }, 10) }, [mutateAsync]) return null } renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(10) expect(callbacks).toEqual([ 'useMutation.onSuccess', 'useMutation.onSettled', 'mutateAsync.onSuccess', 'mutateAsync.onSettled', 'mutateAsync.result:todo', ]) }) it('should be able to override the error callbacks when using mutateAsync', async () => { const callbacks: Array = [] function Page() { const { mutateAsync } = useMutation({ mutationFn: async (_text: string) => Promise.reject(new Error('oops')), onError: () => { callbacks.push('useMutation.onError') return Promise.resolve() }, onSettled: () => { callbacks.push('useMutation.onSettled') return Promise.resolve() }, }) React.useEffect(() => { setActTimeout(async () => { try { await mutateAsync('todo', { onError: () => { callbacks.push('mutateAsync.onError') return Promise.resolve() }, onSettled: () => { callbacks.push('mutateAsync.onSettled') return Promise.resolve() }, }) } catch (error) { callbacks.push(`mutateAsync.error:${(error as Error).message}`) } }, 10) }, [mutateAsync]) return null } renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(10) expect(callbacks).toEqual([ 'useMutation.onError', 'useMutation.onSettled', 'mutateAsync.onError', 'mutateAsync.onSettled', 'mutateAsync.error:oops', ]) }) it('should be able to use mutation defaults', async () => { const key = queryKey() queryClient.setMutationDefaults(key, { mutationFn: async (text: string) => { await sleep(10) return text }, }) const states: Array> = [] function Page() { const state = useMutation({ mutationKey: key }) states.push(state) const { mutate } = state React.useEffect(() => { setActTimeout(() => { mutate('todo') }, 10) }, [mutate]) return null } renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(21) expect(states.length).toBe(3) expect(states[0]).toMatchObject({ data: undefined, isPending: false }) expect(states[1]).toMatchObject({ data: undefined, isPending: true }) expect(states[2]).toMatchObject({ data: 'todo', isPending: false }) }) it('should be able to retry a failed mutation', async () => { let count = 0 function Page() { const { mutate } = useMutation({ mutationFn: (_text: string) => { count++ return Promise.reject(new Error('oops')) }, retry: 1, retryDelay: 5, }) React.useEffect(() => { setActTimeout(() => { mutate('todo') }, 10) }, [mutate]) return null } renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(15) expect(count).toBe(2) }) it('should not retry mutations while offline', async () => { const onlineMock = mockOnlineManagerIsOnline(false) let count = 0 function Page() { const mutation = useMutation({ mutationFn: (_text: string) => { count++ return Promise.reject(new Error('oops')) }, retry: 1, retryDelay: 5, }) return (
error:{' '} {mutation.error instanceof Error ? mutation.error.message : 'null'}, status: {mutation.status}, isPaused: {String(mutation.isPaused)}
) } const rendered = renderWithClient(queryClient, ) expect( rendered.getByText('error: null, status: idle, isPaused: false'), ).toBeInTheDocument() fireEvent.click(rendered.getByRole('button', { name: /mutate/i })) await vi.advanceTimersByTimeAsync(0) expect( rendered.getByText('error: null, status: pending, isPaused: true'), ).toBeInTheDocument() expect(count).toBe(0) onlineMock.mockReturnValue(true) queryClient.getMutationCache().resumePausedMutations() await vi.advanceTimersByTimeAsync(6) expect( rendered.getByText('error: oops, status: error, isPaused: false'), ).toBeInTheDocument() expect(count).toBe(2) onlineMock.mockRestore() }) it('should call onMutate even if paused', async () => { const onlineMock = mockOnlineManagerIsOnline(false) const onMutate = vi.fn() let count = 0 function Page() { const mutation = useMutation({ mutationFn: async (_text: string) => { count++ await sleep(10) return count }, onMutate, }) return (
data: {mutation.data ?? 'null'}, status: {mutation.status}, isPaused: {String(mutation.isPaused)}
) } const rendered = renderWithClient(queryClient, ) expect( rendered.getByText('data: null, status: idle, isPaused: false'), ).toBeInTheDocument() fireEvent.click(rendered.getByRole('button', { name: /mutate/i })) await vi.advanceTimersByTimeAsync(0) expect( rendered.getByText('data: null, status: pending, isPaused: true'), ).toBeInTheDocument() expect(onMutate).toHaveBeenCalledTimes(1) expect(onMutate).toHaveBeenCalledWith('todo', { client: queryClient, meta: undefined, mutationKey: undefined, }) onlineMock.mockReturnValue(true) queryClient.getMutationCache().resumePausedMutations() await vi.advanceTimersByTimeAsync(11) expect( rendered.getByText('data: 1, status: success, isPaused: false'), ).toBeInTheDocument() expect(onMutate).toHaveBeenCalledTimes(1) expect(count).toBe(1) onlineMock.mockRestore() }) it('should optimistically go to paused state if offline', async () => { const onlineMock = mockOnlineManagerIsOnline(false) let count = 0 const states: Array = [] function Page() { const mutation = useMutation({ mutationFn: async (_text: string) => { count++ await sleep(10) return count }, }) states.push(`${mutation.status}, ${mutation.isPaused}`) return (
data: {mutation.data ?? 'null'}, status: {mutation.status}, isPaused: {String(mutation.isPaused)}
) } const rendered = renderWithClient(queryClient, ) expect( rendered.getByText('data: null, status: idle, isPaused: false'), ).toBeInTheDocument() fireEvent.click(rendered.getByRole('button', { name: /mutate/i })) await vi.advanceTimersByTimeAsync(0) expect( rendered.getByText('data: null, status: pending, isPaused: true'), ).toBeInTheDocument() // no intermediate 'pending, false' state is expected because we don't start mutating! expect(states[0]).toBe('idle, false') expect(states[1]).toBe('pending, true') onlineMock.mockReturnValue(true) queryClient.getMutationCache().resumePausedMutations() await vi.advanceTimersByTimeAsync(11) expect( rendered.getByText('data: 1, status: success, isPaused: false'), ).toBeInTheDocument() onlineMock.mockRestore() }) it('should be able to retry a mutation when online', async () => { const onlineMock = mockOnlineManagerIsOnline(false) const key = queryKey() let count = 0 function Page() { const state = useMutation({ mutationKey: key, mutationFn: async (_text: string) => { await sleep(10) count++ return count > 1 ? Promise.resolve(`data${count}`) : Promise.reject(new Error('oops')) }, retry: 1, retryDelay: 5, networkMode: 'offlineFirst', }) return (
status: {state.status}
isPaused: {String(state.isPaused)}
data: {state.data ?? 'null'}
) } const rendered = renderWithClient(queryClient, ) expect(rendered.getByText('status: idle')).toBeInTheDocument() fireEvent.click(rendered.getByRole('button', { name: /mutate/i })) await vi.advanceTimersByTimeAsync(16) expect(rendered.getByText('isPaused: true')).toBeInTheDocument() expect( queryClient.getMutationCache().findAll({ mutationKey: key }).length, ).toBe(1) expect( queryClient.getMutationCache().findAll({ mutationKey: key })[0]?.state, ).toMatchObject({ status: 'pending', isPaused: true, failureCount: 1, failureReason: new Error('oops'), }) onlineMock.mockReturnValue(true) queryClient.getMutationCache().resumePausedMutations() await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('data: data2')).toBeInTheDocument() expect( queryClient.getMutationCache().findAll({ mutationKey: key })[0]?.state, ).toMatchObject({ status: 'success', isPaused: false, failureCount: 0, failureReason: null, data: 'data2', }) onlineMock.mockRestore() }) // eslint-disable-next-line vitest/expect-expect it('should not change state if unmounted', () => { function Mutates() { const { mutate } = useMutation({ mutationFn: () => sleep(10) }) return } function Page() { const [mounted, setMounted] = React.useState(true) return (
{mounted && }
) } const { getByText } = renderWithClient(queryClient, ) fireEvent.click(getByText('mutate')) fireEvent.click(getByText('unmount')) }) it('should be able to throw an error when throwOnError is set to true', async () => { const err = new Error('Expected mock error. All is well!') err.stack = '' const consoleMock = vi .spyOn(console, 'error') .mockImplementation(() => undefined) function Page() { const { mutate } = useMutation({ mutationFn: () => { return Promise.reject(err) }, throwOnError: true, }) return (
) } const { getByText, queryByText } = renderWithClient( queryClient, (
error
)} >
, ) fireEvent.click(getByText('mutate')) await vi.advanceTimersByTimeAsync(0) expect(queryByText('error')).not.toBeNull() expect(consoleMock.mock.calls[0]?.[1]).toBe(err) consoleMock.mockRestore() }) it('should be able to throw an error when throwOnError is a function that returns true', async () => { const consoleMock = vi .spyOn(console, 'error') .mockImplementation(() => undefined) let boundary = false function Page() { const { mutate, error } = useMutation({ mutationFn: () => { const err = new Error('mock error') err.stack = '' return Promise.reject(err) }, throwOnError: () => { return boundary }, }) return (
{error && error.message}
) } const { getByText, queryByText } = renderWithClient( queryClient, (
error boundary
)} >
, ) // first error goes to component fireEvent.click(getByText('mutate')) await vi.advanceTimersByTimeAsync(0) expect(queryByText('mock error')).not.toBeNull() // second error goes to boundary boundary = true fireEvent.click(getByText('mutate')) await vi.advanceTimersByTimeAsync(0) expect(queryByText('error boundary')).not.toBeNull() consoleMock.mockRestore() }) it('should pass meta to mutation', async () => { const errorMock = vi.fn() const successMock = vi.fn() const queryClientMutationMeta = new QueryClient({ mutationCache: new MutationCache({ onSuccess: (_, __, ___, mutation) => { successMock(mutation.meta?.metaSuccessMessage) }, onError: (_, __, ___, mutation) => { errorMock(mutation.meta?.metaErrorMessage) }, }), }) const metaSuccessMessage = 'mutation succeeded' const metaErrorMessage = 'mutation failed' function Page() { const { mutate: succeed, isSuccess } = useMutation({ mutationFn: () => Promise.resolve(''), meta: { metaSuccessMessage }, }) const { mutate: error, isError } = useMutation({ mutationFn: () => { return Promise.reject(new Error('')) }, meta: { metaErrorMessage }, }) return (
{isSuccess &&
successTest
} {isError &&
errorTest
}
) } const { getByText, queryByText } = renderWithClient( queryClientMutationMeta, , ) fireEvent.click(getByText('succeed')) fireEvent.click(getByText('error')) await vi.advanceTimersByTimeAsync(0) expect(queryByText('successTest')).not.toBeNull() expect(queryByText('errorTest')).not.toBeNull() expect(successMock).toHaveBeenCalledTimes(1) expect(successMock).toHaveBeenCalledWith(metaSuccessMessage) expect(errorMock).toHaveBeenCalledTimes(1) expect(errorMock).toHaveBeenCalledWith(metaErrorMessage) }) it('should call cache callbacks when unmounted', async () => { const onSuccess = vi.fn() const onSuccessMutate = vi.fn() const onSettled = vi.fn() const onSettledMutate = vi.fn() const mutationKey = queryKey() let count = 0 function Page() { const [show, setShow] = React.useState(true) return (
{show && }
) } function Component() { const mutation = useMutation({ mutationFn: async (_text: string) => { count++ await sleep(10) return count }, mutationKey, gcTime: 0, onSuccess, onSettled, }) return (
data: {mutation.data ?? 'null'}, status: {mutation.status}, isPaused: {String(mutation.isPaused)}
) } const rendered = renderWithClient(queryClient, ) expect( rendered.getByText('data: null, status: idle, isPaused: false'), ).toBeInTheDocument() fireEvent.click(rendered.getByRole('button', { name: /mutate/i })) fireEvent.click(rendered.getByRole('button', { name: /hide/i })) await vi.advanceTimersByTimeAsync(10) expect( queryClient.getMutationCache().findAll({ mutationKey }), ).toHaveLength(0) expect(count).toBe(1) expect(onSuccess).toHaveBeenCalledTimes(1) expect(onSettled).toHaveBeenCalledTimes(1) expect(onSuccessMutate).toHaveBeenCalledTimes(0) expect(onSettledMutate).toHaveBeenCalledTimes(0) }) it('should call mutate callbacks only for the last observer', async () => { const onSuccess = vi.fn() const onSuccessMutate = vi.fn() const onSettled = vi.fn() const onSettledMutate = vi.fn() let count = 0 function Page() { const mutation = useMutation({ mutationFn: async (text: string) => { count++ const result = `result-${text}` await sleep(10) return result }, onSuccess, onSettled, }) return (
data: {mutation.data ?? 'null'}, status: {mutation.status}
) } const rendered = renderWithClient(queryClient, ) expect(rendered.getByText('data: null, status: idle')).toBeInTheDocument() fireEvent.click(rendered.getByRole('button', { name: /mutate1/i })) fireEvent.click(rendered.getByRole('button', { name: /mutate2/i })) await vi.advanceTimersByTimeAsync(11) expect( rendered.getByText('data: result-todo2, status: success'), ).toBeInTheDocument() expect(count).toBe(2) expect(onSuccess).toHaveBeenCalledTimes(2) expect(onSuccess).toHaveBeenNthCalledWith( 1, 'result-todo1', 'todo1', undefined, { client: queryClient, meta: undefined, mutationKey: undefined, }, ) expect(onSuccess).toHaveBeenNthCalledWith( 2, 'result-todo2', 'todo2', undefined, { client: queryClient, meta: undefined, mutationKey: undefined, }, ) expect(onSettled).toHaveBeenCalledTimes(2) expect(onSuccessMutate).toHaveBeenCalledTimes(1) expect(onSuccessMutate).toHaveBeenCalledWith( 'result-todo2', 'todo2', undefined, { client: queryClient, meta: undefined, mutationKey: undefined, }, ) expect(onSettledMutate).toHaveBeenCalledTimes(1) expect(onSettledMutate).toHaveBeenCalledWith( 'result-todo2', null, 'todo2', undefined, { client: queryClient, meta: undefined, mutationKey: undefined, }, ) }) it('should go to error state if onSuccess callback errors', async () => { const error = new Error('error from onSuccess') const onError = vi.fn() function Page() { const mutation = useMutation({ mutationFn: async (_text: string) => { await sleep(10) return 'result' }, onSuccess: () => Promise.reject(error), onError, }) return (
status: {mutation.status}
) } const rendered = renderWithClient(queryClient, ) expect(rendered.getByText('status: idle')).toBeInTheDocument() rendered.getByRole('button', { name: /mutate/i }).click() await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('status: error')).toBeInTheDocument() expect(onError).toHaveBeenCalledWith(error, 'todo', undefined, { client: queryClient, meta: undefined, mutationKey: undefined, }) }) it('should go to error state if onError callback errors', async () => { const error = new Error('error from onError') const mutateFnError = new Error('mutateFnError') function Page() { const mutation = useMutation({ mutationFn: async (_text: string) => { await sleep(10) throw mutateFnError }, onError: () => Promise.reject(error), }) return (
error:{' '} {mutation.error instanceof Error ? mutation.error.message : 'null'}, status: {mutation.status}
) } const rendered = renderWithClient(queryClient, ) expect(rendered.getByText('error: null, status: idle')).toBeInTheDocument() rendered.getByRole('button', { name: /mutate/i }).click() await vi.advanceTimersByTimeAsync(11) expect( rendered.getByText('error: mutateFnError, status: error'), ).toBeInTheDocument() }) it('should go to error state if onSettled callback errors', async () => { const error = new Error('error from onSettled') const mutateFnError = new Error('mutateFnError') const onError = vi.fn() function Page() { const mutation = useMutation({ mutationFn: async (_text: string) => { await sleep(10) throw mutateFnError }, onSettled: () => Promise.reject(error), onError, }) return (
error:{' '} {mutation.error instanceof Error ? mutation.error.message : 'null'}, status: {mutation.status}
) } const rendered = renderWithClient(queryClient, ) expect(rendered.getByText('error: null, status: idle')).toBeInTheDocument() rendered.getByRole('button', { name: /mutate/i }).click() await vi.advanceTimersByTimeAsync(11) expect( rendered.getByText('error: mutateFnError, status: error'), ).toBeInTheDocument() expect(onError).toHaveBeenCalledWith(mutateFnError, 'todo', undefined, { client: queryClient, meta: undefined, mutationKey: undefined, }) }) it('should use provided custom queryClient', async () => { function Page() { const mutation = useMutation( { mutationFn: async (text: string) => { return Promise.resolve(text) }, }, queryClient, ) return (
data: {mutation.data ?? 'null'}, status: {mutation.status}
) } const rendered = render() expect(rendered.getByText('data: null, status: idle')).toBeInTheDocument() fireEvent.click(rendered.getByRole('button', { name: /mutate/i })) await vi.advanceTimersByTimeAsync(0) expect( rendered.getByText('data: custom client, status: success'), ).toBeInTheDocument() }) }) ================================================ FILE: packages/react-query/src/__tests__/useMutationState.test-d.tsx ================================================ import { describe, expectTypeOf, it } from 'vitest' import { useMutationState } from '../useMutationState' import type { MutationState, MutationStatus } from '@tanstack/query-core' describe('useMutationState', () => { it('should default to QueryState', () => { const result = useMutationState({ filters: { status: 'pending' }, }) expectTypeOf(result).toEqualTypeOf< Array> >() }) it('should infer with select', () => { const result = useMutationState({ filters: { status: 'pending' }, select: (mutation) => mutation.state.status, }) expectTypeOf(result).toEqualTypeOf>() }) }) ================================================ FILE: packages/react-query/src/__tests__/useMutationState.test.tsx ================================================ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { fireEvent, render } from '@testing-library/react' import * as React from 'react' import { sleep } from '@tanstack/query-test-utils' import { QueryClient, useIsMutating, useMutation, useMutationState } from '..' import { renderWithClient } from './utils' describe('useIsMutating', () => { beforeEach(() => { vi.useFakeTimers() }) afterEach(() => { vi.useRealTimers() }) it('should return the number of fetching mutations', async () => { const isMutatingArray: Array = [] const queryClient = new QueryClient() function IsMutating() { const isMutating = useIsMutating() isMutatingArray.push(isMutating) return null } function Mutations() { const { mutate: mutate1 } = useMutation({ mutationKey: ['mutation1'], mutationFn: () => sleep(50).then(() => 'data'), }) const { mutate: mutate2 } = useMutation({ mutationKey: ['mutation2'], mutationFn: () => sleep(10).then(() => 'data'), }) return (
) } function Page() { return (
) } const rendered = renderWithClient(queryClient, ) fireEvent.click(rendered.getByRole('button', { name: /mutate1/i })) await vi.advanceTimersByTimeAsync(10) fireEvent.click(rendered.getByRole('button', { name: /mutate2/i })) // we don't really care if this yields // [ +0, 1, 2, +0 ] // or // [ +0, 1, 2, 1, +0 ] // our batching strategy might yield different results await vi.advanceTimersByTimeAsync(41) expect(isMutatingArray[0]).toEqual(0) expect(isMutatingArray[1]).toEqual(1) expect(isMutatingArray[2]).toEqual(2) expect(isMutatingArray[3]).toEqual(1) expect(isMutatingArray[4]).toEqual(0) expect(isMutatingArray).toEqual([0, 1, 2, 1, 0]) }) it('should filter correctly by mutationKey', async () => { const isMutatingArray: Array = [] const queryClient = new QueryClient() function IsMutating() { const isMutating = useIsMutating({ mutationKey: ['mutation1'] }) isMutatingArray.push(isMutating) return null } function Page() { const { mutate: mutate1 } = useMutation({ mutationKey: ['mutation1'], mutationFn: () => sleep(100).then(() => 'data'), }) const { mutate: mutate2 } = useMutation({ mutationKey: ['mutation2'], mutationFn: () => sleep(100).then(() => 'data'), }) React.useEffect(() => { mutate1() mutate2() }, [mutate1, mutate2]) return } renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(101) expect(isMutatingArray).toEqual([0, 1, 0]) }) it('should filter correctly by predicate', async () => { const isMutatingArray: Array = [] const queryClient = new QueryClient() function IsMutating() { const isMutating = useIsMutating({ predicate: (mutation) => mutation.options.mutationKey?.[0] === 'mutation1', }) isMutatingArray.push(isMutating) return null } function Page() { const { mutate: mutate1 } = useMutation({ mutationKey: ['mutation1'], mutationFn: () => sleep(100).then(() => 'data'), }) const { mutate: mutate2 } = useMutation({ mutationKey: ['mutation2'], mutationFn: () => sleep(100).then(() => 'data'), }) React.useEffect(() => { mutate1() mutate2() }, [mutate1, mutate2]) return } renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(101) expect(isMutatingArray).toEqual([0, 1, 0]) }) it('should use provided custom queryClient', async () => { const queryClient = new QueryClient() function Page() { const isMutating = useIsMutating({}, queryClient) const { mutate } = useMutation( { mutationKey: ['mutation1'], mutationFn: () => sleep(10).then(() => 'data'), }, queryClient, ) React.useEffect(() => { mutate() }, [mutate]) return (
mutating: {isMutating}
) } const rendered = render() await vi.advanceTimersByTimeAsync(0) expect(rendered.getByText('mutating: 1')).toBeInTheDocument() }) }) describe('useMutationState', () => { beforeEach(() => { vi.useFakeTimers() }) afterEach(() => { vi.useRealTimers() }) it('should return variables after calling mutate', async () => { const queryClient = new QueryClient() const variables: Array> = [] const mutationKey = ['mutation'] function Variables() { variables.push( useMutationState({ filters: { mutationKey, status: 'pending' }, select: (mutation) => mutation.state.variables, }), ) return null } function Mutate() { const { mutate, data } = useMutation({ mutationKey, mutationFn: (input: number) => sleep(150).then(() => 'data' + input), }) return (
data: {data ?? 'null'}
) } function Page() { return (
) } const rendered = renderWithClient(queryClient, ) expect(rendered.getByText('data: null')).toBeInTheDocument() fireEvent.click(rendered.getByRole('button', { name: /mutate/i })) await vi.advanceTimersByTimeAsync(151) expect(rendered.getByText('data: data1')).toBeInTheDocument() expect(variables).toEqual([[], [1], []]) }) }) ================================================ FILE: packages/react-query/src/__tests__/usePrefetchInfiniteQuery.test-d.tsx ================================================ import { assertType, describe, expectTypeOf, it } from 'vitest' import { usePrefetchInfiniteQuery } from '..' describe('usePrefetchInfiniteQuery', () => { it('should return nothing', () => { const result = usePrefetchInfiniteQuery({ queryKey: ['key'], queryFn: () => Promise.resolve(5), initialPageParam: 1, getNextPageParam: () => 1, }) expectTypeOf(result).toEqualTypeOf() }) it('should require initialPageParam and getNextPageParam', () => { assertType( // @ts-expect-error TS2345 usePrefetchInfiniteQuery({ queryKey: ['key'], queryFn: () => Promise.resolve(5), }), ) }) it('should not allow refetchInterval, enabled or throwOnError options', () => { assertType( usePrefetchInfiniteQuery({ queryKey: ['key'], queryFn: () => Promise.resolve(5), initialPageParam: 1, getNextPageParam: () => 1, // @ts-expect-error TS2353 refetchInterval: 1000, }), ) assertType( usePrefetchInfiniteQuery({ queryKey: ['key'], queryFn: () => Promise.resolve(5), initialPageParam: 1, getNextPageParam: () => 1, // @ts-expect-error TS2353 enabled: true, }), ) assertType( usePrefetchInfiniteQuery({ queryKey: ['key'], queryFn: () => Promise.resolve(5), initialPageParam: 1, getNextPageParam: () => 1, // @ts-expect-error TS2353 throwOnError: true, }), ) }) }) ================================================ FILE: packages/react-query/src/__tests__/usePrefetchInfiniteQuery.test.tsx ================================================ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import React from 'react' import { act, fireEvent } from '@testing-library/react' import { queryKey, sleep } from '@tanstack/query-test-utils' import { QueryCache, QueryClient, usePrefetchInfiniteQuery, useSuspenseInfiniteQuery, } from '..' import { renderWithClient } from './utils' import type { InfiniteData, UseSuspenseInfiniteQueryOptions } from '..' import type { Mock } from 'vitest' const generateInfiniteQueryOptions = ( data: Array<{ data: string; currentPage: number; totalPages: number }>, ) => { let currentPage = 0 return { queryFn: vi .fn<(...args: Array) => Promise<(typeof data)[number]>>() .mockImplementation(async () => { const currentPageData = data[currentPage] if (!currentPageData) { throw new Error('No data defined for page ' + currentPage) } await sleep(10) currentPage++ return currentPageData }), initialPageParam: 1, getNextPageParam: (lastPage: (typeof data)[number]) => lastPage.currentPage === lastPage.totalPages ? undefined : lastPage.currentPage + 1, } } describe('usePrefetchInfiniteQuery', () => { beforeEach(() => { vi.useFakeTimers() }) afterEach(() => { vi.useRealTimers() }) const queryCache = new QueryCache() const queryClient = new QueryClient({ queryCache }) const Fallback = vi.fn().mockImplementation(() =>
Loading...
) function Suspended(props: { queryOpts: UseSuspenseInfiniteQueryOptions< T, Error, InfiniteData, Array, any > renderPage: (page: T) => React.JSX.Element }) { const state = useSuspenseInfiniteQuery(props.queryOpts) return (
{state.data.pages.map((page, index) => (
{props.renderPage(page)}
))}
) } it('should prefetch an infinite query if query state does not exist', async () => { const data = [ { data: 'Do you fetch on render?', currentPage: 1, totalPages: 3 }, { data: 'Or do you render as you fetch?', currentPage: 2, totalPages: 3 }, { data: 'Either way, Tanstack Query helps you!', currentPage: 3, totalPages: 3, }, ] const queryOpts = { queryKey: queryKey(), ...generateInfiniteQueryOptions(data), } function App() { usePrefetchInfiniteQuery({ ...queryOpts, pages: data.length }) return ( }>
data: {page.data}
} />
) } const rendered = renderWithClient(queryClient, ) await act(() => vi.advanceTimersByTimeAsync(30)) rendered.getByText('data: Do you fetch on render?') fireEvent.click(rendered.getByText('Next Page')) expect( rendered.getByText('data: Or do you render as you fetch?'), ).toBeInTheDocument() fireEvent.click(rendered.getByText('Next Page')) expect( rendered.getByText('data: Either way, Tanstack Query helps you!'), ).toBeInTheDocument() expect(Fallback).toHaveBeenCalledTimes(1) expect(queryOpts.queryFn).toHaveBeenCalledTimes(3) }) it('should not display fallback if the query cache is already populated', async () => { const queryOpts = { queryKey: queryKey(), ...generateInfiniteQueryOptions([ { data: 'Prefetch rocks!', currentPage: 1, totalPages: 3 }, { data: 'No waterfalls, boy!', currentPage: 2, totalPages: 3 }, { data: 'Tanstack Query #ftw', currentPage: 3, totalPages: 3 }, ]), } queryClient.prefetchInfiniteQuery({ ...queryOpts, pages: 3 }) await vi.advanceTimersByTimeAsync(30) ;(queryOpts.queryFn as Mock).mockClear() function App() { usePrefetchInfiniteQuery(queryOpts) return ( }>
data: {page.data}
} />
) } const rendered = renderWithClient(queryClient, ) expect(rendered.getByText('data: Prefetch rocks!')).toBeInTheDocument() fireEvent.click(rendered.getByText('Next Page')) expect(rendered.getByText('data: No waterfalls, boy!')).toBeInTheDocument() fireEvent.click(rendered.getByText('Next Page')) expect(rendered.getByText('data: Tanstack Query #ftw')).toBeInTheDocument() expect(queryOpts.queryFn).not.toHaveBeenCalled() expect(Fallback).not.toHaveBeenCalled() }) it('should not create an endless loop when using inside a suspense boundary', async () => { const queryOpts = { queryKey: queryKey(), ...generateInfiniteQueryOptions([ { data: 'Infinite Page 1', currentPage: 1, totalPages: 3 }, { data: 'Infinite Page 2', currentPage: 1, totalPages: 3 }, { data: 'Infinite Page 3', currentPage: 1, totalPages: 3 }, ]), } function Prefetch({ children }: { children: React.ReactNode }) { usePrefetchInfiniteQuery(queryOpts) return <>{children} } function App() { return (
data: {page.data}
} />
) } const rendered = renderWithClient(queryClient, ) await act(() => vi.advanceTimersByTimeAsync(10)) rendered.getByText('data: Infinite Page 1') fireEvent.click(rendered.getByText('Next Page')) await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('data: Infinite Page 2')).toBeInTheDocument() fireEvent.click(rendered.getByText('Next Page')) await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('data: Infinite Page 3')).toBeInTheDocument() expect(queryOpts.queryFn).toHaveBeenCalledTimes(3) }) }) ================================================ FILE: packages/react-query/src/__tests__/usePrefetchQuery.test-d.tsx ================================================ import { assertType, describe, expectTypeOf, it } from 'vitest' import { skipToken, usePrefetchQuery } from '..' describe('usePrefetchQuery', () => { it('should return nothing', () => { const result = usePrefetchQuery({ queryKey: ['key'], queryFn: () => Promise.resolve(5), }) expectTypeOf(result).toEqualTypeOf() }) it('should not allow refetchInterval, enabled or throwOnError options', () => { assertType( usePrefetchQuery({ queryKey: ['key'], queryFn: () => Promise.resolve(5), // @ts-expect-error TS2345 refetchInterval: 1000, }), ) assertType( usePrefetchQuery({ queryKey: ['key'], queryFn: () => Promise.resolve(5), // @ts-expect-error TS2345 enabled: true, }), ) assertType( usePrefetchQuery({ queryKey: ['key'], queryFn: () => Promise.resolve(5), // @ts-expect-error TS2345 throwOnError: true, }), ) }) it('should not allow skipToken in queryFn', () => { assertType( usePrefetchQuery({ queryKey: ['key'], // @ts-expect-error queryFn: skipToken, }), ) assertType( usePrefetchQuery({ queryKey: ['key'], // @ts-expect-error queryFn: Math.random() > 0.5 ? skipToken : () => Promise.resolve(5), }), ) }) }) ================================================ FILE: packages/react-query/src/__tests__/usePrefetchQuery.test.tsx ================================================ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import React from 'react' import { act, fireEvent } from '@testing-library/react' import { ErrorBoundary } from 'react-error-boundary' import { queryKey, sleep } from '@tanstack/query-test-utils' import { QueryCache, QueryClient, usePrefetchQuery, useQueryErrorResetBoundary, useSuspenseQuery, } from '..' import { renderWithClient } from './utils' import type { UseSuspenseQueryOptions } from '..' const generateQueryFn = (data: string) => vi .fn<(...args: Array) => Promise>() .mockImplementation(async () => { await sleep(10) return data }) describe('usePrefetchQuery', () => { const queryCache = new QueryCache() const queryClient = new QueryClient({ queryCache }) beforeEach(() => { vi.useFakeTimers() }) afterEach(() => { vi.useRealTimers() }) function Suspended(props: { queryOpts: UseSuspenseQueryOptions> children?: React.ReactNode }) { const state = useSuspenseQuery(props.queryOpts) return (
data: {String(state.data)}
{props.children}
) } it('should prefetch query if query state does not exist', async () => { const queryOpts = { queryKey: queryKey(), queryFn: generateQueryFn('prefetchQuery'), } const componentQueryOpts = { ...queryOpts, queryFn: generateQueryFn('useSuspenseQuery'), } function App() { usePrefetchQuery(queryOpts) return ( ) } const rendered = renderWithClient(queryClient, ) await act(() => vi.advanceTimersByTimeAsync(10)) expect(rendered.getByText('data: prefetchQuery')).toBeInTheDocument() expect(queryOpts.queryFn).toHaveBeenCalledTimes(1) }) it('should not prefetch query if query state exists', async () => { const queryOpts = { queryKey: queryKey(), queryFn: generateQueryFn('The usePrefetchQuery hook is smart!'), } function App() { usePrefetchQuery(queryOpts) return ( ) } queryClient.fetchQuery(queryOpts) await vi.advanceTimersByTimeAsync(10) queryOpts.queryFn.mockClear() const rendered = renderWithClient(queryClient, ) expect(rendered.queryByText('fetching: true')).not.toBeInTheDocument() expect( rendered.getByText('data: The usePrefetchQuery hook is smart!'), ).toBeInTheDocument() expect(queryOpts.queryFn).not.toHaveBeenCalled() }) it('should let errors fall through and not refetch failed queries', async () => { const consoleMock = vi.spyOn(console, 'error') consoleMock.mockImplementation(() => undefined) const queryFn = generateQueryFn('Not an error') const queryOpts = { queryKey: queryKey(), queryFn, } queryFn.mockImplementationOnce(async () => { await sleep(10) throw new Error('Oops! Server error!') }) function App() { usePrefetchQuery(queryOpts) return (
Oops!
}>
) } queryClient.prefetchQuery(queryOpts) await vi.advanceTimersByTimeAsync(10) queryFn.mockClear() const rendered = renderWithClient(queryClient, ) expect(rendered.getByText('Oops!')).toBeInTheDocument() expect(rendered.queryByText('data: Not an error')).not.toBeInTheDocument() expect(queryOpts.queryFn).not.toHaveBeenCalled() consoleMock.mockRestore() }) it('should not create an endless loop when using inside a suspense boundary', async () => { const queryFn = generateQueryFn('prefetchedQuery') const queryOpts = { queryKey: queryKey(), queryFn, } function Prefetch({ children }: { children: React.ReactNode }) { usePrefetchQuery(queryOpts) return <>{children} } function App() { return ( ) } const rendered = renderWithClient(queryClient, ) await act(() => vi.advanceTimersByTimeAsync(10)) expect(rendered.getByText('data: prefetchedQuery')).toBeInTheDocument() expect(queryOpts.queryFn).toHaveBeenCalledTimes(1) }) it('should be able to recover from errors and try fetching again', async () => { const consoleMock = vi.spyOn(console, 'error') consoleMock.mockImplementation(() => undefined) const queryFn = generateQueryFn('This is fine :dog: :fire:') const queryOpts = { queryKey: queryKey(), queryFn, } queryFn.mockImplementationOnce(async () => { await sleep(10) throw new Error('Oops! Server error!') }) function App() { const { reset } = useQueryErrorResetBoundary() usePrefetchQuery(queryOpts) return ( (
Oops!
)} >
) } queryClient.prefetchQuery(queryOpts) await vi.advanceTimersByTimeAsync(10) queryFn.mockClear() const rendered = renderWithClient(queryClient, ) expect(rendered.getByText('Oops!')).toBeInTheDocument() fireEvent.click(rendered.getByText('Try again')) await act(() => vi.advanceTimersByTimeAsync(10)) expect( rendered.getByText('data: This is fine :dog: :fire:'), ).toBeInTheDocument() expect(queryOpts.queryFn).toHaveBeenCalledTimes(1) consoleMock.mockRestore() }) it('should not create a suspense waterfall if prefetch is fired', async () => { const firstQueryOpts = { queryKey: queryKey(), queryFn: generateQueryFn('Prefetch is nice!'), } const secondQueryOpts = { queryKey: queryKey(), queryFn: generateQueryFn('Prefetch is really nice!!'), } const thirdQueryOpts = { queryKey: queryKey(), queryFn: generateQueryFn('Prefetch does not create waterfalls!!'), } const Fallback = vi.fn().mockImplementation(() =>
Loading...
) function App() { usePrefetchQuery(firstQueryOpts) usePrefetchQuery(secondQueryOpts) usePrefetchQuery(thirdQueryOpts) return ( }> ) } const rendered = renderWithClient(queryClient, ) expect( queryClient.getQueryState(firstQueryOpts.queryKey)?.fetchStatus, ).toBe('fetching') expect( queryClient.getQueryState(secondQueryOpts.queryKey)?.fetchStatus, ).toBe('fetching') expect( queryClient.getQueryState(thirdQueryOpts.queryKey)?.fetchStatus, ).toBe('fetching') expect(rendered.getByText('Loading...')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(10)) expect(rendered.getByText('data: Prefetch is nice!')).toBeInTheDocument() expect( rendered.getByText('data: Prefetch is really nice!!'), ).toBeInTheDocument() expect( rendered.getByText('data: Prefetch does not create waterfalls!!'), ).toBeInTheDocument() expect(Fallback).toHaveBeenCalledTimes(1) expect(firstQueryOpts.queryFn).toHaveBeenCalledTimes(1) expect(secondQueryOpts.queryFn).toHaveBeenCalledTimes(1) expect(thirdQueryOpts.queryFn).toHaveBeenCalledTimes(1) }) }) ================================================ FILE: packages/react-query/src/__tests__/useQueries.test-d.tsx ================================================ import { describe, expectTypeOf, it } from 'vitest' import { skipToken } from '..' import { useQueries } from '../useQueries' import { queryOptions } from '../queryOptions' import type { OmitKeyof } from '..' import type { UseQueryOptions, UseQueryResult } from '../types' describe('UseQueries config object overload', () => { it('TData should always be defined when initialData is provided as an object', () => { const query1 = { queryKey: ['key1'], queryFn: () => { return { wow: true, } }, initialData: { wow: false, }, } const query2 = { queryKey: ['key2'], queryFn: () => 'Query Data', initialData: 'initial data', } const query3 = { queryKey: ['key2'], queryFn: () => 'Query Data', } const queryResults = useQueries({ queries: [query1, query2, query3] }) const query1Data = queryResults[0].data const query2Data = queryResults[1].data const query3Data = queryResults[2].data expectTypeOf(query1Data).toEqualTypeOf<{ wow: boolean }>() expectTypeOf(query2Data).toEqualTypeOf() expectTypeOf(query3Data).toEqualTypeOf() }) it('TData should be defined when passed through queryOptions', () => { const options = queryOptions({ queryKey: ['key'], queryFn: () => { return { wow: true, } }, initialData: { wow: true, }, }) const queryResults = useQueries({ queries: [options] }) const data = queryResults[0].data expectTypeOf(data).toEqualTypeOf<{ wow: boolean }>() }) it('should be possible to define a different TData than TQueryFnData using select with queryOptions spread into useQuery', () => { const query1 = queryOptions({ queryKey: ['key'], queryFn: () => Promise.resolve(1), select: (data) => data > 1, }) const query2 = { queryKey: ['key'], queryFn: () => Promise.resolve(1), select: (data: number) => data > 1, } const queryResults = useQueries({ queries: [query1, query2] }) const query1Data = queryResults[0].data const query2Data = queryResults[1].data expectTypeOf(query1Data).toEqualTypeOf() expectTypeOf(query2Data).toEqualTypeOf() }) it('TData should have undefined in the union when initialData is provided as a function which can return undefined', () => { const queryResults = useQueries({ queries: [ { queryKey: ['key'], queryFn: () => { return { wow: true, } }, initialData: () => undefined as { wow: boolean } | undefined, }, ], }) const data = queryResults[0].data expectTypeOf(data).toEqualTypeOf<{ wow: boolean } | undefined>() }) describe('custom hook', () => { it('should allow custom hooks using UseQueryOptions', () => { type Data = string const useCustomQueries = ( options?: OmitKeyof, 'queryKey' | 'queryFn'>, ) => { return useQueries({ queries: [ { ...options, queryKey: ['todos-key'], queryFn: () => Promise.resolve('data'), }, ], }) } const queryResults = useCustomQueries() const data = queryResults[0].data expectTypeOf(data).toEqualTypeOf() }) }) it('TData should have correct type when conditional skipToken is passed', () => { const queryResults = useQueries({ queries: [ { queryKey: ['withSkipToken'], queryFn: Math.random() > 0.5 ? skipToken : () => Promise.resolve(5), }, ], }) const firstResult = queryResults[0] expectTypeOf(firstResult).toEqualTypeOf>() expectTypeOf(firstResult.data).toEqualTypeOf() }) it('should return correct data for dynamic queries with mixed result types', () => { const Queries1 = { get: () => queryOptions({ queryKey: ['key1'], queryFn: () => Promise.resolve(1), }), } const Queries2 = { get: () => queryOptions({ queryKey: ['key2'], queryFn: () => Promise.resolve(true), }), } const queries1List = [1, 2, 3].map(() => ({ ...Queries1.get() })) const result = useQueries({ queries: [...queries1List, { ...Queries2.get() }], }) expectTypeOf(result).toEqualTypeOf< [...Array>, UseQueryResult] >() }) }) ================================================ FILE: packages/react-query/src/__tests__/useQueries.test.tsx ================================================ import { afterEach, beforeEach, describe, expect, expectTypeOf, it, vi, } from 'vitest' import { fireEvent, render } from '@testing-library/react' import * as React from 'react' import { ErrorBoundary } from 'react-error-boundary' import { queryKey, sleep } from '@tanstack/query-test-utils' import { QueryCache, QueryClient, queryOptions, skipToken, useQueries, } from '..' import { renderWithClient } from './utils' import type { QueryFunction, QueryKey, QueryObserverResult, UseQueryOptions, UseQueryResult, } from '..' import type { QueryFunctionContext } from '@tanstack/query-core' describe('useQueries', () => { beforeEach(() => { vi.useFakeTimers() }) afterEach(() => { vi.useRealTimers() }) const queryCache = new QueryCache() const queryClient = new QueryClient({ queryCache }) it('should return the correct states', async () => { const key1 = queryKey() const key2 = queryKey() const results: Array> = [] function Page() { const result = useQueries({ queries: [ { queryKey: key1, queryFn: async () => { await sleep(10) return 1 }, }, { queryKey: key2, queryFn: async () => { await sleep(200) return 2 }, }, ], }) results.push(result) return (
data1: {String(result[0].data ?? 'null')}, data2:{' '} {String(result[1].data ?? 'null')}
) } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(201) expect(rendered.getByText('data1: 1, data2: 2')).toBeInTheDocument() expect(results.length).toBe(3) expect(results[0]).toMatchObject([{ data: undefined }, { data: undefined }]) expect(results[1]).toMatchObject([{ data: 1 }, { data: undefined }]) expect(results[2]).toMatchObject([{ data: 1 }, { data: 2 }]) }) it('should track results', async () => { const key1 = queryKey() const results: Array> = [] let count = 0 function Page() { const result = useQueries({ queries: [ { queryKey: key1, queryFn: async () => { await sleep(10) count++ return count }, }, ], }) results.push(result) return (
data: {String(result[0].data ?? 'null')}
) } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('data: 1')).toBeInTheDocument() expect(results.length).toBe(2) expect(results[0]).toMatchObject([{ data: undefined }]) expect(results[1]).toMatchObject([{ data: 1 }]) fireEvent.click(rendered.getByRole('button', { name: /refetch/i })) await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('data: 2')).toBeInTheDocument() // only one render for data update, no render for isFetching transition expect(results.length).toBe(3) expect(results[2]).toMatchObject([{ data: 2 }]) }) it('handles type parameter - tuple of tuples', () => { const key1 = queryKey() const key2 = queryKey() const key3 = queryKey() // @ts-expect-error (Page component is not rendered) function Page() { const result1 = useQueries< [[number], [string], [Array, boolean]] >({ queries: [ { queryKey: key1, queryFn: () => 1, }, { queryKey: key2, queryFn: () => 'string', }, { queryKey: key3, queryFn: () => ['string[]'], }, ], }) expectTypeOf(result1[0]).toEqualTypeOf>() expectTypeOf(result1[1]).toEqualTypeOf>() expectTypeOf(result1[2]).toEqualTypeOf< UseQueryResult, boolean> >() expectTypeOf(result1[0].data).toEqualTypeOf() expectTypeOf(result1[1].data).toEqualTypeOf() expectTypeOf(result1[2].data).toEqualTypeOf | undefined>() expectTypeOf(result1[2].error).toEqualTypeOf() // TData (3rd element) takes precedence over TQueryFnData (1st element) const result2 = useQueries< [[string, unknown, string], [string, unknown, number]] >({ queries: [ { queryKey: key1, queryFn: () => 'string', select: (a) => { expectTypeOf(a).toEqualTypeOf() return a.toLowerCase() }, }, { queryKey: key2, queryFn: () => 'string', select: (a) => { expectTypeOf(a).toEqualTypeOf() return parseInt(a) }, }, ], }) expectTypeOf(result2[0]).toEqualTypeOf>() expectTypeOf(result2[1]).toEqualTypeOf>() expectTypeOf(result2[0].data).toEqualTypeOf() expectTypeOf(result2[1].data).toEqualTypeOf() // types should be enforced useQueries<[[string, unknown, string], [string, boolean, number]]>({ queries: [ { queryKey: key1, queryFn: () => 'string', select: (a) => { expectTypeOf(a).toEqualTypeOf() return a.toLowerCase() }, placeholderData: 'string', // @ts-expect-error (initialData: string) initialData: 123, }, { queryKey: key2, queryFn: () => 'string', select: (a) => { expectTypeOf(a).toEqualTypeOf() return parseInt(a) }, placeholderData: 'string', // @ts-expect-error (initialData: string) initialData: 123, }, ], }) // field names should be enforced useQueries<[[string]]>({ queries: [ { queryKey: key1, queryFn: () => 'string', // @ts-expect-error (invalidField) someInvalidField: [], }, ], }) } }) it('handles type parameter - tuple of objects', () => { const key1 = queryKey() const key2 = queryKey() const key3 = queryKey() // @ts-expect-error (Page component is not rendered) function Page() { const result1 = useQueries< [ { queryFnData: number }, { queryFnData: string }, { queryFnData: Array; error: boolean }, ] >({ queries: [ { queryKey: key1, queryFn: () => 1, }, { queryKey: key2, queryFn: () => 'string', }, { queryKey: key3, queryFn: () => ['string[]'], }, ], }) expectTypeOf(result1[0]).toEqualTypeOf>() expectTypeOf(result1[1]).toEqualTypeOf>() expectTypeOf(result1[2]).toEqualTypeOf< UseQueryResult, boolean> >() expectTypeOf(result1[0].data).toEqualTypeOf() expectTypeOf(result1[1].data).toEqualTypeOf() expectTypeOf(result1[2].data).toEqualTypeOf | undefined>() expectTypeOf(result1[2].error).toEqualTypeOf() // TData (data prop) takes precedence over TQueryFnData (queryFnData prop) const result2 = useQueries< [ { queryFnData: string; data: string }, { queryFnData: string; data: number }, ] >({ queries: [ { queryKey: key1, queryFn: () => 'string', select: (a) => { expectTypeOf(a).toEqualTypeOf() return a.toLowerCase() }, }, { queryKey: key2, queryFn: () => 'string', select: (a) => { expectTypeOf(a).toEqualTypeOf() return parseInt(a) }, }, ], }) expectTypeOf(result2[0]).toEqualTypeOf>() expectTypeOf(result2[1]).toEqualTypeOf>() expectTypeOf(result2[0].data).toEqualTypeOf() expectTypeOf(result2[1].data).toEqualTypeOf() // can pass only TData (data prop) although TQueryFnData will be left unknown const result3 = useQueries<[{ data: string }, { data: number }]>({ queries: [ { queryKey: key1, queryFn: () => 'string', select: (a) => { expectTypeOf(a).toEqualTypeOf() return a as string }, }, { queryKey: key2, queryFn: () => 'string', select: (a) => { expectTypeOf(a).toEqualTypeOf() return a as number }, }, ], }) expectTypeOf(result3[0]).toEqualTypeOf>() expectTypeOf(result3[1]).toEqualTypeOf>() expectTypeOf(result3[0].data).toEqualTypeOf() expectTypeOf(result3[1].data).toEqualTypeOf() // types should be enforced useQueries< [ { queryFnData: string; data: string }, { queryFnData: string; data: number; error: boolean }, ] >({ queries: [ { queryKey: key1, queryFn: () => 'string', select: (a) => { expectTypeOf(a).toEqualTypeOf() return a.toLowerCase() }, placeholderData: 'string', // @ts-expect-error (initialData: string) initialData: 123, }, { queryKey: key2, queryFn: () => 'string', select: (a) => { expectTypeOf(a).toEqualTypeOf() return parseInt(a) }, placeholderData: 'string', // @ts-expect-error (initialData: string) initialData: 123, }, ], }) // field names should be enforced useQueries<[{ queryFnData: string }]>({ queries: [ { queryKey: key1, queryFn: () => 'string', // @ts-expect-error (invalidField) someInvalidField: [], }, ], }) } }) it('correctly returns types when passing through queryOptions', () => { // @ts-expect-error (Page component is not rendered) function Page() { // data and results types are correct when using queryOptions const result4 = useQueries({ queries: [ queryOptions({ queryKey: ['key1'], queryFn: () => 'string', select: (a) => { expectTypeOf(a).toEqualTypeOf() return a.toLowerCase() }, }), queryOptions({ queryKey: ['key2'], queryFn: () => 'string', select: (a) => { expectTypeOf(a).toEqualTypeOf() return parseInt(a) }, }), ], }) expectTypeOf(result4[0]).toEqualTypeOf>() expectTypeOf(result4[1]).toEqualTypeOf>() expectTypeOf(result4[0].data).toEqualTypeOf() expectTypeOf(result4[1].data).toEqualTypeOf() } }) it('handles array literal without type parameter to infer result type', () => { const key1 = queryKey() const key2 = queryKey() const key3 = queryKey() const key4 = queryKey() const key5 = queryKey() type BizError = { code: number } const throwOnError = (_error: BizError) => true // @ts-expect-error (Page component is not rendered) function Page() { // Array.map preserves TQueryFnData const result1 = useQueries({ queries: Array(50).map((_, i) => ({ queryKey: ['key', i] as const, queryFn: () => i + 10, })), }) expectTypeOf(result1).toEqualTypeOf< Array> >() if (result1[0]) { expectTypeOf(result1[0].data).toEqualTypeOf() } // Array.map preserves TError const result1_err = useQueries({ queries: Array(50).map((_, i) => ({ queryKey: ['key', i] as const, queryFn: () => i + 10, throwOnError, })), }) expectTypeOf(result1_err).toEqualTypeOf< Array> >() if (result1_err[0]) { expectTypeOf(result1_err[0].data).toEqualTypeOf() expectTypeOf(result1_err[0].error).toEqualTypeOf() } // Array.map preserves TData const result2 = useQueries({ queries: Array(50).map((_, i) => ({ queryKey: ['key', i] as const, queryFn: () => i + 10, select: (data: number) => data.toString(), })), }) expectTypeOf(result2).toEqualTypeOf< Array> >() const result2_err = useQueries({ queries: Array(50).map((_, i) => ({ queryKey: ['key', i] as const, queryFn: () => i + 10, select: (data: number) => data.toString(), throwOnError, })), }) expectTypeOf(result2_err).toEqualTypeOf< Array> >() const result3 = useQueries({ queries: [ { queryKey: key1, queryFn: () => 1, }, { queryKey: key2, queryFn: () => 'string', }, { queryKey: key3, queryFn: () => ['string[]'], select: () => 123, }, { queryKey: key5, queryFn: () => 'string', throwOnError, }, ], }) expectTypeOf(result3[0]).toEqualTypeOf>() expectTypeOf(result3[1]).toEqualTypeOf>() expectTypeOf(result3[2]).toEqualTypeOf>() expectTypeOf(result3[0].data).toEqualTypeOf() expectTypeOf(result3[1].data).toEqualTypeOf() expectTypeOf(result3[3].data).toEqualTypeOf() // select takes precedence over queryFn expectTypeOf(result3[2].data).toEqualTypeOf() // infer TError from throwOnError expectTypeOf(result3[3].error).toEqualTypeOf() // initialData/placeholderData are enforced useQueries({ queries: [ { queryKey: key1, queryFn: () => 'string', placeholderData: 'string', // @ts-expect-error (initialData: string) initialData: 123, }, { queryKey: key2, queryFn: () => 123, // @ts-expect-error (placeholderData: number) placeholderData: 'string', initialData: 123, }, ], }) // select and throwOnError params are "indirectly" enforced useQueries({ queries: [ // unfortunately TS will not suggest the type for you { queryKey: key1, queryFn: () => 'string', }, // however you can add a type to the callback { queryKey: key2, queryFn: () => 'string', }, // the type you do pass is enforced { queryKey: key3, queryFn: () => 'string', }, { queryKey: key4, queryFn: () => 'string', select: (a: string) => parseInt(a), }, { queryKey: key5, queryFn: () => 'string', throwOnError, }, ], }) // callbacks are also indirectly enforced with Array.map useQueries({ queries: Array(50).map((_, i) => ({ queryKey: ['key', i] as const, queryFn: () => i + 10, select: (data: number) => data.toString(), })), }) useQueries({ queries: Array(50).map((_, i) => ({ queryKey: ['key', i] as const, queryFn: () => i + 10, select: (data: number) => data.toString(), })), }) // results inference works when all the handlers are defined const result4 = useQueries({ queries: [ { queryKey: key1, queryFn: () => 'string', }, { queryKey: key2, queryFn: () => 'string', }, { queryKey: key4, queryFn: () => 'string', select: (a: string) => parseInt(a), }, { queryKey: key5, queryFn: () => 'string', select: (a: string) => parseInt(a), throwOnError, }, ], }) expectTypeOf(result4[0]).toEqualTypeOf>() expectTypeOf(result4[1]).toEqualTypeOf>() expectTypeOf(result4[2]).toEqualTypeOf>() expectTypeOf(result4[3]).toEqualTypeOf>() // handles when queryFn returns a Promise const result5 = useQueries({ queries: [ { queryKey: key1, queryFn: () => Promise.resolve('string'), }, ], }) expectTypeOf(result5[0]).toEqualTypeOf>() // Array as const does not throw error const result6 = useQueries({ queries: [ { queryKey: ['key1'], queryFn: () => 'string', }, { queryKey: ['key1'], queryFn: () => 123, }, { queryKey: key5, queryFn: () => 'string', throwOnError, }, ], } as const) expectTypeOf(result6[0]).toEqualTypeOf>() expectTypeOf(result6[1]).toEqualTypeOf>() expectTypeOf(result6[2]).toEqualTypeOf>() // field names should be enforced - array literal useQueries({ queries: [ { queryKey: key1, queryFn: () => 'string', // @ts-expect-error (invalidField) someInvalidField: [], }, ], }) // field names should be enforced - Array.map() result useQueries({ // @ts-expect-error (invalidField) queries: Array(10).map(() => ({ someInvalidField: '', })), }) // field names should be enforced - array literal useQueries({ queries: [ { queryKey: key1, queryFn: () => 'string', // @ts-expect-error (invalidField) someInvalidField: [], }, ], }) // supports queryFn using fetch() to return Promise - Array.map() result useQueries({ queries: Array(50).map((_, i) => ({ queryKey: ['key', i] as const, queryFn: () => fetch('return Promise').then((resp) => resp.json()), })), }) // supports queryFn using fetch() to return Promise - array literal useQueries({ queries: [ { queryKey: key1, queryFn: () => fetch('return Promise').then((resp) => resp.json()), }, ], }) } }) it('handles strongly typed queryFn factories and useQueries wrappers', () => { // QueryKey + queryFn factory type QueryKeyA = ['queryA'] const getQueryKeyA = (): QueryKeyA => ['queryA'] type GetQueryFunctionA = () => QueryFunction const getQueryFunctionA: GetQueryFunctionA = () => () => { return Promise.resolve(1) } type SelectorA = (data: number) => [number, string] const getSelectorA = (): SelectorA => (data) => [data, data.toString()] type QueryKeyB = ['queryB', string] const getQueryKeyB = (id: string): QueryKeyB => ['queryB', id] type GetQueryFunctionB = () => QueryFunction const getQueryFunctionB: GetQueryFunctionB = () => () => { return Promise.resolve('1') } type SelectorB = (data: string) => [string, number] const getSelectorB = (): SelectorB => (data) => [data, +data] // Wrapper with strongly typed array-parameter function useWrappedQueries< TQueryFnData, TError, TData, TQueryKey extends QueryKey, >(queries: Array>) { return useQueries({ queries: queries.map( // no need to type the mapped query (query) => { const { queryFn: fn, queryKey: key } = query expectTypeOf(fn).toEqualTypeOf< | typeof skipToken | QueryFunction | undefined >() return { queryKey: key, queryFn: fn && fn !== skipToken ? (ctx: QueryFunctionContext) => { // eslint-disable-next-line vitest/valid-expect expectTypeOf(ctx.queryKey) return fn.call({}, ctx) } : undefined, } }, ), }) } // @ts-expect-error (Page component is not rendered) function Page() { const result = useQueries({ queries: [ { queryKey: getQueryKeyA(), queryFn: getQueryFunctionA(), }, { queryKey: getQueryKeyB('id'), queryFn: getQueryFunctionB(), }, ], }) expectTypeOf(result[0]).toEqualTypeOf>() expectTypeOf(result[1]).toEqualTypeOf>() const withSelector = useQueries({ queries: [ { queryKey: getQueryKeyA(), queryFn: getQueryFunctionA(), select: getSelectorA(), }, { queryKey: getQueryKeyB('id'), queryFn: getQueryFunctionB(), select: getSelectorB(), }, ], }) expectTypeOf(withSelector[0]).toEqualTypeOf< UseQueryResult<[number, string], Error> >() expectTypeOf(withSelector[1]).toEqualTypeOf< UseQueryResult<[string, number], Error> >() const withWrappedQueries = useWrappedQueries( Array(10).map(() => ({ queryKey: getQueryKeyA(), queryFn: getQueryFunctionA(), select: getSelectorA(), })), ) expectTypeOf(withWrappedQueries).toEqualTypeOf< Array> >() } }) it("should throw error if in one of queries' queryFn throws and throwOnError is in use", async () => { const consoleMock = vi .spyOn(console, 'error') .mockImplementation(() => undefined) const key1 = queryKey() const key2 = queryKey() const key3 = queryKey() const key4 = queryKey() function Page() { useQueries({ queries: [ { queryKey: key1, queryFn: () => Promise.reject( new Error( 'this should not throw because throwOnError is not set', ), ), }, { queryKey: key2, queryFn: () => Promise.reject(new Error('single query error')), throwOnError: true, retry: false, }, { queryKey: key3, queryFn: () => Promise.resolve(2), }, { queryKey: key4, queryFn: async () => Promise.reject( new Error('this should not throw because query#2 already did'), ), throwOnError: true, retry: false, }, ], }) return null } const rendered = renderWithClient( queryClient, (
error boundary
{error.message}
)} >
, ) await vi.advanceTimersByTimeAsync(0) expect(rendered.getByText('error boundary')).toBeInTheDocument() expect(rendered.getByText('single query error')).toBeInTheDocument() consoleMock.mockRestore() }) it("should throw error if in one of queries' queryFn throws and throwOnError function resolves to true", async () => { const consoleMock = vi .spyOn(console, 'error') .mockImplementation(() => undefined) const key1 = queryKey() const key2 = queryKey() const key3 = queryKey() const key4 = queryKey() function Page() { useQueries({ queries: [ { queryKey: key1, queryFn: () => Promise.reject( new Error( 'this should not throw because throwOnError function resolves to false', ), ), throwOnError: () => false, retry: false, }, { queryKey: key2, queryFn: () => Promise.resolve(2), }, { queryKey: key3, queryFn: () => Promise.reject(new Error('single query error')), throwOnError: () => true, retry: false, }, { queryKey: key4, queryFn: async () => Promise.reject( new Error('this should not throw because query#3 already did'), ), throwOnError: true, retry: false, }, ], }) return null } const rendered = renderWithClient( queryClient, (
error boundary
{error.message}
)} >
, ) await vi.advanceTimersByTimeAsync(0) expect(rendered.getByText('error boundary')).toBeInTheDocument() expect(rendered.getByText('single query error')).toBeInTheDocument() consoleMock.mockRestore() }) it('should use provided custom queryClient', async () => { const key = queryKey() const queryFn = async () => { return Promise.resolve('custom client') } function Page() { const queries = useQueries( { queries: [ { queryKey: key, queryFn, }, ], }, queryClient, ) return
data: {queries[0].data}
} const rendered = render() await vi.advanceTimersByTimeAsync(0) expect(rendered.getByText('data: custom client')).toBeInTheDocument() }) it('should combine queries', async () => { const key1 = queryKey() const key2 = queryKey() function Page() { const queries = useQueries( { queries: [ { queryKey: key1, queryFn: () => Promise.resolve('first result'), }, { queryKey: key2, queryFn: () => Promise.resolve('second result'), }, ], combine: (results) => { return { combined: true, res: results.map((res) => res.data).join(','), } }, }, queryClient, ) return (
data: {String(queries.combined)} {queries.res}
) } const rendered = render() await vi.advanceTimersByTimeAsync(0) expect( rendered.getByText('data: true first result,second result'), ).toBeInTheDocument() }) it('should not return new instances when called without queries', async () => { const key = queryKey() const ids: Array = [] let resultChanged = 0 function Page() { const [count, setCount] = React.useState(0) const result = useQueries({ queries: ids.map((id) => { return { queryKey: [key, id], queryFn: () => { return () => { return Promise.resolve({ id, content: { value: Math.random() }, }) } }, } }), combine: () => ({ empty: 'object' }), }) React.useEffect(() => { resultChanged++ }, [result]) return (
count: {count}
data: {JSON.stringify(result)}
) } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(0) expect(rendered.getByText('data: {"empty":"object"}')).toBeInTheDocument() expect(rendered.getByText('count: 0')).toBeInTheDocument() expect(resultChanged).toBe(1) fireEvent.click(rendered.getByRole('button', { name: /inc/i })) await vi.advanceTimersByTimeAsync(0) expect(rendered.getByText('count: 1')).toBeInTheDocument() // there should be no further effect calls because the returned object is structurally shared expect(resultChanged).toBe(1) }) it('should not have infinite render loops with empty queries (#6645)', () => { let renderCount = 0 function Page() { const result = useQueries({ queries: [], }) React.useEffect(() => { renderCount++ }) return
data: {JSON.stringify(result)}
} renderWithClient(queryClient, ) expect(renderCount).toBe(1) }) it('should only call combine with query results', async () => { const key1 = queryKey() const key2 = queryKey() function Page() { const result = useQueries({ queries: [ { queryKey: key1, queryFn: async () => { await sleep(5) return Promise.resolve('query1') }, }, { queryKey: key2, queryFn: async () => { await sleep(20) return Promise.resolve('query2') }, }, ], combine: ([query1, query2]) => { return { data: { query1: query1.data, query2: query2.data }, } }, }) return
data: {JSON.stringify(result)}
} const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(21) expect( rendered.getByText( 'data: {"data":{"query1":"query1","query2":"query2"}}', ), ).toBeInTheDocument() }) it('should track property access through combine function', async () => { const key1 = queryKey() const key2 = queryKey() let count = 0 const results: Array = [] function Page() { const queries = useQueries( { queries: [ { queryKey: key1, queryFn: async () => { await sleep(5) return Promise.resolve('first result ' + count) }, }, { queryKey: key2, queryFn: async () => { await sleep(50) return Promise.resolve('second result ' + count) }, }, ], combine: (queryResults) => { return { combined: true, refetch: () => queryResults.forEach((res) => res.refetch()), res: queryResults .flatMap((res) => (res.data ? [res.data] : [])) .join(','), } }, }, queryClient, ) results.push(queries) return (
data: {String(queries.combined)} {queries.res}
) } const rendered = render() await vi.advanceTimersByTimeAsync(51) expect( rendered.getByText('data: true first result 0,second result 0'), ).toBeInTheDocument() expect(results.length).toBe(3) expect(results[0]).toStrictEqual({ combined: true, refetch: expect.any(Function), res: '', }) expect(results[1]).toStrictEqual({ combined: true, refetch: expect.any(Function), res: 'first result 0', }) expect(results[2]).toStrictEqual({ combined: true, refetch: expect.any(Function), res: 'first result 0,second result 0', }) count++ fireEvent.click(rendered.getByRole('button', { name: /refetch/i })) await vi.advanceTimersByTimeAsync(51) expect( rendered.getByText('data: true first result 1,second result 1'), ).toBeInTheDocument() const length = results.length expect([4, 5, 6]).toContain(results.length) expect(results[results.length - 1]).toStrictEqual({ combined: true, refetch: expect.any(Function), res: 'first result 1,second result 1', }) fireEvent.click(rendered.getByRole('button', { name: /refetch/i })) await vi.advanceTimersByTimeAsync(100) // no further re-render because data didn't change expect(results.length).toBe(length) }) it('should synchronously track properties of all observer even if a property (isLoading) is only accessed on one observer (#7000)', async () => { const key = queryKey() const ids = [1, 2] function Page() { const { isLoading } = useQueries({ queries: ids.map((id) => ({ queryKey: [key, id], queryFn: () => sleep(10).then(() => { if (id === 2) throw new Error('FAILURE') return { id, title: `Post ${id}` } }), retry: false, })), combine: (results) => { // this tracks data on all observers void results.forEach((result) => result.data) return { // .some aborts early, so `isLoading` might not be accessed (and thus tracked) on all observers // leading to missing re-renders isLoading: results.some((result) => result.isLoading), } }, }) return (

Loading Status: {isLoading ? 'Loading...' : 'Loaded'}

) } const rendered = renderWithClient(queryClient, ) expect(rendered.getByText('Loading Status: Loading...')).toBeInTheDocument() await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('Loading Status: Loaded')).toBeInTheDocument() }) it('should not have stale closures with combine (#6648)', async () => { const key = queryKey() function Page() { const [count, setCount] = React.useState(0) const queries = useQueries( { queries: [ { queryKey: key, queryFn: () => Promise.resolve('result'), }, ], combine: (results) => { return { count, res: results.map((res) => res.data).join(','), } }, }, queryClient, ) return (
data: {String(queries.count)} {queries.res}
) } const rendered = render() await vi.advanceTimersByTimeAsync(0) expect(rendered.getByText('data: 0 result')).toBeInTheDocument() fireEvent.click(rendered.getByRole('button', { name: /inc/i })) await vi.advanceTimersByTimeAsync(0) expect(rendered.getByText('data: 1 result')).toBeInTheDocument() }) it('should optimize combine if it is a stable reference', async () => { const key1 = queryKey() const key2 = queryKey() const client = new QueryClient() const spy = vi.fn() let value = 0 function Page() { const [state, setState] = React.useState(0) const queries = useQueries( { queries: [ { queryKey: key1, queryFn: async () => { await sleep(10) return 'first result:' + value }, }, { queryKey: key2, queryFn: async () => { await sleep(20) return 'second result:' + value }, }, ], combine: React.useCallback((results: Array) => { const result = { combined: true, res: results.map((res) => res.data).join(','), } spy(result) return result }, []), }, client, ) return (
data: {String(queries.combined)} {queries.res}
) } const rendered = render() await vi.advanceTimersByTimeAsync(21) expect( rendered.getByText('data: true first result:0,second result:0'), ).toBeInTheDocument() // both pending, one pending, both resolved expect(spy).toHaveBeenCalledTimes(3) client.refetchQueries() await vi.advanceTimersByTimeAsync(21) // no increase because result hasn't changed expect(spy).toHaveBeenCalledTimes(3) fireEvent.click(rendered.getByRole('button', { name: /rerender/i })) // one extra call due to recomputing the combined result on rerender expect(spy).toHaveBeenCalledTimes(4) value = 1 client.refetchQueries() await vi.advanceTimersByTimeAsync(21) expect( rendered.getByText('data: true first result:1,second result:1'), ).toBeInTheDocument() // refetch with new values triggers: both pending -> one pending -> both resolved expect(spy).toHaveBeenCalledTimes(7) }) it('should re-run combine if the functional reference changes', async () => { const key1 = queryKey() const key2 = queryKey() const client = new QueryClient() const spy = vi.fn() function Page() { const [state, setState] = React.useState(0) const queries = useQueries( { queries: [ { queryKey: [key1], queryFn: async () => { await sleep(10) return 'first result' }, }, { queryKey: [key2], queryFn: async () => { await sleep(20) return 'second result' }, }, ], combine: React.useCallback( (results: Array) => { const result = { combined: true, state, res: results.map((res) => res.data).join(','), } spy(result) return result }, [state], ), }, client, ) return (
data: {String(queries.state)} {queries.res}
) } const rendered = render() await vi.advanceTimersByTimeAsync(21) expect( rendered.getByText('data: 0 first result,second result'), ).toBeInTheDocument() // both pending, one pending, both resolved expect(spy).toHaveBeenCalledTimes(3) fireEvent.click(rendered.getByRole('button', { name: /rerender/i })) // state changed, re-run combine expect(spy).toHaveBeenCalledTimes(4) }) it('should not re-render if combine returns a stable reference', async () => { const key1 = queryKey() const key2 = queryKey() const client = new QueryClient() const queryFns: Array = [] let renders = 0 function Page() { const data = useQueries( { queries: [ { queryKey: [key1], queryFn: async () => { await sleep(10) queryFns.push('first result') return 'first result' }, }, { queryKey: [key2], queryFn: async () => { await sleep(20) queryFns.push('second result') return 'second result' }, }, ], combine: () => 'foo', }, client, ) renders++ return (
data: {data}
) } const rendered = render() await vi.advanceTimersByTimeAsync(21) expect(rendered.getByText('data: foo')).toBeInTheDocument() expect(queryFns).toEqual(['first result', 'second result']) expect(renders).toBe(1) }) it('should re-render once combine returns a different reference', async () => { const key1 = queryKey() const key2 = queryKey() const key3 = queryKey() const client = new QueryClient() let renders = 0 function Page() { const data = useQueries( { queries: [ { queryKey: [key1], queryFn: async () => { await sleep(10) return 'first result' }, }, { queryKey: [key2], queryFn: async () => { await sleep(15) return 'second result' }, }, { queryKey: [key3], queryFn: async () => { await sleep(20) return 'third result' }, }, ], combine: (results) => { const isPending = results.some((res) => res.isPending) return isPending ? 'pending' : 'foo' }, }, client, ) renders++ return (
data: {data}
) } const rendered = render() await vi.advanceTimersByTimeAsync(0) expect(rendered.getByText('data: pending')).toBeInTheDocument() await vi.advanceTimersByTimeAsync(21) expect(rendered.getByText('data: foo')).toBeInTheDocument() // one with pending, one with foo expect(renders).toBe(2) }) it('should track properties correctly with combine', async () => { const key1 = queryKey() const key2 = queryKey() const key3 = queryKey() const client = new QueryClient() function Page() { const data = useQueries( { queries: [ { queryKey: [key1], queryFn: async () => { await sleep(10) return 'first result' }, }, { queryKey: [key2], queryFn: async () => { await sleep(15) return 'second result' }, }, { queryKey: [key3], queryFn: async () => { await sleep(20) return 'third result' }, }, ], combine: (results) => { if (results.find((r) => r.isPending)) { return 'pending' } return results.map((r) => r.data).join(', ') }, }, client, ) return (
data: {data}
) } const rendered = render() await vi.advanceTimersByTimeAsync(0) expect(rendered.getByText('data: pending')).toBeInTheDocument() await vi.advanceTimersByTimeAsync(21) expect( rendered.getByText('data: first result, second result, third result'), ).toBeInTheDocument() fireEvent.click(rendered.getByRole('button', { name: /update/i })) await vi.advanceTimersByTimeAsync(21) expect( rendered.getByText( 'data: first result updated, second result, third result', ), ).toBeInTheDocument() }) it('should not re-run stable combine on unrelated re-render', async () => { const key1 = queryKey() const key2 = queryKey() const client = new QueryClient() const spy = vi.fn() function Page() { const [unrelatedState, setUnrelatedState] = React.useState(0) const queries = useQueries( { queries: [ { queryKey: key1, queryFn: async () => { await sleep(10) return 'first result' }, }, { queryKey: key2, queryFn: async () => { await sleep(20) return 'second result' }, }, ], combine: React.useCallback((results: Array) => { const result = { combined: true, res: results.map((res) => res.data).join(','), } spy(result) return result }, []), }, client, ) return (
data: {String(queries.combined)} {queries.res}
unrelated: {unrelatedState}
) } const rendered = render() await vi.advanceTimersByTimeAsync(21) expect( rendered.getByText('data: true first result,second result'), ).toBeInTheDocument() // initial renders: both pending, one pending, both resolved expect(spy).toHaveBeenCalledTimes(3) fireEvent.click(rendered.getByRole('button', { name: /increment/i })) await vi.advanceTimersByTimeAsync(0) expect(rendered.getByText('unrelated: 1')).toBeInTheDocument() // combine should NOT re-run for unrelated re-render with stable reference expect(spy).toHaveBeenCalledTimes(3) fireEvent.click(rendered.getByRole('button', { name: /increment/i })) await vi.advanceTimersByTimeAsync(0) expect(rendered.getByText('unrelated: 2')).toBeInTheDocument() // still no extra calls to combine expect(spy).toHaveBeenCalledTimes(3) }) it('should not cause infinite re-renders when removing last query', async () => { let renderCount = 0 function Page() { const [queries, setQueries] = React.useState([ { queryKey: ['query1'], queryFn: () => 'data1', }, { queryKey: ['query2'], queryFn: () => 'data2', }, ]) renderCount++ const result = useQueries({ queries }) return (
renders: {renderCount}
queries: {result.length}
) } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(0) renderCount = 0 fireEvent.click(rendered.getByRole('button', { name: /remove last/i })) await vi.advanceTimersByTimeAsync(100) expect(renderCount).toBeLessThan(10) expect(rendered.getByTestId('query-count').textContent).toBe('queries: 1') renderCount = 0 fireEvent.click(rendered.getByRole('button', { name: /remove first/i })) await vi.advanceTimersByTimeAsync(100) expect(renderCount).toBeLessThan(10) expect(rendered.getByTestId('query-count').textContent).toBe('queries: 1') }) }) ================================================ FILE: packages/react-query/src/__tests__/useQuery.promise.test.tsx ================================================ import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest' import * as React from 'react' import { ErrorBoundary } from 'react-error-boundary' import { createRenderStream, useTrackRenders, } from '@testing-library/react-render-stream' import { queryKey } from '@tanstack/query-test-utils' import { QueryClient, QueryClientProvider, QueryErrorResetBoundary, keepPreviousData, useQuery, } from '..' import { QueryCache } from '../index' describe('useQuery().promise', () => { const queryCache = new QueryCache() const queryClient = new QueryClient({ queryCache, }) beforeAll(() => { vi.useFakeTimers({ shouldAdvanceTime: true }) queryClient.setDefaultOptions({ queries: { experimental_prefetchInRender: true }, }) }) afterAll(() => { vi.useRealTimers() queryClient.setDefaultOptions({ queries: { experimental_prefetchInRender: false }, }) }) it('should work with a basic test', async () => { const key = queryKey() const renderStream = createRenderStream({ snapshotDOM: true }) function MyComponent(props: { promise: Promise }) { const data = React.use(props.promise) useTrackRenders() return <>{data} } function Loading() { useTrackRenders() return <>loading.. } function Page() { useTrackRenders() const query = useQuery({ queryKey: key, queryFn: async () => { await vi.advanceTimersByTimeAsync(1) return 'test' }, }) return ( }>
status:{query.status}
) } await renderStream.render( , ) { const { renderedComponents, withinDOM } = await renderStream.takeRender() withinDOM().getByText('loading..') expect(renderedComponents).toEqual([Page, Loading]) } { const { renderedComponents, withinDOM } = await renderStream.takeRender() withinDOM().getByText('test') expect(renderedComponents).toEqual([Page, MyComponent]) } }) it('colocate suspense and promise', async () => { const key = queryKey() let callCount = 0 const renderStream = createRenderStream({ snapshotDOM: true }) function MyComponent() { useTrackRenders() const query = useQuery({ queryKey: key, queryFn: async () => { callCount++ await vi.advanceTimersByTimeAsync(1) return 'test' }, staleTime: 1000, }) const data = React.use(query.promise) return <>{data} } function Loading() { useTrackRenders() return <>loading.. } function Page() { useTrackRenders() return ( }> ) } await renderStream.render( , ) { const { renderedComponents, withinDOM } = await renderStream.takeRender() withinDOM().getByText('loading..') expect(renderedComponents).toEqual([Page, Loading]) } { const { renderedComponents, withinDOM } = await renderStream.takeRender() withinDOM().getByText('test') expect(renderedComponents).toEqual([MyComponent]) } expect(callCount).toBe(1) }) it('parallel queries', async () => { const key = queryKey() const renderStream = createRenderStream({ snapshotDOM: true }) let callCount = 0 function MyComponent() { useTrackRenders() const query = useQuery({ queryKey: key, queryFn: async () => { callCount++ await vi.advanceTimersByTimeAsync(1) return 'test' }, staleTime: 1000, }) const data = React.use(query.promise) return <>{data} } function Loading() { useTrackRenders() return <>loading.. } function Page() { useTrackRenders() return ( <> }> ) } await renderStream.render( , ) { const { renderedComponents, withinDOM } = await renderStream.takeRender() withinDOM().getByText('loading..') expect(renderedComponents).toEqual([Page, Loading]) } { const { renderedComponents, withinDOM } = await renderStream.takeRender() withinDOM().getByText('testtesttesttesttest') expect(renderedComponents).toEqual([ MyComponent, MyComponent, MyComponent, MyComponent, MyComponent, ]) } expect(callCount).toBe(1) }) it('should work with initial data', async () => { const key = queryKey() const renderStream = createRenderStream({ snapshotDOM: true }) function MyComponent(props: { promise: Promise }) { useTrackRenders() const data = React.use(props.promise) return <>{data} } function Loading() { useTrackRenders() return <>loading.. } function Page() { useTrackRenders() const query = useQuery({ queryKey: key, queryFn: async () => { await vi.advanceTimersByTimeAsync(1) return 'test' }, initialData: 'initial', }) return ( }> ) } await renderStream.render( , ) { const { renderedComponents, withinDOM } = await renderStream.takeRender() withinDOM().getByText('initial') expect(renderedComponents).toEqual([Page, MyComponent]) } { const { renderedComponents, withinDOM } = await renderStream.takeRender() withinDOM().getByText('test') expect(renderedComponents).toEqual([Page, MyComponent]) } }) it('should not fetch with initial data and staleTime', async () => { const key = queryKey() const renderStream = createRenderStream({ snapshotDOM: true }) const queryFn = vi.fn().mockImplementation(async () => { await vi.advanceTimersByTimeAsync(1) return 'test' }) function MyComponent(props: { promise: Promise }) { useTrackRenders() const data = React.use(props.promise) return <>{data} } function Loading() { useTrackRenders() return <>loading.. } function Page() { useTrackRenders() const query = useQuery({ queryKey: key, queryFn, initialData: 'initial', staleTime: 1000, }) return ( }> ) } await renderStream.render( , ) { const { renderedComponents, withinDOM } = await renderStream.takeRender() withinDOM().getByText('initial') expect(renderedComponents).toEqual([Page, MyComponent]) } // should not call queryFn because of staleTime + initialData combo expect(queryFn).toHaveBeenCalledTimes(0) }) it('should work with static placeholderData', async () => { const key = queryKey() const renderStream = createRenderStream({ snapshotDOM: true }) function MyComponent(props: { promise: Promise }) { useTrackRenders() const data = React.use(props.promise) return <>{data} } function Loading() { useTrackRenders() return <>loading.. } function Page() { const query = useQuery({ queryKey: key, queryFn: async () => { await vi.advanceTimersByTimeAsync(1) return 'test' }, placeholderData: 'placeholder', }) useTrackRenders() return ( }> ) } await renderStream.render( , ) { const { renderedComponents, withinDOM } = await renderStream.takeRender() withinDOM().getByText('placeholder') expect(renderedComponents).toEqual([Page, MyComponent]) } { const { renderedComponents, withinDOM } = await renderStream.takeRender() withinDOM().getByText('test') expect(renderedComponents).toEqual([Page, MyComponent]) } }) it('should work with placeholderData: keepPreviousData', async () => { const key = queryKey() const renderStream = createRenderStream({ snapshotDOM: true }) function MyComponent(props: { promise: Promise }) { useTrackRenders() const data = React.use(props.promise) return <>{data} } function Loading() { useTrackRenders() return <>loading.. } function Page() { useTrackRenders() const [count, setCount] = React.useState(0) const query = useQuery({ queryKey: [...key, count], queryFn: async () => { await vi.advanceTimersByTimeAsync(1) return 'test-' + count }, placeholderData: keepPreviousData, }) return (
}>
) } const rendered = await renderStream.render( , ) { const { renderedComponents, withinDOM } = await renderStream.takeRender() withinDOM().getByText('loading..') expect(renderedComponents).toEqual([Page, Loading]) } { const { renderedComponents, withinDOM } = await renderStream.takeRender() withinDOM().getByText('test-0') expect(renderedComponents).toEqual([MyComponent]) } rendered.getByRole('button', { name: 'increment' }).click() // re-render because of the increment { const { renderedComponents } = await renderStream.takeRender() expect(renderedComponents).toEqual([Page, MyComponent]) } // re-render with new data, no loading between { const { renderedComponents, withinDOM } = await renderStream.takeRender() withinDOM().getByText('test-1') // no more suspense boundary rendering expect(renderedComponents).toEqual([Page, MyComponent]) } }) it('should be possible to select a part of the data with select', async () => { const key = queryKey() const renderStream = createRenderStream({ snapshotDOM: true }) function MyComponent(props: { promise: Promise }) { useTrackRenders() const data = React.use(props.promise) return <>{data} } function Loading() { useTrackRenders() return <>loading.. } function Page() { const query = useQuery({ queryKey: key, queryFn: async () => { await vi.advanceTimersByTimeAsync(1) return { name: 'test' } }, select: (data) => data.name, }) useTrackRenders() return ( }> ) } await renderStream.render( , ) { const { renderedComponents, withinDOM } = await renderStream.takeRender() withinDOM().getByText('loading..') expect(renderedComponents).toEqual([Page, Loading]) } { const { renderedComponents, withinDOM } = await renderStream.takeRender() withinDOM().getByText('test') expect(renderedComponents).toEqual([MyComponent]) } }) it('should throw error if the promise fails', async () => { const renderStream = createRenderStream({ snapshotDOM: true }) const consoleMock = vi .spyOn(console, 'error') .mockImplementation(() => undefined) const key = queryKey() function MyComponent(props: { promise: Promise }) { const data = React.use(props.promise) return <>{data} } function Loading() { return <>loading.. } let queryCount = 0 function Page() { const query = useQuery({ queryKey: key, queryFn: async () => { await vi.advanceTimersByTimeAsync(1) if (++queryCount > 1) { // second time this query mounts, it should not throw return 'data' } throw new Error('Error test') }, retry: false, }) return ( }> ) } const rendered = await renderStream.render( {({ reset }) => ( (
error boundary
)} >
)}
, ) { const { withinDOM } = await renderStream.takeRender() withinDOM().getByText('loading..') } { const { withinDOM } = await renderStream.takeRender() withinDOM().getByText('error boundary') } consoleMock.mockRestore() rendered.getByText('resetErrorBoundary').click() { const { withinDOM } = await renderStream.takeRender() withinDOM().getByText('loading..') } { const { withinDOM } = await renderStream.takeRender() withinDOM().getByText('data') } expect(queryCount).toBe(2) }) it('should throw error if the promise fails (colocate suspense and promise)', async () => { const renderStream = createRenderStream({ snapshotDOM: true }) const consoleMock = vi .spyOn(console, 'error') .mockImplementation(() => undefined) const key = queryKey() function MyComponent() { const query = useQuery({ queryKey: key, queryFn: async () => { await vi.advanceTimersByTimeAsync(1) throw new Error('Error test') }, retry: false, }) const data = React.use(query.promise) return <>{data} } function Page() { return ( ) } await renderStream.render(
error boundary
}>
, ) { const { withinDOM } = await renderStream.takeRender() expect(withinDOM().getByText('loading..')).toBeInTheDocument() } { const { withinDOM } = await renderStream.takeRender() expect(withinDOM().getByText('error boundary')).toBeInTheDocument() } consoleMock.mockRestore() }) it('should recreate promise with data changes', async () => { const key = queryKey() const renderStream = createRenderStream({ snapshotDOM: true }) function MyComponent(props: { promise: Promise }) { useTrackRenders() const data = React.use(props.promise) return <>{data} } function Loading() { useTrackRenders() return <>loading.. } function Page() { const query = useQuery({ queryKey: key, queryFn: async () => { await vi.advanceTimersByTimeAsync(1) return 'test1' }, }) useTrackRenders() return ( }> ) } await renderStream.render( , ) { const { renderedComponents, withinDOM } = await renderStream.takeRender() withinDOM().getByText('loading..') expect(renderedComponents).toEqual([Page, Loading]) } { const { renderedComponents, withinDOM } = await renderStream.takeRender() withinDOM().getByText('test1') expect(renderedComponents).toEqual([MyComponent]) } queryClient.setQueryData(key, 'test2') { const { renderedComponents, withinDOM } = await renderStream.takeRender() withinDOM().getByText('test2') expect(renderedComponents).toEqual([Page, MyComponent]) } }) it('should dedupe when re-fetched with queryClient.fetchQuery while suspending', async () => { const key = queryKey() const renderStream = createRenderStream({ snapshotDOM: true }) const queryFn = vi.fn().mockImplementation(async () => { await vi.advanceTimersByTimeAsync(10) return 'test' }) const options = { queryKey: key, queryFn, } function MyComponent(props: { promise: Promise }) { const data = React.use(props.promise) return <>{data} } function Loading() { return <>loading.. } function Page() { const query = useQuery(options) return (
}>
) } const rendered = await renderStream.render( , ) { const { withinDOM } = await renderStream.takeRender() withinDOM().getByText('loading..') } rendered.getByText('fetch').click() { const { withinDOM } = await renderStream.takeRender() withinDOM().getByText('test') } expect(queryFn).toHaveBeenCalledOnce() }) it('should dedupe when re-fetched with refetchQueries while suspending', async () => { const key = queryKey() let count = 0 const renderStream = createRenderStream({ snapshotDOM: true }) const queryFn = vi.fn().mockImplementation(async () => { await vi.advanceTimersByTimeAsync(10) return 'test' + count++ }) const options = { queryKey: key, queryFn, } function MyComponent(props: { promise: Promise }) { const data = React.use(props.promise) return <>{data} } function Loading() { return <>loading.. } function Page() { const query = useQuery(options) return (
}>
) } const rendered = await renderStream.render( , ) { const { withinDOM } = await renderStream.takeRender() withinDOM().getByText('loading..') } rendered.getByText('refetch').click() { const { withinDOM } = await renderStream.takeRender() withinDOM().getByText('test0') } expect(queryFn).toHaveBeenCalledOnce() }) it('should stay pending when canceled with cancelQueries while suspending until refetched', async () => { const renderStream = createRenderStream({ snapshotDOM: true }) const key = queryKey() let count = 0 const queryFn = vi.fn().mockImplementation(async () => { await vi.advanceTimersByTimeAsync(10) return 'test' + count++ }) const options = { queryKey: key, queryFn, } function MyComponent(props: { promise: Promise }) { const data = React.use(props.promise) return <>{data} } function Loading() { return <>loading.. } function Page() { const query = useQuery(options) return (
}>
) } const rendered = await renderStream.render( <>error boundary}> , ) { const { withinDOM } = await renderStream.takeRender() withinDOM().getByText('loading..') } rendered.getByText('cancel').click() { await renderStream.takeRender() expect(queryClient.getQueryState(key)).toMatchObject({ status: 'pending', fetchStatus: 'idle', }) } expect(queryFn).toHaveBeenCalledOnce() rendered.getByText('fetch').click() { const { withinDOM } = await renderStream.takeRender() withinDOM().getByText('hello') } }) it('should resolve to previous data when canceled with cancelQueries while suspending', async () => { const renderStream = createRenderStream({ snapshotDOM: true }) const key = queryKey() const queryFn = vi.fn().mockImplementation(async () => { await vi.advanceTimersByTimeAsync(10) return 'test' }) const options = { queryKey: key, queryFn, } function MyComponent(props: { promise: Promise }) { const data = React.use(props.promise) return <>{data} } function Loading() { return <>loading.. } function Page() { const query = useQuery(options) return (
}>
) } queryClient.setQueryData(key, 'initial') const rendered = await renderStream.render( , ) rendered.getByText('cancel').click() { const { withinDOM } = await renderStream.takeRender() withinDOM().getByText('initial') } expect(queryFn).toHaveBeenCalledTimes(1) }) it('should suspend when not enabled', async () => { const renderStream = createRenderStream({ snapshotDOM: true }) const key = queryKey() const options = (count: number) => ({ queryKey: [...key, count], queryFn: async () => { await vi.advanceTimersByTimeAsync(10) return 'test' + count }, }) function MyComponent(props: { promise: Promise }) { const data = React.use(props.promise) return <>{data} } function Loading() { return <>loading.. } function Page() { const [count, setCount] = React.useState(0) const query = useQuery({ ...options(count), enabled: count > 0 }) return (
}>
) } const rendered = await renderStream.render( , ) { const { withinDOM } = await renderStream.takeRender() expect(withinDOM().getByText('loading..')).toBeInTheDocument() } rendered.getByText('enable').click() // loading re-render with enabled await renderStream.takeRender() { const { withinDOM } = await renderStream.takeRender() expect(withinDOM().getByText('test1')).toBeInTheDocument() } }) it('should show correct data when read from cache only (staleTime)', async () => { const key = queryKey() const renderStream = createRenderStream({ snapshotDOM: true }) queryClient.setQueryData(key, 'initial') const queryFn = vi.fn().mockImplementation(async () => { await vi.advanceTimersByTimeAsync(1) return 'test' }) function MyComponent(props: { promise: Promise }) { const data = React.use(props.promise) return <>{data} } function Loading() { return <>loading.. } function Page() { const query = useQuery({ queryKey: key, queryFn, staleTime: Infinity, }) return ( }> ) } await renderStream.render( , ) { const { withinDOM } = await renderStream.takeRender() withinDOM().getByText('initial') } expect(queryFn).toHaveBeenCalledTimes(0) }) it('should show correct data when switching between cache entries without re-fetches', async () => { const key = queryKey() const renderStream = createRenderStream({ snapshotDOM: true }) function MyComponent(props: { promise: Promise }) { useTrackRenders() const data = React.use(props.promise) return <>{data} } function Loading() { useTrackRenders() return <>loading.. } function Page() { useTrackRenders() const [count, setCount] = React.useState(0) const query = useQuery({ queryKey: [key, count], queryFn: async () => { await vi.advanceTimersByTimeAsync(10) return 'test' + count }, staleTime: Infinity, }) return (
}>
) } const rendered = await renderStream.render( , ) { const { renderedComponents, withinDOM } = await renderStream.takeRender() withinDOM().getByText('loading..') expect(renderedComponents).toEqual([Page, Loading]) } { const { renderedComponents, withinDOM } = await renderStream.takeRender() withinDOM().getByText('test0') expect(renderedComponents).toEqual([MyComponent]) } rendered.getByText('inc').click() { const { renderedComponents, withinDOM } = await renderStream.takeRender() withinDOM().getByText('loading..') expect(renderedComponents).toEqual([Page, Loading]) } { const { renderedComponents, withinDOM } = await renderStream.takeRender() withinDOM().getByText('test1') expect(renderedComponents).toEqual([MyComponent]) } rendered.getByText('dec').click() { const { renderedComponents, withinDOM } = await renderStream.takeRender() withinDOM().getByText('test0') expect(renderedComponents).toEqual([Page, MyComponent]) } }) it('should not resolve with intermediate data when keys are switched', async () => { const key = queryKey() const renderStream = createRenderStream<{ data: string }>({ snapshotDOM: true, }) function MyComponent(props: { promise: Promise }) { const data = React.use(props.promise) renderStream.replaceSnapshot({ data }) return <>{data} } function Loading() { return <>loading.. } function Page() { const [count, setCount] = React.useState(0) const query = useQuery({ queryKey: [key, count], queryFn: async () => { await vi.advanceTimersByTimeAsync(10) return 'test' + count }, staleTime: Infinity, }) return (
}>
) } const rendered = await renderStream.render( , ) { const { withinDOM } = await renderStream.takeRender() withinDOM().getByText('loading..') } { const { snapshot, withinDOM } = await renderStream.takeRender() withinDOM().getByText('test0') expect(snapshot).toMatchObject({ data: 'test0' }) } rendered.getByText('inc').click() { const { withinDOM } = await renderStream.takeRender() withinDOM().getByText('loading..') } rendered.getByText('inc').click() await renderStream.takeRender() rendered.getByText('inc').click() await renderStream.takeRender() { const { snapshot, withinDOM } = await renderStream.takeRender() withinDOM().getByText('test3') expect(snapshot).toMatchObject({ data: 'test3' }) } }) it('should not resolve with intermediate data when keys are switched (with background updates)', async () => { const key = queryKey() const renderStream = createRenderStream<{ data: string }>({ snapshotDOM: true, }) let modifier = '' function MyComponent(props: { promise: Promise }) { const data = React.use(props.promise) renderStream.replaceSnapshot({ data }) return <>{data} } function Loading() { return <>loading.. } function Page() { const [count, setCount] = React.useState(0) const query = useQuery({ queryKey: [key, count], queryFn: async () => { await vi.advanceTimersByTimeAsync(10) return 'test' + count + modifier }, }) return (
}>
) } const rendered = await renderStream.render( , ) { const { withinDOM } = await renderStream.takeRender() withinDOM().getByText('loading..') } { const { snapshot, withinDOM } = await renderStream.takeRender() withinDOM().getByText('test0') expect(snapshot).toMatchObject({ data: 'test0' }) } rendered.getByText('inc').click() { const { snapshot } = await renderStream.takeRender() expect(snapshot).toMatchObject({ data: 'test0' }) } rendered.getByText('inc').click() { const { snapshot } = await renderStream.takeRender() expect(snapshot).toMatchObject({ data: 'test0' }) } rendered.getByText('inc').click() { const { snapshot, withinDOM } = await renderStream.takeRender() withinDOM().getByText('loading..') expect(snapshot).toMatchObject({ data: 'test0' }) } { const { snapshot, withinDOM } = await renderStream.takeRender() withinDOM().getByText('test3') expect(snapshot).toMatchObject({ data: 'test3' }) } modifier = 'new' rendered.getByText('dec').click() { const { snapshot } = await renderStream.takeRender() expect(snapshot).toMatchObject({ data: 'test2' }) } rendered.getByText('dec').click() { const { snapshot } = await renderStream.takeRender() expect(snapshot).toMatchObject({ data: 'test1' }) } rendered.getByText('dec').click() { const { snapshot } = await renderStream.takeRender() expect(snapshot).toMatchObject({ data: 'test0' }) } { const { snapshot, withinDOM } = await renderStream.takeRender() withinDOM().getByText('test0new') expect(snapshot).toMatchObject({ data: 'test0new' }) } }) it('should not suspend indefinitely with multiple, nested observers)', async () => { const key = queryKey() const renderStream = createRenderStream({ snapshotDOM: true }) function MyComponent({ input }: { input: string }) { const query = useTheQuery(input) const data = React.use(query.promise) return <>{data} } function useTheQuery(input: string) { return useQuery({ staleTime: Infinity, queryKey: [key, input], queryFn: async () => { await vi.advanceTimersByTimeAsync(1) return input + ' response' }, }) } function Page() { const [input, setInput] = React.useState('defaultInput') useTheQuery(input) return (
) } const rendered = await renderStream.render( , ) { const { withinDOM } = await renderStream.takeRender() withinDOM().getByText('loading..') } { const { withinDOM } = await renderStream.takeRender() withinDOM().getByText('defaultInput response') } expect( queryClient.getQueryCache().find({ queryKey: [key, 'defaultInput'] })! .observers.length, ).toBe(2) rendered.getByText('setInput').click() { const { withinDOM } = await renderStream.takeRender() withinDOM().getByText('loading..') } { const { withinDOM } = await renderStream.takeRender() withinDOM().getByText('someInput response') } expect( queryClient.getQueryCache().find({ queryKey: [key, 'defaultInput'] })! .observers.length, ).toBe(0) expect( queryClient.getQueryCache().find({ queryKey: [key, 'someInput'] })! .observers.length, ).toBe(2) }) }) ================================================ FILE: packages/react-query/src/__tests__/useQuery.test-d.tsx ================================================ import { describe, expectTypeOf, it } from 'vitest' import { queryKey } from '@tanstack/query-test-utils' import { useQuery } from '../useQuery' import { queryOptions } from '../queryOptions' import type { OmitKeyof, QueryFunction, UseQueryOptions } from '..' describe('useQuery', () => { const key = queryKey() // unspecified query function should default to unknown const noQueryFn = useQuery({ queryKey: key }) expectTypeOf(noQueryFn.data).toEqualTypeOf() expectTypeOf(noQueryFn.error).toEqualTypeOf() // it should infer the result type from the query function const fromQueryFn = useQuery({ queryKey: key, queryFn: () => 'test' }) expectTypeOf(fromQueryFn.data).toEqualTypeOf() expectTypeOf(fromQueryFn.error).toEqualTypeOf() expectTypeOf(fromQueryFn.promise).toEqualTypeOf>() // it should be possible to specify the result type const withResult = useQuery({ queryKey: key, queryFn: () => 'test', }) expectTypeOf(withResult.data).toEqualTypeOf() expectTypeOf(withResult.error).toEqualTypeOf() // it should be possible to specify the error type const withError = useQuery({ queryKey: key, queryFn: () => 'test', }) expectTypeOf(withError.data).toEqualTypeOf() expectTypeOf(withError.error).toEqualTypeOf() // it should provide the result type in the configuration useQuery({ queryKey: [key], queryFn: () => Promise.resolve(true), }) // it should be possible to specify a union type as result type const unionTypeSync = useQuery({ queryKey: key, queryFn: () => (Math.random() > 0.5 ? ('a' as const) : ('b' as const)), }) expectTypeOf(unionTypeSync.data).toEqualTypeOf<'a' | 'b' | undefined>() const unionTypeAsync = useQuery<'a' | 'b'>({ queryKey: key, queryFn: () => Promise.resolve(Math.random() > 0.5 ? 'a' : 'b'), }) expectTypeOf(unionTypeAsync.data).toEqualTypeOf<'a' | 'b' | undefined>() // should error when the query function result does not match with the specified type // @ts-expect-error useQuery({ queryKey: key, queryFn: () => 'test' }) // it should infer the result type from a generic query function function queryFn(): Promise { return Promise.resolve({} as T) } const fromGenericQueryFn = useQuery({ queryKey: key, queryFn: () => queryFn(), }) expectTypeOf(fromGenericQueryFn.data).toEqualTypeOf() expectTypeOf(fromGenericQueryFn.error).toEqualTypeOf() const fromGenericOptionsQueryFn = useQuery({ queryKey: key, queryFn: () => queryFn(), }) expectTypeOf(fromGenericOptionsQueryFn.data).toEqualTypeOf< string | undefined >() expectTypeOf(fromGenericOptionsQueryFn.error).toEqualTypeOf() type MyData = number type MyQueryKey = readonly ['my-data', number] const getMyDataArrayKey: QueryFunction = ({ queryKey: [, n], }) => { return Promise.resolve(n + 42) } useQuery({ queryKey: ['my-data', 100], queryFn: getMyDataArrayKey, }) const getMyDataStringKey: QueryFunction = (context) => { expectTypeOf(context.queryKey).toEqualTypeOf<['1']>() return Promise.resolve(Number(context.queryKey[0]) + 42) } useQuery({ queryKey: ['1'], queryFn: getMyDataStringKey, }) // it should handle query-functions that return Promise useQuery({ queryKey: key, queryFn: () => fetch('return Promise').then((resp) => resp.json()), }) // handles wrapped queries with custom fetcher passed as inline queryFn const useWrappedQuery = < TQueryKey extends [string, Record?], TQueryFnData, TError, TData = TQueryFnData, >( qk: TQueryKey, fetcher: ( obj: TQueryKey[1], token: string, // return type must be wrapped with TQueryFnReturn ) => Promise, options?: OmitKeyof< UseQueryOptions, 'queryKey' | 'queryFn' | 'initialData' >, ) => useQuery({ queryKey: qk, queryFn: () => fetcher(qk[1], 'token'), ...options, }) const testQuery = useWrappedQuery([''], () => Promise.resolve('1')) expectTypeOf(testQuery.data).toEqualTypeOf() // handles wrapped queries with custom fetcher passed directly to useQuery const useWrappedFuncStyleQuery = < TQueryKey extends [string, Record?], TQueryFnData, TError, TData = TQueryFnData, >( qk: TQueryKey, fetcher: () => Promise, options?: OmitKeyof< UseQueryOptions, 'queryKey' | 'queryFn' | 'initialData' >, ) => useQuery({ queryKey: qk, queryFn: fetcher, ...options }) const testFuncStyle = useWrappedFuncStyleQuery([''], () => Promise.resolve(true), ) expectTypeOf(testFuncStyle.data).toEqualTypeOf() it('should return the correct states for a successful query', () => { const state = useQuery({ queryKey: key, queryFn: () => Promise.resolve('test'), }) if (state.isPending) { expectTypeOf(state.data).toEqualTypeOf() expectTypeOf(state.error).toEqualTypeOf() return pending } if (state.isLoadingError) { expectTypeOf(state.data).toEqualTypeOf() expectTypeOf(state.error).toEqualTypeOf() return {state.error.message} } expectTypeOf(state.data).toEqualTypeOf() expectTypeOf(state.error).toEqualTypeOf() return {state.data} }) describe('initialData', () => { describe('Config object overload', () => { it('TData should always be defined when initialData is provided as an object', () => { const { data } = useQuery({ queryKey: ['key'], queryFn: () => ({ wow: true }), initialData: { wow: true }, }) expectTypeOf(data).toEqualTypeOf<{ wow: boolean }>() }) it('TData should be defined when passed through queryOptions', () => { const options = queryOptions({ queryKey: ['key'], queryFn: () => { return { wow: true, } }, initialData: { wow: true, }, }) const { data } = useQuery(options) expectTypeOf(data).toEqualTypeOf<{ wow: boolean }>() }) it('should be possible to define a different TData than TQueryFnData using select with queryOptions spread into useQuery', () => { const options = queryOptions({ queryKey: ['key'], queryFn: () => Promise.resolve(1), }) const query = useQuery({ ...options, select: (data) => data > 1, }) expectTypeOf(query.data).toEqualTypeOf() }) it('TData should always be defined when initialData is provided as a function which ALWAYS returns the data', () => { const { data } = useQuery({ queryKey: ['key'], queryFn: () => { return { wow: true, } }, initialData: () => ({ wow: true, }), }) expectTypeOf(data).toEqualTypeOf<{ wow: boolean }>() }) it('TData should have undefined in the union when initialData is NOT provided', () => { const { data } = useQuery({ queryKey: ['key'], queryFn: () => { return { wow: true, } }, }) expectTypeOf(data).toEqualTypeOf<{ wow: boolean } | undefined>() }) it('TData should have undefined in the union when initialData is provided as a function which can return undefined', () => { const { data } = useQuery({ queryKey: ['key'], queryFn: () => { return { wow: true, } }, initialData: () => undefined as { wow: boolean } | undefined, }) expectTypeOf(data).toEqualTypeOf<{ wow: boolean } | undefined>() }) it('TData should be narrowed after an isSuccess check when initialData is provided as a function which can return undefined', () => { const { data, isSuccess } = useQuery({ queryKey: ['key'], queryFn: () => { return { wow: true, } }, initialData: () => undefined as { wow: boolean } | undefined, }) if (isSuccess) { expectTypeOf(data).toEqualTypeOf<{ wow: boolean }>() } }) // eslint-disable-next-line vitest/expect-expect it('TData should depend from only arguments, not the result', () => { // @ts-expect-error const result: UseQueryResult<{ wow: string }> = useQuery({ queryKey: ['key'], queryFn: () => { return { wow: true, } }, initialData: () => undefined as { wow: boolean } | undefined, }) void result }) it('data should not have undefined when initialData is provided', () => { const { data } = useQuery({ queryKey: ['query-key'], initialData: 42, }) expectTypeOf(data).toEqualTypeOf() }) }) describe('custom hook', () => { it('should allow custom hooks using UseQueryOptions', () => { type Data = string const useCustomQuery = ( options?: OmitKeyof, 'queryKey' | 'queryFn'>, ) => { return useQuery({ ...options, queryKey: ['todos-key'], queryFn: () => Promise.resolve('data'), }) } const { data } = useCustomQuery() expectTypeOf(data).toEqualTypeOf() }) }) describe('structuralSharing', () => { it('should be able to use structuralSharing with unknown types', () => { // https://github.com/TanStack/query/issues/6525#issuecomment-1938411343 useQuery({ queryKey: ['key'], queryFn: () => 5, structuralSharing: (oldData, newData) => { expectTypeOf(oldData).toBeUnknown() expectTypeOf(newData).toBeUnknown() return newData }, }) }) }) }) }) ================================================ FILE: packages/react-query/src/__tests__/useQuery.test.tsx ================================================ import { afterEach, beforeEach, describe, expect, it, test, vi } from 'vitest' import { act, fireEvent, render } from '@testing-library/react' import * as React from 'react' import { ErrorBoundary } from 'react-error-boundary' import { mockVisibilityState, queryKey, sleep, } from '@tanstack/query-test-utils' import { QueryCache, QueryClient, dehydrate, hydrate, keepPreviousData, skipToken, useQuery, } from '..' import { Blink, mockOnlineManagerIsOnline, renderWithClient, setActTimeout, } from './utils' import type { DefinedUseQueryResult, QueryFunction, UseQueryResult } from '..' import type { Mock } from 'vitest' describe('useQuery', () => { let queryCache: QueryCache let queryClient: QueryClient beforeEach(() => { queryCache = new QueryCache() queryClient = new QueryClient({ queryCache, }) vi.useFakeTimers() }) afterEach(() => { vi.useRealTimers() }) // See https://github.com/tannerlinsley/react-query/issues/105 it('should allow to set default data value', async () => { const key = queryKey() function Page() { const { data = 'default' } = useQuery({ queryKey: key, queryFn: () => sleep(10).then(() => 'test'), }) return (

{data}

) } const rendered = renderWithClient(queryClient, ) expect(rendered.getByText('default')).toBeInTheDocument() await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('test')).toBeInTheDocument() }) it('should return the correct states for a successful query', async () => { const key = queryKey() const states: Array> = [] function Page() { const state = useQuery({ queryKey: key, queryFn: async () => { await sleep(10) return 'test' }, }) states.push(state) if (state.isPending) { return pending } if (state.isLoadingError) { return {state.error.message} } return {state.data} } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(11) rendered.getByText('test') expect(states.length).toEqual(2) expect(states[0]).toEqual({ data: undefined, dataUpdatedAt: 0, error: null, errorUpdatedAt: 0, failureCount: 0, failureReason: null, errorUpdateCount: 0, isError: false, isFetched: false, isFetchedAfterMount: false, isFetching: true, isPaused: false, isPending: true, isInitialLoading: true, isLoading: true, isLoadingError: false, isPlaceholderData: false, isRefetchError: false, isRefetching: false, isStale: true, isSuccess: false, isEnabled: true, refetch: expect.any(Function), status: 'pending', fetchStatus: 'fetching', promise: expect.any(Promise), }) expect(states[1]).toEqual({ data: 'test', dataUpdatedAt: expect.any(Number), error: null, errorUpdatedAt: 0, failureCount: 0, failureReason: null, errorUpdateCount: 0, isError: false, isFetched: true, isFetchedAfterMount: true, isFetching: false, isPaused: false, isPending: false, isInitialLoading: false, isLoading: false, isLoadingError: false, isPlaceholderData: false, isRefetchError: false, isRefetching: false, isStale: true, isSuccess: true, isEnabled: true, refetch: expect.any(Function), status: 'success', fetchStatus: 'idle', promise: expect.any(Promise), }) expect(states[0]!.promise).toEqual(states[1]!.promise) }) it('should return the correct states for an unsuccessful query', async () => { const key = queryKey() const states: Array = [] let index = 0 function Page() { const state = useQuery({ queryKey: key, queryFn: () => Promise.reject(new Error(`rejected #${++index}`)), retry: 1, retryDelay: 1, }) states.push(state) return (

Status: {state.status}

Failure Count: {state.failureCount}
Failure Reason: {state.failureReason?.message}
) } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(2) rendered.getByText('Status: error') expect(states[0]).toEqual({ data: undefined, dataUpdatedAt: 0, error: null, errorUpdatedAt: 0, failureCount: 0, failureReason: null, errorUpdateCount: 0, isError: false, isFetched: false, isFetchedAfterMount: false, isFetching: true, isPaused: false, isPending: true, isInitialLoading: true, isLoading: true, isLoadingError: false, isPlaceholderData: false, isRefetchError: false, isRefetching: false, isStale: true, isSuccess: false, isEnabled: true, refetch: expect.any(Function), status: 'pending', fetchStatus: 'fetching', promise: expect.any(Promise), }) expect(states[1]).toEqual({ data: undefined, dataUpdatedAt: 0, error: null, errorUpdatedAt: 0, failureCount: 1, failureReason: new Error('rejected #1'), errorUpdateCount: 0, isError: false, isFetched: false, isFetchedAfterMount: false, isFetching: true, isPaused: false, isPending: true, isInitialLoading: true, isLoading: true, isLoadingError: false, isPlaceholderData: false, isRefetchError: false, isRefetching: false, isStale: true, isSuccess: false, isEnabled: true, refetch: expect.any(Function), status: 'pending', fetchStatus: 'fetching', promise: expect.any(Promise), }) expect(states[2]).toEqual({ data: undefined, dataUpdatedAt: 0, error: new Error('rejected #2'), errorUpdatedAt: expect.any(Number), failureCount: 2, failureReason: new Error('rejected #2'), errorUpdateCount: 1, isError: true, isFetched: true, isFetchedAfterMount: true, isFetching: false, isPaused: false, isPending: false, isInitialLoading: false, isLoading: false, isLoadingError: true, isPlaceholderData: false, isRefetchError: false, isRefetching: false, isStale: true, isSuccess: false, isEnabled: true, refetch: expect.any(Function), status: 'error', fetchStatus: 'idle', promise: expect.any(Promise), }) expect(states[0]!.promise).toEqual(states[1]!.promise) expect(states[1]!.promise).toEqual(states[2]!.promise) }) it('should set isFetchedAfterMount to true after a query has been fetched', async () => { const key = queryKey() await queryClient.prefetchQuery({ queryKey: key, queryFn: () => 'prefetched', }) function Page() { const result = useQuery({ queryKey: key, queryFn: () => 'new data' }) return ( <>
data: {result.data}
isFetched: {result.isFetched ? 'true' : 'false'}
isFetchedAfterMount: {result.isFetchedAfterMount ? 'true' : 'false'}
) } const rendered = renderWithClient(queryClient, ) expect(rendered.getByText('data: prefetched')).toBeInTheDocument() expect(rendered.getByText('isFetched: true')).toBeInTheDocument() expect(rendered.getByText('isFetchedAfterMount: false')).toBeInTheDocument() await vi.advanceTimersByTimeAsync(0) expect(rendered.getByText('data: new data')).toBeInTheDocument() expect(rendered.getByText('isFetched: true')).toBeInTheDocument() expect(rendered.getByText('isFetchedAfterMount: true')).toBeInTheDocument() }) it('should not cancel an ongoing fetch when refetch is called with cancelRefetch=false if we have data already', async () => { const key = queryKey() let fetchCount = 0 function Page() { const { refetch } = useQuery({ queryKey: key, queryFn: async () => { fetchCount++ await sleep(10) return 'data' }, enabled: false, initialData: 'initialData', }) React.useEffect(() => { setActTimeout(() => { refetch() }, 5) setActTimeout(() => { refetch({ cancelRefetch: false }) }, 5) }, [refetch]) return null } renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(15) // first refetch only, second refetch is ignored expect(fetchCount).toBe(1) }) it('should cancel an ongoing fetch when refetch is called (cancelRefetch=true) if we have data already', async () => { const key = queryKey() let fetchCount = 0 function Page() { const { refetch } = useQuery({ queryKey: key, queryFn: async () => { fetchCount++ await sleep(10) return 'data' }, enabled: false, initialData: 'initialData', }) React.useEffect(() => { setActTimeout(() => { refetch() }, 5) setActTimeout(() => { refetch() }, 5) }, [refetch]) return null } renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(15) // first refetch (gets cancelled) and second refetch expect(fetchCount).toBe(2) }) it('should not cancel an ongoing fetch when refetch is called (cancelRefetch=true) if we do not have data yet', async () => { const key = queryKey() let fetchCount = 0 function Page() { const { refetch } = useQuery({ queryKey: key, queryFn: async () => { fetchCount++ await sleep(10) return 'data' }, enabled: false, }) React.useEffect(() => { setActTimeout(() => { refetch() }, 5) setActTimeout(() => { refetch() }, 5) }, [refetch]) return null } renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(15) // first refetch will not get cancelled, second one gets skipped expect(fetchCount).toBe(1) }) it('should be able to watch a query without providing a query function', async () => { const key = queryKey() const states: Array> = [] queryClient.setQueryDefaults(key, { queryFn: () => 'data' }) function Page() { const state = useQuery({ queryKey: key }) states.push(state) return null } renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(0) expect(states.length).toBe(2) expect(states[0]).toMatchObject({ data: undefined }) expect(states[1]).toMatchObject({ data: 'data' }) }) it('should pick up a query when re-mounting with gcTime 0', async () => { const key = queryKey() const states: Array> = [] function Page() { const [toggle, setToggle] = React.useState(false) return (
{toggle ? ( ) : ( )}
) } function Component({ value }: { value: string }) { const state = useQuery({ queryKey: key, queryFn: async () => { await sleep(10) return 'data: ' + value }, gcTime: 0, notifyOnChangeProps: 'all', }) states.push(state) return (
{state.data}
) } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(11) rendered.getByText('data: 1') fireEvent.click(rendered.getByRole('button', { name: /toggle/i })) await vi.advanceTimersByTimeAsync(11) rendered.getByText('data: 2') expect(states.length).toBe(4) // First load expect(states[0]).toMatchObject({ isPending: true, isSuccess: false, isFetching: true, }) // First success expect(states[1]).toMatchObject({ isPending: false, isSuccess: true, isFetching: false, }) // Switch, goes to fetching expect(states[2]).toMatchObject({ isPending: false, isSuccess: true, isFetching: true, }) // Second success expect(states[3]).toMatchObject({ isPending: false, isSuccess: true, isFetching: false, }) }) it('should not get into an infinite loop when removing a query with gcTime 0 and rerendering', async () => { const key = queryKey() const states: Array> = [] function Page() { const [, rerender] = React.useState({}) const state = useQuery({ queryKey: key, queryFn: async () => { await sleep(5) return 'data' }, gcTime: 0, notifyOnChangeProps: ['isPending', 'isSuccess', 'data'], }) states.push(state) return ( <>
{state.data}
) } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(6) rendered.getByText('data') fireEvent.click(rendered.getByRole('button', { name: 'remove' })) await vi.advanceTimersByTimeAsync(6) rendered.getByText('data') expect(states.length).toBe(4) // First load expect(states[0]).toMatchObject({ isPending: true, isSuccess: false, data: undefined, }) // First success expect(states[1]).toMatchObject({ isPending: false, isSuccess: true, data: 'data', }) // Remove expect(states[2]).toMatchObject({ isPending: true, isSuccess: false, data: undefined, }) // Second success expect(states[3]).toMatchObject({ isPending: false, isSuccess: true, data: 'data', }) }) it('should fetch when refetchOnMount is false and nothing has been fetched yet', async () => { const key = queryKey() const states: Array> = [] function Page() { const state = useQuery({ queryKey: key, queryFn: () => 'test', refetchOnMount: false, }) states.push(state) return null } renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(0) expect(states.length).toBe(2) expect(states[0]).toMatchObject({ data: undefined }) expect(states[1]).toMatchObject({ data: 'test' }) }) it('should not fetch when refetchOnMount is false and data has been fetched already', () => { const key = queryKey() const states: Array> = [] queryClient.setQueryData(key, 'prefetched') function Page() { const state = useQuery({ queryKey: key, queryFn: () => 'test', refetchOnMount: false, }) states.push(state) return null } renderWithClient(queryClient, ) expect(states.length).toBe(1) expect(states[0]).toMatchObject({ data: 'prefetched' }) }) it('should be able to select a part of the data with select', async () => { const key = queryKey() const states: Array> = [] function Page() { const state = useQuery({ queryKey: key, queryFn: () => ({ name: 'test' }), select: (data) => data.name, }) states.push(state) return
{state.data}
} const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(0) rendered.getByText('test') expect(states.length).toBe(2) expect(states[0]).toMatchObject({ data: undefined }) expect(states[1]).toMatchObject({ data: 'test' }) }) it('should be able to select a part of the data with select in object syntax', async () => { const key = queryKey() const states: Array> = [] function Page() { const state = useQuery({ queryKey: key, queryFn: () => ({ name: 'test' }), select: (data) => data.name, }) states.push(state) return
{state.data}
} const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(0) rendered.getByText('test') expect(states.length).toBe(2) expect(states[0]).toMatchObject({ data: undefined }) expect(states[1]).toMatchObject({ data: 'test' }) }) it('should throw an error when a selector throws', async () => { const key = queryKey() const states: Array> = [] const error = new Error('Select Error') function Page() { const state = useQuery({ queryKey: key, queryFn: () => ({ name: 'test' }), select: () => { throw error }, }) states.push(state) return
{state.status}
} const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(0) rendered.getByText('error') expect(states.length).toBe(2) expect(states[0]).toMatchObject({ status: 'pending', data: undefined }) expect(states[1]).toMatchObject({ status: 'error', error }) }) it('should not re-run a stable select when it re-renders if selector throws an error', async () => { const key = queryKey() const error = new Error('Select Error') let runs = 0 function Page() { const [, rerender] = React.useReducer(() => ({}), {}) const state = useQuery({ queryKey: key, queryFn: () => (runs === 0 ? 'test' : 'test2'), select: React.useCallback(() => { runs++ throw error }, []), }) return (
error: {state.error?.message}
) } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(0) rendered.getByText('error: Select Error') expect(runs).toEqual(1) fireEvent.click(rendered.getByRole('button', { name: 'rerender' })) await vi.advanceTimersByTimeAsync(0) expect(runs).toEqual(1) fireEvent.click(rendered.getByRole('button', { name: 'refetch' })) await vi.advanceTimersByTimeAsync(0) expect(runs).toEqual(2) }) it('should track properties and only re-render when a tracked property changes', async () => { const key = queryKey() const states: Array> = [] let count = 0 function Page() { const state = useQuery({ queryKey: key, queryFn: async () => { await sleep(10) count++ return 'test' + count }, }) states.push(state) return (

{state.data ?? null}

) } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(11) rendered.getByText('test1') fireEvent.click(rendered.getByRole('button', { name: /refetch/i })) await vi.advanceTimersByTimeAsync(11) rendered.getByText('test2') expect(states.length).toBe(3) expect(states[0]).toMatchObject({ data: undefined }) expect(states[1]).toMatchObject({ data: 'test1' }) expect(states[2]).toMatchObject({ data: 'test2' }) }) it('should always re-render if we are tracking props but not using any', async () => { const key = queryKey() let renderCount = 0 const states: Array> = [] function Page() { const state = useQuery({ queryKey: key, queryFn: () => 'test' }) states.push(state) React.useEffect(() => { renderCount++ }, [state]) return (

hello

) } renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(0) expect(renderCount).toBe(2) expect(states.length).toBe(2) expect(states[0]).toMatchObject({ data: undefined }) expect(states[1]).toMatchObject({ data: 'test' }) }) it('should be able to remove a query', async () => { const key = queryKey() const states: Array> = [] let count = 0 function Page() { const [, rerender] = React.useState({}) const state = useQuery({ queryKey: key, queryFn: () => ++count, notifyOnChangeProps: 'all', }) states.push(state) return (
data: {state.data ?? 'null'}
) } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(0) rendered.getByText('data: 1') fireEvent.click(rendered.getByRole('button', { name: /remove/i })) await vi.advanceTimersByTimeAsync(0) fireEvent.click(rendered.getByRole('button', { name: /rerender/i })) await vi.advanceTimersByTimeAsync(0) rendered.getByText('data: 2') expect(states.length).toBe(4) // Initial expect(states[0]).toMatchObject({ status: 'pending', data: undefined }) // Fetched expect(states[1]).toMatchObject({ status: 'success', data: 1 }) // Remove + Hook state update, batched expect(states[2]).toMatchObject({ status: 'pending', data: undefined }) // Fetched expect(states[3]).toMatchObject({ status: 'success', data: 2 }) }) it('should create a new query when refetching a removed query', async () => { const key = queryKey() const states: Array> = [] let count = 0 function Page() { const state = useQuery({ queryKey: key, queryFn: async () => { await sleep(10) return ++count }, notifyOnChangeProps: 'all', }) states.push(state) const { refetch } = state return (
data: {state.data ?? 'null'}
) } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(11) rendered.getByText('data: 1') fireEvent.click(rendered.getByRole('button', { name: /remove/i })) fireEvent.click(rendered.getByRole('button', { name: /refetch/i })) await vi.advanceTimersByTimeAsync(11) rendered.getByText('data: 2') expect(states.length).toBe(4) // Initial expect(states[0]).toMatchObject({ data: undefined, dataUpdatedAt: 0 }) // Fetched expect(states[1]).toMatchObject({ data: 1 }) // Switch expect(states[2]).toMatchObject({ data: undefined, dataUpdatedAt: 0 }) // Fetched expect(states[3]).toMatchObject({ data: 2 }) }) it('should share equal data structures between query results', async () => { const key = queryKey() const result1 = [ { id: '1', done: false }, { id: '2', done: false }, ] const result2 = [ { id: '1', done: false }, { id: '2', done: true }, ] const states: Array> = [] let count = 0 function Page() { const state = useQuery({ queryKey: key, queryFn: async () => { await sleep(10) count++ return count === 1 ? result1 : result2 }, notifyOnChangeProps: 'all', }) states.push(state) const { refetch } = state return (
data: {String(state.data?.[1]?.done)}
) } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(11) rendered.getByText('data: false') fireEvent.click(rendered.getByRole('button', { name: /refetch/i })) await vi.advanceTimersByTimeAsync(11) rendered.getByText('data: true') expect(states.length).toBe(4) const todos = states[2]?.data const todo1 = todos?.[0] const todo2 = todos?.[1] const newTodos = states[3]?.data const newTodo1 = newTodos?.[0] const newTodo2 = newTodos?.[1] expect(todos).toEqual(result1) expect(newTodos).toEqual(result2) expect(newTodos).not.toBe(todos) expect(newTodo1).toBe(todo1) expect(newTodo2).not.toBe(todo2) return null }) it('should use query function from hook when the existing query does not have a query function', async () => { const key = queryKey() queryClient.setQueryData(key, 'set') function Page() { const result = useQuery({ queryKey: key, queryFn: async () => { await sleep(10) return 'fetched' }, initialData: 'initial', staleTime: Infinity, }) return (
isFetching: {result.isFetching}
data: {result.data}
) } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('data: set')).toBeInTheDocument() fireEvent.click(rendered.getByRole('button', { name: /refetch/i })) await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('data: fetched')).toBeInTheDocument() }) it('should update query stale state and refetch when invalidated with invalidateQueries', async () => { const key = queryKey() let count = 0 function Page() { const state = useQuery({ queryKey: key, queryFn: async () => { await sleep(10) count++ return count }, staleTime: Infinity, }) return (
data: {state.data}, isStale: {String(state.isStale)}, isFetching:{' '} {String(state.isFetching)}
) } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(11) expect( rendered.getByText('data: 1, isStale: false, isFetching: false'), ).toBeInTheDocument() fireEvent.click(rendered.getByRole('button', { name: /invalidate/i })) await vi.advanceTimersByTimeAsync(0) expect( rendered.getByText('data: 1, isStale: true, isFetching: true'), ).toBeInTheDocument() await vi.advanceTimersByTimeAsync(11) expect( rendered.getByText('data: 2, isStale: false, isFetching: false'), ).toBeInTheDocument() }) it('should not update disabled query when refetching with refetchQueries', async () => { const key = queryKey() const states: Array> = [] let count = 0 function Page() { const state = useQuery({ queryKey: key, queryFn: async () => { await sleep(10) count++ return count }, enabled: false, }) states.push(state) React.useEffect(() => { setActTimeout(() => { queryClient.refetchQueries({ queryKey: key }) }, 20) }, []) return null } renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(31) expect(states.length).toBe(1) expect(states[0]).toMatchObject({ data: undefined, isFetching: false, isSuccess: false, isStale: false, }) }) it('should not refetch disabled query when invalidated with invalidateQueries', () => { const key = queryKey() const states: Array> = [] let count = 0 function Page() { const state = useQuery({ queryKey: key, queryFn: async () => { await sleep(10) count++ return count }, enabled: false, }) states.push(state) React.useEffect(() => { setActTimeout(() => { queryClient.invalidateQueries({ queryKey: key }) }, 10) }, []) return null } renderWithClient(queryClient, ) expect(states.length).toBe(1) expect(states[0]).toMatchObject({ data: undefined, isFetching: false, isSuccess: false, isStale: false, }) }) it('should not fetch when switching to a disabled query', async () => { const key = queryKey() const states: Array> = [] function Page() { const [count, setCount] = React.useState(0) const state = useQuery({ queryKey: [key, count], queryFn: async () => { await sleep(5) return count }, enabled: count === 0, }) states.push(state) return (
data: {state.data ?? 'undefined'}
count: {count}
) } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(6) rendered.getByText('data: 0') fireEvent.click(rendered.getByRole('button', { name: /increment/i })) await vi.advanceTimersByTimeAsync(6) rendered.getByText('count: 1') rendered.getByText('data: undefined') expect(states.length).toBe(3) // Fetch query expect(states[0]).toMatchObject({ data: undefined, isFetching: true, isSuccess: false, }) // Fetched query expect(states[1]).toMatchObject({ data: 0, isFetching: false, isSuccess: true, }) // Switch to disabled query expect(states[2]).toMatchObject({ data: undefined, isFetching: false, isSuccess: false, }) }) it('should keep the previous data when placeholderData is set', async () => { const key = queryKey() const states: Array> = [] function Page() { const [count, setCount] = React.useState(0) const state = useQuery({ queryKey: [key, count], queryFn: async () => { await sleep(10) return count }, placeholderData: keepPreviousData, }) states.push(state) return (
data: {state.data}
) } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(11) rendered.getByText('data: 0') fireEvent.click(rendered.getByRole('button', { name: 'setCount' })) await vi.advanceTimersByTimeAsync(11) rendered.getByText('data: 1') // Initial expect(states[0]).toMatchObject({ data: undefined, isFetching: true, isSuccess: false, isPlaceholderData: false, }) // Fetched expect(states[1]).toMatchObject({ data: 0, isFetching: false, isSuccess: true, isPlaceholderData: false, }) // Set state expect(states[2]).toMatchObject({ data: 0, isFetching: true, isSuccess: true, isPlaceholderData: true, }) // New data expect(states[3]).toMatchObject({ data: 1, isFetching: false, isSuccess: true, isPlaceholderData: false, }) }) it('should keep the previous data when placeholderData is set and select fn transform is used', async () => { const key = queryKey() const states: Array> = [] function Page() { const [count, setCount] = React.useState(0) const state = useQuery({ queryKey: [key, count], queryFn: async () => { await sleep(10) return { count, } }, select(data) { return data.count }, placeholderData: keepPreviousData, }) states.push(state) return (
data: {state.data}
) } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(11) rendered.getByText('data: 0') fireEvent.click(rendered.getByRole('button', { name: 'setCount' })) await vi.advanceTimersByTimeAsync(11) rendered.getByText('data: 1') // Initial expect(states[0]).toMatchObject({ data: undefined, isFetching: true, isSuccess: false, isPlaceholderData: false, }) // Fetched expect(states[1]).toMatchObject({ data: 0, isFetching: false, isSuccess: true, isPlaceholderData: false, }) // Set state expect(states[2]).toMatchObject({ data: 0, isFetching: true, isSuccess: true, isPlaceholderData: true, }) // New data expect(states[3]).toMatchObject({ data: 1, isFetching: false, isSuccess: true, isPlaceholderData: false, }) }) it('should keep the previous queryKey (from prevQuery) between multiple pending queries when placeholderData is set and select fn transform is used', async () => { const keys: Array | null> = [] const key = queryKey() const states: Array> = [] function Page() { const [count, setCount] = React.useState(0) const state = useQuery({ queryKey: [key, count], queryFn: async () => { await sleep(10) return { count, } }, select(data) { return data.count }, placeholderData: (prevData, prevQuery) => { if (prevQuery) { keys.push(prevQuery.queryKey) } return prevData }, }) states.push(state) return (
data: {state.data}
) } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(11) rendered.getByText('data: 0') fireEvent.click(rendered.getByRole('button', { name: 'setCount' })) fireEvent.click(rendered.getByRole('button', { name: 'setCount' })) fireEvent.click(rendered.getByRole('button', { name: 'setCount' })) await vi.advanceTimersByTimeAsync(11) rendered.getByText('data: 3') const allPreviousKeysAreTheFirstQueryKey = keys.every( (k) => JSON.stringify(k) === JSON.stringify([key, 0]), ) expect(allPreviousKeysAreTheFirstQueryKey).toBe(true) }) it('should show placeholderData between multiple pending queries when select fn transform is used', async () => { const key = queryKey() const states: Array> = [] function Page() { const [count, setCount] = React.useState(0) const state = useQuery({ queryKey: [key, count], queryFn: async () => { await sleep(10) return { count, } }, select(data) { return data.count }, placeholderData: keepPreviousData, }) states.push(state) return (
data: {state.data}
) } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(11) rendered.getByText('data: 0') fireEvent.click(rendered.getByRole('button', { name: 'setCount' })) fireEvent.click(rendered.getByRole('button', { name: 'setCount' })) fireEvent.click(rendered.getByRole('button', { name: 'setCount' })) await vi.advanceTimersByTimeAsync(11) rendered.getByText('data: 3') // Initial expect(states[0]).toMatchObject({ data: undefined, isFetching: true, isSuccess: false, isPlaceholderData: false, }) // Fetched expect(states[1]).toMatchObject({ data: 0, isFetching: false, isSuccess: true, isPlaceholderData: false, }) // Set state -> count = 1 expect(states[2]).toMatchObject({ data: 0, isFetching: true, isSuccess: true, isPlaceholderData: true, }) // Set state -> count = 2 expect(states[3]).toMatchObject({ data: 0, isFetching: true, isSuccess: true, isPlaceholderData: true, }) // Set state -> count = 3 expect(states[4]).toMatchObject({ data: 0, isFetching: true, isSuccess: true, isPlaceholderData: true, }) // New data expect(states[5]).toMatchObject({ data: 3, isFetching: false, isSuccess: true, isPlaceholderData: false, }) }) it('should transition to error state when placeholderData is set', async () => { const key = queryKey() const states: Array> = [] function Page({ count }: { count: number }) { const state = useQuery({ queryKey: [key, count], queryFn: async () => { await sleep(10) if (count === 2) { throw new Error('Error test') } return Promise.resolve(count) }, retry: false, placeholderData: keepPreviousData, }) states.push(state) return (

data: {state.data}

error: {state.error?.message}

placeholder data: {state.isPlaceholderData}

) } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(11) rendered.getByText('data: 0') rendered.rerender() await vi.advanceTimersByTimeAsync(11) rendered.getByText('data: 1') rendered.rerender() await vi.advanceTimersByTimeAsync(11) rendered.getByText('error: Error test') expect(states.length).toBe(6) // Initial expect(states[0]).toMatchObject({ data: undefined, isFetching: true, status: 'pending', error: null, isPlaceholderData: false, }) // Fetched expect(states[1]).toMatchObject({ data: 0, isFetching: false, status: 'success', error: null, isPlaceholderData: false, }) // rerender Page 1 expect(states[2]).toMatchObject({ data: 0, isFetching: true, status: 'success', error: null, isPlaceholderData: true, }) // New data expect(states[3]).toMatchObject({ data: 1, isFetching: false, status: 'success', error: null, isPlaceholderData: false, }) // rerender Page 2 expect(states[4]).toMatchObject({ data: 1, isFetching: true, status: 'success', error: null, isPlaceholderData: true, }) // Error expect(states[5]).toMatchObject({ data: undefined, isFetching: false, status: 'error', isPlaceholderData: false, }) expect(states[5]!.error).toHaveProperty('message', 'Error test') }) it('should not show initial data from next query if placeholderData is set', async () => { const key = queryKey() const states: Array> = [] function Page() { const [count, setCount] = React.useState(0) const state = useQuery({ queryKey: [key, count], queryFn: async () => { await sleep(10) return count }, initialData: 99, placeholderData: keepPreviousData, }) states.push(state) return (

data: {state.data}, count: {count}, isFetching:{' '} {String(state.isFetching)}

) } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(11) rendered.getByText('data: 0, count: 0, isFetching: false') fireEvent.click(rendered.getByRole('button', { name: 'inc' })) await vi.advanceTimersByTimeAsync(11) rendered.getByText('data: 1, count: 1, isFetching: false') expect(states.length).toBe(4) // Initial expect(states[0]).toMatchObject({ data: 99, isFetching: true, isSuccess: true, isPlaceholderData: false, }) // Fetched expect(states[1]).toMatchObject({ data: 0, isFetching: false, isSuccess: true, isPlaceholderData: false, }) // Set state expect(states[2]).toMatchObject({ data: 99, isFetching: true, isSuccess: true, isPlaceholderData: false, }) // New data expect(states[3]).toMatchObject({ data: 1, isFetching: false, isSuccess: true, isPlaceholderData: false, }) }) it('should keep the previous data on disabled query when placeholderData is set', async () => { const key = queryKey() const states: Array> = [] function Page() { const [count, setCount] = React.useState(0) const state = useQuery({ queryKey: [key, count], queryFn: async () => { await sleep(10) return count }, enabled: false, placeholderData: keepPreviousData, notifyOnChangeProps: 'all', }) states.push(state) return (
data: {state.data ?? 'undefined'}
) } const rendered = renderWithClient(queryClient, ) rendered.getByText('data: undefined') fireEvent.click(rendered.getByRole('button', { name: 'refetch' })) await vi.advanceTimersByTimeAsync(11) rendered.getByText('data: 0') fireEvent.click(rendered.getByRole('button', { name: 'setCount' })) await vi.advanceTimersByTimeAsync(11) rendered.getByText('data: 0') fireEvent.click(rendered.getByRole('button', { name: 'refetch' })) await vi.advanceTimersByTimeAsync(11) rendered.getByText('data: 1') expect(states.length).toBe(6) // Disabled query expect(states[0]).toMatchObject({ data: undefined, isFetching: false, isSuccess: false, isPlaceholderData: false, }) // Fetching query expect(states[1]).toMatchObject({ data: undefined, isFetching: true, isSuccess: false, isPlaceholderData: false, }) // Fetched query expect(states[2]).toMatchObject({ data: 0, isFetching: false, isSuccess: true, isPlaceholderData: false, }) // Set state expect(states[3]).toMatchObject({ data: 0, isFetching: false, isSuccess: true, isPlaceholderData: true, }) // Fetching new query expect(states[4]).toMatchObject({ data: 0, isFetching: true, isSuccess: true, isPlaceholderData: true, }) // Fetched new query expect(states[5]).toMatchObject({ data: 1, isFetching: false, isSuccess: true, isPlaceholderData: false, }) }) it('should keep the previous data on disabled query when placeholderData is set and switching query key multiple times', async () => { const key = queryKey() const states: Array> = [] queryClient.setQueryData([key, 10], 10) await vi.advanceTimersByTimeAsync(10) function Page() { const [count, setCount] = React.useState(10) const state = useQuery({ queryKey: [key, count], queryFn: async () => { await sleep(10) return count }, enabled: false, placeholderData: keepPreviousData, notifyOnChangeProps: 'all', }) states.push(state) const { refetch } = state React.useEffect(() => { setActTimeout(() => { setCount(11) }, 20) setActTimeout(() => { setCount(12) }, 30) setActTimeout(() => { refetch() }, 40) }, [refetch]) return null } renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(51) expect(states.length).toBe(5) // Disabled query expect(states[0]).toMatchObject({ data: 10, isFetching: false, isSuccess: true, isPlaceholderData: false, }) // Set state expect(states[1]).toMatchObject({ data: 10, isFetching: false, isSuccess: true, isPlaceholderData: true, }) // State update expect(states[2]).toMatchObject({ data: 10, isFetching: false, isSuccess: true, isPlaceholderData: true, }) // Refetch expect(states[3]).toMatchObject({ data: 10, isFetching: true, isSuccess: true, isPlaceholderData: true, }) // Refetch done expect(states[4]).toMatchObject({ data: 12, isFetching: false, isSuccess: true, isPlaceholderData: false, }) }) it('should use the correct query function when components use different configurations', async () => { const key = queryKey() const states: Array> = [] function FirstComponent() { const state = useQuery({ queryKey: key, queryFn: async () => { await sleep(10) return 1 }, notifyOnChangeProps: 'all', }) const refetch = state.refetch states.push(state) return (
data: {state.data}
) } function SecondComponent() { useQuery({ queryKey: key, queryFn: () => 2, notifyOnChangeProps: 'all' }) return null } function Page() { return ( <> ) } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(11) rendered.getByText('data: 1') fireEvent.click(rendered.getByRole('button', { name: /refetch/i })) await vi.advanceTimersByTimeAsync(11) expect(states.length).toBe(4) expect(states[0]).toMatchObject({ data: undefined, }) expect(states[1]).toMatchObject({ data: 1, }) expect(states[2]).toMatchObject({ data: 1, }) // This state should be 1 instead of 2 expect(states[3]).toMatchObject({ data: 1, }) }) it('should be able to set different stale times for a query', async () => { const key = queryKey() const states1: Array> = [] const states2: Array> = [] queryClient.prefetchQuery({ queryKey: key, queryFn: async () => { await sleep(10) return 'prefetch' }, }) await vi.advanceTimersByTimeAsync(20) function FirstComponent() { const state = useQuery({ queryKey: key, queryFn: async () => { await sleep(10) return 'one' }, staleTime: 100, }) states1.push(state) return null } function SecondComponent() { const state = useQuery({ queryKey: key, queryFn: async () => { await sleep(10) return 'two' }, staleTime: 10, }) states2.push(state) return null } function Page() { return ( <> ) } renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(200) expect(states1.length).toBe(4) expect(states2.length).toBe(3) expect(states1).toMatchObject([ // First render { data: 'prefetch', isStale: false, }, // Second useQuery started fetching { data: 'prefetch', isStale: false, }, // Second useQuery data came in { data: 'two', isStale: false, }, // Data became stale after 100ms { data: 'two', isStale: true, }, ]) expect(states2).toMatchObject([ // First render, data is stale and starts fetching { data: 'prefetch', isStale: true, }, // Second useQuery data came in { data: 'two', isStale: false, }, // Data became stale after 5ms { data: 'two', isStale: true, }, ]) }) it('should re-render when a query becomes stale', async () => { const key = queryKey() const states: Array> = [] function Page() { const state = useQuery({ queryKey: key, queryFn: () => 'test', staleTime: 50, }) states.push(state) return null } renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(100) expect(states.length).toBe(3) expect(states[0]).toMatchObject({ isStale: true }) expect(states[1]).toMatchObject({ isStale: false }) expect(states[2]).toMatchObject({ isStale: true }) }) it('should re-render disabled observers when other observers trigger a query (#8741)', async () => { const key = queryKey() const useUserInfoQuery = ({ id, enabled, }: { id: number | null enabled: boolean }) => { return useQuery({ queryKey: [key, id], queryFn: async () => { await sleep(10) return { id, name: 'John' } }, enabled: !!id && enabled, }) } const Page = () => { const [id, setId] = React.useState(null) const searchQuery = useUserInfoQuery({ id, enabled: false }) return ( <>
User fetching status is {searchQuery.fetchStatus}
) } function UserInfo({ id }: { id: number | null }) { const searchQuery = useUserInfoQuery({ id, enabled: true }) return
UserInfo data is {JSON.stringify(searchQuery.data)}
} const rendered = renderWithClient(queryClient, ) rendered.getByText('User fetching status is idle') fireEvent.click(rendered.getByRole('button', { name: /set id/i })) await vi.advanceTimersByTimeAsync(0) expect( rendered.getByText('User fetching status is fetching'), ).toBeInTheDocument() await vi.advanceTimersByTimeAsync(11) expect( rendered.getByText('UserInfo data is {"id":42,"name":"John"}'), ).toBeInTheDocument() expect( rendered.getByText('User fetching status is idle'), ).toBeInTheDocument() }) describe('notifyOnChangeProps', () => { it('should not re-render when it should only re-render only data change and the selected data did not change', async () => { const key = queryKey() const states: Array> = [] function Page() { const state = useQuery({ queryKey: key, queryFn: () => ({ name: 'test' }), select: (data) => data.name, notifyOnChangeProps: ['data'], }) states.push(state) return (
{state.data}
) } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(0) rendered.getByText('test') fireEvent.click(rendered.getByRole('button', { name: 'refetch' })) await vi.advanceTimersByTimeAsync(0) rendered.getByText('test') expect(states[0]).toMatchObject({ data: undefined }) expect(states[1]).toMatchObject({ data: 'test' }) // make sure no additional renders happen await vi.advanceTimersByTimeAsync(50) expect(states.length).toBe(2) }) it('should not re-render when it should only re-render on data changes and the data did not change', async () => { const key = queryKey() const states: Array> = [] function Page() { const state = useQuery({ queryKey: key, queryFn: async () => { await sleep(5) return 'test' }, notifyOnChangeProps: ['data'], }) states.push(state) return ( <>
{state.data}
) } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(6) rendered.getByText('test') fireEvent.click(rendered.getByRole('button', { name: 'refetch' })) // sleep is required to make sure no additional renders happen after click await vi.advanceTimersByTimeAsync(20) expect(states.length).toBe(2) expect(states[0]).toMatchObject({ data: undefined, status: 'pending', isFetching: true, }) expect(states[1]).toMatchObject({ data: 'test', status: 'success', isFetching: false, }) }) // See https://github.com/TanStack/query/discussions/5588 describe('function', () => { it('should not re-render when it should only re-render on data changes and the data did not change', async () => { const key = queryKey() const states: Array> = [] function Page() { const state = useQuery({ queryKey: key, queryFn: async () => { await sleep(5) return 'test' }, notifyOnChangeProps: () => ['data'], }) states.push(state) return ( <>
{state.data}
) } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(6) rendered.getByText('test') fireEvent.click(rendered.getByRole('button', { name: 'refetch' })) await vi.advanceTimersByTimeAsync(20) expect(states.length).toBe(2) expect(states[0]).toMatchObject({ data: undefined, status: 'pending', isFetching: true, }) expect(states[1]).toMatchObject({ data: 'test', status: 'success', isFetching: false, }) }) it('should not re-render when change props are not actively being tracked', async () => { const key = queryKey() const states: Array> = [] function Page() { const fetchCounterRef = React.useRef(0) const trackChangesRef = React.useRef(true) const notifyOnChangeProps = React.useCallback(() => { return trackChangesRef.current ? 'all' : [] }, []) const state = useQuery({ queryKey: key, queryFn: async () => { await sleep(5) fetchCounterRef.current++ return `fetch counter: ${fetchCounterRef.current}` }, notifyOnChangeProps, }) states.push(state) return ( <>
{state.data}
) } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(6) rendered.getByText('fetch counter: 1') expect(states.length).toBe(2) expect(states[0]).toMatchObject({ data: undefined, isFetching: true, status: 'pending', }) expect(states[1]).toMatchObject({ data: 'fetch counter: 1', status: 'success', isFetching: false, }) // disable tracking and refetch to check for re-renders fireEvent.click( rendered.getByRole('button', { name: 'disableTracking' }), ) fireEvent.click(rendered.getByRole('button', { name: 'refetch' })) await vi.advanceTimersByTimeAsync(20) // still expect to only have two re-renders from the initial fetch expect(states.length).toBe(2) // enable tracking and refetch to check for re-renders fireEvent.click( rendered.getByRole('button', { name: 'enableTracking' }), ) fireEvent.click(rendered.getByRole('button', { name: 'refetch' })) await vi.advanceTimersByTimeAsync(6) rendered.getByText('fetch counter: 3') await vi.advanceTimersByTimeAsync(20) expect(states.length).toBe(4) expect(states[2]).toMatchObject({ data: 'fetch counter: 2', status: 'success', isFetching: true, }) expect(states[3]).toMatchObject({ data: 'fetch counter: 3', status: 'success', isFetching: false, }) }) }) }) // See https://github.com/tannerlinsley/react-query/issues/137 it('should not override initial data in dependent queries', () => { const key1 = queryKey() const key2 = queryKey() function Page() { const first = useQuery({ queryKey: key1, queryFn: () => 'data', enabled: false, initialData: 'init', }) const second = useQuery({ queryKey: key2, queryFn: () => 'data', enabled: false, initialData: 'init', }) return (

First Data: {first.data}

Second Data: {second.data}

First Status: {first.status}
Second Status: {second.status}
) } const rendered = renderWithClient(queryClient, ) expect(rendered.getByText('First Data: init')).toBeInTheDocument() expect(rendered.getByText('Second Data: init')).toBeInTheDocument() expect(rendered.getByText('First Status: success')).toBeInTheDocument() expect(rendered.getByText('Second Status: success')).toBeInTheDocument() }) it('should update query options', () => { const key = queryKey() const queryFn = async () => { await sleep(10) return 'data1' } function Page() { useQuery({ queryKey: key, queryFn, retryDelay: 10 }) useQuery({ queryKey: key, queryFn, retryDelay: 20 }) return null } renderWithClient(queryClient, ) expect(queryCache.find({ queryKey: key })!.options.retryDelay).toBe(20) }) it('should batch re-renders', async () => { const key = queryKey() let renders = 0 const queryFn = async () => { await sleep(15) return 'data' } function Page() { const query1 = useQuery({ queryKey: key, queryFn }) const query2 = useQuery({ queryKey: key, queryFn }) renders++ return (
{query1.data} {query2.data}
) } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(16) rendered.getByText('data data') // Should be 2 instead of 3 expect(renders).toBe(2) }) it('should render latest data even if react has discarded certain renders', async () => { const key = queryKey() function Page() { const [, setNewState] = React.useState('state') const state = useQuery({ queryKey: key, queryFn: () => 'data' }) React.useEffect(() => { setActTimeout(() => { queryClient.setQueryData(key, 'new') // Update with same state to make react discard the next render setNewState('state') }, 10) }, []) return
{state.data}
} const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('new')).toBeInTheDocument() }) // See https://github.com/tannerlinsley/react-query/issues/170 it('should start with status pending, fetchStatus idle if enabled is false', async () => { const key1 = queryKey() const key2 = queryKey() function Page() { const first = useQuery({ queryKey: key1, queryFn: async () => { await sleep(10) return 'data' }, enabled: false, }) const second = useQuery({ queryKey: key2, queryFn: async () => { await sleep(10) return 'data' }, }) return (
First Status: {first.status}, {first.fetchStatus}
Second Status: {second.status}, {second.fetchStatus}
) } const rendered = renderWithClient(queryClient, ) // use "act" to wait for state update and prevent console warning expect( rendered.getByText('First Status: pending, idle'), ).toBeInTheDocument() await vi.advanceTimersByTimeAsync(0) expect( rendered.getByText('Second Status: pending, fetching'), ).toBeInTheDocument() await vi.advanceTimersByTimeAsync(11) expect( rendered.getByText('Second Status: success, idle'), ).toBeInTheDocument() }) // See https://github.com/tannerlinsley/react-query/issues/144 it('should be in "pending" state by default', () => { const key = queryKey() function Page() { const { status } = useQuery({ queryKey: key, queryFn: async () => { await sleep(10) return 'test' }, }) return
status: {status}
} const rendered = renderWithClient(queryClient, ) expect(rendered.getByText('status: pending')).toBeInTheDocument() }) it('should not refetch query on focus when `enabled` is set to `false`', async () => { const key = queryKey() const queryFn = vi .fn<(...args: Array) => string>() .mockReturnValue('data') function Page() { const { data = 'default' } = useQuery({ queryKey: key, queryFn, enabled: false, }) return (

{data}

) } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(0) rendered.getByText('default') act(() => { window.dispatchEvent(new Event('visibilitychange')) }) expect(queryFn).not.toHaveBeenCalled() }) it('should not refetch stale query on focus when `refetchOnWindowFocus` is set to `false`', async () => { const key = queryKey() const states: Array> = [] let count = 0 function Page() { const state = useQuery({ queryKey: key, queryFn: () => count++, staleTime: 0, refetchOnWindowFocus: false, }) states.push(state) return null } renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(10) act(() => { window.dispatchEvent(new Event('visibilitychange')) }) await vi.advanceTimersByTimeAsync(10) expect(states.length).toBe(2) expect(states[0]).toMatchObject({ data: undefined, isFetching: true }) expect(states[1]).toMatchObject({ data: 0, isFetching: false }) }) it('should not refetch stale query on focus when `refetchOnWindowFocus` is set to a function that returns `false`', async () => { const key = queryKey() const states: Array> = [] let count = 0 function Page() { const state = useQuery({ queryKey: key, queryFn: () => count++, staleTime: 0, refetchOnWindowFocus: () => false, }) states.push(state) return null } renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(10) act(() => { window.dispatchEvent(new Event('visibilitychange')) }) await vi.advanceTimersByTimeAsync(10) expect(states.length).toBe(2) expect(states[0]).toMatchObject({ data: undefined, isFetching: true }) expect(states[1]).toMatchObject({ data: 0, isFetching: false }) }) it('should not refetch fresh query on focus when `refetchOnWindowFocus` is set to `true`', async () => { const key = queryKey() const states: Array> = [] let count = 0 function Page() { const state = useQuery({ queryKey: key, queryFn: () => count++, staleTime: Infinity, refetchOnWindowFocus: true, }) states.push(state) return null } renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(10) act(() => { window.dispatchEvent(new Event('visibilitychange')) }) await vi.advanceTimersByTimeAsync(10) expect(states.length).toBe(2) expect(states[0]).toMatchObject({ data: undefined, isFetching: true }) expect(states[1]).toMatchObject({ data: 0, isFetching: false }) }) it('should refetch fresh query on focus when `refetchOnWindowFocus` is set to `always`', async () => { const key = queryKey() const states: Array> = [] let count = 0 function Page() { const state = useQuery({ queryKey: key, queryFn: async () => { await sleep(10) return count++ }, staleTime: Infinity, refetchOnWindowFocus: 'always', }) states.push(state) return (
data: {state.data}, isFetching: {String(state.isFetching)}
) } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('data: 0, isFetching: false')).toBeInTheDocument() act(() => { window.dispatchEvent(new Event('visibilitychange')) }) await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('data: 1, isFetching: false')).toBeInTheDocument() }) it('should calculate focus behavior for `refetchOnWindowFocus` depending on function', async () => { const key = queryKey() const states: Array> = [] let count = 0 function Page() { const state = useQuery({ queryKey: key, queryFn: async () => { await sleep(10) return count++ }, staleTime: 0, retry: 0, refetchOnWindowFocus: (query) => (query.state.data || 0) < 1, }) states.push(state) return
data: {String(state.data)}
} const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(11) rendered.getByText('data: 0') expect(states.length).toBe(2) expect(states[0]).toMatchObject({ data: undefined, isFetching: true }) expect(states[1]).toMatchObject({ data: 0, isFetching: false }) act(() => { window.dispatchEvent(new Event('visibilitychange')) }) await vi.advanceTimersByTimeAsync(11) rendered.getByText('data: 1') // refetch should happen expect(states.length).toBe(4) expect(states[2]).toMatchObject({ data: 0, isFetching: true }) expect(states[3]).toMatchObject({ data: 1, isFetching: false }) act(() => { window.dispatchEvent(new Event('visibilitychange')) }) await vi.advanceTimersByTimeAsync(20) // no more refetch now expect(states.length).toBe(4) }) it('should refetch fresh query when refetchOnMount is set to always', async () => { const key = queryKey() const states: Array> = [] await queryClient.prefetchQuery({ queryKey: key, queryFn: () => 'prefetched', }) function Page() { const state = useQuery({ queryKey: key, queryFn: () => 'data', refetchOnMount: 'always', staleTime: Infinity, }) states.push(state) return null } renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(0) expect(states.length).toBe(2) expect(states[0]).toMatchObject({ data: 'prefetched', isStale: false, isFetching: true, }) expect(states[1]).toMatchObject({ data: 'data', isStale: false, isFetching: false, }) }) it('should refetch stale query when refetchOnMount is set to true', async () => { const key = queryKey() const states: Array> = [] await queryClient.prefetchQuery({ queryKey: key, queryFn: () => 'prefetched', }) await vi.advanceTimersByTimeAsync(0) function Page() { const state = useQuery({ queryKey: key, queryFn: () => 'data', refetchOnMount: true, staleTime: 0, }) states.push(state) return null } renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(0) expect(states.length).toBe(2) expect(states[0]).toMatchObject({ data: 'prefetched', isStale: true, isFetching: true, }) expect(states[1]).toMatchObject({ data: 'data', isStale: true, isFetching: false, }) }) it('should set status to error if queryFn throws', async () => { const consoleMock = vi .spyOn(console, 'error') .mockImplementation(() => undefined) const key = queryKey() function Page() { const { status, error } = useQuery({ queryKey: key, queryFn: () => { return Promise.reject(new Error('Error test')) }, retry: false, }) return (

{status}

{error?.message}

) } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(0) expect(rendered.getByText('error')).toBeInTheDocument() expect(rendered.getByText('Error test')).toBeInTheDocument() consoleMock.mockRestore() }) it('should throw error if queryFn throws and throwOnError is in use', async () => { const consoleMock = vi .spyOn(console, 'error') .mockImplementation(() => undefined) const key = queryKey() function Page() { const { status, error } = useQuery({ queryKey: key, queryFn: () => Promise.reject(new Error('Error test')), retry: false, throwOnError: true, }) return (

{status}

{error}

) } const rendered = renderWithClient( queryClient,
error boundary
}>
, ) await vi.advanceTimersByTimeAsync(0) expect(rendered.getByText('error boundary')).toBeInTheDocument() consoleMock.mockRestore() }) it('should update with data if we observe no properties and throwOnError', async () => { const key = queryKey() let result: UseQueryResult | undefined function Page() { const query = useQuery({ queryKey: key, queryFn: () => Promise.resolve('data'), throwOnError: true, }) React.useEffect(() => { result = query }) return null } renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(0) expect(queryClient.isFetching()).toBe(0) expect(result?.data).toBe('data') }) it('should set status to error instead of throwing when error should not be thrown', async () => { const key = queryKey() function Page() { const { status, error } = useQuery({ queryKey: key, queryFn: () => Promise.reject(new Error('Local Error')), retry: false, throwOnError: (err) => err.message !== 'Local Error', }) return (

{status}

{error?.message}

) } const rendered = renderWithClient( queryClient,
error boundary
}>
, ) await vi.advanceTimersByTimeAsync(0) expect(rendered.getByText('error')).toBeInTheDocument() expect(rendered.getByText('Local Error')).toBeInTheDocument() }) it('should throw error instead of setting status when error should be thrown', async () => { const consoleMock = vi .spyOn(console, 'error') .mockImplementation(() => undefined) const key = queryKey() function Page() { const { status, error } = useQuery({ queryKey: key, queryFn: () => Promise.reject(new Error('Remote Error')), retry: false, throwOnError: (err) => err.message !== 'Local Error', }) return (

{status}

{error?.message ?? ''}

) } const rendered = renderWithClient( queryClient, (
error boundary
{error?.message}
)} >
, ) await vi.advanceTimersByTimeAsync(0) expect(rendered.getByText('error boundary')).toBeInTheDocument() expect(rendered.getByText('Remote Error')).toBeInTheDocument() consoleMock.mockRestore() }) it('should continue retries when observers unmount and remount while waiting for a retry (#3031)', async () => { const key = queryKey() let count = 0 function Page() { const result = useQuery({ queryKey: key, queryFn: async () => { count++ await sleep(10) return Promise.reject(new Error('some error')) }, retry: 2, retryDelay: 100, }) return (
error: {result.error?.message ?? 'null'}
failureCount: {result.failureCount}
failureReason: {result.failureReason?.message}
) } function App() { const [show, toggle] = React.useReducer((x) => !x, true) return (
{show && }
) } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('failureCount: 1')).toBeInTheDocument() expect(rendered.getByText('failureReason: some error')).toBeInTheDocument() await vi.advanceTimersByTimeAsync(90) fireEvent.click(rendered.getByRole('button', { name: /hide/i })) await vi.advanceTimersByTimeAsync(11) expect(rendered.getByRole('button', { name: /show/i })).toBeInTheDocument() fireEvent.click(rendered.getByRole('button', { name: /show/i })) await vi.advanceTimersByTimeAsync(11) await vi.advanceTimersByTimeAsync(110) await vi.advanceTimersByTimeAsync(110) expect(rendered.getByText('error: some error')).toBeInTheDocument() expect(count).toBe(4) }) it('should restart when observers unmount and remount while waiting for a retry when query was cancelled in between (#3031)', async () => { const key = queryKey() let count = 0 function Page() { const result = useQuery({ queryKey: key, queryFn: async () => { count++ await sleep(10) return Promise.reject(new Error('some error')) }, retry: 2, retryDelay: 100, }) return (
error: {result.error?.message ?? 'null'}
failureCount: {result.failureCount}
failureReason: {result.failureReason?.message}
) } function App() { const [show, toggle] = React.useReducer((x) => !x, true) return (
{show && }
) } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('failureCount: 1')).toBeInTheDocument() expect(rendered.getByText('failureReason: some error')).toBeInTheDocument() fireEvent.click(rendered.getByRole('button', { name: /hide/i })) fireEvent.click(rendered.getByRole('button', { name: /cancel/i })) expect(rendered.getByRole('button', { name: /show/i })).toBeInTheDocument() await vi.advanceTimersByTimeAsync(1) fireEvent.click(rendered.getByRole('button', { name: /show/i })) await vi.advanceTimersByTimeAsync(11) await vi.advanceTimersByTimeAsync(110) await vi.advanceTimersByTimeAsync(110) expect(rendered.getByText('error: some error')).toBeInTheDocument() // initial fetch (1), which will be cancelled, followed by new mount(2) + 2 retries = 4 expect(count).toBe(4) }) it('should always fetch if refetchOnMount is set to always', async () => { const key = queryKey() const states: Array> = [] await queryClient.prefetchQuery({ queryKey: key, queryFn: () => 'prefetched', }) function Page() { const state = useQuery({ queryKey: key, queryFn: () => 'data', refetchOnMount: 'always', staleTime: 50, }) states.push(state) return (
data: {state.data ?? 'null'}
isFetching: {state.isFetching}
isStale: {state.isStale}
) } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(0) rendered.getByText('data: data') await vi.advanceTimersByTimeAsync(52) expect(states.length).toBe(3) expect(states[0]).toMatchObject({ data: 'prefetched', isStale: false, isFetching: true, }) expect(states[1]).toMatchObject({ data: 'data', isStale: false, isFetching: false, }) expect(states[2]).toMatchObject({ data: 'data', isStale: true, isFetching: false, }) }) it('should fetch if initial data is set', async () => { const key = queryKey() const states: Array> = [] function Page() { const state = useQuery({ queryKey: key, queryFn: () => 'data', initialData: 'initial', }) states.push(state) return null } renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(0) expect(states.length).toBe(2) expect(states[0]).toMatchObject({ data: 'initial', isStale: true, isFetching: true, }) expect(states[1]).toMatchObject({ data: 'data', isStale: true, isFetching: false, }) }) it('should not fetch if initial data is set with a stale time', async () => { const key = queryKey() const states: Array> = [] function Page() { const state = useQuery({ queryKey: key, queryFn: () => 'data', staleTime: 50, initialData: 'initial', }) states.push(state) return null } renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(52) expect(states.length).toBe(2) expect(states[0]).toMatchObject({ data: 'initial', isStale: false, isFetching: false, }) expect(states[1]).toMatchObject({ data: 'initial', isStale: true, isFetching: false, }) }) it('should fetch if initial data updated at is older than stale time', async () => { const key = queryKey() const states: Array> = [] const oneSecondAgo = Date.now() - 1000 function Page() { const state = useQuery({ queryKey: key, queryFn: () => 'data', staleTime: 50, initialData: 'initial', initialDataUpdatedAt: oneSecondAgo, }) states.push(state) return null } renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(52) expect(states.length).toBe(3) expect(states[0]).toMatchObject({ data: 'initial', isStale: true, isFetching: true, }) expect(states[1]).toMatchObject({ data: 'data', isStale: false, isFetching: false, }) expect(states[2]).toMatchObject({ data: 'data', isStale: true, isFetching: false, }) }) it('should fetch if "initial data updated at" is exactly 0', async () => { const key = queryKey() const states: Array> = [] function Page() { const state = useQuery({ queryKey: key, queryFn: () => 'data', staleTime: 10 * 1000, // 10 seconds initialData: 'initial', initialDataUpdatedAt: 0, }) states.push(state) return null } renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(0) expect(states.length).toBe(2) expect(states[0]).toMatchObject({ data: 'initial', isStale: true, isFetching: true, }) expect(states[1]).toMatchObject({ data: 'data', isStale: false, isFetching: false, }) }) it('should keep initial data when the query key changes', async () => { const key = queryKey() const states: Array> = [] function Page() { const [count, setCount] = React.useState(0) const state = useQuery({ queryKey: [key, count], queryFn: () => ({ count: 10 }), staleTime: Infinity, initialData: () => ({ count }), }) states.push(state) React.useEffect(() => { setActTimeout(() => { setCount(1) }, 10) }, []) return null } renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(11) expect(states.length).toBe(2) // Initial expect(states[0]).toMatchObject({ data: { count: 0 } }) // Set state expect(states[1]).toMatchObject({ data: { count: 1 } }) }) it('should retry specified number of times', async () => { const key = queryKey() const queryFn = vi.fn<(...args: Array) => unknown>() queryFn.mockImplementation(() => { return Promise.reject(new Error('Error test Barrett')) }) function Page() { const { status, failureCount, failureReason } = useQuery({ queryKey: key, queryFn, retry: 1, retryDelay: 1, }) return (

{status}

Failed {failureCount} times

Failed because {failureReason?.message}

) } const rendered = renderWithClient(queryClient, ) rendered.getByText('pending') await vi.advanceTimersByTimeAsync(2) rendered.getByText('error') // query should fail `retry + 1` times, since first time isn't a "retry" rendered.getByText('Failed 2 times') rendered.getByText('Failed because Error test Barrett') expect(queryFn).toHaveBeenCalledTimes(2) }) it('should not retry if retry function `false`', async () => { const key = queryKey() const queryFn = vi.fn<(...args: Array) => unknown>() queryFn.mockImplementationOnce(() => { return Promise.reject(new Error('Error test Tanner')) }) queryFn.mockImplementation(() => { return Promise.reject(new Error('NoRetry')) }) function Page() { const { status, failureCount, failureReason, error } = useQuery({ queryKey: key, queryFn, retryDelay: 1, retry: (_failureCount, err) => err.message !== 'NoRetry', }) return (

{status}

Failed {failureCount} times

Failed because {failureReason?.message}

{error?.message}

) } const rendered = renderWithClient(queryClient, ) rendered.getByText('pending') await vi.advanceTimersByTimeAsync(2) rendered.getByText('error') rendered.getByText('Failed 2 times') rendered.getByText('Failed because NoRetry') rendered.getByText('NoRetry') expect(queryFn).toHaveBeenCalledTimes(2) }) it('should extract retryDelay from error', async () => { const key = queryKey() type DelayError = { delay: number } const queryFn = vi.fn<(...args: Array) => unknown>() queryFn.mockImplementation(() => { return Promise.reject({ delay: 50 }) }) function Page() { const { status, failureCount, failureReason } = useQuery({ queryKey: key, queryFn, retry: 1, retryDelay: (_, error: DelayError) => error.delay, }) return (

{status}

Failed {failureCount} times

Failed because DelayError: {failureReason?.delay}ms

) } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(0) expect(queryFn).toHaveBeenCalledTimes(1) rendered.getByText('Failed because DelayError: 50ms') await vi.advanceTimersByTimeAsync(51) rendered.getByText('Failed 2 times') expect(queryFn).toHaveBeenCalledTimes(2) }) // See https://github.com/tannerlinsley/react-query/issues/160 it('should continue retry after focus regain', async () => { const key = queryKey() // make page unfocused const visibilityMock = mockVisibilityState('hidden') let count = 0 function Page() { const query = useQuery({ queryKey: key, queryFn: () => { count++ return Promise.reject(`fetching error ${count}`) }, retry: 3, retryDelay: 1, }) return (
error {String(query.error)}
status {query.status}
failureCount {query.failureCount}
failureReason {query.failureReason}
) } const rendered = renderWithClient(queryClient, ) // The query should display the first error result await vi.advanceTimersByTimeAsync(0) expect(rendered.getByText('failureCount 1')).toBeInTheDocument() expect( rendered.getByText('failureReason fetching error 1'), ).toBeInTheDocument() expect(rendered.getByText('status pending')).toBeInTheDocument() expect(rendered.getByText('error null')).toBeInTheDocument() // Check if the query really paused await vi.advanceTimersByTimeAsync(0) expect(rendered.getByText('failureCount 1')).toBeInTheDocument() expect( rendered.getByText('failureReason fetching error 1'), ).toBeInTheDocument() act(() => { // reset visibilityState to original value visibilityMock.mockRestore() window.dispatchEvent(new Event('visibilitychange')) }) // Wait for the final result await vi.advanceTimersByTimeAsync(4) expect(rendered.getByText('failureCount 4')).toBeInTheDocument() expect( rendered.getByText('failureReason fetching error 4'), ).toBeInTheDocument() expect(rendered.getByText('status error')).toBeInTheDocument() expect(rendered.getByText('error fetching error 4')).toBeInTheDocument() // Check if the query really stopped await vi.advanceTimersByTimeAsync(10) expect(rendered.getByText('failureCount 4')).toBeInTheDocument() expect( rendered.getByText('failureReason fetching error 4'), ).toBeInTheDocument() }) it('should fetch on mount when a query was already created with setQueryData', async () => { const key = queryKey() const states: Array> = [] queryClient.setQueryData(key, 'prefetched') function Page() { const state = useQuery({ queryKey: key, queryFn: () => 'data' }) states.push(state) return null } renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(0) expect(states.length).toBe(2) expect(states).toMatchObject([ { data: 'prefetched', isFetching: true, isStale: true, }, { data: 'data', isFetching: false, isStale: true, }, ]) }) it('should refetch after focus regain', async () => { const key = queryKey() const states: Array> = [] // make page unfocused const visibilityMock = mockVisibilityState('hidden') // set data in cache to check if the hook query fn is actually called queryClient.setQueryData(key, 'prefetched') function Page() { const state = useQuery({ queryKey: key, queryFn: async () => { await sleep(10) return 'data' }, }) states.push(state) return (
{state.data}, {state.isStale}, {state.isFetching}
) } renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(11) expect(states.length).toBe(2) act(() => { // reset visibilityState to original value visibilityMock.mockRestore() window.dispatchEvent(new Event('visibilitychange')) }) await vi.advanceTimersByTimeAsync(11) expect(states.length).toBe(4) expect(states).toMatchObject([ { data: 'prefetched', isFetching: true, isStale: true, }, { data: 'data', isFetching: false, isStale: true, }, { data: 'data', isFetching: true, isStale: true, }, { data: 'data', isFetching: false, isStale: true, }, ]) }) // See https://github.com/tannerlinsley/react-query/issues/195 it('should refetch if stale after a prefetch', async () => { const key = queryKey() const states: Array> = [] const queryFn = vi.fn<(...args: Array) => string>() queryFn.mockImplementation(() => 'data') const prefetchQueryFn = vi.fn<(...args: Array) => string>() prefetchQueryFn.mockImplementation(() => 'not yet...') await queryClient.prefetchQuery({ queryKey: key, queryFn: prefetchQueryFn, staleTime: 10, }) await vi.advanceTimersByTimeAsync(10) function Page() { const state = useQuery({ queryKey: key, queryFn }) states.push(state) return null } renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(11) expect(states.length).toBe(2) expect(prefetchQueryFn).toHaveBeenCalledTimes(1) expect(queryFn).toHaveBeenCalledTimes(1) }) it('should not refetch if not stale after a prefetch', async () => { const key = queryKey() const queryFn = vi.fn<(...args: Array) => string>() queryFn.mockImplementation(() => 'data') const prefetchQueryFn = vi.fn<(...args: Array) => Promise>() prefetchQueryFn.mockImplementation(async () => { await sleep(10) return 'not yet...' }) queryClient.prefetchQuery({ queryKey: key, queryFn: prefetchQueryFn, staleTime: 1000, }) await vi.advanceTimersByTimeAsync(0) function Page() { useQuery({ queryKey: key, queryFn, staleTime: 1000 }) return null } renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(0) expect(prefetchQueryFn).toHaveBeenCalledTimes(1) expect(queryFn).toHaveBeenCalledTimes(0) }) // See https://github.com/tannerlinsley/react-query/issues/190 it('should reset failureCount on successful fetch', async () => { const key = queryKey() let counter = 0 function Page() { const query = useQuery({ queryKey: key, queryFn: () => { if (counter < 2) { counter++ return Promise.reject(new Error('error')) } else { return Promise.resolve('data') } }, retryDelay: 10, }) return (
failureCount {query.failureCount}
failureReason {query.failureReason?.message ?? 'null'}
) } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('failureCount 2')).toBeInTheDocument() expect(rendered.getByText('failureReason error')).toBeInTheDocument() await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('failureCount 0')).toBeInTheDocument() expect(rendered.getByText('failureReason null')).toBeInTheDocument() }) // See https://github.com/tannerlinsley/react-query/issues/199 it('should use prefetched data for dependent query', async () => { const key = queryKey() let count = 0 function Page() { const [enabled, setEnabled] = React.useState(false) const [isPrefetched, setPrefetched] = React.useState(false) const query = useQuery({ queryKey: key, queryFn: async () => { count++ await sleep(10) return count }, enabled, }) React.useEffect(() => { async function prefetch() { await queryClient.prefetchQuery({ queryKey: key, queryFn: () => Promise.resolve('prefetched data'), }) act(() => setPrefetched(true)) } prefetch() }, []) return (
{isPrefetched &&
isPrefetched
}
data: {query.data}
) } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(0) expect(rendered.getByText('isPrefetched')).toBeInTheDocument() fireEvent.click(rendered.getByText('setKey')) expect(rendered.getByText('data: prefetched data')).toBeInTheDocument() await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('data: 1')).toBeInTheDocument() expect(count).toBe(1) }) it('should support dependent queries via the enable config option', async () => { const key = queryKey() function Page() { const [shouldFetch, setShouldFetch] = React.useState(false) const query = useQuery({ queryKey: key, queryFn: async () => { await sleep(10) return 'data' }, enabled: shouldFetch, }) return (
FetchStatus: {query.fetchStatus}

Data: {query.data || 'no data'}

{shouldFetch ? null : ( )}
) } const rendered = renderWithClient(queryClient, ) expect(rendered.getByText('FetchStatus: idle')).toBeInTheDocument() expect(rendered.getByText('Data: no data')).toBeInTheDocument() fireEvent.click(rendered.getByText('fetch')) await vi.advanceTimersByTimeAsync(0) expect(rendered.getByText('FetchStatus: fetching')).toBeInTheDocument() await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('FetchStatus: idle')).toBeInTheDocument() expect(rendered.getByText('Data: data')).toBeInTheDocument() }) it('should mark query as fetching, when using initialData', async () => { const key = queryKey() const results: Array> = [] function Page() { const result = useQuery({ queryKey: key, queryFn: async () => { await sleep(10) return 'serverData' }, initialData: 'initialData', }) results.push(result) return
data: {result.data}
} const rendered = renderWithClient(queryClient, ) rendered.getByText('data: initialData') await vi.advanceTimersByTimeAsync(11) rendered.getByText('data: serverData') expect(results.length).toBe(2) expect(results[0]).toMatchObject({ data: 'initialData', isFetching: true }) expect(results[1]).toMatchObject({ data: 'serverData', isFetching: false }) }) it('should initialize state properly, when initialData is falsy', async () => { const key = queryKey() const results: Array> = [] function Page() { const result = useQuery({ queryKey: key, queryFn: () => 1, initialData: 0, }) results.push(result) return null } renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(0) expect(results.length).toBe(2) expect(results[0]).toMatchObject({ data: 0, isFetching: true }) expect(results[1]).toMatchObject({ data: 1, isFetching: false }) }) it('should show the correct data when switching keys with initialData, placeholderData & staleTime', async () => { const key = queryKey() const ALL_TODOS = [ { name: 'todo A', priority: 'high' }, { name: 'todo B', priority: 'medium' }, ] const initialTodos = ALL_TODOS function Page() { const [filter, setFilter] = React.useState('') const { data: todos } = useQuery({ queryKey: [...key, filter], queryFn: () => { return Promise.resolve( ALL_TODOS.filter((todo) => filter ? todo.priority === filter : true, ), ) }, initialData() { return filter === '' ? initialTodos : undefined }, placeholderData: keepPreviousData, staleTime: 5000, }) return (
Current Todos, filter: {filter || 'all'}
    {(todos ?? []).map((todo) => (
  • {todo.name} - {todo.priority}
  • ))}
) } const rendered = renderWithClient(queryClient, ) expect(rendered.getByText('Current Todos, filter: all')).toBeInTheDocument() fireEvent.click(rendered.getByRole('button', { name: /high/i })) await vi.advanceTimersByTimeAsync(0) expect( rendered.getByText('Current Todos, filter: high'), ).toBeInTheDocument() fireEvent.click(rendered.getByRole('button', { name: /all/i })) await vi.advanceTimersByTimeAsync(0) expect(rendered.getByText('todo B - medium')).toBeInTheDocument() }) // // See https://github.com/tannerlinsley/react-query/issues/214 it('data should persist when enabled is changed to false', async () => { const key = queryKey() const results: Array> = [] function Page() { const [shouldFetch, setShouldFetch] = React.useState(true) const result = useQuery({ queryKey: key, queryFn: () => 'fetched data', enabled: shouldFetch, initialData: shouldFetch ? 'initial' : 'initial falsy', }) results.push(result) return (
{result.data}
{shouldFetch ? 'enabled' : 'disabled'}
) } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(0) expect(rendered.getByText('fetched data')).toBeInTheDocument() expect(rendered.getByText('enabled')).toBeInTheDocument() fireEvent.click(rendered.getByRole('button', { name: /enable/i })) await vi.advanceTimersByTimeAsync(0) expect(rendered.getByText('fetched data')).toBeInTheDocument() expect(rendered.getByText('disabled')).toBeInTheDocument() expect(results.length).toBe(3) expect(results[0]).toMatchObject({ data: 'initial', isStale: true }) expect(results[1]).toMatchObject({ data: 'fetched data', isStale: true }) // disabled observers are not stale expect(results[2]).toMatchObject({ data: 'fetched data', isStale: false }) }) it('should support enabled:false in query object syntax', () => { const key = queryKey() const queryFn = vi.fn<(...args: Array) => string>() queryFn.mockImplementation(() => 'data') function Page() { const { fetchStatus } = useQuery({ queryKey: key, queryFn, enabled: false, }) return
fetchStatus: {fetchStatus}
} const rendered = renderWithClient(queryClient, ) expect(queryFn).not.toHaveBeenCalled() expect(queryCache.find({ queryKey: key })).not.toBeUndefined() expect(rendered.getByText('fetchStatus: idle')).toBeInTheDocument() }) // See https://github.com/tannerlinsley/react-query/issues/360 test('should init to status:pending, fetchStatus:idle when enabled is false', async () => { const key = queryKey() function Page() { const query = useQuery({ queryKey: key, queryFn: () => 'data', enabled: false, }) return (
status: {query.status}, {query.fetchStatus}
) } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(0) expect(rendered.getByText('status: pending, idle')).toBeInTheDocument() }) test('should not schedule garbage collection, if gcTimeout is set to `Infinity`', async () => { const key = queryKey() function Page() { const query = useQuery({ queryKey: key, queryFn: () => 'fetched data', gcTime: Infinity, }) return
{query.data}
} const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(0) rendered.getByText('fetched data') const setTimeoutSpy = vi.spyOn(globalThis.window, 'setTimeout') rendered.unmount() expect(setTimeoutSpy).not.toHaveBeenCalled() }) test('should schedule garbage collection, if gcTimeout is not set to infinity', async () => { const key = queryKey() function Page() { const query = useQuery({ queryKey: key, queryFn: () => 'fetched data', gcTime: 1000 * 60 * 10, // 10 Minutes }) return
{query.data}
} const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(0) rendered.getByText('fetched data') const setTimeoutSpy = vi.spyOn(globalThis.window, 'setTimeout') rendered.unmount() expect(setTimeoutSpy).toHaveBeenLastCalledWith( expect.any(Function), 1000 * 60 * 10, ) }) it('should not cause memo churn when data does not change', async () => { const key = queryKey() const queryFn = vi .fn<(...args: Array) => string>() .mockReturnValue('data') const memoFn = vi.fn() function Page() { const result = useQuery({ queryKey: key, queryFn: async () => { await sleep(10) return ( queryFn() || { data: { nested: true, }, } ) }, }) React.useMemo(() => { memoFn() return result.data }, [result.data]) return (
status {result.status}
isFetching {result.isFetching ? 'true' : 'false'}
) } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(0) rendered.getByText('status pending') await vi.advanceTimersByTimeAsync(11) rendered.getByText('status success') fireEvent.click(rendered.getByText('refetch')) await vi.advanceTimersByTimeAsync(0) rendered.getByText('isFetching true') await vi.advanceTimersByTimeAsync(11) rendered.getByText('isFetching false') expect(queryFn).toHaveBeenCalledTimes(2) expect(memoFn).toHaveBeenCalledTimes(2) }) it('should update data upon interval changes', async () => { const key = queryKey() let count = 0 function Page() { const [int, setInt] = React.useState(200) const { data } = useQuery({ queryKey: key, queryFn: () => count++, refetchInterval: int, }) React.useEffect(() => { if (data === 2) { setInt(0) } }, [data]) return
count: {data}
} const rendered = renderWithClient(queryClient, ) // mount await vi.advanceTimersByTimeAsync(0) expect(rendered.getByText('count: 0')).toBeInTheDocument() await vi.advanceTimersByTimeAsync(201) expect(rendered.getByText('count: 1')).toBeInTheDocument() await vi.advanceTimersByTimeAsync(201) expect(rendered.getByText('count: 2')).toBeInTheDocument() }) it('should refetch in an interval depending on function result', async () => { const key = queryKey() let count = 0 const states: Array> = [] function Page() { const queryInfo = useQuery({ queryKey: key, queryFn: async () => { await sleep(10) return count++ }, refetchInterval: ({ state: { data = 0 } }) => (data < 2 ? 10 : false), }) states.push(queryInfo) return (

count: {queryInfo.data}

status: {queryInfo.status}

data: {queryInfo.data}

refetch: {queryInfo.isRefetching}

) } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(51) rendered.getByText('count: 2') expect(states.length).toEqual(6) expect(states).toMatchObject([ { status: 'pending', isFetching: true, data: undefined, }, { status: 'success', isFetching: false, data: 0, }, { status: 'success', isFetching: true, data: 0, }, { status: 'success', isFetching: false, data: 1, }, { status: 'success', isFetching: true, data: 1, }, { status: 'success', isFetching: false, data: 2, }, ]) }) it('should not interval fetch with a refetchInterval of 0', async () => { const key = queryKey() const queryFn = vi.fn(() => 1) function Page() { const queryInfo = useQuery({ queryKey: key, queryFn, refetchInterval: 0, }) return
count: {queryInfo.data}
} const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(0) rendered.getByText('count: 1') await vi.advanceTimersByTimeAsync(10) // extra sleep to make sure we're not re-fetching expect(queryFn).toHaveBeenCalledTimes(1) }) it('should accept an empty string as query key', async () => { function Page() { const result = useQuery({ queryKey: [''], queryFn: (ctx) => ctx.queryKey, }) return <>{JSON.stringify(result.data)} } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(0) expect(rendered.getByText('')).toBeInTheDocument() }) it('should accept an object as query key', async () => { function Page() { const result = useQuery({ queryKey: [{ a: 'a' }], queryFn: (ctx) => ctx.queryKey, }) return <>{JSON.stringify(result.data)} } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(0) expect(rendered.getByText('[{"a":"a"}]')).toBeInTheDocument() }) it('should refetch if any query instance becomes enabled', async () => { const key = queryKey() const queryFn = vi .fn<(...args: Array) => string>() .mockReturnValue('data') function Disabled() { useQuery({ queryKey: key, queryFn, enabled: false }) return null } function Page() { const [enabled, setEnabled] = React.useState(false) const result = useQuery({ queryKey: key, queryFn, enabled }) return ( <>
{result.data}
) } const rendered = renderWithClient(queryClient, ) expect(queryFn).toHaveBeenCalledTimes(0) fireEvent.click(rendered.getByText('enable')) await vi.advanceTimersByTimeAsync(0) rendered.getByText('data') expect(queryFn).toHaveBeenCalledTimes(1) }) it('should use placeholder data while the query loads', async () => { const key1 = queryKey() const states: Array> = [] function Page() { const state = useQuery({ queryKey: key1, queryFn: () => 'data', placeholderData: 'placeholder', }) states.push(state) return (

Data: {state.data}

Status: {state.status}
) } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(0) rendered.getByText('Data: data') expect(states).toMatchObject([ { isSuccess: true, isPlaceholderData: true, data: 'placeholder', }, { isSuccess: true, isPlaceholderData: false, data: 'data', }, ]) }) it('should use placeholder data even for disabled queries', async () => { const key1 = queryKey() const states: Array<{ state: UseQueryResult; count: number }> = [] function Page() { const [count, setCount] = React.useState(0) const state = useQuery({ queryKey: key1, queryFn: () => 'data', placeholderData: 'placeholder', enabled: count === 0, }) states.push({ state, count }) React.useEffect(() => { setCount(1) }, []) return (

Data: {state.data}

Status: {state.status}
) } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(0) rendered.getByText('Data: data') expect(states).toMatchObject([ { state: { isSuccess: true, isPlaceholderData: true, data: 'placeholder', }, count: 0, }, { state: { isSuccess: true, isPlaceholderData: true, data: 'placeholder', }, count: 1, }, { state: { isSuccess: true, isPlaceholderData: false, data: 'data', }, count: 1, }, ]) }) it('placeholder data should run through select', async () => { const key1 = queryKey() const states: Array> = [] function Page() { const state = useQuery({ queryKey: key1, queryFn: () => 1, placeholderData: 23, select: (data) => String(data * 2), }) states.push(state) return (

Data: {state.data}

Status: {state.status}
) } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(0) rendered.getByText('Data: 2') expect(states).toMatchObject([ { isSuccess: true, isPlaceholderData: true, data: '46', }, { isSuccess: true, isPlaceholderData: false, data: '2', }, ]) }) it('placeholder data function result should run through select', async () => { const key1 = queryKey() const states: Array> = [] let placeholderFunctionRunCount = 0 function Page() { const state = useQuery({ queryKey: key1, queryFn: () => 1, placeholderData: () => { placeholderFunctionRunCount++ return 23 }, select: (data) => String(data * 2), }) states.push(state) return (

Data: {state.data}

Status: {state.status}
) } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(0) rendered.getByText('Data: 2') rendered.rerender() expect(states).toMatchObject([ { isSuccess: true, isPlaceholderData: true, data: '46', }, { isSuccess: true, isPlaceholderData: false, data: '2', }, { isSuccess: true, isPlaceholderData: false, data: '2', }, ]) expect(placeholderFunctionRunCount).toEqual(1) }) it('select should only run when dependencies change if memoized', async () => { const key1 = queryKey() let selectRun = 0 function Page() { const [count, inc] = React.useReducer((prev) => prev + 1, 2) const state = useQuery({ queryKey: key1, queryFn: async () => { await sleep(10) return 0 }, select: React.useCallback( (data: number) => { selectRun++ return `selected ${data + count}` }, [count], ), placeholderData: 99, }) return (

Data: {state.data}

) } const rendered = renderWithClient(queryClient, ) rendered.getByText('Data: selected 101') // 99 + 2 expect(selectRun).toBe(1) await vi.advanceTimersByTimeAsync(11) rendered.getByText('Data: selected 2') // 0 + 2 expect(selectRun).toBe(2) fireEvent.click(rendered.getByRole('button', { name: /inc/i })) await vi.advanceTimersByTimeAsync(11) rendered.getByText('Data: selected 3') // 0 + 3 expect(selectRun).toBe(3) }) it('select should always return the correct state', async () => { const key1 = queryKey() function Page() { const [count, inc] = React.useReducer((prev) => prev + 1, 2) const [forceValue, forceUpdate] = React.useReducer((prev) => prev + 1, 1) const state = useQuery({ queryKey: key1, queryFn: async () => { await sleep(10) return 0 }, select: React.useCallback( (data: number) => { return `selected ${data + count}` }, [count], ), placeholderData: 99, }) return (

Data: {state.data}

forceValue: {forceValue}

) } const rendered = renderWithClient(queryClient, ) expect(rendered.getByText('Data: selected 101')).toBeInTheDocument() // 99 + 2 await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('Data: selected 2')).toBeInTheDocument() // 0 + 2 fireEvent.click(rendered.getByRole('button', { name: /inc/i })) await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('Data: selected 3')).toBeInTheDocument() // 0 + 3 fireEvent.click(rendered.getByRole('button', { name: /forceUpdate/i })) expect(rendered.getByText('forceValue: 2')).toBeInTheDocument() // data should still be 3 after an independent re-render await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('Data: selected 3')).toBeInTheDocument() }) it('select should structurally share data', async () => { const key1 = queryKey() const states: Array> = [] function Page() { const [forceValue, forceUpdate] = React.useReducer((prev) => prev + 1, 1) const state = useQuery({ queryKey: key1, queryFn: async () => { await sleep(10) return [1, 2] }, select: (res) => res.map((x) => x + 1), }) React.useEffect(() => { if (state.data) { states.push(state.data) } }, [state.data]) return (

Data: {JSON.stringify(state.data)}

forceValue: {forceValue}

) } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(11) rendered.getByText('Data: [2,3]') expect(states).toHaveLength(1) fireEvent.click(rendered.getByRole('button', { name: /forceUpdate/i })) await vi.advanceTimersByTimeAsync(11) rendered.getByText('forceValue: 2') rendered.getByText('Data: [2,3]') // effect should not be triggered again due to structural sharing expect(states).toHaveLength(1) }) it('should cancel the query function when there are no more subscriptions', async () => { const key = queryKey() let cancelFn: Mock = vi.fn() const queryFn = ({ signal }: { signal?: AbortSignal }) => { const promise = new Promise((resolve, reject) => { cancelFn = vi.fn(() => reject('Cancelled')) signal?.addEventListener('abort', cancelFn) sleep(20).then(() => resolve('OK')) }) return promise } function Page() { const state = useQuery({ queryKey: key, queryFn }) return (

Status: {state.status}

) } const rendered = renderWithClient( queryClient, , ) await vi.advanceTimersByTimeAsync(6) rendered.getByText('off') expect(cancelFn).toHaveBeenCalled() }) it('should cancel the query if the signal was consumed and there are no more subscriptions', async () => { const key = queryKey() const states: Array> = [] const queryFn: QueryFunction = async ( ctx, ) => { const [, limit] = ctx.queryKey const value = limit % 2 && ctx.signal ? 'abort' : `data ${limit}` await sleep(25) return value } function Page(props: { limit: number }) { const state = useQuery({ queryKey: [key, props.limit], queryFn }) states[props.limit] = state return (

Status: {state.status}

data: {state.data}

) } const rendered = renderWithClient( queryClient, , ) await vi.advanceTimersByTimeAsync(6) rendered.getByText('off') await vi.advanceTimersByTimeAsync(20) expect(states).toHaveLength(4) expect(queryCache.find({ queryKey: [key, 0] })?.state).toMatchObject({ data: 'data 0', status: 'success', dataUpdateCount: 1, }) expect(queryCache.find({ queryKey: [key, 1] })?.state).toMatchObject({ data: undefined, status: 'pending', fetchStatus: 'idle', }) expect(queryCache.find({ queryKey: [key, 2] })?.state).toMatchObject({ data: 'data 2', status: 'success', dataUpdateCount: 1, }) expect(queryCache.find({ queryKey: [key, 3] })?.state).toMatchObject({ data: undefined, status: 'pending', fetchStatus: 'idle', }) }) it('should refetch when quickly switching to a failed query', async () => { const key = queryKey() const states: Array> = [] const queryFn = async () => { await sleep(50) return 'OK' } function Page() { const [id, setId] = React.useState(1) const [hasChanged, setHasChanged] = React.useState(false) const state = useQuery({ queryKey: [key, id], queryFn }) states.push(state) React.useEffect(() => { setId((prevId) => (prevId === 1 ? 2 : 1)) setHasChanged(true) }, [hasChanged]) return null } renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(51) expect(states.length).toBe(4) // Load query 1 expect(states[0]).toMatchObject({ status: 'pending', error: null, }) // Load query 2 expect(states[1]).toMatchObject({ status: 'pending', error: null, }) // Load query 1 expect(states[2]).toMatchObject({ status: 'pending', error: null, }) // Loaded query 1 expect(states[3]).toMatchObject({ status: 'success', error: null, }) }) it('should update query state and refetch when reset with resetQueries', async () => { const key = queryKey() const states: Array> = [] let count = 0 function Page() { const state = useQuery({ queryKey: key, queryFn: async () => { await sleep(10) count++ return count }, staleTime: Infinity, }) states.push(state) return (
data: {state.data ?? 'null'}
isFetching: {state.isFetching}
) } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(11) rendered.getByText('data: 1') fireEvent.click(rendered.getByRole('button', { name: /reset/i })) await vi.advanceTimersByTimeAsync(11) expect(states.length).toBe(4) rendered.getByText('data: 2') expect(count).toBe(2) expect(states[0]).toMatchObject({ data: undefined, isPending: true, isFetching: true, isSuccess: false, isStale: true, }) expect(states[1]).toMatchObject({ data: 1, isPending: false, isFetching: false, isSuccess: true, isStale: false, }) expect(states[2]).toMatchObject({ data: undefined, isPending: true, isFetching: true, isSuccess: false, isStale: true, }) expect(states[3]).toMatchObject({ data: 2, isPending: false, isFetching: false, isSuccess: true, isStale: false, }) }) it('should update query state and not refetch when resetting a disabled query with resetQueries', async () => { const key = queryKey() const states: Array> = [] let count = 0 function Page() { const state = useQuery({ queryKey: key, queryFn: async () => { await sleep(10) count++ return count }, staleTime: Infinity, enabled: false, notifyOnChangeProps: 'all', }) states.push(state) const { refetch } = state return (
data: {state.data ?? 'null'}
) } const rendered = renderWithClient(queryClient, ) rendered.getByText('data: null') fireEvent.click(rendered.getByRole('button', { name: /refetch/i })) await vi.advanceTimersByTimeAsync(11) rendered.getByText('data: 1') fireEvent.click(rendered.getByRole('button', { name: /reset/i })) await vi.advanceTimersByTimeAsync(0) rendered.getByText('data: null') expect(states.length).toBe(4) expect(count).toBe(1) expect(states[0]).toMatchObject({ data: undefined, isPending: true, isFetching: false, isSuccess: false, isStale: false, }) expect(states[1]).toMatchObject({ data: undefined, isPending: true, isFetching: true, isSuccess: false, isStale: false, }) expect(states[2]).toMatchObject({ data: 1, isPending: false, isFetching: false, isSuccess: true, isStale: false, }) expect(states[3]).toMatchObject({ data: undefined, isPending: true, isFetching: false, isSuccess: false, isStale: false, }) }) it('should only call the query hash function once each render', async () => { const key = queryKey() let hashes = 0 let renders = 0 function queryKeyHashFn(x: any) { hashes++ return JSON.stringify(x) } function Page() { React.useEffect(() => { renders++ }) useQuery({ queryKey: key, queryFn: () => 'test', queryKeyHashFn }) return null } renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(0) expect(renders).toBe(hashes) }) it('should hash query keys that contain bigints given a supported query hash function', async () => { const key = [queryKey(), 1n] function queryKeyHashFn(x: any) { return JSON.stringify(x, (_, value) => { if (typeof value === 'bigint') return value.toString() return value }) } function Page() { useQuery({ queryKey: key, queryFn: () => 'test', queryKeyHashFn }) return null } renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(0) const query = queryClient.getQueryCache().get(queryKeyHashFn(key)) expect(query?.state.data).toBe('test') }) it('should refetch when changed enabled to true in error state', async () => { const queryFn = vi.fn<(...args: Array) => unknown>() queryFn.mockImplementation(async () => { await sleep(10) return Promise.reject(new Error('Suspense Error Bingo')) }) function Page({ enabled }: { enabled: boolean }) { const { error, isPending } = useQuery({ queryKey: ['key'], queryFn, enabled, retry: false, retryOnMount: false, refetchOnMount: false, refetchOnWindowFocus: false, }) if (isPending) { return
status: pending
} if (error instanceof Error) { return
error
} return
rendered
} function App() { const [enabled, toggle] = React.useReducer((x) => !x, true) return (
) } const rendered = renderWithClient(queryClient, ) // initial state check rendered.getByText('status: pending') // // render error state component await vi.advanceTimersByTimeAsync(11) rendered.getByText('error') expect(queryFn).toBeCalledTimes(1) // change to enabled to false fireEvent.click(rendered.getByLabelText('retry')) await vi.advanceTimersByTimeAsync(11) rendered.getByText('error') expect(queryFn).toBeCalledTimes(1) // // change to enabled to true fireEvent.click(rendered.getByLabelText('retry')) expect(queryFn).toBeCalledTimes(2) }) it('should refetch when query key changed when previous status is error', async () => { function Page({ id }: { id: number }) { const { error, isPending } = useQuery({ queryKey: [id], queryFn: async () => { await sleep(10) if (id % 2 === 1) { return Promise.reject(new Error('Error')) } else { return 'data' } }, retry: false, retryOnMount: false, refetchOnMount: false, refetchOnWindowFocus: false, }) if (isPending) { return
status: pending
} if (error instanceof Error) { return
error
} return
rendered
} function App() { const [id, changeId] = React.useReducer((x) => x + 1, 1) return (
) } const rendered = renderWithClient(queryClient, ) // initial state check expect(rendered.getByText('status: pending')).toBeInTheDocument() // render error state component await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('error')).toBeInTheDocument() // change to unmount query fireEvent.click(rendered.getByLabelText('change')) await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('rendered')).toBeInTheDocument() // change to mount new query fireEvent.click(rendered.getByLabelText('change')) await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('error')).toBeInTheDocument() }) it('should refetch when query key changed when switching between erroneous queries', async () => { function Page({ id }: { id: boolean }) { const { error, isFetching } = useQuery({ queryKey: [id], queryFn: async () => { await sleep(10) return Promise.reject(new Error('Error')) }, retry: false, retryOnMount: false, refetchOnMount: false, refetchOnWindowFocus: false, }) if (isFetching) { return
status: fetching
} if (error instanceof Error) { return
error
} return
rendered
} function App() { const [value, toggle] = React.useReducer((x) => !x, true) return (
) } const rendered = renderWithClient(queryClient, ) // initial state check expect(rendered.getByText('status: fetching')).toBeInTheDocument() // render error state component await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('error')).toBeInTheDocument() // change to mount second query fireEvent.click(rendered.getByLabelText('change')) expect(rendered.getByText('status: fetching')).toBeInTheDocument() await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('error')).toBeInTheDocument() // change to mount first query again fireEvent.click(rendered.getByLabelText('change')) expect(rendered.getByText('status: fetching')).toBeInTheDocument() await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('error')).toBeInTheDocument() }) it('should have no error in pending state when refetching after error occurred', async () => { const key = queryKey() const states: Array> = [] const error = new Error('oops') let count = 0 function Page() { const state = useQuery({ queryKey: key, queryFn: async () => { await sleep(10) if (count === 0) { count++ throw error } return 5 }, retry: false, }) states.push(state) if (state.isPending) { return
status: pending
} if (state.error instanceof Error) { return (
error
) } return
data: {state.data}
} const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(11) rendered.getByText('error') fireEvent.click(rendered.getByRole('button', { name: 'refetch' })) await vi.advanceTimersByTimeAsync(11) rendered.getByText('data: 5') expect(states.length).toBe(4) expect(states[0]).toMatchObject({ status: 'pending', data: undefined, error: null, }) expect(states[1]).toMatchObject({ status: 'error', data: undefined, error, }) expect(states[2]).toMatchObject({ status: 'pending', data: undefined, error: null, }) expect(states[3]).toMatchObject({ status: 'success', data: 5, error: null, }) }) describe('networkMode online', () => { it('online queries should not start fetching if you are offline', async () => { const onlineMock = mockOnlineManagerIsOnline(false) const key = queryKey() const states: Array = [] function Page() { const state = useQuery({ queryKey: key, queryFn: async () => { await sleep(10) return 'data' }, }) React.useEffect(() => { states.push(state.fetchStatus) }) return (
status: {state.status}, isPaused: {String(state.isPaused)}
data: {state.data}
) } const rendered = renderWithClient(queryClient, ) rendered.getByText('status: pending, isPaused: true') onlineMock.mockReturnValue(true) queryClient.getQueryCache().onOnline() await vi.advanceTimersByTimeAsync(11) rendered.getByText('status: success, isPaused: false') expect(rendered.getByText('data: data')).toBeInTheDocument() expect(states).toEqual(['paused', 'fetching', 'idle']) onlineMock.mockRestore() }) it('online queries should not refetch if you are offline', async () => { const key = queryKey() let count = 0 function Page() { const state = useQuery({ queryKey: key, queryFn: async () => { count++ await sleep(10) return 'data' + count }, }) return (
status: {state.status}, fetchStatus: {state.fetchStatus}, failureCount: {state.failureCount}
failureReason: {state.failureReason ?? 'null'}
data: {state.data}
) } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(11) rendered.getByText('data: data1') const onlineMock = mockOnlineManagerIsOnline(false) fireEvent.click(rendered.getByRole('button', { name: /invalidate/i })) await vi.advanceTimersByTimeAsync(11) rendered.getByText( 'status: success, fetchStatus: paused, failureCount: 0', ) rendered.getByText('failureReason: null') onlineMock.mockReturnValue(true) queryClient.getQueryCache().onOnline() await vi.advanceTimersByTimeAsync(0) rendered.getByText( 'status: success, fetchStatus: fetching, failureCount: 0', ) rendered.getByText('failureReason: null') await vi.advanceTimersByTimeAsync(11) rendered.getByText('status: success, fetchStatus: idle, failureCount: 0') rendered.getByText('failureReason: null') expect(rendered.getByText('data: data2')).toBeInTheDocument() onlineMock.mockRestore() }) it('online queries should not refetch if you are offline and refocus', async () => { const key = queryKey() let count = 0 function Page() { const state = useQuery({ queryKey: key, queryFn: async () => { count++ await sleep(10) return 'data' + count }, }) return (
status: {state.status}, fetchStatus: {state.fetchStatus}
data: {state.data}
) } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(11) rendered.getByText('data: data1') const onlineMock = mockOnlineManagerIsOnline(false) fireEvent.click(rendered.getByRole('button', { name: /invalidate/i })) await vi.advanceTimersByTimeAsync(0) rendered.getByText('status: success, fetchStatus: paused') window.dispatchEvent(new Event('visibilitychange')) await vi.advanceTimersByTimeAsync(11) expect(rendered.queryByText('data: data2')).not.toBeInTheDocument() expect(count).toBe(1) onlineMock.mockRestore() }) it('online queries should not refetch while already paused', async () => { const key = queryKey() let count = 0 function Page() { const state = useQuery({ queryKey: key, queryFn: async () => { count++ await sleep(10) return 'data' + count }, }) return (
status: {state.status}, fetchStatus: {state.fetchStatus}
data: {state.data}
) } const onlineMock = mockOnlineManagerIsOnline(false) const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(0) rendered.getByText('status: pending, fetchStatus: paused') fireEvent.click(rendered.getByRole('button', { name: /invalidate/i })) await vi.advanceTimersByTimeAsync(11) // invalidation should not trigger a refetch rendered.getByText('status: pending, fetchStatus: paused') expect(count).toBe(0) onlineMock.mockRestore() }) it('online queries should not refetch while already paused if data is in the cache', async () => { const key = queryKey() let count = 0 function Page() { const state = useQuery({ queryKey: key, queryFn: async () => { count++ await sleep(10) return 'data' + count }, initialData: 'initial', }) return (
status: {state.status}, fetchStatus: {state.fetchStatus}
data: {state.data}
) } const onlineMock = mockOnlineManagerIsOnline(false) const rendered = renderWithClient(queryClient, ) rendered.getByText('status: success, fetchStatus: paused') expect(rendered.getByText('data: initial')).toBeInTheDocument() fireEvent.click(rendered.getByRole('button', { name: /invalidate/i })) await vi.advanceTimersByTimeAsync(11) // invalidation should not trigger a refetch rendered.getByText('status: success, fetchStatus: paused') expect(count).toBe(0) onlineMock.mockRestore() }) it('online queries should not get stuck in fetching state when pausing multiple times', async () => { const key = queryKey() let count = 0 function Page() { const state = useQuery({ queryKey: key, queryFn: async () => { count++ await sleep(10) return 'data' + count }, initialData: 'initial', }) return (
status: {state.status}, fetchStatus: {state.fetchStatus}
data: {state.data}
) } const onlineMock = mockOnlineManagerIsOnline(false) const rendered = renderWithClient(queryClient, ) rendered.getByText('status: success, fetchStatus: paused') expect(rendered.getByText('data: initial')).toBeInTheDocument() // triggers one pause fireEvent.click(rendered.getByRole('button', { name: /invalidate/i })) await vi.advanceTimersByTimeAsync(11) rendered.getByText('status: success, fetchStatus: paused') // triggers a second pause act(() => { window.dispatchEvent(new Event('visibilitychange')) }) onlineMock.mockReturnValue(true) queryClient.getQueryCache().onOnline() await vi.advanceTimersByTimeAsync(11) rendered.getByText('status: success, fetchStatus: idle') expect(rendered.getByText('data: data1')).toBeInTheDocument() expect(count).toBe(1) onlineMock.mockRestore() }) it('online queries should pause retries if you are offline', async () => { const key = queryKey() let count = 0 function Page() { const state = useQuery({ queryKey: key, queryFn: async (): Promise => { count++ await sleep(10) throw new Error('failed' + count) }, retry: 2, retryDelay: 10, }) return (
status: {state.status}, fetchStatus: {state.fetchStatus}, failureCount: {state.failureCount}
failureReason: {state.failureReason?.message ?? 'null'}
) } const rendered = renderWithClient(queryClient, ) rendered.getByText(/status: pending, fetchStatus: fetching/i) const onlineMock = mockOnlineManagerIsOnline(false) await vi.advanceTimersByTimeAsync(31) rendered.getByText( 'status: pending, fetchStatus: paused, failureCount: 1', ) rendered.getByText('failureReason: failed1') expect(count).toBe(1) onlineMock.mockReturnValue(true) queryClient.getQueryCache().onOnline() await vi.advanceTimersByTimeAsync(31) rendered.getByText('status: error, fetchStatus: idle, failureCount: 3') rendered.getByText('failureReason: failed3') expect(count).toBe(3) onlineMock.mockRestore() }) it('online queries should fetch if paused and we go online even if already unmounted (because not cancelled)', async () => { const key = queryKey() let count = 0 function Component() { const state = useQuery({ queryKey: key, queryFn: async () => { count++ await sleep(10) return 'data' + count }, }) return (
status: {state.status}, fetchStatus: {state.fetchStatus}
data: {state.data}
) } function Page() { const [show, setShow] = React.useState(true) return (
{show && }
) } const onlineMock = mockOnlineManagerIsOnline(false) const rendered = renderWithClient(queryClient, ) rendered.getByText('status: pending, fetchStatus: paused') fireEvent.click(rendered.getByRole('button', { name: /hide/i })) onlineMock.mockReturnValue(true) queryClient.getQueryCache().onOnline() await vi.advanceTimersByTimeAsync(11) expect(queryClient.getQueryState(key)).toMatchObject({ fetchStatus: 'idle', status: 'success', }) // give it a bit more time to make sure queryFn is not called again expect(count).toBe(1) onlineMock.mockRestore() }) it('online queries should not fetch if paused and we go online when cancelled and no refetchOnReconnect', async () => { const key = queryKey() let count = 0 function Page() { const state = useQuery({ queryKey: key, queryFn: async () => { count++ await sleep(10) return 'data' + count }, refetchOnReconnect: false, }) return (
status: {state.status}, fetchStatus: {state.fetchStatus}
data: {state.data}
) } const onlineMock = mockOnlineManagerIsOnline(false) const rendered = renderWithClient(queryClient, ) rendered.getByText('status: pending, fetchStatus: paused') fireEvent.click(rendered.getByRole('button', { name: /cancel/i })) await vi.advanceTimersByTimeAsync(11) rendered.getByText('status: pending, fetchStatus: idle') expect(count).toBe(0) onlineMock.mockReturnValue(true) queryClient.getQueryCache().onOnline() await vi.advanceTimersByTimeAsync(11) rendered.getByText('status: pending, fetchStatus: idle') expect(count).toBe(0) onlineMock.mockRestore() }) it('online queries should not fetch if paused and we go online if already unmounted when signal consumed', async () => { const key = queryKey() let count = 0 function Component() { const state = useQuery({ queryKey: key, queryFn: async ({ signal: _signal }) => { count++ await sleep(10) return `signal${count}` }, }) return (
status: {state.status}, fetchStatus: {state.fetchStatus}
data: {state.data}
) } function Page() { const [show, setShow] = React.useState(true) return (
{show && }
) } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(11) rendered.getByText('status: success, fetchStatus: idle') const onlineMock = mockOnlineManagerIsOnline(false) fireEvent.click(rendered.getByRole('button', { name: /invalidate/i })) await vi.advanceTimersByTimeAsync(0) rendered.getByText('status: success, fetchStatus: paused') fireEvent.click(rendered.getByRole('button', { name: /hide/i })) onlineMock.mockReturnValue(true) queryClient.getQueryCache().onOnline() await vi.advanceTimersByTimeAsync(11) expect(queryClient.getQueryState(key)).toMatchObject({ fetchStatus: 'idle', status: 'success', }) expect(count).toBe(1) onlineMock.mockRestore() }) }) describe('networkMode always', () => { it('always queries should start fetching even if you are offline', async () => { const onlineMock = mockOnlineManagerIsOnline(false) const key = queryKey() let count = 0 function Page() { const state = useQuery({ queryKey: key, queryFn: async () => { count++ await sleep(10) return 'data ' + count }, networkMode: 'always', }) return (
status: {state.status}, isPaused: {String(state.isPaused)}
data: {state.data}
) } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(11) rendered.getByText('status: success, isPaused: false') expect(rendered.getByText('data: data 1')).toBeInTheDocument() onlineMock.mockRestore() }) it('always queries should not pause retries', async () => { const onlineMock = mockOnlineManagerIsOnline(false) const key = queryKey() let count = 0 function Page() { const state = useQuery({ queryKey: key, queryFn: async (): Promise => { count++ await sleep(10) throw new Error('error ' + count) }, networkMode: 'always', retry: 1, retryDelay: 5, }) return (
status: {state.status}, isPaused: {String(state.isPaused)}
error: {state.error instanceof Error && state.error.message}
) } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(26) rendered.getByText('status: error, isPaused: false') expect(rendered.getByText('error: error 2')).toBeInTheDocument() expect(count).toBe(2) onlineMock.mockRestore() }) }) describe('networkMode offlineFirst', () => { it('offlineFirst queries should start fetching if you are offline, but pause retries', async () => { const onlineMock = mockOnlineManagerIsOnline(false) const key = queryKey() let count = 0 function Page() { const state = useQuery({ queryKey: key, queryFn: async (): Promise => { count++ await sleep(10) throw new Error('failed' + count) }, retry: 2, retryDelay: 1, networkMode: 'offlineFirst', }) return (
status: {state.status}, fetchStatus: {state.fetchStatus}, failureCount: {state.failureCount}
failureReason: {state.failureReason?.message ?? 'null'}
) } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(12) rendered.getByText( 'status: pending, fetchStatus: paused, failureCount: 1', ) rendered.getByText('failureReason: failed1') expect(count).toBe(1) onlineMock.mockReturnValue(true) queryClient.getQueryCache().onOnline() await vi.advanceTimersByTimeAsync(22) rendered.getByText('status: error, fetchStatus: idle, failureCount: 3') rendered.getByText('failureReason: failed3') expect(count).toBe(3) onlineMock.mockRestore() }) }) describe('subscribed', () => { it('should be able to toggle subscribed', async () => { const key = queryKey() const queryFn = vi.fn(() => Promise.resolve('data')) function Page() { const [subscribed, setSubscribed] = React.useState(true) const { data } = useQuery({ queryKey: key, queryFn, subscribed, }) return (
data: {data}
) } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(0) rendered.getByText('data: data') expect( queryClient.getQueryCache().find({ queryKey: key })!.observers.length, ).toBe(1) fireEvent.click(rendered.getByRole('button', { name: 'toggle' })) expect( queryClient.getQueryCache().find({ queryKey: key })!.observers.length, ).toBe(0) expect(queryFn).toHaveBeenCalledTimes(1) fireEvent.click(rendered.getByRole('button', { name: 'toggle' })) // background refetch when we re-subscribe await vi.advanceTimersByTimeAsync(0) expect(queryFn).toHaveBeenCalledTimes(2) expect( queryClient.getQueryCache().find({ queryKey: key })!.observers.length, ).toBe(1) }) it('should not be attached to the query when subscribed is false', async () => { const key = queryKey() const queryFn = vi.fn(() => Promise.resolve('data')) function Page() { const { data } = useQuery({ queryKey: key, queryFn, subscribed: false, }) return (
data: {data}
) } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(0) rendered.getByText('data:') expect( queryClient.getQueryCache().find({ queryKey: key })!.observers.length, ).toBe(0) expect(queryFn).toHaveBeenCalledTimes(0) }) it('should not re-render when data is added to the cache when subscribed is false', async () => { const key = queryKey() let renders = 0 function Page() { const { data } = useQuery({ queryKey: key, queryFn: () => Promise.resolve('data'), subscribed: false, }) renders++ return (
{data ? 'has data' + data : 'no data'}
) } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(0) rendered.getByText('no data') fireEvent.click(rendered.getByRole('button', { name: 'set data' })) await vi.advanceTimersByTimeAsync(0) rendered.getByText('no data') expect(renders).toBe(1) }) }) it('should have status=error on mount when a query has failed', async () => { const key = queryKey() const states: Array> = [] const error = new Error('oops') const queryFn = (): Promise => { return Promise.reject(error) } function Page() { const state = useQuery({ queryKey: key, queryFn, retry: false, retryOnMount: false, }) states.push(state) return <> } await queryClient.prefetchQuery({ queryKey: key, queryFn }) renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(0) expect(states).toHaveLength(1) expect(states[0]).toMatchObject({ status: 'error', error, }) }) it('setQueryData - should respect updatedAt', async () => { const key = queryKey() function Page() { const state = useQuery({ queryKey: key, queryFn: () => 'data' }) return (
data: {state.data}
dataUpdatedAt: {state.dataUpdatedAt}
) } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(0) rendered.getByText('data: data') fireEvent.click(rendered.getByRole('button', { name: /setQueryData/i })) await vi.advanceTimersByTimeAsync(0) rendered.getByText('data: newData') expect(rendered.getByText('dataUpdatedAt: 100')).toBeInTheDocument() }) it('errorUpdateCount should increased on each fetch failure', async () => { const key = queryKey() const error = new Error('oops') function Page() { const { refetch, errorUpdateCount } = useQuery({ queryKey: key, queryFn: (): Promise => { return Promise.reject(error) }, retry: false, }) return (
data: {errorUpdateCount}
) } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(0) const fetchBtn = rendered.getByRole('button', { name: 'refetch' }) expect(rendered.getByText('data: 1')).toBeInTheDocument() fireEvent.click(fetchBtn) await vi.advanceTimersByTimeAsync(0) expect(rendered.getByText('data: 2')).toBeInTheDocument() fireEvent.click(fetchBtn) await vi.advanceTimersByTimeAsync(0) expect(rendered.getByText('data: 3')).toBeInTheDocument() }) it('should use provided custom queryClient', async () => { const key = queryKey() const queryFn = async () => { return Promise.resolve('custom client') } function Page() { const { data } = useQuery( { queryKey: key, queryFn, }, queryClient, ) return
data: {data}
} const rendered = render() await vi.advanceTimersByTimeAsync(0) expect(rendered.getByText('data: custom client')).toBeInTheDocument() }) it('should be notified of updates between create and subscribe', async () => { const key = queryKey() function Page() { const mounted = React.useRef(false) const { data, status } = useQuery({ enabled: false, queryKey: key, queryFn: async () => { await sleep(10) return 5 }, }) // this simulates a synchronous update between the time the query is created // and the time it is subscribed to that could be missed otherwise if (!mounted.current) { mounted.current = true queryClient.setQueryData(key, 1) } return (
status: {status} data: {data}
) } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('status: success')).toBeInTheDocument() expect(rendered.getByText('data: 1')).toBeInTheDocument() }) it('should reuse same data object reference when queryKey changes back to some cached data', async () => { const key = queryKey() const spy = vi.fn() async function fetchNumber(id: number) { await sleep(5) return { numbers: { current: { id } } } } function Test() { const [id, setId] = React.useState(1) const { data } = useQuery({ select: selector, queryKey: [key, 'user', id], queryFn: () => fetchNumber(id), }) React.useEffect(() => { spy(data) }, [data]) return (
Rendered Id: {data?.id}
) } function selector(data: any) { return data.numbers.current } const rendered = renderWithClient(queryClient, ) expect(spy).toHaveBeenCalledTimes(1) spy.mockClear() await vi.advanceTimersByTimeAsync(6) rendered.getByText('Rendered Id: 1') expect(spy).toHaveBeenCalledTimes(1) spy.mockClear() fireEvent.click(rendered.getByRole('button', { name: /2/ })) await vi.advanceTimersByTimeAsync(6) rendered.getByText('Rendered Id: 2') expect(spy).toHaveBeenCalledTimes(2) // called with undefined because id changed spy.mockClear() fireEvent.click(rendered.getByRole('button', { name: /1/ })) await vi.advanceTimersByTimeAsync(6) rendered.getByText('Rendered Id: 1') expect(spy).toHaveBeenCalledTimes(1) spy.mockClear() fireEvent.click(rendered.getByRole('button', { name: /2/ })) await vi.advanceTimersByTimeAsync(6) rendered.getByText('Rendered Id: 2') expect(spy).toHaveBeenCalledTimes(1) }) it('should reuse same data object reference when queryKey changes and placeholderData is present', async () => { const key = queryKey() const spy = vi.fn() async function fetchNumber(id: number) { await sleep(5) return { numbers: { current: { id } } } } function Test() { const [id, setId] = React.useState(1) const { data } = useQuery({ select: selector, queryKey: [key, 'user', id], queryFn: () => fetchNumber(id), placeholderData: { numbers: { current: { id: 99 } } }, }) React.useEffect(() => { spy(data) }, [data]) return (
Rendered Id: {data?.id}
) } function selector(data: any) { return data.numbers.current } const rendered = renderWithClient(queryClient, ) expect(spy).toHaveBeenCalledTimes(1) spy.mockClear() rendered.getByText('Rendered Id: 99') await vi.advanceTimersByTimeAsync(6) rendered.getByText('Rendered Id: 1') expect(spy).toHaveBeenCalledTimes(1) spy.mockClear() fireEvent.click(rendered.getByRole('button', { name: /2/ })) rendered.getByText('Rendered Id: 99') await vi.advanceTimersByTimeAsync(6) rendered.getByText('Rendered Id: 2') expect(spy).toHaveBeenCalledTimes(2) // called with undefined because id changed spy.mockClear() fireEvent.click(rendered.getByRole('button', { name: /1/ })) await vi.advanceTimersByTimeAsync(6) rendered.getByText('Rendered Id: 1') expect(spy).toHaveBeenCalledTimes(1) spy.mockClear() fireEvent.click(rendered.getByRole('button', { name: /2/ })) await vi.advanceTimersByTimeAsync(6) rendered.getByText('Rendered Id: 2') expect(spy).toHaveBeenCalledTimes(1) }) it('should not cause an infinite render loop when using unstable callback ref', async () => { const key = queryKey() function Test() { const [_, setRef] = React.useState() const { data } = useQuery({ queryKey: [key], queryFn: async () => { await sleep(5) return 'Works' }, }) return
setRef(value)}>{data}
} const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(6) expect(rendered.getByText('Works')).toBeInTheDocument() }) it('should keep the previous data when placeholderData is set and cache is used', async () => { const key = queryKey() const states: Array> = [] const steps = [0, 1, 0, 2] function Page() { const [count, setCount] = React.useState(0) const state = useQuery({ staleTime: Infinity, queryKey: [key, steps[count]], queryFn: async () => { await sleep(10) return steps[count] }, placeholderData: keepPreviousData, }) states.push(state) return (
data: {state.data}
) } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(11) rendered.getByText('data: 0') fireEvent.click(rendered.getByRole('button', { name: 'setCount' })) await vi.advanceTimersByTimeAsync(11) rendered.getByText('data: 1') fireEvent.click(rendered.getByRole('button', { name: 'setCount' })) await vi.advanceTimersByTimeAsync(11) rendered.getByText('data: 0') fireEvent.click(rendered.getByRole('button', { name: 'setCount' })) await vi.advanceTimersByTimeAsync(11) rendered.getByText('data: 2') // Initial expect(states[0]).toMatchObject({ data: undefined, isFetching: true, isSuccess: false, isPlaceholderData: false, }) // Fetched expect(states[1]).toMatchObject({ data: 0, isFetching: false, isSuccess: true, isPlaceholderData: false, }) // Set state expect(states[2]).toMatchObject({ data: 0, isFetching: true, isSuccess: true, isPlaceholderData: true, }) // New data expect(states[3]).toMatchObject({ data: 1, isFetching: false, isSuccess: true, isPlaceholderData: false, }) // Set state with existing data expect(states[4]).toMatchObject({ data: 0, isFetching: false, isSuccess: true, isPlaceholderData: false, }) // Set state where the placeholder value should come from cache request expect(states[5]).toMatchObject({ data: 0, isFetching: true, isSuccess: true, isPlaceholderData: true, }) // New data expect(states[6]).toMatchObject({ data: 2, isFetching: false, isSuccess: true, isPlaceholderData: false, }) }) // For Project without TS, when migrating from v4 to v5, make sure invalid calls due to bad parameters are tracked. it('should throw in case of bad arguments to enhance DevX', () => { // Mock console error to avoid noise when test is run const consoleMock = vi .spyOn(console, 'error') .mockImplementation(() => undefined) const key = queryKey() const queryFn = () => 'data' function Page() { // Invalid call on purpose // @ts-expect-error useQuery(key, { queryFn }) return
Does not matter
} expect(() => render()).toThrow('Bad argument type') consoleMock.mockRestore() }) it('should respect skipToken and refetch when skipToken is taken away', async () => { const key = queryKey() function Page({ enabled }: { enabled: boolean }) { const { data, status } = useQuery({ queryKey: [key], queryFn: enabled ? async () => { await sleep(10) return Promise.resolve('data') } : skipToken, retry: false, retryOnMount: false, refetchOnMount: false, refetchOnWindowFocus: false, }) return (
status: {status}
data: {String(data)}
) } function App() { const [enabled, toggle] = React.useReducer((x) => !x, false) return (
) } const rendered = renderWithClient(queryClient, ) expect(rendered.getByText('status: pending')).toBeInTheDocument() fireEvent.click(rendered.getByRole('button', { name: 'enable' })) await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('status: success')).toBeInTheDocument() expect(rendered.getByText('data: data')).toBeInTheDocument() }) it('should allow enabled: true and queryFn: skipToken', () => { const consoleMock = vi .spyOn(console, 'error') .mockImplementation(() => undefined) const key = queryKey() function App() { const query = useQuery({ queryKey: key, queryFn: skipToken, enabled: true, }) return (
status: {query.status}, fetchStatus: {query.fetchStatus}
) } const rendered = renderWithClient(queryClient, ) rendered.getByText('status: pending, fetchStatus: idle') // no warnings expected about skipToken / missing queryFn expect(consoleMock).toHaveBeenCalledTimes(0) consoleMock.mockRestore() }) it('should return correct optimistic result when fetching after error', async () => { const key = queryKey() const error = new Error('oh no') const results: Array> = [] function Page() { const query = useQuery({ queryKey: key, queryFn: async () => { await sleep(10) return Promise.reject(error) }, retry: false, notifyOnChangeProps: 'all', }) results.push(query) return (
status: {query.status}, {query.fetchStatus}
error: {query.error?.message}
) } function App() { const [enabled, setEnabled] = React.useState(true) return (
{enabled && }
) } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(11) rendered.getByText('status: error, idle') fireEvent.click(rendered.getByRole('button', { name: 'toggle' })) fireEvent.click(rendered.getByRole('button', { name: 'toggle' })) await vi.advanceTimersByTimeAsync(11) rendered.getByText('status: error, idle') expect(results).toHaveLength(4) // initial fetch expect(results[0]).toMatchObject({ status: 'pending', fetchStatus: 'fetching', error: null, errorUpdatedAt: 0, errorUpdateCount: 0, isLoading: true, failureCount: 0, failureReason: null, }) // error state expect(results[1]).toMatchObject({ status: 'error', fetchStatus: 'idle', error, errorUpdateCount: 1, isLoading: false, failureCount: 1, failureReason: error, }) expect(results[1]?.errorUpdatedAt).toBeGreaterThan(0) // refetch, optimistic state, no errors anymore expect(results[2]).toMatchObject({ status: 'pending', fetchStatus: 'fetching', error: null, errorUpdateCount: 1, isLoading: true, failureCount: 0, failureReason: null, }) expect(results[2]?.errorUpdatedAt).toBeGreaterThan(0) // final state expect(results[3]).toMatchObject({ status: 'error', fetchStatus: 'idle', error: error, errorUpdateCount: 2, isLoading: false, failureCount: 1, failureReason: error, }) expect(results[3]?.errorUpdatedAt).toBeGreaterThan(0) }) it('should pick up an initialPromise', async () => { const key = queryKey() const serverQueryClient = new QueryClient({ defaultOptions: { dehydrate: { shouldDehydrateQuery: () => true } }, }) void serverQueryClient.prefetchQuery({ queryKey: key, queryFn: async () => { await sleep(10) return Promise.resolve('server') }, }) const dehydrated = dehydrate(serverQueryClient) let count = 0 function Page() { const query = useQuery({ queryKey: key, queryFn: async () => { count++ await sleep(10) return Promise.resolve('client') }, }) return (
data: {query.data}
) } const clientQueryClient = new QueryClient() hydrate(clientQueryClient, dehydrated) const rendered = renderWithClient(clientQueryClient, ) await vi.advanceTimersByTimeAsync(11) rendered.getByText('data: server') expect(count).toBe(0) fireEvent.click(rendered.getByRole('button', { name: 'refetch' })) await vi.advanceTimersByTimeAsync(11) rendered.getByText('data: client') expect(count).toBe(1) }) it('should retry failed initialPromise on the client', async () => { const consoleMock = vi .spyOn(console, 'error') .mockImplementation(() => undefined) const key = queryKey() const serverQueryClient = new QueryClient({ defaultOptions: { dehydrate: { shouldDehydrateQuery: () => true }, }, }) void serverQueryClient.prefetchQuery({ queryKey: key, queryFn: async () => { await sleep(10) return Promise.reject(new Error('server error')) }, }) const dehydrated = dehydrate(serverQueryClient) let count = 0 function Page() { const query = useQuery({ queryKey: key, queryFn: async () => { count++ await sleep(10) return Promise.resolve('client') }, }) return (
failure: {query.failureReason?.message}
data: {query.data}
) } const clientQueryClient = new QueryClient({ defaultOptions: { hydrate: { queries: { retry: 1, retryDelay: 10 } } }, }) hydrate(clientQueryClient, dehydrated) const rendered = renderWithClient(clientQueryClient, ) await vi.advanceTimersByTimeAsync(11) rendered.getByText('failure: redacted') await vi.advanceTimersByTimeAsync(21) rendered.getByText('data: client') expect(count).toBe(1) const query = clientQueryClient.getQueryCache().find({ queryKey: key }) expect(consoleMock).toHaveBeenCalledTimes(1) expect(consoleMock).toHaveBeenCalledWith( `A query that was dehydrated as pending ended up rejecting. [${query?.queryHash}]: Error: server error; The error will be redacted in production builds`, ) consoleMock.mockRestore() }) it('should console.error when there is no queryFn', () => { const consoleErrorMock = vi.spyOn(console, 'error') const key = queryKey() function Example() { useQuery({ queryKey: key }) return <> } renderWithClient(queryClient, ) expect(consoleErrorMock).toHaveBeenCalledTimes(1) expect(consoleErrorMock).toHaveBeenCalledWith( `[${queryClient.getQueryCache().find({ queryKey: key })?.queryHash}]: No queryFn was passed as an option, and no default queryFn was found. The queryFn parameter is only optional when using a default queryFn. More info here: https://tanstack.com/query/latest/docs/framework/react/guides/default-query-function`, ) consoleErrorMock.mockRestore() }) }) ================================================ FILE: packages/react-query/src/__tests__/useSuspenseInfiniteQuery.test-d.tsx ================================================ import { assertType, describe, expectTypeOf, it } from 'vitest' import { skipToken } from '@tanstack/query-core' import { useSuspenseInfiniteQuery } from '../useSuspenseInfiniteQuery' import type { InfiniteData } from '@tanstack/query-core' describe('useSuspenseInfiniteQuery', () => { it('should always have data defined', () => { const { data } = useSuspenseInfiniteQuery({ queryKey: ['key'], queryFn: () => Promise.resolve(5), initialPageParam: 1, getNextPageParam: () => 1, }) expectTypeOf(data).toEqualTypeOf>() }) it('should not allow skipToken in queryFn', () => { assertType( useSuspenseInfiniteQuery({ queryKey: ['key'], // @ts-expect-error queryFn: skipToken, }), ) assertType( useSuspenseInfiniteQuery({ queryKey: ['key'], // @ts-expect-error queryFn: Math.random() > 0.5 ? skipToken : () => Promise.resolve(5), }), ) }) it('should not have pending status', () => { const { status } = useSuspenseInfiniteQuery({ queryKey: ['key'], queryFn: () => Promise.resolve(5), initialPageParam: 1, getNextPageParam: () => 1, }) expectTypeOf(status).toEqualTypeOf<'error' | 'success'>() }) it('should not allow placeholderData, enabled or throwOnError props', () => { assertType( useSuspenseInfiniteQuery({ queryKey: ['key'], queryFn: () => Promise.resolve(5), initialPageParam: 1, getNextPageParam: () => 1, // @ts-expect-error TS2345 placeholderData: 5, enabled: true, }), ) assertType( useSuspenseInfiniteQuery({ queryKey: ['key'], queryFn: () => Promise.resolve(5), initialPageParam: 1, getNextPageParam: () => 1, // @ts-expect-error TS2345 enabled: true, }), ) assertType( useSuspenseInfiniteQuery({ queryKey: ['key'], queryFn: () => Promise.resolve(5), initialPageParam: 1, getNextPageParam: () => 1, // @ts-expect-error TS2345 throwOnError: true, }), ) }) it('should not return isPlaceholderData', () => { const query = useSuspenseInfiniteQuery({ queryKey: ['key'], queryFn: () => Promise.resolve(5), initialPageParam: 1, getNextPageParam: () => 1, }) expectTypeOf(query).not.toHaveProperty('isPlaceholderData') }) }) ================================================ FILE: packages/react-query/src/__tests__/useSuspenseInfiniteQuery.test.tsx ================================================ import { describe, expect, it, vi } from 'vitest' import * as React from 'react' import { queryKey } from '@tanstack/query-test-utils' import { QueryCache, QueryClient, skipToken, useSuspenseInfiniteQuery, } from '..' import { renderWithClient } from './utils' describe('useSuspenseInfiniteQuery', () => { const queryCache = new QueryCache() const queryClient = new QueryClient({ queryCache }) it('should log an error when skipToken is passed as queryFn', () => { const consoleErrorSpy = vi .spyOn(console, 'error') .mockImplementation(() => {}) const key = queryKey() function Page() { useSuspenseInfiniteQuery({ queryKey: key, initialPageParam: 1, getNextPageParam: () => 1, // @ts-expect-error queryFn: Math.random() >= 0 ? skipToken : () => Promise.resolve(5), }) return null } function App() { return ( ) } renderWithClient(queryClient, ) expect(consoleErrorSpy).toHaveBeenCalledWith( 'skipToken is not allowed for useSuspenseInfiniteQuery', ) consoleErrorSpy.mockRestore() }) it('should log an error when skipToken is used in development environment', () => { const envCopy = process.env.NODE_ENV process.env.NODE_ENV = 'development' const consoleErrorSpy = vi .spyOn(console, 'error') .mockImplementation(() => undefined) const key = queryKey() function Page() { useSuspenseInfiniteQuery({ queryKey: key, queryFn: skipToken as any, initialPageParam: 1, getNextPageParam: () => 1, }) return null } renderWithClient( queryClient, , ) expect(consoleErrorSpy).toHaveBeenCalledWith( 'skipToken is not allowed for useSuspenseInfiniteQuery', ) consoleErrorSpy.mockRestore() process.env.NODE_ENV = envCopy }) it('should not log an error when skipToken is used in production environment', () => { const envCopy = process.env.NODE_ENV process.env.NODE_ENV = 'production' const consoleErrorSpy = vi .spyOn(console, 'error') .mockImplementation(() => undefined) const key = queryKey() function Page() { useSuspenseInfiniteQuery({ queryKey: key, queryFn: skipToken as any, initialPageParam: 1, getNextPageParam: () => 1, }) return null } renderWithClient( queryClient, , ) expect(consoleErrorSpy).not.toHaveBeenCalled() consoleErrorSpy.mockRestore() process.env.NODE_ENV = envCopy }) }) ================================================ FILE: packages/react-query/src/__tests__/useSuspenseQueries.test-d.tsx ================================================ import { assertType, describe, expectTypeOf, it } from 'vitest' import { skipToken, useSuspenseQueries } from '..' import { queryOptions } from '../queryOptions' import type { OmitKeyof } from '..' import type { UseQueryOptions, UseSuspenseQueryResult } from '../types' describe('UseSuspenseQueries config object overload', () => { it('TData should always be defined', () => { const query1 = { queryKey: ['key1'], queryFn: () => { return { wow: true, } }, initialData: { wow: false, }, } const query2 = { queryKey: ['key2'], queryFn: () => 'Query Data', } const queryResults = useSuspenseQueries({ queries: [query1, query2] }) const query1Data = queryResults[0].data const query2Data = queryResults[1].data expectTypeOf(query1Data).toEqualTypeOf<{ wow: boolean }>() expectTypeOf(query2Data).toEqualTypeOf() }) it('TData should be defined when passed through queryOptions', () => { const options = queryOptions({ queryKey: ['key'], queryFn: () => { return { wow: true, } }, }) const queryResults = useSuspenseQueries({ queries: [options] }) const data = queryResults[0].data expectTypeOf(data).toEqualTypeOf<{ wow: boolean }>() }) it('should be possible to define a different TData than TQueryFnData using select with queryOptions spread into useQuery', () => { const query1 = queryOptions({ queryKey: ['key'], queryFn: () => Promise.resolve(1), select: (data) => data > 1, }) const query2 = { queryKey: ['key'], queryFn: () => Promise.resolve(1), select: (data: number) => data > 1, } const queryResults = useSuspenseQueries({ queries: [query1, query2] }) const query1Data = queryResults[0].data const query2Data = queryResults[1].data expectTypeOf(query1Data).toEqualTypeOf() expectTypeOf(query2Data).toEqualTypeOf() }) it('TData should have undefined in the union when initialData is provided as a function which can return undefined', () => { const queryResults = useSuspenseQueries({ queries: [ { queryKey: ['key'], queryFn: () => { return { wow: true, } }, initialData: () => undefined as { wow: boolean } | undefined, }, ], }) const data = queryResults[0].data expectTypeOf(data).toEqualTypeOf<{ wow: boolean }>() }) it('should not allow skipToken in queryFn', () => { assertType( useSuspenseQueries({ queries: [ { queryKey: ['key'], // @ts-expect-error queryFn: skipToken, }, ], }), ) assertType( useSuspenseQueries({ queries: [ { queryKey: ['key'], // @ts-expect-error queryFn: Math.random() > 0.5 ? skipToken : () => Promise.resolve(5), }, ], }), ) }) it('TData should have correct type when conditional skipToken is passed', () => { const queryResults = useSuspenseQueries({ queries: [ { queryKey: ['withSkipToken'], // @ts-expect-error queryFn: Math.random() > 0.5 ? skipToken : () => Promise.resolve(5), }, ], }) const firstResult = queryResults[0] expectTypeOf(firstResult).toEqualTypeOf< UseSuspenseQueryResult >() expectTypeOf(firstResult.data).toEqualTypeOf() }) describe('custom hook', () => { it('should allow custom hooks using UseQueryOptions', () => { type Data = string const useCustomQueries = ( options?: OmitKeyof, 'queryKey' | 'queryFn'>, ) => { return useSuspenseQueries({ queries: [ { ...options, queryKey: ['todos-key'], queryFn: () => Promise.resolve('data'), }, ], }) } const queryResults = useCustomQueries() const data = queryResults[0].data expectTypeOf(data).toEqualTypeOf() }) }) it('should return correct data for dynamic queries with mixed result types', () => { const Queries1 = { get: () => queryOptions({ queryKey: ['key1'], queryFn: () => Promise.resolve(1), }), } const Queries2 = { get: () => queryOptions({ queryKey: ['key2'], queryFn: () => Promise.resolve(true), }), } const queries1List = [1, 2, 3].map(() => ({ ...Queries1.get() })) const result = useSuspenseQueries({ queries: [ ...queries1List, { ...Queries2.get(), select(data: boolean) { return data }, }, ], }) expectTypeOf(result).toEqualTypeOf< [ ...Array>, UseSuspenseQueryResult, ] >() }) it('queryOptions with initialData works on useSuspenseQueries', () => { const query1 = queryOptions({ queryKey: ['key1'], queryFn: () => 'Query Data', initialData: 'initial data', }) const queryResults = useSuspenseQueries({ queries: [query1] }) const query1Data = queryResults[0].data expectTypeOf(query1Data).toEqualTypeOf() }) it('queryOptions with skipToken in queryFn should not work on useSuspenseQueries', () => { assertType( useSuspenseQueries({ queries: [ // @ts-expect-error queryOptions({ queryKey: ['key1'], queryFn: Math.random() > 0.5 ? skipToken : () => Promise.resolve(5), }), ], }), ) assertType( useSuspenseQueries({ queries: [ // @ts-expect-error queryOptions({ queryKey: ['key1'], queryFn: Math.random() > 0.5 ? skipToken : () => Promise.resolve(5), initialData: 5, }), ], }), ) }) it('should not show type error when using spreaded queryOptions', () => { assertType( useSuspenseQueries({ queries: [ { ...queryOptions({ queryKey: ['key1'], queryFn: () => 'Query Data', }), select(data: string) { return data }, }, ], }), ) }) }) ================================================ FILE: packages/react-query/src/__tests__/useSuspenseQueries.test.tsx ================================================ import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi, } from 'vitest' import { act, fireEvent, render } from '@testing-library/react' import * as React from 'react' import { ErrorBoundary } from 'react-error-boundary' import { queryKey, sleep } from '@tanstack/query-test-utils' import { QueryClient, skipToken, useSuspenseQueries, useSuspenseQuery, } from '..' import { renderWithClient } from './utils' import type { UseSuspenseQueryOptions } from '..' type NumberQueryOptions = UseSuspenseQueryOptions const QUERY_DURATION = 1000 const createQuery: (id: number) => NumberQueryOptions = (id) => ({ queryKey: [id], queryFn: () => sleep(QUERY_DURATION).then(() => id), }) const resolveQueries = () => vi.advanceTimersByTimeAsync(QUERY_DURATION) const queryClient = new QueryClient() describe('useSuspenseQueries', () => { const onSuspend = vi.fn() const onQueriesResolution = vi.fn() beforeAll(() => { vi.useFakeTimers() }) afterAll(() => { vi.useRealTimers() }) afterEach(() => { queryClient.clear() onSuspend.mockClear() onQueriesResolution.mockClear() }) function SuspenseFallback() { React.useEffect(() => { onSuspend() }, []) return
loading
} const withSuspenseWrapper = (Component: React.FC) => { function SuspendedComponent(props: T) { return ( }> ) } return SuspendedComponent } function QueriesContainer({ queries, }: { queries: Array }) { const queriesResults = useSuspenseQueries( { queries, combine: (results) => results.map((r) => r.data) }, queryClient, ) React.useEffect(() => { onQueriesResolution(queriesResults) }, [queriesResults]) return null } const TestComponent = withSuspenseWrapper(QueriesContainer) it('should suspend on mount', () => { render() expect(onSuspend).toHaveBeenCalledOnce() }) it('should resolve queries', async () => { render() await act(resolveQueries) expect(onQueriesResolution).toHaveBeenCalledTimes(1) expect(onQueriesResolution).toHaveBeenLastCalledWith([1, 2]) }) it('should not suspend on mount if query has been already fetched', () => { const query = createQuery(1) queryClient.setQueryData(query.queryKey, query.queryFn) render() expect(onSuspend).not.toHaveBeenCalled() }) it('should not break suspense when queries change without resolving', async () => { const initQueries = [1, 2].map(createQuery) const nextQueries = [3, 4, 5, 6].map(createQuery) const { rerender } = render() rerender() await act(resolveQueries) expect(onSuspend).toHaveBeenCalledTimes(1) expect(onQueriesResolution).toHaveBeenCalledTimes(1) expect(onQueriesResolution).toHaveBeenLastCalledWith([3, 4, 5, 6]) }) it('should suspend only once per queries change', async () => { const initQueries = [1, 2].map(createQuery) const nextQueries = [3, 4, 5, 6].map(createQuery) const { rerender } = render() await act(resolveQueries) rerender() await act(resolveQueries) expect(onSuspend).toHaveBeenCalledTimes(2) expect(onQueriesResolution).toHaveBeenCalledTimes(2) expect(onQueriesResolution).toHaveBeenLastCalledWith([3, 4, 5, 6]) }) it('should only call combine after resolving', async () => { const spy = vi.fn() const key = queryKey() function Page() { const data = useSuspenseQueries({ queries: [1, 2, 3].map((value) => ({ queryKey: [...key, { value }], queryFn: () => sleep(value * 10).then(() => ({ value: value * 10 })), })), combine: (result) => { spy(result) return 'data' }, }) return

{data}

} const rendered = renderWithClient( queryClient, , ) expect(rendered.getByText('loading')).toBeInTheDocument() expect(spy).not.toHaveBeenCalled() await act(() => vi.advanceTimersByTimeAsync(30)) expect(rendered.getByText('data')).toBeInTheDocument() expect(spy).toHaveBeenCalled() }) }) describe('useSuspenseQueries 2', () => { beforeEach(() => { vi.useFakeTimers() }) afterEach(() => { vi.useRealTimers() }) it('should suspend all queries in parallel', async () => { const key1 = queryKey() const key2 = queryKey() const results: Array = [] function Fallback() { results.push('loading') return
loading
} function Page() { const result = useSuspenseQueries({ queries: [ { queryKey: key1, queryFn: () => sleep(10).then(() => { results.push('1') return '1' }), }, { queryKey: key2, queryFn: () => sleep(20).then(() => { results.push('2') return '2' }), }, ], }) return (

data: {result.map((item) => item.data ?? 'null').join(',')}

) } const rendered = renderWithClient( queryClient, }> , ) expect(rendered.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(20)) expect(rendered.getByText('data: 1,2')).toBeInTheDocument() expect(results).toEqual(['loading', '1', '2']) }) it("shouldn't unmount before all promises fetched", async () => { const key1 = queryKey() const key2 = queryKey() const results: Array = [] const refs: Array = [] function Fallback() { results.push('loading') return
loading
} function Page() { const ref = React.useRef(Math.random()) const result = useSuspenseQueries({ queries: [ { queryKey: key1, queryFn: () => sleep(10).then(() => { refs.push(ref.current) results.push('1') return '1' }), }, { queryKey: key2, queryFn: () => sleep(20).then(() => { refs.push(ref.current) results.push('2') return '2' }), }, ], }) return (

data: {result.map((item) => item.data ?? 'null').join(',')}

) } const rendered = renderWithClient( queryClient, }> , ) expect(rendered.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(20)) expect(rendered.getByText('data: 1,2')).toBeInTheDocument() expect(refs.length).toBe(2) expect(refs[0]).toBe(refs[1]) }) // this addresses the following issue: // https://github.com/TanStack/query/issues/6344 it('should suspend on offline when query changes, and data should not be undefined', async () => { function Page() { const [id, setId] = React.useState(0) const { data } = useSuspenseQuery({ queryKey: [id], queryFn: () => sleep(10).then(() => `Data ${id}`), }) // defensive guard here if (data === undefined) { throw new Error('data cannot be undefined') } return ( <>
{data}
) } const rendered = renderWithClient( queryClient, loading}> , ) expect(rendered.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(10)) expect(rendered.getByText('Data 0')).toBeInTheDocument() // go offline document.dispatchEvent(new CustomEvent('offline')) fireEvent.click(rendered.getByText('fetch')) expect(rendered.getByText('Data 0')).toBeInTheDocument() // go back online document.dispatchEvent(new CustomEvent('online')) fireEvent.click(rendered.getByText('fetch')) expect(rendered.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(10)) // query should resume expect(rendered.getByText('Data 1')).toBeInTheDocument() }) it('should throw error when queryKey changes and new query fails', async () => { const consoleMock = vi .spyOn(console, 'error') .mockImplementation(() => undefined) const key = queryKey() function Page() { const [fail, setFail] = React.useState(false) const { data } = useSuspenseQuery({ queryKey: [key, fail], queryFn: () => sleep(10).then(() => { if (fail) throw new Error('Suspense Error Bingo') return 'data' }), retry: 0, }) return (
rendered: {data}
) } const rendered = renderWithClient( queryClient,
error boundary
}>
, ) expect(rendered.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(10)) expect(rendered.getByText('rendered: data')).toBeInTheDocument() fireEvent.click(rendered.getByText('trigger fail')) expect(rendered.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(10)) expect(rendered.getByText('error boundary')).toBeInTheDocument() expect(consoleMock.mock.calls[0]?.[1]).toStrictEqual( new Error('Suspense Error Bingo'), ) consoleMock.mockRestore() }) it('should keep previous data when wrapped in a transition', async () => { const key = queryKey() function Page() { const [count, setCount] = React.useState(0) const [isPending, startTransition] = React.useTransition() const { data } = useSuspenseQuery({ queryKey: [key, count], queryFn: () => sleep(10).then(() => 'data' + count), }) return (
{isPending ? 'pending' : data}
) } const rendered = renderWithClient( queryClient, , ) expect(rendered.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(10)) expect(rendered.getByText('data0')).toBeInTheDocument() fireEvent.click(rendered.getByText('inc')) expect(rendered.getByText('pending')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(10)) expect(rendered.getByText('data1')).toBeInTheDocument() }) it('should not request old data inside transitions (issue #6486)', async () => { const key = queryKey() let queryFnCount = 0 function App() { const [count, setCount] = React.useState(0) return (
) } function Page({ count }: { count: number }) { const { data } = useSuspenseQuery({ queryKey: [key, count], queryFn: () => sleep(10).then(() => { queryFnCount++ return 'data' + count }), }) return (
{String(data)}
) } const rendered = renderWithClient( queryClient, , ) expect(rendered.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(10)) expect(rendered.getByText('data0')).toBeInTheDocument() fireEvent.click(rendered.getByText('inc')) await act(() => vi.advanceTimersByTimeAsync(10)) expect(rendered.getByText('data1')).toBeInTheDocument() expect(queryFnCount).toBe(2) }) it('should still suspense if queryClient has placeholderData config', async () => { const key = queryKey() const queryClientWithPlaceholder = new QueryClient({ defaultOptions: { queries: { placeholderData: (previousData: any) => previousData, }, }, }) function Page() { const [count, setCount] = React.useState(0) const [isPending, startTransition] = React.useTransition() const { data } = useSuspenseQuery({ queryKey: [key, count], queryFn: () => sleep(10).then(() => 'data' + count), }) return (
{isPending ? 'pending' : data}
) } const rendered = renderWithClient( queryClientWithPlaceholder, , ) expect(rendered.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(10)) expect(rendered.getByText('data0')).toBeInTheDocument() fireEvent.click(rendered.getByText('inc')) expect(rendered.getByText('pending')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(10)) expect(rendered.getByText('data1')).toBeInTheDocument() }) it('should show error boundary even with gcTime:0 (#7853)', async () => { const consoleMock = vi .spyOn(console, 'error') .mockImplementation(() => undefined) const key = queryKey() let count = 0 function Page() { useSuspenseQuery({ queryKey: key, queryFn: () => sleep(10).then(() => { count++ throw new Error('Query failed') }), gcTime: 0, retry: false, }) return null } function App() { return ( { return
There was an error!
}} >
) } const rendered = renderWithClient(queryClient, ) expect(rendered.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(10)) expect(rendered.getByText('There was an error!')).toBeInTheDocument() expect(count).toBe(1) consoleMock.mockRestore() }) describe('gc (with fake timers)', () => { beforeAll(() => { vi.useFakeTimers() }) afterAll(() => { vi.useRealTimers() }) it('should gc when unmounted while fetching with low gcTime (#8159)', async () => { const key = queryKey() function Page() { return ( ) } function Component() { const { data } = useSuspenseQuery({ queryKey: key, queryFn: () => sleep(3000).then(() => 'data'), gcTime: 1000, }) return
{data}
} function Page2() { return
page2
} function App() { const [show, setShow] = React.useState(true) return (
{show ? : }
) } const rendered = renderWithClient(queryClient, ) expect(rendered.getByText('loading')).toBeInTheDocument() fireEvent.click(rendered.getByText('hide')) expect(rendered.getByText('page2')).toBeInTheDocument() // wait for query to be resolved await act(() => vi.advanceTimersByTimeAsync(3000)) expect(queryClient.getQueryData(key)).toBe('data') // wait for gc await act(() => vi.advanceTimersByTimeAsync(1000)) expect(queryClient.getQueryData(key)).toBe(undefined) }) }) it('should log an error when skipToken is passed as queryFn', () => { const consoleErrorSpy = vi .spyOn(console, 'error') .mockImplementation(() => {}) const key = queryKey() function Page() { useSuspenseQueries({ queries: [ { queryKey: key, // @ts-expect-error queryFn: Math.random() >= 0 ? skipToken : () => Promise.resolve(5), }, ], }) return null } function App() { return ( ) } renderWithClient(queryClient, ) expect(consoleErrorSpy).toHaveBeenCalledWith( 'skipToken is not allowed for useSuspenseQueries', ) consoleErrorSpy.mockRestore() }) it('should log an error when skipToken is used in development environment', () => { const envCopy = process.env.NODE_ENV process.env.NODE_ENV = 'development' const consoleErrorSpy = vi .spyOn(console, 'error') .mockImplementation(() => undefined) const key = queryKey() function Page() { useSuspenseQueries({ queries: [ { queryKey: key, queryFn: skipToken as any, }, ], }) return null } renderWithClient( queryClient, , ) expect(consoleErrorSpy).toHaveBeenCalledWith( 'skipToken is not allowed for useSuspenseQueries', ) consoleErrorSpy.mockRestore() process.env.NODE_ENV = envCopy }) it('should not log an error when skipToken is used in production environment', () => { const envCopy = process.env.NODE_ENV process.env.NODE_ENV = 'production' const consoleErrorSpy = vi .spyOn(console, 'error') .mockImplementation(() => undefined) const key = queryKey() function Page() { useSuspenseQueries({ queries: [ { queryKey: key, queryFn: skipToken as any, }, ], }) return null } renderWithClient( queryClient, , ) expect(consoleErrorSpy).not.toHaveBeenCalled() consoleErrorSpy.mockRestore() process.env.NODE_ENV = envCopy }) }) ================================================ FILE: packages/react-query/src/__tests__/useSuspenseQuery.test-d.tsx ================================================ import { assertType, describe, expectTypeOf, it } from 'vitest' import { skipToken } from '@tanstack/query-core' import { useSuspenseQuery } from '../useSuspenseQuery' describe('useSuspenseQuery', () => { it('should always have data defined', () => { const { data } = useSuspenseQuery({ queryKey: ['key'], queryFn: () => Promise.resolve(5), }) expectTypeOf(data).toEqualTypeOf() }) it('should not have pending status', () => { const { status } = useSuspenseQuery({ queryKey: ['key'], queryFn: () => Promise.resolve(5), }) expectTypeOf(status).toEqualTypeOf<'error' | 'success'>() }) it('should not allow skipToken in queryFn', () => { assertType( useSuspenseQuery({ queryKey: ['key'], // @ts-expect-error queryFn: skipToken, }), ) assertType( useSuspenseQuery({ queryKey: ['key'], // @ts-expect-error queryFn: Math.random() > 0.5 ? skipToken : () => Promise.resolve(5), }), ) }) it('should not allow placeholderData, enabled or throwOnError props', () => { assertType( useSuspenseQuery({ queryKey: ['key'], queryFn: () => Promise.resolve(5), // @ts-expect-error TS2345 placeholderData: 5, enabled: true, }), ) assertType( useSuspenseQuery({ queryKey: ['key'], queryFn: () => Promise.resolve(5), // @ts-expect-error TS2345 enabled: true, }), ) assertType( useSuspenseQuery({ queryKey: ['key'], queryFn: () => Promise.resolve(5), // @ts-expect-error TS2345 throwOnError: true, }), ) }) it('should not return isPlaceholderData', () => { expectTypeOf( useSuspenseQuery({ queryKey: ['key'], queryFn: () => Promise.resolve(5), }), ).not.toHaveProperty('isPlaceholderData') }) it('should type-narrow the error field', () => { const query = useSuspenseQuery({ queryKey: ['key'], queryFn: () => Promise.resolve(5), }) if (query.status === 'error') { expectTypeOf(query.error).toEqualTypeOf() } }) }) ================================================ FILE: packages/react-query/src/__tests__/useSuspenseQuery.test.tsx ================================================ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { act, fireEvent } from '@testing-library/react' import * as React from 'react' import { ErrorBoundary } from 'react-error-boundary' import { queryKey, sleep } from '@tanstack/query-test-utils' import { QueryCache, QueryClient, QueryErrorResetBoundary, skipToken, useQueryErrorResetBoundary, useSuspenseInfiniteQuery, useSuspenseQuery, } from '..' import { renderWithClient } from './utils' import type { InfiniteData, UseSuspenseInfiniteQueryResult, UseSuspenseQueryResult, } from '..' describe('useSuspenseQuery', () => { beforeEach(() => { vi.useFakeTimers() }) afterEach(() => { vi.useRealTimers() }) const queryCache = new QueryCache() const queryClient = new QueryClient({ queryCache }) it('should render the correct amount of times in Suspense mode', async () => { const key = queryKey() const states: Array> = [] let count = 0 let renders = 0 function Page() { renders++ const [stateKey, setStateKey] = React.useState(key) const state = useSuspenseQuery({ queryKey: stateKey, queryFn: () => sleep(10).then(() => ++count), }) states.push(state) return (
) } const rendered = renderWithClient( queryClient, , ) expect(rendered.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(10)) expect(rendered.getByText('data: 1')).toBeInTheDocument() fireEvent.click(rendered.getByLabelText('toggle')) expect(rendered.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(10)) expect(rendered.getByText('data: 2')).toBeInTheDocument() expect(renders).toBe(6) expect(states.length).toBe(2) expect(states[0]).toMatchObject({ data: 1, status: 'success' }) expect(states[1]).toMatchObject({ data: 2, status: 'success' }) }) it('should return the correct states for a successful infinite query', async () => { const key = queryKey() const states: Array>> = [] function Page() { const [multiplier, setMultiplier] = React.useState(1) const state = useSuspenseInfiniteQuery({ queryKey: [`${key}_${multiplier}`], queryFn: ({ pageParam }) => sleep(10).then(() => pageParam * multiplier), initialPageParam: 1, getNextPageParam: (lastPage) => lastPage + 1, }) states.push(state) return (
data: {state.data?.pages.join(',')}
) } const rendered = renderWithClient( queryClient, , ) expect(rendered.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(10)) expect(rendered.getByText('data: 1')).toBeInTheDocument() expect(states.length).toBe(1) expect(states[0]).toMatchObject({ data: { pages: [1], pageParams: [1] }, status: 'success', }) fireEvent.click(rendered.getByText('next')) expect(rendered.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(10)) expect(rendered.getByText('data: 2')).toBeInTheDocument() expect(states.length).toBe(2) expect(states[1]).toMatchObject({ data: { pages: [2], pageParams: [1] }, status: 'success', }) }) it('should not call the queryFn twice when used in Suspense mode', async () => { const key = queryKey() const queryFn = vi.fn(() => sleep(10).then(() => 'data')) function Page() { useSuspenseQuery({ queryKey: [key], queryFn }) return <>rendered } const rendered = renderWithClient( queryClient, , ) expect(rendered.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(10)) expect(rendered.getByText('rendered')).toBeInTheDocument() expect(queryFn).toHaveBeenCalledTimes(1) }) it('should remove query instance when component unmounted', async () => { const key = queryKey() function Page() { useSuspenseQuery({ queryKey: key, queryFn: () => sleep(10).then(() => 'data'), }) return <>rendered } function App() { const [show, setShow] = React.useState(false) return ( <> {show && } )} > )} , ) expect(rendered.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(70)) expect(rendered.getByText('error boundary')).toBeInTheDocument() expect(rendered.getByText('retry')).toBeInTheDocument() fireEvent.click(rendered.getByText('retry')) expect(rendered.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(10)) expect(rendered.getByText('rendered')).toBeInTheDocument() expect(consoleMock.mock.calls[0]?.[1]).toStrictEqual( new Error('Suspense Error Bingo'), ) consoleMock.mockRestore() }) it('should retry fetch if the reset error boundary has been reset', async () => { const consoleMock = vi .spyOn(console, 'error') .mockImplementation(() => undefined) const key = queryKey() let succeed = false function Page() { useSuspenseQuery({ queryKey: key, queryFn: () => sleep(10).then(() => { if (!succeed) throw new Error('Suspense Error Bingo') return 'data' }), retry: false, }) return
rendered
} const rendered = renderWithClient( queryClient, {({ reset }) => ( (
error boundary
)} >
)}
, ) expect(rendered.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(10)) expect(rendered.getByText('error boundary')).toBeInTheDocument() expect(rendered.getByText('retry')).toBeInTheDocument() fireEvent.click(rendered.getByText('retry')) expect(rendered.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(10)) expect(rendered.getByText('error boundary')).toBeInTheDocument() expect(rendered.getByText('retry')).toBeInTheDocument() succeed = true fireEvent.click(rendered.getByText('retry')) expect(rendered.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(10)) expect(rendered.getByText('rendered')).toBeInTheDocument() consoleMock.mockRestore() }) it('should set staleTime when having passed a function', async () => { const key = queryKey() let count = 0 function Component() { const result = useSuspenseQuery({ queryKey: key, queryFn: () => sleep(10).then(() => ++count), staleTime: () => 60 * 1000, }) return (
data: {result.data}
) } function Page() { return ( ) } const rendered = renderWithClient(queryClient, ) expect(rendered.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(10)) expect(rendered.getByText('data: 1')).toBeInTheDocument() expect( typeof queryClient.getQueryCache().find({ queryKey: key })?.observers[0] ?.options.staleTime, ).toBe('function') }) it('should suspend when switching to a new query', async () => { const key1 = queryKey() const key2 = queryKey() function Component(props: { queryKey: Array }) { const result = useSuspenseQuery({ queryKey: props.queryKey, queryFn: () => sleep(10).then(() => props.queryKey), retry: false, }) return
data: {result.data}
} function Page() { const [key, setKey] = React.useState(key1) return (
) } const rendered = renderWithClient(queryClient, ) expect(rendered.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(10)) expect(rendered.getByText(`data: ${key1}`)).toBeInTheDocument() fireEvent.click(rendered.getByText('switch')) expect(rendered.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(10)) expect(rendered.getByText(`data: ${key2}`)).toBeInTheDocument() }) it('should retry fetch if the reset error boundary has been reset with global hook', async () => { const consoleMock = vi .spyOn(console, 'error') .mockImplementation(() => undefined) const key = queryKey() let succeed = false function Page() { useSuspenseQuery({ queryKey: key, queryFn: () => sleep(10).then(() => { if (!succeed) throw new Error('Suspense Error Bingo') return 'data' }), retry: false, }) return
rendered
} function App() { const { reset } = useQueryErrorResetBoundary() return ( (
error boundary
)} >
) } const rendered = renderWithClient(queryClient, ) expect(rendered.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(10)) expect(rendered.getByText('error boundary')).toBeInTheDocument() expect(rendered.getByText('retry')).toBeInTheDocument() fireEvent.click(rendered.getByText('retry')) expect(rendered.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(10)) expect(rendered.getByText('error boundary')).toBeInTheDocument() expect(rendered.getByText('retry')).toBeInTheDocument() succeed = true fireEvent.click(rendered.getByText('retry')) expect(rendered.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(10)) expect(rendered.getByText('rendered')).toBeInTheDocument() consoleMock.mockRestore() }) it('should throw errors to the error boundary by default', async () => { const consoleMock = vi .spyOn(console, 'error') .mockImplementation(() => undefined) const key = queryKey() function Page() { useSuspenseQuery({ queryKey: key, queryFn: () => sleep(10).then(() => Promise.reject(new Error('Suspense Error a1x'))), retry: false, }) return
rendered
} function App() { return ( (
error boundary
)} >
) } const rendered = renderWithClient(queryClient, ) expect(rendered.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(10)) expect(rendered.getByText('error boundary')).toBeInTheDocument() consoleMock.mockRestore() }) it('should throw select errors to the error boundary by default', async () => { const consoleMock = vi .spyOn(console, 'error') .mockImplementation(() => undefined) const key = queryKey() function Page() { useSuspenseQuery({ queryKey: key, queryFn: () => sleep(10).then(() => ({ a: { b: 'c' } })), select: () => { throw new Error('foo') }, }) return
rendered
} function App() { return ( (
error boundary
)} >
) } const rendered = renderWithClient(queryClient, ) expect(rendered.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(10)) expect(rendered.getByText('error boundary')).toBeInTheDocument() consoleMock.mockRestore() }) it('should error caught in error boundary without infinite loop', async () => { const consoleMock = vi .spyOn(console, 'error') .mockImplementation(() => undefined) const key = queryKey() let succeed = true function Page() { const [nonce] = React.useState(0) const queryKeys = [`${key}-${succeed}`] const result = useSuspenseQuery({ queryKey: queryKeys, queryFn: () => sleep(10).then(() => { if (!succeed) throw new Error('Suspense Error Bingo') return nonce }), retry: false, }) return (
rendered {result.data}
) } function App() { const { reset } = useQueryErrorResetBoundary() return (
error boundary
} >
) } const rendered = renderWithClient(queryClient, ) // render suspense fallback (loading) expect(rendered.getByText('loading')).toBeInTheDocument() // resolve promise -> render Page (rendered) await act(() => vi.advanceTimersByTimeAsync(10)) expect(rendered.getByText('rendered')).toBeInTheDocument() // change query key succeed = false // reset query -> and throw error fireEvent.click(rendered.getByLabelText('fail')) // render error boundary fallback (error boundary) await act(() => vi.advanceTimersByTimeAsync(10)) expect(rendered.getByText('error boundary')).toBeInTheDocument() expect(consoleMock.mock.calls[0]?.[1]).toStrictEqual( new Error('Suspense Error Bingo'), ) consoleMock.mockRestore() }) it('should error caught in error boundary without infinite loop when query keys changed', async () => { const consoleMock = vi .spyOn(console, 'error') .mockImplementation(() => undefined) let succeed = true function Page() { const [key, rerender] = React.useReducer((x) => x + 1, 0) const queryKeys = [key, succeed] const result = useSuspenseQuery({ queryKey: queryKeys, queryFn: () => sleep(10).then(() => { if (!succeed) throw new Error('Suspense Error Bingo') return 'data' }), retry: false, }) if (result.error) { throw result.error } return (
rendered {result.data}
) } function App() { const { reset } = useQueryErrorResetBoundary() return (
error boundary
} >
) } const rendered = renderWithClient(queryClient, ) // render suspense fallback (loading) expect(rendered.getByText('loading')).toBeInTheDocument() // resolve promise -> render Page (rendered) await act(() => vi.advanceTimersByTimeAsync(10)) expect(rendered.getByText('rendered')).toBeInTheDocument() // change promise result to error succeed = false // change query key fireEvent.click(rendered.getByLabelText('fail')) expect(rendered.getByText('loading')).toBeInTheDocument() // render error boundary fallback (error boundary) await act(() => vi.advanceTimersByTimeAsync(10)) expect(rendered.getByText('error boundary')).toBeInTheDocument() expect(consoleMock.mock.calls[0]?.[1]).toStrictEqual( new Error('Suspense Error Bingo'), ) consoleMock.mockRestore() }) it('should render the correct amount of times in Suspense mode when gcTime is set to 0', async () => { const key = queryKey() let state: UseSuspenseQueryResult | null = null let count = 0 let renders = 0 function Page() { renders++ state = useSuspenseQuery({ queryKey: key, queryFn: () => sleep(10).then(() => ++count), gcTime: 0, }) return (
rendered
) } const rendered = renderWithClient( queryClient, , ) await act(() => vi.advanceTimersByTimeAsync(10)) expect(rendered.getByText('rendered')).toBeInTheDocument() expect(state).toMatchObject({ data: 1, status: 'success', }) expect(renders).toBe(3) }) it('should not throw background errors to the error boundary', async () => { const consoleMock = vi .spyOn(console, 'error') .mockImplementation(() => undefined) let succeed = true const key = queryKey() function Page() { const result = useSuspenseQuery({ queryKey: key, queryFn: () => sleep(10).then(() => { if (!succeed) throw new Error('Suspense Error Bingo') return 'data' }), retry: false, }) return (
rendered {result.data} {result.status}
) } function App() { const { reset } = useQueryErrorResetBoundary() return (
error boundary
} >
) } const rendered = renderWithClient(queryClient, ) // render suspense fallback (loading) expect(rendered.getByText('loading')).toBeInTheDocument() // resolve promise -> render Page (rendered) await act(() => vi.advanceTimersByTimeAsync(10)) expect(rendered.getByText('rendered data success')).toBeInTheDocument() // change promise result to error succeed = false // refetch fireEvent.click(rendered.getByRole('button', { name: 'refetch' })) // we are now in error state but still have data to show await act(() => vi.advanceTimersByTimeAsync(11)) expect(rendered.getByText('rendered data error')).toBeInTheDocument() consoleMock.mockRestore() }) it('should still suspense if queryClient has placeholderData config', async () => { const key = queryKey() const queryClientWithPlaceholder = new QueryClient({ defaultOptions: { queries: { placeholderData: (previousData: any) => previousData, }, }, }) const states: Array> = [] let count = 0 function Page() { const [stateKey, setStateKey] = React.useState(key) const state = useSuspenseQuery({ queryKey: stateKey, queryFn: async () => sleep(10).then(() => ++count), }) states.push(state) return (
) } const rendered = renderWithClient( queryClientWithPlaceholder, , ) expect(rendered.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(10)) expect(rendered.getByText('data: 1')).toBeInTheDocument() fireEvent.click(rendered.getByLabelText('toggle')) expect(rendered.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(10)) expect(rendered.getByText('data: 2')).toBeInTheDocument() }) it('should log an error when skipToken is passed as queryFn', () => { const consoleErrorSpy = vi .spyOn(console, 'error') .mockImplementation(() => {}) const key = queryKey() function Page() { useSuspenseQuery({ queryKey: key, // @ts-expect-error queryFn: Math.random() >= 0 ? skipToken : () => Promise.resolve(5), }) return null } function App() { return ( ) } renderWithClient(queryClient, ) expect(consoleErrorSpy).toHaveBeenCalledWith( 'skipToken is not allowed for useSuspenseQuery', ) consoleErrorSpy.mockRestore() }) it('should properly refresh data when refetchInterval is set', async () => { const key = queryKey() let count = 0 function Page() { const state = useSuspenseQuery({ queryKey: key, queryFn: () => sleep(10).then(() => ++count), refetchInterval: 10, }) return
count: {state.data}
} const rendered = renderWithClient( queryClient, , ) expect(rendered.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(10)) expect(rendered.getByText('count: 1')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(21)) expect(rendered.getByText('count: 2')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(21)) expect(rendered.getByText('count: 3')).toBeInTheDocument() expect(count).toBeGreaterThanOrEqual(3) }) it('should log an error when skipToken is used in development environment', () => { const envCopy = process.env.NODE_ENV process.env.NODE_ENV = 'development' const consoleErrorSpy = vi .spyOn(console, 'error') .mockImplementation(() => undefined) const key = queryKey() function Page() { useSuspenseQuery({ queryKey: key, queryFn: skipToken as any, }) return null } renderWithClient( queryClient, , ) expect(consoleErrorSpy).toHaveBeenCalledWith( 'skipToken is not allowed for useSuspenseQuery', ) consoleErrorSpy.mockRestore() process.env.NODE_ENV = envCopy }) it('should not log an error when skipToken is used in production environment', () => { const envCopy = process.env.NODE_ENV process.env.NODE_ENV = 'production' const consoleErrorSpy = vi .spyOn(console, 'error') .mockImplementation(() => undefined) const key = queryKey() function Page() { useSuspenseQuery({ queryKey: key, queryFn: skipToken as any, }) return null } renderWithClient( queryClient, , ) expect(consoleErrorSpy).not.toHaveBeenCalled() consoleErrorSpy.mockRestore() process.env.NODE_ENV = envCopy }) }) ================================================ FILE: packages/react-query/src/__tests__/utils.tsx ================================================ import { vi } from 'vitest' import * as React from 'react' import { act, render } from '@testing-library/react' import * as utils from '@tanstack/query-core' import { QueryClientProvider, onlineManager } from '..' import type { QueryClient } from '..' import type { MockInstance } from 'vitest' export function renderWithClient( client: QueryClient, ui: React.ReactElement, ): ReturnType { const { rerender, ...result } = render( {ui}, ) return { ...result, rerender: (rerenderUi: React.ReactElement) => rerender( {rerenderUi}, ), } as any } export function Blink({ duration, children, }: { duration: number children: React.ReactNode }) { const [shouldShow, setShouldShow] = React.useState(true) React.useEffect(() => { setShouldShow(true) const timeout = setActTimeout(() => setShouldShow(false), duration) return () => { clearTimeout(timeout) } }, [duration, children]) return shouldShow ? <>{children} : <>off } export function mockOnlineManagerIsOnline( value: boolean, ): MockInstance<() => boolean> { return vi.spyOn(onlineManager, 'isOnline').mockReturnValue(value) } export function setActTimeout(fn: () => void, ms?: number) { return setTimeout(() => { act(() => { fn() }) }, ms) } // This monkey-patches the isServer-value from utils, // so that we can pretend to be in a server environment export function setIsServer(isServer: boolean) { const original = utils.isServer Object.defineProperty(utils, 'isServer', { get: () => isServer, }) return () => { Object.defineProperty(utils, 'isServer', { get: () => original, }) } } ================================================ FILE: packages/react-query-devtools/eslint.config.js ================================================ // @ts-check import pluginReact from '@eslint-react/eslint-plugin' import * as reactHooks from 'eslint-plugin-react-hooks' import rootConfig from './root.eslint.config.js' export default [ ...rootConfig, reactHooks.configs.recommended, { files: ['**/*.{ts,tsx}'], ...pluginReact.configs.recommended, rules: { '@eslint-react/no-context-provider': 'off', // We need to be React 18 compatible }, }, { rules: { 'react-hooks/exhaustive-deps': 'error', 'react-hooks/rules-of-hooks': 'error', 'react-hooks/react-compiler': 'error', }, }, ] ================================================ FILE: packages/react-query-devtools/package.json ================================================ { "name": "@tanstack/react-query-devtools", "version": "5.89.0", "description": "Developer tools to interact with and visualize the TanStack/react-query cache", "author": "tannerlinsley", "license": "MIT", "repository": { "type": "git", "url": "https://github.com/TanStack/query.git", "directory": "packages/react-query-devtools" }, "homepage": "https://tanstack.com/query", "funding": { "type": "github", "url": "https://github.com/sponsors/tannerlinsley" }, "scripts": { "clean": "premove ./build ./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 tsconfig.legacy.json", "test:types:ts51": "node ../../node_modules/typescript51/lib/tsc.js --build tsconfig.legacy.json", "test:types:ts52": "node ../../node_modules/typescript52/lib/tsc.js --build tsconfig.legacy.json", "test:types:ts53": "node ../../node_modules/typescript53/lib/tsc.js --build tsconfig.legacy.json", "test:types:ts54": "node ../../node_modules/typescript54/lib/tsc.js --build tsconfig.legacy.json", "test:types:ts55": "node ../../node_modules/typescript55/lib/tsc.js --build tsconfig.legacy.json", "test:types:ts56": "node ../../node_modules/typescript56/lib/tsc.js --build tsconfig.legacy.json", "test:types:ts57": "node ../../node_modules/typescript57/lib/tsc.js --build tsconfig.legacy.json", "test:types:tscurrent": "tsc --build", "test:lib": "vitest", "test:lib:dev": "pnpm run test:lib --watch", "test:build": "publint --strict && attw --pack", "build": "tsup --tsconfig tsconfig.prod.json", "build:dev": "tsup --watch" }, "type": "module", "types": "build/legacy/index.d.ts", "main": "build/legacy/index.cjs", "module": "build/legacy/index.js", "exports": { ".": { "@tanstack/custom-condition": "./src/index.ts", "import": { "types": "./build/modern/index.d.ts", "default": "./build/modern/index.js" }, "require": { "types": "./build/modern/index.d.cts", "default": "./build/modern/index.cjs" } }, "./production": { "import": { "types": "./build/modern/production.d.ts", "default": "./build/modern/production.js" }, "require": { "types": "./build/modern/production.d.cts", "default": "./build/modern/production.cjs" } }, "./build/modern/production.js": { "import": { "types": "./build/modern/production.d.ts", "default": "./build/modern/production.js" }, "require": { "types": "./build/modern/production.d.cts", "default": "./build/modern/production.cjs" } }, "./package.json": "./package.json" }, "sideEffects": false, "files": [ "build", "src", "!src/__tests__" ], "dependencies": { "@tanstack/query-devtools": "workspace:*" }, "devDependencies": { "@tanstack/react-query": "workspace:*", "@testing-library/react": "^16.1.0", "@types/react": "^19.0.1", "@vitejs/plugin-react": "^4.3.4", "npm-run-all2": "^5.0.0", "react": "^19.0.0" }, "peerDependencies": { "@tanstack/react-query": "workspace:^", "react": "^18 || ^19" } } ================================================ FILE: packages/react-query-devtools/test-setup.ts ================================================ import '@testing-library/jest-dom/vitest' import { act, cleanup } from '@testing-library/react' import { afterEach } from 'vitest' import { notifyManager } from '@tanstack/react-query' // https://testing-library.com/docs/react-testing-library/api#cleanup afterEach(() => cleanup()) // Wrap notifications with act to make sure React knows about React Query updates notifyManager.setNotifyFunction((fn) => { act(fn) }) ================================================ FILE: packages/react-query-devtools/tsconfig.json ================================================ { "extends": "../../tsconfig.json", "compilerOptions": { "outDir": "./dist-ts", "rootDir": ".", "jsx": "react-jsx" }, "include": ["src", "test-setup.ts", "*.config.*", "package.json"], "references": [{ "path": "../query-devtools" }, { "path": "../react-query" }] } ================================================ FILE: packages/react-query-devtools/tsconfig.legacy.json ================================================ { "extends": "./tsconfig.json", "compilerOptions": { "jsx": "react-jsx", "outDir": "./dist-ts/legacy" }, "include": ["src"], "exclude": ["src/__tests__"], "references": [{ "path": "../query-devtools" }, { "path": "../react-query" }] } ================================================ FILE: packages/react-query-devtools/tsconfig.prod.json ================================================ { "extends": "./tsconfig.json", "compilerOptions": { "incremental": false, "composite": false, "rootDir": "../../" } } ================================================ FILE: packages/react-query-devtools/tsup.config.ts ================================================ import { defineConfig } from 'tsup' import { legacyConfig, modernConfig } from './root.tsup.config.js' export default defineConfig([ modernConfig({ entry: ['src/*.ts', 'src/*.tsx'] }), legacyConfig({ entry: ['src/*.ts', 'src/*.tsx'] }), ]) ================================================ FILE: packages/react-query-devtools/vite.config.ts ================================================ import { defineConfig } from 'vitest/config' import react from '@vitejs/plugin-react' import packageJson from './package.json' export default defineConfig({ plugins: [react()], // 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 }, restoreMocks: true, }, }) ================================================ FILE: packages/react-query-devtools/.attw.json ================================================ { "ignoreRules": ["no-resolution"] } ================================================ SYMLINK: packages/react-query-devtools/root.eslint.config.js -> eslint.config.js ================================================ ================================================ SYMLINK: packages/react-query-devtools/root.tsup.config.js -> getTsupConfig.js ================================================ ================================================ FILE: packages/react-query-devtools/src/index.ts ================================================ 'use client' import * as Devtools from './ReactQueryDevtools' import * as DevtoolsPanel from './ReactQueryDevtoolsPanel' export const ReactQueryDevtools: (typeof Devtools)['ReactQueryDevtools'] = process.env.NODE_ENV !== 'development' ? function () { return null } : Devtools.ReactQueryDevtools export const ReactQueryDevtoolsPanel: (typeof DevtoolsPanel)['ReactQueryDevtoolsPanel'] = process.env.NODE_ENV !== 'development' ? function () { return null } : DevtoolsPanel.ReactQueryDevtoolsPanel export type DevtoolsPanelOptions = DevtoolsPanel.DevtoolsPanelOptions ================================================ FILE: packages/react-query-devtools/src/production.ts ================================================ 'use client' import * as Devtools from './ReactQueryDevtools' import * as DevtoolsPanel from './ReactQueryDevtoolsPanel' export const ReactQueryDevtools = Devtools.ReactQueryDevtools export const ReactQueryDevtoolsPanel = DevtoolsPanel.ReactQueryDevtoolsPanel ================================================ FILE: packages/react-query-devtools/src/ReactQueryDevtools.tsx ================================================ 'use client' import * as React from 'react' import { onlineManager, useQueryClient } from '@tanstack/react-query' import { TanstackQueryDevtools } from '@tanstack/query-devtools' import type { DevtoolsButtonPosition, DevtoolsErrorType, DevtoolsPosition, } from '@tanstack/query-devtools' import type { QueryClient } from '@tanstack/react-query' export interface DevtoolsOptions { /** * Set this true if you want the dev tools to default to being open */ initialIsOpen?: boolean /** * The position of the React Query logo to open and close the devtools panel. * 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' * Defaults to 'bottom-right'. */ buttonPosition?: DevtoolsButtonPosition /** * The position of the React Query devtools panel. * 'top' | 'bottom' | 'left' | 'right' * Defaults to 'bottom'. */ position?: DevtoolsPosition /** * Custom instance of QueryClient */ client?: QueryClient /** * Use this so you can define custom errors that can be shown in the devtools. */ errorTypes?: Array /** * Use this to pass a nonce to the style tag that is added to the document head. This is useful if you are using a Content Security Policy (CSP) nonce to allow inline styles. */ styleNonce?: string /** * Use this so you can attach the devtool's styles to specific element in the DOM. */ shadowDOMTarget?: ShadowRoot /** * Set this to true to hide disabled queries from the devtools panel. */ hideDisabledQueries?: boolean } export function ReactQueryDevtools( props: DevtoolsOptions, ): React.ReactElement | null { const queryClient = useQueryClient(props.client) const ref = React.useRef(null) const { buttonPosition, position, initialIsOpen, errorTypes, styleNonce, shadowDOMTarget, hideDisabledQueries, } = props const [devtools] = React.useState( new TanstackQueryDevtools({ client: queryClient, queryFlavor: 'React Query', version: '5', onlineManager, buttonPosition, position, initialIsOpen, errorTypes, styleNonce, shadowDOMTarget, hideDisabledQueries, }), ) React.useEffect(() => { devtools.setClient(queryClient) }, [queryClient, devtools]) React.useEffect(() => { if (buttonPosition) { devtools.setButtonPosition(buttonPosition) } }, [buttonPosition, devtools]) React.useEffect(() => { if (position) { devtools.setPosition(position) } }, [position, devtools]) React.useEffect(() => { devtools.setInitialIsOpen(initialIsOpen || false) }, [initialIsOpen, devtools]) React.useEffect(() => { devtools.setErrorTypes(errorTypes || []) }, [errorTypes, devtools]) React.useEffect(() => { if (ref.current) { devtools.mount(ref.current) } return () => { devtools.unmount() } }, [devtools]) return
} ================================================ FILE: packages/react-query-devtools/src/ReactQueryDevtoolsPanel.tsx ================================================ 'use client' import * as React from 'react' import { onlineManager, useQueryClient } from '@tanstack/react-query' import { TanstackQueryDevtoolsPanel } from '@tanstack/query-devtools' import type { DevtoolsErrorType } from '@tanstack/query-devtools' import type { QueryClient } from '@tanstack/react-query' export interface DevtoolsPanelOptions { /** * Custom instance of QueryClient */ client?: QueryClient /** * Use this so you can define custom errors that can be shown in the devtools. */ errorTypes?: Array /** * Use this to pass a nonce to the style tag that is added to the document head. This is useful if you are using a Content Security Policy (CSP) nonce to allow inline styles. */ styleNonce?: string /** * Use this so you can attach the devtool's styles to specific element in the DOM. */ shadowDOMTarget?: ShadowRoot /** * Custom styles for the devtools panel * @default { height: '500px' } * @example { height: '100%' } * @example { height: '100%', width: '100%' } */ style?: React.CSSProperties /** * Callback function that is called when the devtools panel is closed */ onClose?: () => unknown /** * Set this to true to hide disabled queries from the devtools panel. */ hideDisabledQueries?: boolean } export function ReactQueryDevtoolsPanel( props: DevtoolsPanelOptions, ): React.ReactElement | null { const queryClient = useQueryClient(props.client) const ref = React.useRef(null) const { errorTypes, styleNonce, shadowDOMTarget, hideDisabledQueries } = props const [devtools] = React.useState( new TanstackQueryDevtoolsPanel({ client: queryClient, queryFlavor: 'React Query', version: '5', onlineManager, buttonPosition: 'bottom-left', position: 'bottom', initialIsOpen: true, errorTypes, styleNonce, shadowDOMTarget, onClose: props.onClose, hideDisabledQueries, }), ) React.useEffect(() => { devtools.setClient(queryClient) }, [queryClient, devtools]) React.useEffect(() => { devtools.setOnClose(props.onClose ?? (() => {})) }, [props.onClose, devtools]) React.useEffect(() => { devtools.setErrorTypes(errorTypes || []) }, [errorTypes, devtools]) React.useEffect(() => { if (ref.current) { devtools.mount(ref.current) } return () => { devtools.unmount() } }, [devtools]) return (
) } ================================================ FILE: packages/react-query-devtools/src/__tests__/devtools.test.tsx ================================================ import { describe, expect, it } from 'vitest' describe('ReactQueryDevtools', () => { it('should be able to open and close devtools', () => { expect(1).toBe(1) }) }) ================================================ FILE: packages/react-query-devtools/src/__tests__/not-development.test.tsx ================================================ import { describe, expect, it } from 'vitest' import { ReactQueryDevtools } from '..' describe('ReactQueryDevtools not in process.env.NODE_ENV=development', () => { it('should return null', () => { expect(process.env.NODE_ENV).not.toBe('development') expect(ReactQueryDevtools({})).toBeNull() }) }) ================================================ FILE: packages/react-query-next-experimental/eslint.config.js ================================================ // @ts-check import pluginReact from '@eslint-react/eslint-plugin' import * as reactHooks from 'eslint-plugin-react-hooks' import rootConfig from './root.eslint.config.js' export default [ ...rootConfig, reactHooks.configs.recommended, { files: ['**/*.{ts,tsx}'], ...pluginReact.configs.recommended, rules: { '@eslint-react/no-context-provider': 'off', // We need to be React 18 compatible }, }, { rules: { '@eslint-react/no-unstable-context-value': 'off', 'react-hooks/exhaustive-deps': 'error', 'react-hooks/rules-of-hooks': 'error', 'react-hooks/react-compiler': 'error', }, }, ] ================================================ FILE: packages/react-query-next-experimental/package.json ================================================ { "name": "@tanstack/react-query-next-experimental", "version": "5.89.0", "description": "Hydration utils for React Query in the NextJs app directory", "author": "tannerlinsley", "license": "MIT", "repository": { "type": "git", "url": "https://github.com/TanStack/query.git", "directory": "packages/react-query-next-experimental" }, "homepage": "https://tanstack.com/query", "funding": { "type": "github", "url": "https://github.com/sponsors/tannerlinsley" }, "scripts": { "clean": "premove ./build ./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 tsconfig.legacy.json", "test:types:ts51": "node ../../node_modules/typescript51/lib/tsc.js --build tsconfig.legacy.json", "test:types:ts52": "node ../../node_modules/typescript52/lib/tsc.js --build tsconfig.legacy.json", "test:types:ts53": "node ../../node_modules/typescript53/lib/tsc.js --build tsconfig.legacy.json", "test:types:ts54": "node ../../node_modules/typescript54/lib/tsc.js --build tsconfig.legacy.json", "test:types:ts55": "node ../../node_modules/typescript55/lib/tsc.js --build tsconfig.legacy.json", "test:types:ts56": "node ../../node_modules/typescript56/lib/tsc.js --build tsconfig.legacy.json", "test:types:ts57": "node ../../node_modules/typescript57/lib/tsc.js --build tsconfig.legacy.json", "test:types:tscurrent": "tsc --build", "test:build": "publint --strict && attw --pack", "build": "tsup --tsconfig tsconfig.prod.json" }, "type": "module", "types": "build/legacy/index.d.ts", "main": "build/legacy/index.cjs", "module": "build/legacy/index.js", "exports": { ".": { "@tanstack/custom-condition": "./src/index.ts", "import": { "types": "./build/modern/index.d.ts", "default": "./build/modern/index.js" }, "require": { "types": "./build/modern/index.d.cts", "default": "./build/modern/index.cjs" } }, "./package.json": "./package.json" }, "sideEffects": false, "files": [ "build", "src", "!src/__tests__" ], "devDependencies": { "@tanstack/react-query": "workspace:*", "@types/react": "^19.0.1", "@vitejs/plugin-react": "^4.3.4", "next": "^15.3.1", "npm-run-all2": "^5.0.0", "react": "^19.0.0" }, "peerDependencies": { "@tanstack/react-query": "workspace:^", "next": "^13 || ^14 || ^15", "react": "^18 || ^19" } } ================================================ FILE: packages/react-query-next-experimental/tsconfig.json ================================================ { "extends": "../../tsconfig.json", "compilerOptions": { "outDir": "./dist-ts", "rootDir": ".", "jsx": "react-jsx" }, "include": ["src", "*.config.*", "package.json"], "references": [{ "path": "../react-query" }] } ================================================ FILE: packages/react-query-next-experimental/tsconfig.legacy.json ================================================ { "extends": "./tsconfig.json", "compilerOptions": { "jsx": "react-jsx", "outDir": "./dist-ts/legacy" }, "include": ["src"], "exclude": ["src/__tests__"], "references": [{ "path": "../react-query" }] } ================================================ FILE: packages/react-query-next-experimental/tsconfig.prod.json ================================================ { "extends": "./tsconfig.json", "compilerOptions": { "incremental": false, "composite": false, "rootDir": "../../" } } ================================================ FILE: packages/react-query-next-experimental/tsup.config.ts ================================================ import { defineConfig } from 'tsup' import { legacyConfig, modernConfig } from './root.tsup.config.js' export default defineConfig([ modernConfig({ entry: ['src/*.ts', 'src/*.tsx'] }), legacyConfig({ entry: ['src/*.ts', 'src/*.tsx'] }), ]) ================================================ FILE: packages/react-query-next-experimental/vite.config.ts ================================================ import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' export default defineConfig({ plugins: [react()], // fix from https://github.com/vitest-dev/vitest/issues/6992#issuecomment-2509408660 resolve: { conditions: ['@tanstack/custom-condition'], }, environments: { ssr: { resolve: { conditions: ['@tanstack/custom-condition'], }, }, }, }) ================================================ SYMLINK: packages/react-query-next-experimental/root.eslint.config.js -> eslint.config.js ================================================ ================================================ SYMLINK: packages/react-query-next-experimental/root.tsup.config.js -> getTsupConfig.js ================================================ ================================================ FILE: packages/react-query-next-experimental/src/htmlescape.ts ================================================ // -------------------------------------------------------------------------------- // // copied from // https://github.com/vercel/next.js/blob/6bc07792a4462a4bf921a72ab30dc4ab2c4e1bda/packages/next/src/server/htmlescape.ts // License: https://github.com/vercel/next.js/blob/6bc07792a4462a4bf921a72ab30dc4ab2c4e1bda/packages/next/license.md // // -------------------------------------------------------------------------------- // This utility is based on https://github.com/zertosh/htmlescape // License: https://github.com/zertosh/htmlescape/blob/0527ca7156a524d256101bb310a9f970f63078ad/LICENSE const ESCAPE_LOOKUP: Record = { '&': '\\u0026', '>': '\\u003e', '<': '\\u003c', '\u2028': '\\u2028', '\u2029': '\\u2029', } export const ESCAPE_REGEX = /[&><\u2028\u2029]/g export function htmlEscapeJsonString(str: string): string { return str.replace(ESCAPE_REGEX, (match) => ESCAPE_LOOKUP[match]!) } ================================================ FILE: packages/react-query-next-experimental/src/HydrationStreamProvider.tsx ================================================ 'use client' import { isServer } from '@tanstack/react-query' import { useServerInsertedHTML } from 'next/navigation' import * as React from 'react' import { htmlEscapeJsonString } from './htmlescape' const serializedSymbol = Symbol('serialized') interface DataTransformer { serialize: (object: any) => any deserialize: (object: any) => any } type Serialized = unknown & { [serializedSymbol]: TData } interface TypedDataTransformer { serialize: (obj: TData) => Serialized deserialize: (obj: Serialized) => TData } interface HydrationStreamContext { id: string stream: { /** * **Server method** * Push a new entry to the stream * Will be ignored on the client */ push: (...shape: Array) => void } } export interface HydrationStreamProviderProps { children: React.ReactNode /** * Optional transformer to serialize/deserialize the data * Example devalue, superjson et al */ transformer?: DataTransformer /** * **Client method** * Called in the browser when new entries are received */ onEntries: (entries: Array) => void /** * **Server method** * onFlush is called on the server when the cache is flushed */ onFlush?: () => Array /** * A nonce that'll allow the inline script to be executed when Content Security Policy is enforced */ nonce?: string } export function createHydrationStreamProvider() { const context = React.createContext>( null as any, ) /** * 1. (Happens on server): `useServerInsertedHTML()` is called **on the server** whenever a `Suspense`-boundary completes * - This means that we might have some new entries in the cache that needs to be flushed * - We pass these to the client by inserting a ` ================================================ FILE: packages/svelte-query/src/index.ts ================================================ /* istanbul ignore file */ // Re-export core export * from '@tanstack/query-core' // Svelte Query export * from './types.js' export * from './context.js' export { createQuery } from './createQuery.js' export type { QueriesResults, QueriesOptions } from './createQueries.js' export type { DefinedInitialDataOptions, UndefinedInitialDataOptions, } from './queryOptions.js' export { queryOptions } from './queryOptions.js' export { createQueries } from './createQueries.js' export { createInfiniteQuery } from './createInfiniteQuery.js' export { infiniteQueryOptions } from './infiniteQueryOptions.js' export { createMutation } from './createMutation.js' export { useMutationState } from './useMutationState.js' export { useQueryClient } from './useQueryClient.js' export { useIsFetching } from './useIsFetching.js' export { useIsMutating } from './useIsMutating.js' export { useIsRestoring } from './useIsRestoring.js' export { useHydrate } from './useHydrate.js' export { default as HydrationBoundary } from './HydrationBoundary.svelte' export { default as QueryClientProvider } from './QueryClientProvider.svelte' ================================================ FILE: packages/svelte-query/src/infiniteQueryOptions.ts ================================================ import type { DefaultError, InfiniteData, QueryKey } from '@tanstack/query-core' import type { CreateInfiniteQueryOptions } from './types.js' export function infiniteQueryOptions< TQueryFnData, TError = DefaultError, TData = InfiniteData, TQueryKey extends QueryKey = QueryKey, TPageParam = unknown, >( options: CreateInfiniteQueryOptions< TQueryFnData, TError, TData, TQueryKey, TPageParam >, ): CreateInfiniteQueryOptions< TQueryFnData, TError, TData, TQueryKey, TPageParam > { return options } ================================================ FILE: packages/svelte-query/src/QueryClientProvider.svelte ================================================ ================================================ FILE: packages/svelte-query/src/queryOptions.ts ================================================ import type { DataTag, DefaultError, InitialDataFunction, NonUndefinedGuard, QueryKey, } from '@tanstack/query-core' import type { CreateQueryOptions } from './types.js' export type UndefinedInitialDataOptions< TQueryFnData = unknown, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, > = CreateQueryOptions & { initialData?: undefined | InitialDataFunction> } export type DefinedInitialDataOptions< TQueryFnData = unknown, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, > = CreateQueryOptions & { initialData: | NonUndefinedGuard | (() => NonUndefinedGuard) } export function queryOptions< TQueryFnData = unknown, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, >( options: DefinedInitialDataOptions, ): DefinedInitialDataOptions & { queryKey: DataTag } export function queryOptions< TQueryFnData = unknown, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, >( options: UndefinedInitialDataOptions, ): UndefinedInitialDataOptions & { queryKey: DataTag } export function queryOptions(options: unknown) { return options } ================================================ FILE: packages/svelte-query/src/types.ts ================================================ import type { DefaultError, DefinedQueryObserverResult, InfiniteQueryObserverOptions, InfiniteQueryObserverResult, MutateFunction, Mutation, MutationFilters, MutationObserverOptions, MutationObserverResult, MutationState, OmitKeyof, Override, QueryKey, QueryObserverOptions, QueryObserverResult, } from '@tanstack/query-core' import type { Readable } from 'svelte/store' /** Allows a type to be either the base object or a store of that object */ export type StoreOrVal = T | Readable /** Options for createBaseQuery */ export type CreateBaseQueryOptions< TQueryFnData = unknown, TError = DefaultError, TData = TQueryFnData, TQueryData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, > = QueryObserverOptions /** Result from createBaseQuery */ export type CreateBaseQueryResult< TData = unknown, TError = DefaultError, > = Readable> /** Options for createQuery */ export type CreateQueryOptions< TQueryFnData = unknown, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, > = CreateBaseQueryOptions /** Result from createQuery */ export type CreateQueryResult< TData = unknown, TError = DefaultError, > = CreateBaseQueryResult /** Options for createInfiniteQuery */ export type CreateInfiniteQueryOptions< TQueryFnData = unknown, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, TPageParam = unknown, > = InfiniteQueryObserverOptions< TQueryFnData, TError, TData, TQueryKey, TPageParam > /** Result from createInfiniteQuery */ export type CreateInfiniteQueryResult< TData = unknown, TError = DefaultError, > = Readable> /** Options for createBaseQuery with initialData */ export type DefinedCreateBaseQueryResult< TData = unknown, TError = DefaultError, > = Readable> /** Options for createQuery with initialData */ export type DefinedCreateQueryResult< TData = unknown, TError = DefaultError, > = DefinedCreateBaseQueryResult /** Options for createMutation */ export type CreateMutationOptions< TData = unknown, TError = DefaultError, TVariables = void, TOnMutateResult = unknown, > = 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 > } /** Result from createMutation */ export type CreateMutationResult< TData = unknown, TError = DefaultError, TVariables = unknown, TOnMutateResult = unknown, > = Readable< CreateBaseMutationResult > /** Options for useMutationState */ export type MutationStateOptions = { filters?: MutationFilters select?: ( mutation: Mutation, ) => TResult } ================================================ FILE: packages/svelte-query/src/useHydrate.ts ================================================ import { hydrate } from '@tanstack/query-core' import { useQueryClient } from './useQueryClient.js' import type { HydrateOptions, QueryClient } from '@tanstack/query-core' export function useHydrate( state?: unknown, options?: HydrateOptions, queryClient?: QueryClient, ) { const client = useQueryClient(queryClient) if (state) { hydrate(client, state, options) } } ================================================ FILE: packages/svelte-query/src/useIsFetching.ts ================================================ import { notifyManager } from '@tanstack/query-core' import { readable } from 'svelte/store' import { useQueryClient } from './useQueryClient.js' import type { Readable } from 'svelte/store' import type { QueryClient, QueryFilters } from '@tanstack/query-core' export function useIsFetching( filters?: QueryFilters, queryClient?: QueryClient, ): Readable { const client = useQueryClient(queryClient) const cache = client.getQueryCache() // isFetching is the prev value initialized on mount * let isFetching = client.isFetching(filters) const { subscribe } = readable(isFetching, (set) => { return cache.subscribe( notifyManager.batchCalls(() => { const newIsFetching = client.isFetching(filters) if (isFetching !== newIsFetching) { // * and update with each change isFetching = newIsFetching set(isFetching) } }), ) }) return { subscribe } } ================================================ FILE: packages/svelte-query/src/useIsMutating.ts ================================================ import { notifyManager } from '@tanstack/query-core' import { readable } from 'svelte/store' import { useQueryClient } from './useQueryClient.js' import type { Readable } from 'svelte/store' import type { MutationFilters, QueryClient } from '@tanstack/query-core' export function useIsMutating( filters?: MutationFilters, queryClient?: QueryClient, ): Readable { const client = useQueryClient(queryClient) const cache = client.getMutationCache() // isMutating is the prev value initialized on mount * let isMutating = client.isMutating(filters) const { subscribe } = readable(isMutating, (set) => { return cache.subscribe( notifyManager.batchCalls(() => { const newIisMutating = client.isMutating(filters) if (isMutating !== newIisMutating) { // * and update with each change isMutating = newIisMutating set(isMutating) } }), ) }) return { subscribe } } ================================================ FILE: packages/svelte-query/src/useIsRestoring.ts ================================================ import { getIsRestoringContext } from './context.js' import type { Readable } from 'svelte/store' export function useIsRestoring(): Readable { return getIsRestoringContext() } ================================================ FILE: packages/svelte-query/src/useMutationState.ts ================================================ import { readable } from 'svelte/store' import { notifyManager, replaceEqualDeep } from '@tanstack/query-core' import { useQueryClient } from './useQueryClient.js' import type { MutationCache, MutationState, QueryClient, } from '@tanstack/query-core' import type { Readable } from 'svelte/store' import type { MutationStateOptions } from './types.js' 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 function useMutationState( options: MutationStateOptions = {}, queryClient?: QueryClient, ): Readable> { const client = useQueryClient(queryClient) const mutationCache = client.getMutationCache() let result = getResult(mutationCache, options) const { subscribe } = readable(result, (set) => { return mutationCache.subscribe( notifyManager.batchCalls(() => { const nextResult = replaceEqualDeep( result, getResult(mutationCache, options), ) if (result !== nextResult) { result = nextResult set(result) } }), ) }) return { subscribe } } ================================================ FILE: packages/svelte-query/src/useQueryClient.ts ================================================ import { getQueryClientContext } from './context.js' import type { QueryClient } from '@tanstack/query-core' export function useQueryClient(queryClient?: QueryClient): QueryClient { if (queryClient) return queryClient return getQueryClientContext() } ================================================ FILE: packages/svelte-query/src/utils.ts ================================================ import type { Readable } from 'svelte/store' import type { StoreOrVal } from './types.js' export function isSvelteStore( obj: StoreOrVal, ): obj is Readable { return 'subscribe' in obj && typeof obj.subscribe === 'function' } ================================================ FILE: packages/svelte-query/tests/test-setup.ts ================================================ import '@testing-library/jest-dom/vitest' ================================================ FILE: packages/svelte-query/tests/context/BaseExample.svelte ================================================ ================================================ FILE: packages/svelte-query/tests/context/context.test.ts ================================================ import { describe, expect, test } from 'vitest' import { render } from '@testing-library/svelte' import { getIsRestoringContext } from '../../src/index.js' import BaseExample from './BaseExample.svelte' describe('getQueryClientContext', () => { test('Throw when called without a client in context', () => { expect(() => render(BaseExample)).toThrowError( 'No QueryClient was found in Svelte context. Did you forget to wrap your component with QueryClientProvider?', ) }) }) describe('getIsRestoringContext', () => { test('Do not throw when called outside of a component', () => { expect(() => getIsRestoringContext()).not.toThrowError() }) }) ================================================ FILE: packages/svelte-query/tests/createInfiniteQuery/BaseExample.svelte ================================================
Status: {$query.status}
================================================ FILE: packages/svelte-query/tests/createInfiniteQuery/createInfiniteQuery.test.ts ================================================ import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest' import { render } from '@testing-library/svelte' import { get, writable } from 'svelte/store' import BaseExample from './BaseExample.svelte' import SelectExample from './SelectExample.svelte' import type { Writable } from 'svelte/store' import type { QueryObserverResult } from '@tanstack/query-core' describe('createInfiniteQuery', () => { beforeEach(() => { vi.useFakeTimers() }) afterEach(() => { vi.useRealTimers() }) test('Return the correct states for a successful query', async () => { const statesStore: Writable> = writable([]) const rendered = render(BaseExample, { props: { states: statesStore, }, }) await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('Status: success')).toBeInTheDocument() const states = get(statesStore) expect(states).toHaveLength(2) expect(states[0]).toEqual({ data: undefined, dataUpdatedAt: 0, error: null, errorUpdatedAt: 0, failureCount: 0, failureReason: null, errorUpdateCount: 0, fetchNextPage: expect.any(Function), fetchPreviousPage: expect.any(Function), hasNextPage: false, hasPreviousPage: false, isError: false, isFetched: false, isFetchedAfterMount: false, isFetching: true, isPaused: false, isFetchNextPageError: false, isFetchingNextPage: false, isFetchPreviousPageError: false, isFetchingPreviousPage: false, isLoading: true, isPending: true, isInitialLoading: true, isLoadingError: false, isPlaceholderData: false, isRefetchError: false, isRefetching: false, isStale: true, isSuccess: false, isEnabled: true, refetch: expect.any(Function), status: 'pending', fetchStatus: 'fetching', promise: expect.any(Promise), }) expect(states[1]).toEqual({ data: { pages: [0], pageParams: [0] }, dataUpdatedAt: expect.any(Number), error: null, errorUpdatedAt: 0, failureCount: 0, failureReason: null, errorUpdateCount: 0, fetchNextPage: expect.any(Function), fetchPreviousPage: expect.any(Function), hasNextPage: true, hasPreviousPage: false, isError: false, isFetched: true, isFetchedAfterMount: true, isFetching: false, isPaused: false, isFetchNextPageError: false, isFetchingNextPage: false, isFetchPreviousPageError: false, isFetchingPreviousPage: false, isLoading: false, isPending: false, isInitialLoading: false, isLoadingError: false, isPlaceholderData: false, isRefetchError: false, isRefetching: false, isStale: true, isSuccess: true, isEnabled: true, refetch: expect.any(Function), status: 'success', fetchStatus: 'idle', promise: expect.any(Promise), }) }) test('Select a part of the data', async () => { const statesStore: Writable> = writable([]) const rendered = render(SelectExample, { props: { states: statesStore, }, }) await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('count: 1')).toBeInTheDocument() const states = get(statesStore) expect(states).toHaveLength(2) expect(states[0]).toMatchObject({ data: undefined, isSuccess: false, }) expect(states[1]).toMatchObject({ data: { pages: ['count: 1'] }, isSuccess: true, }) }) }) ================================================ FILE: packages/svelte-query/tests/createInfiniteQuery/SelectExample.svelte ================================================
{$query.data?.pages.join(',')}
================================================ FILE: packages/svelte-query/tests/createMutation/createMutation.test.ts ================================================ import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest' import { fireEvent, render } from '@testing-library/svelte' import { sleep } from '@tanstack/query-test-utils' import ResetExample from './ResetExample.svelte' import OnSuccessExample from './OnSuccessExample.svelte' import FailureExample from './FailureExample.svelte' describe('createMutation', () => { beforeEach(() => { vi.useFakeTimers() }) afterEach(() => { vi.useRealTimers() }) test('Able to reset `error`', async () => { const rendered = render(ResetExample) expect(rendered.queryByText('Error: undefined')).toBeInTheDocument() fireEvent.click(rendered.getByRole('button', { name: /Mutate/i })) await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('Error: Expected mock error')).toBeInTheDocument() fireEvent.click(rendered.getByRole('button', { name: /Reset/i })) await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('Error: undefined')).toBeInTheDocument() }) test('Able to call `onSuccess` and `onSettled` after each successful mutate', async () => { const onSuccessMock = vi.fn() const onSettledMock = vi.fn() const rendered = render(OnSuccessExample, { props: { onSuccessMock, onSettledMock, }, }) expect(rendered.queryByText('Count: 0')).toBeInTheDocument() fireEvent.click(rendered.getByRole('button', { name: /Mutate/i })) fireEvent.click(rendered.getByRole('button', { name: /Mutate/i })) fireEvent.click(rendered.getByRole('button', { name: /Mutate/i })) await vi.advanceTimersByTimeAsync(11) expect(rendered.queryByText('Count: 3')).toBeInTheDocument() expect(onSuccessMock).toHaveBeenCalledTimes(3) expect(onSuccessMock).toHaveBeenNthCalledWith(1, 1) expect(onSuccessMock).toHaveBeenNthCalledWith(2, 2) expect(onSuccessMock).toHaveBeenNthCalledWith(3, 3) expect(onSettledMock).toHaveBeenCalledTimes(3) expect(onSettledMock).toHaveBeenNthCalledWith(1, 1) expect(onSettledMock).toHaveBeenNthCalledWith(2, 2) expect(onSettledMock).toHaveBeenNthCalledWith(3, 3) }) test('Set correct values for `failureReason` and `failureCount` on multiple mutate calls', async () => { type Value = { count: number } const mutationFn = vi.fn<(value: Value) => Promise>() mutationFn.mockImplementationOnce(() => sleep(20).then(() => Promise.reject(`Expected mock error`)), ) mutationFn.mockImplementation((value) => sleep(10).then(() => value)) const rendered = render(FailureExample, { props: { mutationFn, }, }) expect(rendered.queryByText('Data: undefined')).toBeInTheDocument() fireEvent.click(rendered.getByRole('button', { name: /Mutate/i })) expect(rendered.getByText('Data: undefined')).toBeInTheDocument() await vi.advanceTimersByTimeAsync(21) expect(rendered.getByText('Status: error')).toBeInTheDocument() expect(rendered.getByText('Failure Count: 1')).toBeInTheDocument() expect( rendered.getByText('Failure Reason: Expected mock error'), ).toBeInTheDocument() fireEvent.click(rendered.getByRole('button', { name: /Mutate/i })) await vi.advanceTimersByTimeAsync(0) expect(rendered.getByText('Status: pending')).toBeInTheDocument() await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('Status: success')).toBeInTheDocument() expect(rendered.getByText('Data: 2')).toBeInTheDocument() expect(rendered.getByText('Failure Count: 0')).toBeInTheDocument() expect(rendered.getByText('Failure Reason: null')).toBeInTheDocument() }) }) ================================================ FILE: packages/svelte-query/tests/createMutation/FailureExample.svelte ================================================
Data: {$mutation.data?.count ?? 'undefined'}
Status: {$mutation.status}
Failure Count: {$mutation.failureCount ?? 'undefined'}
Failure Reason: {$mutation.failureReason ?? 'null'}
================================================ FILE: packages/svelte-query/tests/createMutation/OnSuccessExample.svelte ================================================
Count: {$count}
================================================ FILE: packages/svelte-query/tests/createMutation/ResetExample.svelte ================================================
Error: {$mutation.error?.message ?? 'undefined'}
================================================ FILE: packages/svelte-query/tests/createQueries/BaseExample.svelte ================================================ {#each $queries as query, index}
Status {index + 1}: {query.status}
Data {index + 1}: {query.data}
{/each} ================================================ FILE: packages/svelte-query/tests/createQueries/CombineExample.svelte ================================================
isPending: {$queries.isPending}
Data: {$queries.data ?? 'undefined'}
================================================ FILE: packages/svelte-query/tests/createQueries/createQueries.test-d.ts ================================================ import { describe, expectTypeOf, test } from 'vitest' import { get } from 'svelte/store' import { skipToken } from '@tanstack/query-core' import { createQueries, queryOptions } from '../../src/index.js' import type { Readable } from 'svelte/store' import type { OmitKeyof, QueryObserverResult } from '@tanstack/query-core' import type { CreateQueryOptions } from '../../src/index.js' describe('createQueries', () => { test('TData should be defined when passed through queryOptions', () => { const options = queryOptions({ queryKey: ['key'], queryFn: () => { return { wow: true, } }, initialData: { wow: true, }, }) const queryResults = createQueries({ queries: [options] }) const data = get(queryResults)[0].data expectTypeOf(data).toEqualTypeOf<{ wow: boolean }>() }) test('Allow custom hooks using UseQueryOptions', () => { type Data = string const useCustomQueries = ( options?: OmitKeyof, 'queryKey' | 'queryFn'>, ) => { return createQueries({ queries: [ { ...options, queryKey: ['todos-key'], queryFn: () => Promise.resolve('data'), }, ], }) } const query = useCustomQueries() const data = get(query)[0].data expectTypeOf(data).toEqualTypeOf() }) test('TData should have correct type when conditional skipToken is passed', () => { const queryResults = createQueries({ queries: [ { queryKey: ['withSkipToken'], queryFn: Math.random() > 0.5 ? skipToken : () => Promise.resolve(5), }, ], }) const firstResult = get(queryResults)[0] expectTypeOf(firstResult).toEqualTypeOf< QueryObserverResult >() expectTypeOf(firstResult.data).toEqualTypeOf() }) test('should return correct data for dynamic queries with mixed result types', () => { const Queries1 = { get: () => queryOptions({ queryKey: ['key1'], queryFn: () => Promise.resolve(1), }), } const Queries2 = { get: () => queryOptions({ queryKey: ['key2'], queryFn: () => Promise.resolve(true), }), } const queries1List = [1, 2, 3].map(() => ({ ...Queries1.get() })) const result = createQueries({ queries: [...queries1List, { ...Queries2.get() }], }) expectTypeOf(result).toEqualTypeOf< Readable< [ ...Array>, QueryObserverResult, ] > >() expectTypeOf(get(result)[0].data).toEqualTypeOf< number | boolean | undefined >() }) }) ================================================ FILE: packages/svelte-query/tests/createQueries/createQueries.test.ts ================================================ import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest' import { render } from '@testing-library/svelte' import { QueryClient } from '@tanstack/query-core' import { sleep } from '@tanstack/query-test-utils' import BaseExample from './BaseExample.svelte' import CombineExample from './CombineExample.svelte' describe('createQueries', () => { beforeEach(() => { vi.useFakeTimers() }) afterEach(() => { vi.useRealTimers() }) test('Render and wait for success', async () => { const rendered = render(BaseExample, { props: { options: { queries: [ { queryKey: ['key-1'], queryFn: () => sleep(10).then(() => 'Success 1'), }, { queryKey: ['key-2'], queryFn: () => sleep(10).then(() => 'Success 2'), }, ], }, queryClient: new QueryClient(), }, }) expect(rendered.getByText('Status 1: pending')).toBeInTheDocument() expect(rendered.getByText('Status 2: pending')).toBeInTheDocument() await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('Status 1: success')).toBeInTheDocument() expect(rendered.getByText('Status 2: success')).toBeInTheDocument() }) test('Render and wait for success when queries resolve at different times', async () => { const rendered = render(BaseExample, { props: { options: { queries: [ { queryKey: ['key-1'], queryFn: () => sleep(10).then(() => 'Success 1'), }, { queryKey: ['key-2'], queryFn: () => sleep(20).then(() => 'Success 2'), }, ], }, queryClient: new QueryClient(), }, }) expect(rendered.getByText('Status 1: pending')).toBeInTheDocument() expect(rendered.getByText('Status 2: pending')).toBeInTheDocument() await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('Status 1: success')).toBeInTheDocument() await vi.advanceTimersByTimeAsync(10) expect(rendered.getByText('Status 2: success')).toBeInTheDocument() }) test('Combine queries', async () => { const rendered = render(CombineExample, { props: { queryClient: new QueryClient(), }, }) expect(rendered.getByText('isPending: true')).toBeInTheDocument() await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('Data: 1,2,3')).toBeInTheDocument() }) }) ================================================ FILE: packages/svelte-query/tests/createQuery/BaseExample.svelte ================================================
Status: {$query.status}
Failure Count: {$query.failureCount}
Data: {$query.data ?? 'undefined'}
================================================ FILE: packages/svelte-query/tests/createQuery/createQuery.test-d.ts ================================================ import { describe, expectTypeOf, test } from 'vitest' import { get } from 'svelte/store' import { createQuery, queryOptions } from '../../src/index.js' import type { OmitKeyof } from '@tanstack/query-core' import type { CreateQueryOptions } from '../../src/index.js' describe('createQuery', () => { test('TData should always be defined when initialData is provided as an object', () => { const query = createQuery({ queryKey: ['key'], queryFn: () => ({ wow: true }), initialData: { wow: true }, }) expectTypeOf(get(query).data).toEqualTypeOf<{ wow: boolean }>() }) test('TData should be defined when passed through queryOptions', () => { const options = queryOptions({ queryKey: ['key'], queryFn: () => ({ wow: true }), initialData: { wow: true }, }) const query = createQuery(options) expectTypeOf(get(query).data).toEqualTypeOf<{ wow: boolean }>() }) test('TData should always be defined when initialData is provided as a function which ALWAYS returns the data', () => { const query = createQuery({ queryKey: ['key'], queryFn: () => ({ wow: true }), initialData: () => ({ wow: true }), }) expectTypeOf(get(query).data).toEqualTypeOf<{ wow: boolean }>() }) test('TData should have undefined in the union when initialData is NOT provided', () => { const query = createQuery({ queryKey: ['key'], queryFn: () => { return { wow: true, } }, }) expectTypeOf(get(query).data).toEqualTypeOf<{ wow: boolean } | undefined>() }) test('Allow custom hooks using CreateQueryOptions', () => { type Data = string const useCustomQuery = ( options?: OmitKeyof, 'queryKey' | 'queryFn'>, ) => { return createQuery({ ...options, queryKey: ['todos-key'], queryFn: () => Promise.resolve('data'), }) } const query = useCustomQuery() expectTypeOf(get(query).data).toEqualTypeOf() }) }) ================================================ FILE: packages/svelte-query/tests/createQuery/createQuery.test.ts ================================================ import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest' import { fireEvent, render } from '@testing-library/svelte' import { derived, get, writable } from 'svelte/store' import { QueryClient } from '@tanstack/query-core' import { sleep } from '@tanstack/query-test-utils' import BaseExample from './BaseExample.svelte' import DisabledExample from './DisabledExample.svelte' import PlaceholderData from './PlaceholderData.svelte' import RefetchExample from './RefetchExample.svelte' import type { Writable } from 'svelte/store' import type { QueryObserverResult } from '@tanstack/query-core' describe('createQuery', () => { beforeEach(() => { vi.useFakeTimers() }) afterEach(() => { vi.useRealTimers() }) test('Return the correct states for a successful query', async () => { const statesStore: Writable> = writable([]) const options = { queryKey: ['test'], queryFn: () => sleep(10).then(() => 'Success'), } const rendered = render(BaseExample, { props: { options, queryClient: new QueryClient(), states: statesStore, }, }) await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('Status: success')).toBeInTheDocument() const states = get(statesStore) expect(states).toHaveLength(2) expect(states[0]).toMatchObject({ data: undefined, dataUpdatedAt: 0, error: null, errorUpdatedAt: 0, failureCount: 0, failureReason: null, errorUpdateCount: 0, isError: false, isFetched: false, isFetchedAfterMount: false, isFetching: true, isPaused: false, isPending: true, isInitialLoading: true, isLoading: true, isLoadingError: false, isPlaceholderData: false, isRefetchError: false, isRefetching: false, isStale: true, isSuccess: false, refetch: expect.any(Function), status: 'pending', fetchStatus: 'fetching', }) expect(states[1]).toMatchObject({ data: 'Success', dataUpdatedAt: expect.any(Number), error: null, errorUpdatedAt: 0, failureCount: 0, failureReason: null, errorUpdateCount: 0, isError: false, isFetched: true, isFetchedAfterMount: true, isFetching: false, isPaused: false, isPending: false, isInitialLoading: false, isLoading: false, isLoadingError: false, isPlaceholderData: false, isRefetchError: false, isRefetching: false, isStale: true, isSuccess: true, refetch: expect.any(Function), status: 'success', fetchStatus: 'idle', }) }) test('Return the correct states for an unsuccessful query', async () => { const statesStore: Writable> = writable([]) const options = { queryKey: ['test'], queryFn: () => sleep(10).then(() => Promise.reject(new Error('Rejected'))), retry: 1, retryDelay: 10, } const rendered = render(BaseExample, { props: { options, queryClient: new QueryClient(), states: statesStore, }, }) await vi.advanceTimersByTimeAsync(31) expect(rendered.getByText('Status: error')).toBeInTheDocument() const states = get(statesStore) expect(states).toHaveLength(3) expect(states[0]).toMatchObject({ data: undefined, dataUpdatedAt: 0, error: null, errorUpdatedAt: 0, failureCount: 0, failureReason: null, errorUpdateCount: 0, isError: false, isFetched: false, isFetchedAfterMount: false, isFetching: true, isPaused: false, isPending: true, isInitialLoading: true, isLoading: true, isLoadingError: false, isPlaceholderData: false, isRefetchError: false, isRefetching: false, isStale: true, isSuccess: false, refetch: expect.any(Function), status: 'pending', fetchStatus: 'fetching', }) expect(states[1]).toMatchObject({ data: undefined, dataUpdatedAt: 0, error: null, errorUpdatedAt: 0, failureCount: 1, failureReason: new Error('Rejected'), errorUpdateCount: 0, isError: false, isFetched: false, isFetchedAfterMount: false, isFetching: true, isPaused: false, isPending: true, isInitialLoading: true, isLoading: true, isLoadingError: false, isPlaceholderData: false, isRefetchError: false, isRefetching: false, isStale: true, isSuccess: false, refetch: expect.any(Function), status: 'pending', fetchStatus: 'fetching', }) expect(states[2]).toMatchObject({ data: undefined, dataUpdatedAt: 0, error: new Error('Rejected'), errorUpdatedAt: expect.any(Number), failureCount: 2, failureReason: new Error('Rejected'), errorUpdateCount: 1, isError: true, isFetched: true, isFetchedAfterMount: true, isFetching: false, isPaused: false, isPending: false, isInitialLoading: false, isLoading: false, isLoadingError: true, isPlaceholderData: false, isRefetchError: false, isRefetching: false, isStale: true, isSuccess: false, refetch: expect.any(Function), status: 'error', fetchStatus: 'idle', }) }) test('Accept a writable store for options', async () => { const statesStore: Writable> = writable([]) const optionsStore = writable({ queryKey: ['test'], queryFn: () => sleep(10).then(() => 'Success'), }) const rendered = render(BaseExample, { props: { options: optionsStore, queryClient: new QueryClient(), states: statesStore, }, }) await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('Status: success')).toBeInTheDocument() }) test('Accept a derived store for options', async () => { const statesStore: Writable> = writable([]) const writableStore = writable('test') const derivedStore = derived(writableStore, ($store) => ({ queryKey: [$store], queryFn: () => sleep(10).then(() => 'Success'), })) const rendered = render(BaseExample, { props: { options: derivedStore, queryClient: new QueryClient(), states: statesStore, }, }) await vi.advanceTimersByTimeAsync(11) expect(rendered.queryByText('Status: success')).toBeInTheDocument() }) test('Ensure reactivity when queryClient defaults are set', async () => { const statesStore: Writable> = writable([]) const writableStore = writable(1) const derivedStore = derived(writableStore, ($store) => ({ queryKey: [$store], queryFn: () => sleep(10).then(() => $store), })) const rendered = render(BaseExample, { props: { options: derivedStore, queryClient: new QueryClient({ defaultOptions: { queries: { staleTime: 60 * 1000 } }, }), states: statesStore, }, }) await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('Data: 1')).toBeInTheDocument() expect(rendered.queryByText('Data: 2')).not.toBeInTheDocument() writableStore.set(2) await vi.advanceTimersByTimeAsync(11) expect(rendered.queryByText('Data: 1')).not.toBeInTheDocument() expect(rendered.getByText('Data: 2')).toBeInTheDocument() writableStore.set(1) await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('Data: 1')).toBeInTheDocument() expect(rendered.queryByText('Data: 2')).not.toBeInTheDocument() }) test('Keep previous data when placeholderData is set', async () => { const statesStore: Writable> = writable([]) const rendered = render(PlaceholderData, { props: { queryClient: new QueryClient(), states: statesStore, }, }) await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('Data: 0')).toBeInTheDocument() fireEvent.click(rendered.getByRole('button', { name: 'setCount' })) await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('Data: 1')).toBeInTheDocument() const states = get(statesStore) expect(states).toHaveLength(4) // Initial expect(states[0]).toMatchObject({ data: undefined, isFetching: true, isSuccess: false, isPlaceholderData: false, }) // Fetched expect(states[1]).toMatchObject({ data: 0, isFetching: false, isSuccess: true, isPlaceholderData: false, }) // Set state expect(states[2]).toMatchObject({ data: 0, isFetching: true, isSuccess: true, isPlaceholderData: true, }) // New data expect(states[3]).toMatchObject({ data: 1, isFetching: false, isSuccess: true, isPlaceholderData: false, }) }) test('Should not fetch when switching to a disabled query', async () => { const statesStore: Writable> = writable([]) const rendered = render(DisabledExample, { props: { states: statesStore, }, }) await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('Data: 0')).toBeInTheDocument() fireEvent.click(rendered.getByRole('button', { name: /Increment/i })) await vi.advanceTimersByTimeAsync(0) expect(rendered.getByText('Count: 1')).toBeInTheDocument() expect(rendered.getByText('Data: undefined')).toBeInTheDocument() const states = get(statesStore) expect(states).toHaveLength(4) // Fetch query expect(states[0]).toMatchObject({ data: undefined, isFetching: true, isSuccess: false, }) // Fetched query expect(states[1]).toMatchObject({ data: 0, isFetching: false, isSuccess: true, }) // Switch to query disable expect(states[2]).toMatchObject({ data: undefined, isFetching: false, isSuccess: false, }) // Fetched disabled query expect(states[3]).toMatchObject({ data: undefined, isFetching: false, isSuccess: false, }) }) test('Create a new query when refetching a removed query', async () => { const statesStore: Writable> = writable([]) const rendered = render(RefetchExample, { props: { states: statesStore, }, }) await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('Data: 1')).toBeInTheDocument() fireEvent.click(rendered.getByRole('button', { name: /Remove/i })) fireEvent.click(rendered.getByRole('button', { name: /Refetch/i })) await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('Data: 2')).toBeInTheDocument() const states = get(statesStore) expect(states.length).toBe(4) // Initial expect(states[0]).toMatchObject({ data: undefined, dataUpdatedAt: 0 }) // Fetched expect(states[1]).toMatchObject({ data: 1 }) // Switch expect(states[2]).toMatchObject({ data: undefined, dataUpdatedAt: 0 }) // Fetched expect(states[3]).toMatchObject({ data: 2 }) }) }) ================================================ FILE: packages/svelte-query/tests/createQuery/DisabledExample.svelte ================================================
Data: {$query.data ?? 'undefined'}
Count: {$count}
================================================ FILE: packages/svelte-query/tests/createQuery/PlaceholderData.svelte ================================================
Status: {$query.status}
Data: {$query.data ?? 'undefined'}
================================================ FILE: packages/svelte-query/tests/createQuery/RefetchExample.svelte ================================================
Data: {$query.data ?? 'undefined'}
================================================ FILE: packages/svelte-query/tests/infiniteQueryOptions/infiniteQueryOptions.test-d.ts ================================================ import { describe, expectTypeOf, test } from 'vitest' import { get } from 'svelte/store' import { QueryClient } from '@tanstack/query-core' import { createInfiniteQuery, infiniteQueryOptions } from '../../src/index.js' import type { InfiniteData } from '@tanstack/query-core' describe('queryOptions', () => { test('Should not allow excess properties', () => { 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, }) }) test('Should infer types for callbacks', () => { infiniteQueryOptions({ queryKey: ['key'], queryFn: () => Promise.resolve('data'), staleTime: 1000, getNextPageParam: () => 1, initialPageParam: 1, select: (data) => { expectTypeOf(data).toEqualTypeOf>() }, }) }) test('Should work when passed to createInfiniteQuery', () => { const options = infiniteQueryOptions({ queryKey: ['key'], queryFn: () => Promise.resolve('string'), getNextPageParam: () => 1, initialPageParam: 1, }) const query = createInfiniteQuery(options) // known issue: type of pageParams is unknown when returned from useInfiniteQuery expectTypeOf(get(query).data).toEqualTypeOf< InfiniteData | undefined >() }) test('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>() }) }) ================================================ FILE: packages/svelte-query/tests/QueryClientProvider/ChildComponent.svelte ================================================
Data: {$query.data ?? 'undefined'}
================================================ FILE: packages/svelte-query/tests/QueryClientProvider/ParentComponent.svelte ================================================ ================================================ FILE: packages/svelte-query/tests/QueryClientProvider/QueryClientProvider.test.ts ================================================ import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest' import { render } from '@testing-library/svelte' import { QueryCache } from '@tanstack/query-core' import ParentComponent from './ParentComponent.svelte' describe('QueryClientProvider', () => { beforeEach(() => { vi.useFakeTimers() }) afterEach(() => { vi.useRealTimers() }) test('Sets a specific cache for all queries to use', async () => { const queryCache = new QueryCache() const rendered = render(ParentComponent, { props: { queryCache: queryCache, }, }) await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('Data: test')).toBeInTheDocument() expect(queryCache.find({ queryKey: ['hello'] })).toBeDefined() }) }) ================================================ FILE: packages/svelte-query/tests/queryOptions/queryOptions.test-d.ts ================================================ import { describe, expectTypeOf, test } from 'vitest' import { get } from 'svelte/store' import { QueriesObserver, QueryClient, dataTagSymbol, skipToken, } from '@tanstack/query-core' import { createQueries, queryOptions } from '../../src/index.js' import type { QueryObserverResult } from '@tanstack/query-core' describe('queryOptions', () => { test('Should not allow excess properties', () => { queryOptions({ queryKey: ['key'], queryFn: () => Promise.resolve(5), // @ts-expect-error this is a good error, because stallTime does not exist! stallTime: 1000, }) }) test('Should infer types for callbacks', () => { queryOptions({ queryKey: ['key'], queryFn: () => Promise.resolve(5), staleTime: 1000, select: (data) => { expectTypeOf(data).toEqualTypeOf() }, }) }) test('Should work when passed to fetchQuery', async () => { const options = queryOptions({ queryKey: ['key'], queryFn: () => Promise.resolve(5), }) const data = await new QueryClient().fetchQuery(options) expectTypeOf(data).toEqualTypeOf() }) test('Should work when passed to createQueries', () => { const options = queryOptions({ queryKey: ['key'], queryFn: () => Promise.resolve(5), }) const queries = createQueries({ queries: [options], }) expectTypeOf(get(queries)[0].data).toEqualTypeOf() }) test('Should tag the queryKey with the result type of the QueryFn', () => { const { queryKey } = queryOptions({ queryKey: ['key'], queryFn: () => Promise.resolve(5), }) expectTypeOf(queryKey[dataTagSymbol]).toEqualTypeOf() }) test('Should tag the queryKey even if no promise is returned', () => { const { queryKey } = queryOptions({ queryKey: ['key'], queryFn: () => 5, }) expectTypeOf(queryKey[dataTagSymbol]).toEqualTypeOf() }) test('Should tag the queryKey with unknown if there is no queryFn', () => { const { queryKey } = queryOptions({ queryKey: ['key'], }) expectTypeOf(queryKey[dataTagSymbol]).toEqualTypeOf() }) test('Should tag the queryKey with the result type of the QueryFn if select is used', () => { const { queryKey } = queryOptions({ queryKey: ['key'], queryFn: () => Promise.resolve(5), select: (data) => data.toString(), }) expectTypeOf(queryKey[dataTagSymbol]).toEqualTypeOf() }) test('Should return the proper type when passed to getQueryData', () => { const { queryKey } = queryOptions({ queryKey: ['key'], queryFn: () => Promise.resolve(5), }) const queryClient = new QueryClient() const data = queryClient.getQueryData(queryKey) expectTypeOf(data).toEqualTypeOf() }) test('Should return the proper type when passed to getQueryState', () => { const { queryKey } = queryOptions({ queryKey: ['key'], queryFn: () => Promise.resolve(5), }) const queryClient = new QueryClient() const state = queryClient.getQueryState(queryKey) expectTypeOf(state?.data).toEqualTypeOf() }) test('Should properly type updaterFn when passed to setQueryData', () => { const { queryKey } = queryOptions({ queryKey: ['key'], queryFn: () => Promise.resolve(5), }) const queryClient = new QueryClient() const data = queryClient.setQueryData(queryKey, (prev) => { expectTypeOf(prev).toEqualTypeOf() return prev }) expectTypeOf(data).toEqualTypeOf() }) test('Should properly type value when passed to setQueryData', () => { const { queryKey } = queryOptions({ queryKey: ['key'], queryFn: () => Promise.resolve(5), }) const queryClient = new QueryClient() // @ts-expect-error value should be a number queryClient.setQueryData(queryKey, '5') // @ts-expect-error value should be a number queryClient.setQueryData(queryKey, () => '5') const data = queryClient.setQueryData(queryKey, 5) expectTypeOf(data).toEqualTypeOf() }) test('Should infer even if there is a conditional skipToken', () => { const options = queryOptions({ queryKey: ['key'], queryFn: Math.random() > 0.5 ? skipToken : () => Promise.resolve(5), }) const queryClient = new QueryClient() const data = queryClient.getQueryData(options.queryKey) expectTypeOf(data).toEqualTypeOf() }) test('Should infer to unknown if we disable a query with just a skipToken', () => { const options = queryOptions({ queryKey: ['key'], queryFn: skipToken, }) const queryClient = new QueryClient() const data = queryClient.getQueryData(options.queryKey) expectTypeOf(data).toEqualTypeOf() }) test('Should return the proper type when passed to QueriesObserver', () => { const options = queryOptions({ queryKey: ['key'], queryFn: () => Promise.resolve(5), }) const queryClient = new QueryClient() const queriesObserver = new QueriesObserver(queryClient, [options]) expectTypeOf(queriesObserver).toEqualTypeOf< QueriesObserver> >() }) test('Should allow undefined response in initialData', () => { return (id: string | null) => queryOptions({ queryKey: ['todo', id], queryFn: () => Promise.resolve({ id: '1', title: 'Do Laundry', }), initialData: () => !id ? undefined : { id, title: 'Initial Data', }, }) }) }) ================================================ FILE: packages/svelte-query/tests/useIsFetching/BaseExample.svelte ================================================
isFetching: {$isFetching}
Data: {$query.data ?? 'undefined'}
================================================ FILE: packages/svelte-query/tests/useIsFetching/useIsFetching.test.ts ================================================ import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest' import { fireEvent, render } from '@testing-library/svelte' import BaseExample from './BaseExample.svelte' describe('useIsFetching', () => { beforeEach(() => { vi.useFakeTimers() }) afterEach(() => { vi.useRealTimers() }) test('should update as queries start and stop fetching', async () => { const rendered = render(BaseExample) expect(rendered.getByText('isFetching: 0')).toBeInTheDocument() fireEvent.click(rendered.getByRole('button', { name: /setReady/i })) await vi.advanceTimersByTimeAsync(0) expect(rendered.getByText('isFetching: 1')).toBeInTheDocument() await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('isFetching: 0')).toBeInTheDocument() }) }) ================================================ FILE: packages/svelte-query/tests/useIsMutating/BaseExample.svelte ================================================
isMutating: {$isMutating}
================================================ FILE: packages/svelte-query/tests/useIsMutating/useIsMutating.test.ts ================================================ import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest' import { fireEvent, render } from '@testing-library/svelte' import BaseExample from './BaseExample.svelte' describe('useIsMutating', () => { beforeEach(() => { vi.useFakeTimers() }) afterEach(() => { vi.useRealTimers() }) test('should update as queries start and stop mutating', async () => { const rendered = render(BaseExample) expect(rendered.getByText('isMutating: 0')).toBeInTheDocument() fireEvent.click(rendered.getByRole('button', { name: /Trigger/i })) await vi.advanceTimersByTimeAsync(0) expect(rendered.getByText('isMutating: 1')).toBeInTheDocument() await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('isMutating: 0')).toBeInTheDocument() }) }) ================================================ FILE: packages/svelte-query/tests/useMutationState/BaseExample.svelte ================================================
{JSON.stringify(statuses)}
================================================ FILE: packages/svelte-query/tests/useMutationState/useMutationState.test.ts ================================================ import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest' import { fireEvent, render } from '@testing-library/svelte' import { sleep } from '@tanstack/query-test-utils' import BaseExample from './BaseExample.svelte' describe('useMutationState', () => { beforeEach(() => { vi.useFakeTimers() }) afterEach(() => { vi.useRealTimers() }) test('Run few mutation functions and check from useMutationState', async () => { const successMutationFn = vi.fn(() => sleep(10).then(() => 'data')) const errorMutationFn = vi .fn() .mockImplementation(() => sleep(20).then(() => Promise.reject(new Error('error'))), ) const rendered = render(BaseExample, { props: { successMutationOpts: { mutationKey: ['success'], mutationFn: successMutationFn, }, errorMutationOpts: { mutationKey: ['error'], mutationFn: errorMutationFn, }, }, }) fireEvent.click(rendered.getByText('success')) await vi.advanceTimersByTimeAsync(11) expect(successMutationFn).toHaveBeenCalledTimes(1) expect(rendered.getByText('["success"]')).toBeInTheDocument() fireEvent.click(rendered.getByText('error')) await vi.advanceTimersByTimeAsync(21) expect(errorMutationFn).toHaveBeenCalledTimes(1) expect(rendered.getByText('["success","error"]')).toBeInTheDocument() }) test('Can select specific type of mutation ( i.e: error only )', async () => { const successMutationFn = vi.fn(() => sleep(10).then(() => 'data')) const errorMutationFn = vi .fn() .mockImplementation(() => sleep(20).then(() => Promise.reject(new Error('error'))), ) const rendered = render(BaseExample, { props: { successMutationOpts: { mutationKey: ['success'], mutationFn: successMutationFn, }, errorMutationOpts: { mutationKey: ['error'], mutationFn: errorMutationFn, }, mutationStateOpts: { filters: { status: 'error' }, }, }, }) fireEvent.click(rendered.getByText('success')) await vi.advanceTimersByTimeAsync(11) expect(successMutationFn).toHaveBeenCalledTimes(1) expect(rendered.getByText('[]')).toBeInTheDocument() fireEvent.click(rendered.getByText('error')) await vi.advanceTimersByTimeAsync(21) expect(errorMutationFn).toHaveBeenCalledTimes(1) expect(rendered.getByText('["error"]')).toBeInTheDocument() }) test('Can select specific mutation using mutation key', async () => { const successMutationFn = vi.fn(() => sleep(10).then(() => 'data')) const errorMutationFn = vi .fn() .mockImplementation(() => sleep(20).then(() => Promise.reject(new Error('error'))), ) const rendered = render(BaseExample, { props: { successMutationOpts: { mutationKey: ['success'], mutationFn: successMutationFn, }, errorMutationOpts: { mutationKey: ['error'], mutationFn: errorMutationFn, }, mutationStateOpts: { filters: { mutationKey: ['success'] }, }, }, }) fireEvent.click(rendered.getByText('success')) await vi.advanceTimersByTimeAsync(11) expect(successMutationFn).toHaveBeenCalledTimes(1) expect(rendered.getByText('["success"]')).toBeInTheDocument() fireEvent.click(rendered.getByText('error')) await vi.advanceTimersByTimeAsync(21) expect(errorMutationFn).toHaveBeenCalledTimes(1) expect(rendered.getByText('["success"]')).toBeInTheDocument() }) }) ================================================ FILE: packages/svelte-query-devtools/eslint.config.js ================================================ // @ts-check import pluginSvelte from 'eslint-plugin-svelte' import rootConfig from './root.eslint.config.js' import svelteConfig from './svelte.config.js' export default [ ...rootConfig, ...pluginSvelte.configs['flat/recommended'], { files: ['**/*.svelte'], languageOptions: { parserOptions: { parser: '@typescript-eslint/parser', svelteConfig, }, }, }, { rules: { 'svelte/block-lang': ['error', { script: ['ts'] }], 'svelte/no-svelte-internal': 'error', 'svelte/valid-compile': 'off', }, }, ] ================================================ FILE: packages/svelte-query-devtools/package.json ================================================ { "name": "@tanstack/svelte-query-devtools", "version": "5.89.0", "description": "Developer tools to interact with and visualize the TanStack/svelte-query cache", "author": "Lachlan Collins", "license": "MIT", "repository": { "type": "git", "url": "https://github.com/TanStack/query.git", "directory": "packages/svelte-query-devtools" }, "homepage": "https://tanstack.com/query", "funding": { "type": "github", "url": "https://github.com/sponsors/tannerlinsley" }, "scripts": { "clean": "premove ./dist ./coverage ./.svelte-kit ./dist-ts", "compile": "tsc --build", "test:types": "svelte-check --tsconfig ./tsconfig.json", "test:eslint": "eslint --concurrency=auto ./src", "test:build": "publint --strict && attw --pack", "build": "svelte-package --input ./src --output ./dist" }, "type": "module", "types": "dist/index.d.ts", "module": "dist/index.js", "svelte": "./dist/index.js", "exports": { ".": { "@tanstack/custom-condition": "./src/index.ts", "types": "./dist/index.d.ts", "svelte": "./dist/index.js", "import": "./dist/index.js" }, "./package.json": "./package.json" }, "files": [ "dist", "src", "!src/__tests__" ], "dependencies": { "@tanstack/query-devtools": "workspace:*", "esm-env": "^1.2.1" }, "devDependencies": { "@sveltejs/package": "^2.4.0", "@sveltejs/vite-plugin-svelte": "^5.1.1", "@tanstack/svelte-query": "workspace:*", "eslint-plugin-svelte": "^3.11.0", "svelte": "^5.39.3", "svelte-check": "^4.3.1" }, "peerDependencies": { "@tanstack/svelte-query": "workspace:^", "svelte": "^3.54.0 || ^4.0.0 || ^5.0.0" } } ================================================ FILE: packages/svelte-query-devtools/svelte.config.js ================================================ import { vitePreprocess } from '@sveltejs/vite-plugin-svelte' const config = { preprocess: vitePreprocess(), } export default config ================================================ FILE: packages/svelte-query-devtools/tsconfig.json ================================================ { "extends": "../../tsconfig.json", "compilerOptions": { "outDir": "./dist-ts", "rootDir": "." }, "include": ["src", "*.config.*", "package.json"], "references": [{ "path": "../query-devtools" }, { "path": "../svelte-query" }] } ================================================ FILE: packages/svelte-query-devtools/vite.config.ts ================================================ import { svelte } from '@sveltejs/vite-plugin-svelte' import { defineConfig } from 'vite' export default defineConfig({ plugins: [svelte()], // fix from https://github.com/vitest-dev/vitest/issues/6992#issuecomment-2509408660 resolve: { conditions: ['@tanstack/custom-condition'], }, environments: { ssr: { resolve: { conditions: ['@tanstack/custom-condition'], }, }, }, }) ================================================ FILE: packages/svelte-query-devtools/.attw.json ================================================ { "ignoreRules": ["cjs-resolves-to-esm", "internal-resolution-error"] } ================================================ SYMLINK: packages/svelte-query-devtools/root.eslint.config.js -> eslint.config.js ================================================ ================================================ FILE: packages/svelte-query-devtools/src/Devtools.svelte ================================================
================================================ FILE: packages/svelte-query-devtools/src/index.ts ================================================ export { default as SvelteQueryDevtools } from './Devtools.svelte' ================================================ FILE: packages/svelte-query-persist-client/eslint.config.js ================================================ // @ts-check import pluginSvelte from 'eslint-plugin-svelte' import rootConfig from './root.eslint.config.js' import svelteConfig from './svelte.config.js' export default [ ...rootConfig, ...pluginSvelte.configs['flat/recommended'], { files: ['**/*.svelte'], languageOptions: { parserOptions: { parser: '@typescript-eslint/parser', svelteConfig, }, }, }, { rules: { 'svelte/block-lang': ['error', { script: ['ts'] }], 'svelte/no-svelte-internal': 'error', 'svelte/valid-compile': 'off', }, }, ] ================================================ FILE: packages/svelte-query-persist-client/package.json ================================================ { "name": "@tanstack/svelte-query-persist-client", "version": "5.89.0", "description": "Svelte bindings to work with persisters in TanStack/svelte-query", "author": "Lachlan Collins", "license": "MIT", "repository": { "type": "git", "url": "https://github.com/TanStack/query.git", "directory": "packages/svelte-query-persist-client" }, "homepage": "https://tanstack.com/query", "funding": { "type": "github", "url": "https://github.com/sponsors/tannerlinsley" }, "scripts": { "clean": "premove ./dist ./coverage ./.svelte-kit ./dist-ts", "compile": "tsc --build", "test:types": "svelte-check --tsconfig ./tsconfig.json", "test:eslint": "eslint --concurrency=auto ./src", "test:lib": "vitest", "test:lib:dev": "pnpm run test:lib --watch", "test:build": "publint --strict && attw --pack", "build": "svelte-package --input ./src --output ./dist" }, "type": "module", "types": "dist/index.d.ts", "module": "dist/index.js", "svelte": "./dist/index.js", "exports": { ".": { "@tanstack/custom-condition": "./src/index.ts", "types": "./dist/index.d.ts", "svelte": "./dist/index.js", "import": "./dist/index.js" }, "./package.json": "./package.json" }, "sideEffects": false, "files": [ "dist", "src", "!src/__tests__" ], "dependencies": { "@tanstack/query-persist-client-core": "workspace:*" }, "devDependencies": { "@sveltejs/package": "^2.4.0", "@sveltejs/vite-plugin-svelte": "^5.1.1", "@tanstack/query-test-utils": "workspace:*", "@tanstack/svelte-query": "workspace:*", "@testing-library/svelte": "^5.2.8", "eslint-plugin-svelte": "^3.11.0", "svelte": "^5.39.3", "svelte-check": "^4.3.1" }, "peerDependencies": { "@tanstack/svelte-query": "workspace:^", "svelte": "^3.54.0 || ^4.0.0 || ^5.0.0" } } ================================================ FILE: packages/svelte-query-persist-client/svelte.config.js ================================================ import { vitePreprocess } from '@sveltejs/vite-plugin-svelte' const config = { preprocess: vitePreprocess(), } export default config ================================================ FILE: packages/svelte-query-persist-client/tsconfig.json ================================================ { "extends": "../../tsconfig.json", "compilerOptions": { "outDir": "./dist-ts", "rootDir": "." }, "include": ["src", "tests", "*.config.*", "package.json"], "references": [ { "path": "../query-persist-client-core" }, { "path": "../svelte-query" } ] } ================================================ FILE: packages/svelte-query-persist-client/vite.config.ts ================================================ import { svelte } from '@sveltejs/vite-plugin-svelte' import { defineConfig } from 'vitest/config' import { svelteTesting } from '@testing-library/svelte/vite' import packageJson from './package.json' export default defineConfig({ plugins: [svelte(), svelteTesting()], // 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: './tests', watch: false, environment: 'jsdom', setupFiles: ['./tests/test-setup.ts'], coverage: { enabled: true, provider: 'istanbul', include: ['src/**/*'] }, typecheck: { enabled: true }, restoreMocks: true, }, }) ================================================ FILE: packages/svelte-query-persist-client/.attw.json ================================================ { "ignoreRules": ["cjs-resolves-to-esm", "internal-resolution-error"] } ================================================ SYMLINK: packages/svelte-query-persist-client/root.eslint.config.js -> eslint.config.js ================================================ ================================================ FILE: packages/svelte-query-persist-client/src/index.ts ================================================ // Re-export core export * from '@tanstack/query-persist-client-core' export { default as PersistQueryClientProvider } from './PersistQueryClientProvider.svelte' ================================================ FILE: packages/svelte-query-persist-client/src/PersistQueryClientProvider.svelte ================================================ ================================================ FILE: packages/svelte-query-persist-client/tests/PersistQueryClientProvider.test.ts ================================================ import { render, waitFor } from '@testing-library/svelte' import { describe, expect, test, vi } from 'vitest' import { persistQueryClientSave } from '@tanstack/query-persist-client-core' import { get, writable } from 'svelte/store' import { sleep } from '@tanstack/query-test-utils' import { QueryClient } from '@tanstack/svelte-query' import AwaitOnSuccess from './AwaitOnSuccess/Provider.svelte' import FreshData from './FreshData/Provider.svelte' import OnSuccess from './OnSuccess/Provider.svelte' import InitialData from './InitialData/Provider.svelte' import RemoveCache from './RemoveCache/Provider.svelte' import RestoreCache from './RestoreCache/Provider.svelte' import UseQueries from './UseQueries/Provider.svelte' import type { PersistedClient, Persister, } from '@tanstack/query-persist-client-core' import type { Writable } from 'svelte/store' import type { StatusResult } from './utils.js' const createMockPersister = (): Persister => { let storedState: PersistedClient | undefined return { persistClient(persistClient: PersistedClient) { storedState = persistClient }, async restoreClient() { await sleep(5) return storedState }, removeClient() { storedState = undefined }, } } const createMockErrorPersister = ( removeClient: Persister['removeClient'], ): [Error, Persister] => { const error = new Error('restore failed') return [ error, { async persistClient() { // noop }, async restoreClient() { await sleep(5) throw error }, removeClient, }, ] } describe('PersistQueryClientProvider', () => { test('restores cache from persister', async () => { const statesStore: Writable>> = writable([]) const queryClient = new QueryClient() await queryClient.prefetchQuery({ queryKey: ['test'], queryFn: () => Promise.resolve('hydrated'), }) const persister = createMockPersister() await persistQueryClientSave({ queryClient, persister }) queryClient.clear() const rendered = render(RestoreCache, { props: { queryClient, persistOptions: { persister }, states: statesStore, }, }) await waitFor(() => rendered.getByText('fetchStatus: idle')) await waitFor(() => rendered.getByText('hydrated')) await waitFor(() => rendered.getByText('fetched')) const states = get(statesStore) expect(states).toHaveLength(5) expect(states[0]).toMatchObject({ status: 'pending', fetchStatus: 'idle', data: undefined, }) expect(states[1]).toMatchObject({ status: 'success', fetchStatus: 'fetching', data: 'hydrated', }) expect(states[2]).toMatchObject({ status: 'success', fetchStatus: 'fetching', data: 'hydrated', }) expect(states[3]).toMatchObject({ status: 'success', fetchStatus: 'fetching', data: 'hydrated', }) expect(states[4]).toMatchObject({ status: 'success', fetchStatus: 'idle', data: 'fetched', }) }) test('should also put useQueries into idle state', async () => { const statesStore: Writable>> = writable([]) const queryClient = new QueryClient() await queryClient.prefetchQuery({ queryKey: ['test'], queryFn: () => Promise.resolve('hydrated'), }) const persister = createMockPersister() await persistQueryClientSave({ queryClient, persister }) queryClient.clear() const rendered = render(UseQueries, { props: { queryClient, persistOptions: { persister }, states: statesStore, }, }) await waitFor(() => rendered.getByText('fetchStatus: idle')) await waitFor(() => rendered.getByText('hydrated')) await waitFor(() => rendered.getByText('fetched')) const states = get(statesStore) expect(states).toHaveLength(5) expect(states[0]).toMatchObject({ status: 'pending', fetchStatus: 'idle', data: undefined, }) expect(states[1]).toMatchObject({ status: 'success', fetchStatus: 'fetching', data: 'hydrated', }) expect(states[2]).toMatchObject({ status: 'success', fetchStatus: 'fetching', data: 'hydrated', }) expect(states[3]).toMatchObject({ status: 'success', fetchStatus: 'fetching', data: 'hydrated', }) expect(states[4]).toMatchObject({ status: 'success', fetchStatus: 'idle', data: 'fetched', }) }) test('should show initialData while restoring', async () => { const statesStore: Writable>> = writable([]) const queryClient = new QueryClient() await queryClient.prefetchQuery({ queryKey: ['test'], queryFn: () => Promise.resolve('hydrated'), }) const persister = createMockPersister() await persistQueryClientSave({ queryClient, persister }) queryClient.clear() const rendered = render(InitialData, { props: { queryClient, persistOptions: { persister }, states: statesStore, }, }) await waitFor(() => rendered.getByText('initial')) await waitFor(() => rendered.getByText('hydrated')) await waitFor(() => rendered.getByText('fetched')) const states = get(statesStore) expect(states).toHaveLength(5) expect(states[0]).toMatchObject({ status: 'success', fetchStatus: 'idle', data: 'initial', }) expect(states[1]).toMatchObject({ status: 'success', fetchStatus: 'fetching', data: 'hydrated', }) expect(states[2]).toMatchObject({ status: 'success', fetchStatus: 'fetching', data: 'hydrated', }) expect(states[3]).toMatchObject({ status: 'success', fetchStatus: 'fetching', data: 'hydrated', }) expect(states[4]).toMatchObject({ status: 'success', fetchStatus: 'idle', data: 'fetched', }) }) test('should not refetch after restoring when data is fresh', async () => { const statesStore: Writable>> = writable([]) const queryClient = new QueryClient() await queryClient.prefetchQuery({ queryKey: ['test'], queryFn: () => Promise.resolve('hydrated'), }) const persister = createMockPersister() await persistQueryClientSave({ queryClient, persister }) queryClient.clear() const fetched = writable(false) const rendered = render(FreshData, { props: { queryClient, persistOptions: { persister }, states: statesStore, fetched, }, }) await waitFor(() => rendered.getByText('data: undefined')) await waitFor(() => rendered.getByText('data: hydrated')) const states = get(statesStore) expect(states).toHaveLength(2) expect(get(fetched)).toBe(false) expect(states[0]).toMatchObject({ status: 'pending', fetchStatus: 'idle', data: undefined, }) expect(states[1]).toMatchObject({ status: 'success', fetchStatus: 'idle', data: 'hydrated', }) }) test('should call onSuccess after successful restoring', async () => { const queryClient = new QueryClient() await queryClient.prefetchQuery({ queryKey: ['test'], queryFn: () => Promise.resolve('hydrated'), }) const persister = createMockPersister() await persistQueryClientSave({ queryClient, persister }) queryClient.clear() const onSuccess = vi.fn() const rendered = render(OnSuccess, { props: { queryClient, persistOptions: { persister }, onSuccess, }, }) expect(onSuccess).toHaveBeenCalledTimes(0) await waitFor(() => rendered.getByText('hydrated')) expect(onSuccess).toHaveBeenCalledTimes(1) await waitFor(() => rendered.getByText('fetched')) }) test('should await onSuccess after successful restoring', async () => { const queryClient = new QueryClient() await queryClient.prefetchQuery({ queryKey: ['test'], queryFn: () => Promise.resolve('hydrated'), }) const persister = createMockPersister() await persistQueryClientSave({ queryClient, persister }) queryClient.clear() const statesStore: Writable> = writable([]) const rendered = render(AwaitOnSuccess, { props: { queryClient, persistOptions: { persister }, states: statesStore, onSuccess: async () => { statesStore.update((s) => [...s, 'onSuccess']) await sleep(20) statesStore.update((s) => [...s, 'onSuccess done']) }, }, }) await waitFor(() => rendered.getByText('hydrated')) await waitFor(() => rendered.getByText('fetched')) const states = get(statesStore) expect(states).toEqual([ 'onSuccess', 'onSuccess done', 'fetching', 'fetched', ]) }) test('should remove cache after non-successful restoring', async () => { const consoleMock = vi.spyOn(console, 'error') const consoleWarn = vi .spyOn(console, 'warn') .mockImplementation(() => undefined) consoleMock.mockImplementation(() => undefined) const queryClient = new QueryClient() const removeClient = vi.fn() const onSuccess = vi.fn() const onError = vi.fn() const [error, persister] = createMockErrorPersister(removeClient) const rendered = render(RemoveCache, { props: { queryClient, persistOptions: { persister }, onError, onSuccess }, }) await waitFor(() => rendered.getByText('fetched')) expect(removeClient).toHaveBeenCalledTimes(1) expect(onSuccess).toHaveBeenCalledTimes(0) expect(onError).toHaveBeenCalledTimes(1) expect(consoleMock).toHaveBeenCalledTimes(1) expect(consoleMock).toHaveBeenNthCalledWith(1, error) consoleMock.mockRestore() consoleWarn.mockRestore() }) }) ================================================ FILE: packages/svelte-query-persist-client/tests/test-setup.ts ================================================ import '@testing-library/jest-dom/vitest' ================================================ FILE: packages/svelte-query-persist-client/tests/utils.ts ================================================ export type StatusResult = { status: string fetchStatus: string data: T | undefined } ================================================ FILE: packages/svelte-query-persist-client/tests/AwaitOnSuccess/AwaitOnSuccess.svelte ================================================
{$query.data}
fetchStatus: {$query.fetchStatus}
================================================ FILE: packages/svelte-query-persist-client/tests/AwaitOnSuccess/Provider.svelte ================================================ ================================================ FILE: packages/svelte-query-persist-client/tests/FreshData/FreshData.svelte ================================================
data: {$query.data ?? 'undefined'}
fetchStatus: {$query.fetchStatus}
================================================ FILE: packages/svelte-query-persist-client/tests/FreshData/Provider.svelte ================================================ ================================================ FILE: packages/svelte-query-persist-client/tests/InitialData/InitialData.svelte ================================================
{$query.data}
fetchStatus: {$query.fetchStatus}
================================================ FILE: packages/svelte-query-persist-client/tests/InitialData/Provider.svelte ================================================ ================================================ FILE: packages/svelte-query-persist-client/tests/OnSuccess/OnSuccess.svelte ================================================
{$query.data}
fetchStatus: {$query.fetchStatus}
================================================ FILE: packages/svelte-query-persist-client/tests/OnSuccess/Provider.svelte ================================================ ================================================ FILE: packages/svelte-query-persist-client/tests/RemoveCache/Provider.svelte ================================================ ================================================ FILE: packages/svelte-query-persist-client/tests/RemoveCache/RemoveCache.svelte ================================================
{$query.data}
fetchStatus: {$query.fetchStatus}
================================================ FILE: packages/svelte-query-persist-client/tests/RestoreCache/Provider.svelte ================================================ ================================================ FILE: packages/svelte-query-persist-client/tests/RestoreCache/RestoreCache.svelte ================================================
{$query.data}
fetchStatus: {$query.fetchStatus}
================================================ FILE: packages/svelte-query-persist-client/tests/UseQueries/Provider.svelte ================================================ ================================================ FILE: packages/svelte-query-persist-client/tests/UseQueries/UseQueries.svelte ================================================
{$queries[0].data}
fetchStatus: {$queries[0].fetchStatus}
================================================ FILE: packages/vue-query/README.md ================================================ [![Vue Query logo](https://raw.githubusercontent.com/TanStack/query/main/packages/vue-query/media/vue-query.png)](https://github.com/TanStack/query/tree/main/packages/vue-query) [![npm version](https://img.shields.io/npm/v/@tanstack/vue-query)](https://www.npmjs.com/package/@tanstack/vue-query) [![npm license](https://img.shields.io/npm/l/@tanstack/vue-query)](https://github.com/TanStack/query/blob/main/LICENSE) [![bundle size](https://img.shields.io/bundlephobia/minzip/@tanstack/vue-query)](https://bundlephobia.com/package/@tanstack/vue-query) [![npm](https://img.shields.io/npm/dm/@tanstack/vue-query)](https://www.npmjs.com/package/@tanstack/vue-query) # Vue Query Hooks for fetching, caching and updating asynchronous data in Vue. Support for Vue 2.x via [vue-demi](https://github.com/vueuse/vue-demi) # Documentation Visit https://tanstack.com/query/latest/docs/vue/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 - (experimental) [Suspense](https://v3.vuejs.org/guide/migration/suspense.html#introduction) + Fetch-As-You-Render Query Prefetching - (experimental) SSR support - Dedicated Devtools - [![npm bundle size](https://img.shields.io/bundlephobia/minzip/@tanstack/vue-query)](https://bundlephobia.com/package/@tanstack/vue-query) (depending on features imported) # Quick Start 1. Install `vue-query` ```bash $ npm i @tanstack/vue-query ``` or ```bash $ pnpm add @tanstack/vue-query ``` or ```bash $ yarn add @tanstack/vue-query ``` or ```bash $ bun add @tanstack/vue-query ``` > If you are using Vue 2.6, make sure to also setup [@vue/composition-api](https://github.com/vuejs/composition-api) 2. Initialize **Vue Query** via **VueQueryPlugin** ```tsx import { createApp } from 'vue' import { VueQueryPlugin } from '@tanstack/vue-query' import App from './App.vue' createApp(App).use(VueQueryPlugin).mount('#app') ``` 3. Use query ```tsx import { defineComponent } from 'vue' import { useQuery } from '@tanstack/vue-query' export default defineComponent({ name: 'MyComponent', setup() { const query = useQuery({ queryKey: ['todos'], queryFn: getTodos }) return { query, } }, }) ``` 4. If you need to update options on your query dynamically, make sure to pass them as reactive variables ```tsx const id = ref(1) const enabled = ref(false) const query = useQuery({ queryKey: ['todos', id], queryFn: () => getTodos(id), enabled, }) ``` ================================================ FILE: packages/vue-query/eslint.config.js ================================================ // @ts-check // @ts-expect-error import pluginVue from 'eslint-plugin-vue' import rootConfig from './root.eslint.config.js' export default [...rootConfig, ...pluginVue.configs['flat/base']] ================================================ FILE: packages/vue-query/package.json ================================================ { "name": "@tanstack/vue-query", "version": "5.89.0", "description": "Hooks for managing, caching and syncing asynchronous and remote data in Vue", "author": "Damian Osipiuk", "license": "MIT", "repository": { "type": "git", "url": "https://github.com/TanStack/query.git", "directory": "packages/vue-query" }, "homepage": "https://tanstack.com/query", "funding": { "type": "github", "url": "https://github.com/sponsors/tannerlinsley" }, "scripts": { "clean": "premove ./build ./coverage ./dist-ts", "compile": "tsc --build", "test:eslint": "eslint --concurrency=auto ./src", "test:types": "vue-demi-switch 3 && pnpm run \"/^test:types:ts[0-9]{2}$/\"", "test:types:ts50": "node ../../node_modules/typescript50/lib/tsc.js --build tsconfig.legacy.json", "test:types:ts51": "node ../../node_modules/typescript51/lib/tsc.js --build tsconfig.legacy.json", "test:types:ts52": "node ../../node_modules/typescript52/lib/tsc.js --build tsconfig.legacy.json", "test:types:ts53": "node ../../node_modules/typescript53/lib/tsc.js --build tsconfig.legacy.json", "test:types:ts54": "node ../../node_modules/typescript54/lib/tsc.js --build tsconfig.legacy.json", "test:types:ts55": "node ../../node_modules/typescript55/lib/tsc.js --build tsconfig.legacy.json", "test:types:ts56": "node ../../node_modules/typescript56/lib/tsc.js --build tsconfig.legacy.json", "test:types:ts57": "node ../../node_modules/typescript57/lib/tsc.js --build tsconfig.legacy.json", "test:types:tscurrent": "tsc --build", "test:lib": "pnpm run test:lib:2 && pnpm run test:lib:2.7 && pnpm run test:lib:3", "test:lib:2": "vue-demi-switch 2 vue2 && vitest", "test:lib:2.7": "vue-demi-switch 2.7 vue2.7 && vitest", "test:lib:3": "vue-demi-switch 3 && vitest", "test:lib:dev": "pnpm run test:lib --watch", "test:build": "publint --strict && attw --pack", "build": "tsup --tsconfig tsconfig.prod.json" }, "type": "module", "types": "build/legacy/index.d.ts", "main": "build/legacy/index.cjs", "module": "build/legacy/index.js", "exports": { ".": { "@tanstack/custom-condition": "./src/index.ts", "import": { "types": "./build/modern/index.d.ts", "default": "./build/modern/index.js" }, "require": { "types": "./build/modern/index.d.cts", "default": "./build/modern/index.cjs" } }, "./package.json": "./package.json" }, "sideEffects": false, "files": [ "build", "src", "!src/__tests__" ], "dependencies": { "@tanstack/match-sorter-utils": "^8.19.4", "@tanstack/query-core": "workspace:*", "@vue/devtools-api": "^6.6.3", "vue-demi": "^0.14.10" }, "devDependencies": { "@tanstack/query-test-utils": "workspace:*", "@vitejs/plugin-vue": "^5.2.4", "@vue/composition-api": "1.7.2", "eslint-plugin-vue": "^9.27.0", "vue": "^3.4.27", "vue2": "npm:vue@2.6", "vue2.7": "npm:vue@2.7" }, "peerDependencies": { "@vue/composition-api": "^1.1.2", "vue": "^2.6.0 || ^3.3.0" }, "peerDependenciesMeta": { "@vue/composition-api": { "optional": true } } } ================================================ FILE: packages/vue-query/test-setup.ts ================================================ import { vi } from 'vitest' vi.mock('vue-demi', async () => { const vue = await vi.importActual('vue-demi') return { ...(vue as any), inject: vi.fn(), provide: vi.fn(), onScopeDispose: vi.fn(), getCurrentInstance: vi.fn(() => ({ proxy: {} })), hasInjectionContext: vi.fn(() => true), } }) ================================================ FILE: packages/vue-query/tsconfig.json ================================================ { "extends": "../../tsconfig.json", "compilerOptions": { "outDir": "./dist-ts", "rootDir": "." }, "include": ["src", "test-setup.ts", "*.config.*", "package.json"], "references": [{ "path": "../query-core" }] } ================================================ FILE: packages/vue-query/tsconfig.legacy.json ================================================ { "extends": "./tsconfig.json", "compilerOptions": { "outDir": "./dist-ts/legacy" }, "include": ["src"], "exclude": ["src/__mocks__", "src/__tests__"], "references": [{ "path": "../query-core" }] } ================================================ FILE: packages/vue-query/tsconfig.prod.json ================================================ { "extends": "./tsconfig.json", "compilerOptions": { "incremental": false, "composite": false, "rootDir": "../../" } } ================================================ FILE: packages/vue-query/tsup.config.ts ================================================ import { defineConfig } from 'tsup' import { legacyConfig, modernConfig } from './root.tsup.config.js' export default defineConfig([ modernConfig({ entry: ['src/*.ts', 'src/devtools/*.ts'] }), legacyConfig({ entry: ['src/*.ts', 'src/devtools/*.ts'] }), ]) ================================================ FILE: packages/vue-query/vite.config.ts ================================================ import { defineConfig } from 'vitest/config' import vue from '@vitejs/plugin-vue' import packageJson from './package.json' export default defineConfig({ plugins: [vue()], // 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 }, onConsoleLog: function (log) { if (log.includes('Download the Vue Devtools extension')) { return false } return undefined }, }, }) ================================================ SYMLINK: packages/vue-query/root.eslint.config.js -> eslint.config.js ================================================ ================================================ SYMLINK: packages/vue-query/root.tsup.config.js -> getTsupConfig.js ================================================ ================================================ FILE: packages/vue-query/src/index.ts ================================================ export * from '@tanstack/query-core' export { useQueryClient } from './useQueryClient' export { VueQueryPlugin } from './vueQueryPlugin' export { QueryClient } from './queryClient' export { QueryCache } from './queryCache' export { queryOptions } from './queryOptions' export { infiniteQueryOptions } from './infiniteQueryOptions' export type { DefinedInitialDataInfiniteOptions, UndefinedInitialDataInfiniteOptions, } from './infiniteQueryOptions' export { MutationCache } from './mutationCache' export { useQuery } from './useQuery' export { useQueries } from './useQueries' export { useInfiniteQuery } from './useInfiniteQuery' export { useMutation } from './useMutation' export { useIsFetching } from './useIsFetching' export { useIsMutating, useMutationState } from './useMutationState' export { VUE_QUERY_CLIENT } from './utils' export type { UseQueryOptions, UseQueryReturnType, UseQueryDefinedReturnType, UndefinedInitialQueryOptions, DefinedInitialQueryOptions, } from './useQuery' export type { UseInfiniteQueryOptions, UseInfiniteQueryReturnType, } from './useInfiniteQuery' export type { UseMutationOptions, UseMutationReturnType } from './useMutation' export type { UseQueriesOptions, UseQueriesResults } from './useQueries' export type { MutationFilters, MutationStateOptions } from './useMutationState' export type { QueryFilters } from './useIsFetching' export type { VueQueryPluginOptions } from './vueQueryPlugin' ================================================ FILE: packages/vue-query/src/infiniteQueryOptions.ts ================================================ import type { DataTag, DefaultError, InfiniteData, NonUndefinedGuard, QueryKey, } from '@tanstack/query-core' import type { UseInfiniteQueryOptions } from './useInfiniteQuery' export type UndefinedInitialDataInfiniteOptions< TQueryFnData, TError = DefaultError, TData = InfiniteData, TQueryKey extends QueryKey = QueryKey, TPageParam = unknown, > = UseInfiniteQueryOptions< TQueryFnData, TError, TData, TQueryKey, TPageParam > & { initialData?: undefined } export type DefinedInitialDataInfiniteOptions< TQueryFnData, TError = DefaultError, TData = InfiniteData, TQueryKey extends QueryKey = QueryKey, TPageParam = unknown, > = UseInfiniteQueryOptions< TQueryFnData, TError, TData, TQueryKey, TPageParam > & { initialData: | NonUndefinedGuard> | (() => NonUndefinedGuard>) } 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> } 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> } export function infiniteQueryOptions(options: unknown) { return options } ================================================ FILE: packages/vue-query/src/mutationCache.ts ================================================ import { MutationCache as MC } from '@tanstack/query-core' import { cloneDeepUnref } from './utils' import type { DefaultError, Mutation, MutationFilters, } from '@tanstack/query-core' import type { MaybeRefDeep } from './types' export class MutationCache extends MC { find< TData = unknown, TError = DefaultError, TVariables = any, TOnMutateResult = unknown, >( filters: MaybeRefDeep, ): Mutation | undefined { return super.find(cloneDeepUnref(filters)) } findAll(filters: MaybeRefDeep = {}): Array { return super.findAll(cloneDeepUnref(filters)) } } ================================================ FILE: packages/vue-query/src/queryCache.ts ================================================ import { QueryCache as QC } from '@tanstack/query-core' import { cloneDeepUnref } from './utils' import type { DefaultError, Query, QueryFilters, WithRequired, } from '@tanstack/query-core' import type { MaybeRefDeep } from './types' export class QueryCache extends QC { find( filters: MaybeRefDeep>, ): Query | undefined { return super.find(cloneDeepUnref(filters)) } findAll(filters: MaybeRefDeep = {}): Array { return super.findAll(cloneDeepUnref(filters)) } } ================================================ FILE: packages/vue-query/src/queryClient.ts ================================================ import { nextTick, ref } from 'vue-demi' import { QueryClient as QC } from '@tanstack/query-core' import { cloneDeepUnref } from './utils' import { QueryCache } from './queryCache' import { MutationCache } from './mutationCache' import type { UseQueryOptions } from './useQuery' import type { Ref } from 'vue-demi' import type { MaybeRefDeep, NoUnknown, QueryClientConfig } from './types' import type { CancelOptions, DefaultError, DefaultOptions, EnsureQueryDataOptions, FetchInfiniteQueryOptions, FetchQueryOptions, InferDataFromTag, InferErrorFromTag, InfiniteData, InvalidateOptions, InvalidateQueryFilters, MutationFilters, MutationKey, MutationObserverOptions, NoInfer, OmitKeyof, QueryFilters, QueryKey, QueryObserverOptions, QueryState, RefetchOptions, RefetchQueryFilters, ResetOptions, SetDataOptions, Updater, } from '@tanstack/query-core' export class QueryClient extends QC { constructor(config: QueryClientConfig = {}) { const vueQueryConfig = { defaultOptions: config.defaultOptions, queryCache: config.queryCache || new QueryCache(), mutationCache: config.mutationCache || new MutationCache(), } super(vueQueryConfig) } isRestoring?: Ref = ref(false) isFetching(filters: MaybeRefDeep = {}): number { return super.isFetching(cloneDeepUnref(filters)) } isMutating(filters: MaybeRefDeep = {}): number { return super.isMutating(cloneDeepUnref(filters)) } getQueryData( queryKey: TTaggedQueryKey, ): InferDataFromTag | undefined getQueryData( queryKey: MaybeRefDeep, ): TData | undefined getQueryData( queryKey: MaybeRefDeep, ): TData | undefined { return super.getQueryData(cloneDeepUnref(queryKey)) } ensureQueryData< TQueryFnData, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, >( options: EnsureQueryDataOptions, ): Promise ensureQueryData< TQueryFnData, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, >( options: MaybeRefDeep< EnsureQueryDataOptions >, ): Promise ensureQueryData< TQueryFnData, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, >( options: MaybeRefDeep< EnsureQueryDataOptions >, ): Promise { return super.ensureQueryData(cloneDeepUnref(options)) } getQueriesData( filters: MaybeRefDeep, ): Array<[QueryKey, TData | undefined]> { return super.getQueriesData(cloneDeepUnref(filters)) } setQueryData< TQueryFnData = unknown, TTaggedQueryKey extends QueryKey = QueryKey, TInferredQueryFnData = InferDataFromTag, >( queryKey: TTaggedQueryKey, updater: Updater< NoInfer | undefined, NoInfer | undefined >, options?: MaybeRefDeep, ): NoInfer | undefined setQueryData>( queryKey: MaybeRefDeep, updater: Updater | undefined, NoInfer | undefined>, options?: MaybeRefDeep, ): NoInfer | undefined setQueryData( queryKey: MaybeRefDeep, updater: Updater, options: MaybeRefDeep = {}, ): NoInfer | undefined { return super.setQueryData( cloneDeepUnref(queryKey), updater, cloneDeepUnref(options), ) } setQueriesData( filters: MaybeRefDeep, updater: Updater, options: MaybeRefDeep = {}, ): Array<[QueryKey, TData | undefined]> { return super.setQueriesData( cloneDeepUnref(filters), updater, cloneDeepUnref(options), ) } getQueryState( queryKey: MaybeRefDeep, ): QueryState | undefined { return super.getQueryState(cloneDeepUnref(queryKey)) } removeQueries< TQueryFnData = unknown, TError = DefaultError, TTaggedQueryKey extends QueryKey = QueryKey, TInferredQueryFnData = InferDataFromTag, TInferredError = InferErrorFromTag, >(filters?: QueryFilters): void removeQueries(filters: MaybeRefDeep = {}): void { return super.removeQueries(cloneDeepUnref(filters)) } resetQueries< TQueryFnData = unknown, TError = DefaultError, TTaggedQueryKey extends QueryKey = QueryKey, TInferredQueryFnData = InferDataFromTag, TInferredError = InferErrorFromTag, >( filters?: QueryFilters, options?: MaybeRefDeep, ): Promise resetQueries( filters: MaybeRefDeep = {}, options: MaybeRefDeep = {}, ): Promise { return super.resetQueries(cloneDeepUnref(filters), cloneDeepUnref(options)) } cancelQueries< TQueryFnData = unknown, TError = DefaultError, TTaggedQueryKey extends QueryKey = QueryKey, TInferredQueryFnData = InferDataFromTag, TInferredError = InferErrorFromTag, >( filters?: QueryFilters, options?: MaybeRefDeep, ): Promise cancelQueries( filters: MaybeRefDeep = {}, options: MaybeRefDeep = {}, ): Promise { return super.cancelQueries(cloneDeepUnref(filters), cloneDeepUnref(options)) } invalidateQueries< TQueryFnData = unknown, TError = DefaultError, TTaggedQueryKey extends QueryKey = QueryKey, TInferredQueryFnData = InferDataFromTag, TInferredError = InferErrorFromTag, >( filters?: InvalidateQueryFilters, options?: MaybeRefDeep, ): Promise invalidateQueries( filters: MaybeRefDeep> = {}, options: MaybeRefDeep = {}, ): Promise { const filtersCloned = cloneDeepUnref(filters) const optionsCloned = cloneDeepUnref(options) super.invalidateQueries( { ...filtersCloned, refetchType: 'none' }, optionsCloned, ) if (filtersCloned.refetchType === 'none') { return Promise.resolve() } const refetchFilters: RefetchQueryFilters = { ...filtersCloned, type: filtersCloned.refetchType ?? filtersCloned.type ?? 'active', } // (dosipiuk): We need to delay `refetchQueries` execution to next macro task for all reactive values to be updated. // This ensures that `context` in `queryFn` while `invalidating` along reactive variable change has correct return nextTick().then(() => { return super.refetchQueries(refetchFilters, optionsCloned) }) } refetchQueries< TQueryFnData = unknown, TError = DefaultError, TTaggedQueryKey extends QueryKey = QueryKey, TInferredQueryFnData = InferDataFromTag, TInferredError = InferErrorFromTag, >( filters?: RefetchQueryFilters, options?: MaybeRefDeep, ): Promise refetchQueries( filters: MaybeRefDeep = {}, options: MaybeRefDeep = {}, ): Promise { return super.refetchQueries( cloneDeepUnref(filters), cloneDeepUnref(options), ) } fetchQuery< TQueryFnData, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, TPageParam = never, >( options: FetchQueryOptions< TQueryFnData, TError, TData, TQueryKey, TPageParam >, ): Promise fetchQuery< TQueryFnData, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, TPageParam = never, >( options: MaybeRefDeep< FetchQueryOptions >, ): Promise fetchQuery< TQueryFnData, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, TPageParam = never, >( options: MaybeRefDeep< FetchQueryOptions >, ): Promise { return super.fetchQuery(cloneDeepUnref(options)) } prefetchQuery< TQueryFnData = unknown, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, >( options: FetchQueryOptions, ): Promise prefetchQuery< TQueryFnData = unknown, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, >( options: MaybeRefDeep< FetchQueryOptions >, ): Promise prefetchQuery< TQueryFnData = unknown, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, >( options: MaybeRefDeep< FetchQueryOptions >, ): Promise { return super.prefetchQuery(cloneDeepUnref(options)) } fetchInfiniteQuery< TQueryFnData = unknown, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, TPageParam = unknown, >( options: FetchInfiniteQueryOptions< TQueryFnData, TError, TData, TQueryKey, TPageParam >, ): Promise> fetchInfiniteQuery< TQueryFnData, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, TPageParam = unknown, >( options: MaybeRefDeep< FetchInfiniteQueryOptions< TQueryFnData, TError, TData, TQueryKey, TPageParam > >, ): Promise> fetchInfiniteQuery< TQueryFnData, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, TPageParam = unknown, >( options: MaybeRefDeep< FetchInfiniteQueryOptions< TQueryFnData, TError, TData, TQueryKey, TPageParam > >, ): Promise> { return super.fetchInfiniteQuery(cloneDeepUnref(options)) } prefetchInfiniteQuery< TQueryFnData, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, TPageParam = unknown, >( options: FetchInfiniteQueryOptions< TQueryFnData, TError, TData, TQueryKey, TPageParam >, ): Promise prefetchInfiniteQuery< TQueryFnData, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, TPageParam = unknown, >( options: MaybeRefDeep< FetchInfiniteQueryOptions< TQueryFnData, TError, TData, TQueryKey, TPageParam > >, ): Promise prefetchInfiniteQuery< TQueryFnData, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, TPageParam = unknown, >( options: MaybeRefDeep< FetchInfiniteQueryOptions< TQueryFnData, TError, TData, TQueryKey, TPageParam > >, ): Promise { return super.prefetchInfiniteQuery(cloneDeepUnref(options)) } setDefaultOptions(options: MaybeRefDeep): void { super.setDefaultOptions(cloneDeepUnref(options)) } setQueryDefaults< TQueryFnData = unknown, TError = DefaultError, TData = TQueryFnData, TQueryData = TQueryFnData, >( queryKey: MaybeRefDeep, options: MaybeRefDeep< Omit, 'queryKey'> >, ): void { super.setQueryDefaults(cloneDeepUnref(queryKey), cloneDeepUnref(options)) } getQueryDefaults( queryKey: MaybeRefDeep, ): OmitKeyof, 'queryKey'> { return super.getQueryDefaults(cloneDeepUnref(queryKey)) } setMutationDefaults< TData = unknown, TError = DefaultError, TVariables = void, TOnMutateResult = unknown, >( mutationKey: MaybeRefDeep, options: MaybeRefDeep< MutationObserverOptions >, ): void { super.setMutationDefaults( cloneDeepUnref(mutationKey), cloneDeepUnref(options), ) } getMutationDefaults( mutationKey: MaybeRefDeep, ): MutationObserverOptions { return super.getMutationDefaults(cloneDeepUnref(mutationKey)) } } ================================================ FILE: packages/vue-query/src/queryOptions.ts ================================================ import type { DataTag, DefaultError, QueryKey } from '@tanstack/query-core' import type { DefinedInitialQueryOptions, UndefinedInitialQueryOptions, } from './useQuery' export function queryOptions< TQueryFnData = unknown, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, >( options: DefinedInitialQueryOptions, ): DefinedInitialQueryOptions & { queryKey: DataTag } export function queryOptions< TQueryFnData = unknown, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, >( options: UndefinedInitialQueryOptions, ): UndefinedInitialQueryOptions & { queryKey: DataTag } export function queryOptions(options: unknown) { return options } ================================================ FILE: packages/vue-query/src/types.ts ================================================ import type { DefaultError, DehydrateOptions, HydrateOptions, MutationCache, MutationObserverOptions, OmitKeyof, QueryCache, QueryObserverOptions, } from '@tanstack/query-core' import type { ComputedRef, Ref, UnwrapRef } from 'vue-demi' type Primitive = string | number | boolean | bigint | symbol | undefined | null type UnwrapLeaf = | Primitive | Function | Date | Error | RegExp | Map | WeakMap | Set | WeakSet export type MaybeRef = Ref | ComputedRef | T export type MaybeRefOrGetter = MaybeRef | (() => T) export type MaybeRefDeep = MaybeRef< T extends Function ? T : T extends object ? { [Property in keyof T]: MaybeRefDeep } : T > export type NoUnknown = Equal extends true ? never : T export type Equal = (() => T extends TTargetA ? 1 : 2) extends () => T extends TTargetB ? 1 : 2 ? true : false export type DeepUnwrapRef = T extends UnwrapLeaf ? T : T extends Ref ? DeepUnwrapRef : T extends {} ? { [Property in keyof T]: DeepUnwrapRef } : UnwrapRef export type ShallowOption = { /** * Return data in a shallow ref object (it is `false` by default). It can be set to `true` to return data in a shallow ref object, which can improve performance if your data does not need to be deeply reactive. */ shallow?: boolean } export interface DefaultOptions { queries?: OmitKeyof, 'queryKey'> & ShallowOption mutations?: MutationObserverOptions & ShallowOption hydrate?: HydrateOptions['defaultOptions'] dehydrate?: DehydrateOptions } export interface QueryClientConfig { queryCache?: QueryCache mutationCache?: MutationCache defaultOptions?: DefaultOptions } ================================================ FILE: packages/vue-query/src/useBaseQuery.ts ================================================ import { computed, getCurrentScope, onScopeDispose, reactive, readonly, shallowReactive, shallowReadonly, toRefs, watch, } from 'vue-demi' import { shouldThrowError } from '@tanstack/query-core' import { useQueryClient } from './useQueryClient' import { cloneDeepUnref, updateState } from './utils' import type { Ref } from 'vue-demi' import type { DefaultedQueryObserverOptions, QueryKey, QueryObserver, QueryObserverResult, } from '@tanstack/query-core' import type { QueryClient } from './queryClient' import type { UseQueryOptions } from './useQuery' import type { UseInfiniteQueryOptions } from './useInfiniteQuery' export type UseBaseQueryReturnType< TData, TError, TResult = QueryObserverResult, > = { [K in keyof TResult]: K extends | 'fetchNextPage' | 'fetchPreviousPage' | 'refetch' ? TResult[K] : Ref[K]> } & { suspense: () => Promise } type UseQueryOptionsGeneric< TQueryFnData, TError, TData, TQueryData, TQueryKey extends QueryKey = QueryKey, TPageParam = unknown, > = | UseQueryOptions | UseInfiniteQueryOptions export function useBaseQuery< TQueryFnData, TError, TData, TQueryData, TQueryKey extends QueryKey, TPageParam, >( Observer: typeof QueryObserver, options: UseQueryOptionsGeneric< TQueryFnData, TError, TData, TQueryData, TQueryKey, TPageParam >, queryClient?: QueryClient, ): UseBaseQueryReturnType { if (process.env.NODE_ENV === 'development') { if (!getCurrentScope()) { console.warn( 'vue-query composable like "useQuery()" should only be used inside a "setup()" function or a running effect scope. They might otherwise lead to memory leaks.', ) } } const client = queryClient || useQueryClient() const defaultedOptions = computed(() => { const clonedOptions = cloneDeepUnref(options as any) if (typeof clonedOptions.enabled === 'function') { clonedOptions.enabled = clonedOptions.enabled() } const defaulted: DefaultedQueryObserverOptions< TQueryFnData, TError, TData, TQueryData, TQueryKey > = client.defaultQueryOptions(clonedOptions) defaulted._optimisticResults = client.isRestoring?.value ? 'isRestoring' : 'optimistic' return defaulted }) const observer = new Observer(client, defaultedOptions.value) // @ts-expect-error const state = defaultedOptions.value.shallow ? shallowReactive(observer.getCurrentResult()) : reactive(observer.getCurrentResult()) let unsubscribe = () => { // noop } if (client.isRestoring) { watch( client.isRestoring, (isRestoring) => { if (!isRestoring) { unsubscribe() unsubscribe = observer.subscribe((result) => { updateState(state, result) }) } }, { immediate: true }, ) } const updater = () => { observer.setOptions(defaultedOptions.value) updateState(state, observer.getCurrentResult()) } watch(defaultedOptions, updater) onScopeDispose(() => { unsubscribe() }) // fix #5910 const refetch = (...args: Parameters<(typeof state)['refetch']>) => { updater() return state.refetch(...args) } const suspense = () => { return new Promise>( (resolve, reject) => { let stopWatch = () => { // noop } const run = () => { if (defaultedOptions.value.enabled !== false) { // fix #6133 observer.setOptions(defaultedOptions.value) const optimisticResult = observer.getOptimisticResult( defaultedOptions.value, ) if (optimisticResult.isStale) { stopWatch() observer .fetchOptimistic(defaultedOptions.value) .then(resolve, (error: TError) => { if ( shouldThrowError(defaultedOptions.value.throwOnError, [ error, observer.getCurrentQuery(), ]) ) { reject(error) } else { resolve(observer.getCurrentResult()) } }) } else { stopWatch() resolve(optimisticResult) } } } run() stopWatch = watch(defaultedOptions, run) }, ) } // Handle error boundary watch( () => state.error, (error) => { if ( state.isError && !state.isFetching && shouldThrowError(defaultedOptions.value.throwOnError, [ error as TError, observer.getCurrentQuery(), ]) ) { throw error } }, ) // @ts-expect-error const readonlyState = defaultedOptions.value.shallow ? shallowReadonly(state) : readonly(state) const object: any = toRefs(readonlyState) for (const key in state) { if (typeof state[key as keyof typeof state] === 'function') { object[key] = state[key as keyof typeof state] } } object.suspense = suspense object.refetch = refetch return object as UseBaseQueryReturnType } ================================================ FILE: packages/vue-query/src/useInfiniteQuery.ts ================================================ import { InfiniteQueryObserver } from '@tanstack/query-core' import { useBaseQuery } from './useBaseQuery' import type { DefinedInitialDataInfiniteOptions, UndefinedInitialDataInfiniteOptions, } from './infiniteQueryOptions' import type { DefaultError, InfiniteData, InfiniteQueryObserverOptions, InfiniteQueryObserverResult, QueryKey, QueryObserver, } from '@tanstack/query-core' import type { UseBaseQueryReturnType } from './useBaseQuery' import type { DeepUnwrapRef, MaybeRef, MaybeRefDeep, MaybeRefOrGetter, ShallowOption, } from './types' import type { QueryClient } from './queryClient' export type UseInfiniteQueryOptions< TQueryFnData = unknown, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, TPageParam = unknown, > = MaybeRef< { [Property in keyof InfiniteQueryObserverOptions< TQueryFnData, TError, TData, TQueryKey, TPageParam >]: Property extends 'enabled' ? MaybeRefOrGetter< InfiniteQueryObserverOptions< TQueryFnData, TError, TData, DeepUnwrapRef >[Property] > : MaybeRefDeep< InfiniteQueryObserverOptions< TQueryFnData, TError, TData, DeepUnwrapRef, TPageParam >[Property] > } & ShallowOption > export type UseInfiniteQueryReturnType = UseBaseQueryReturnType< TData, TError, InfiniteQueryObserverResult > export function useInfiniteQuery< TQueryFnData, TError = DefaultError, TData = InfiniteData, TQueryKey extends QueryKey = QueryKey, TPageParam = unknown, >( options: DefinedInitialDataInfiniteOptions< TQueryFnData, TError, TData, TQueryKey, TPageParam >, queryClient?: QueryClient, ): UseInfiniteQueryReturnType export function useInfiniteQuery< TQueryFnData, TError = DefaultError, TData = InfiniteData, TQueryKey extends QueryKey = QueryKey, TPageParam = unknown, >( options: UndefinedInitialDataInfiniteOptions< TQueryFnData, TError, TData, TQueryKey, TPageParam >, queryClient?: QueryClient, ): UseInfiniteQueryReturnType export function useInfiniteQuery< TQueryFnData, TError = DefaultError, TData = InfiniteData, TQueryKey extends QueryKey = QueryKey, TPageParam = unknown, >( options: UseInfiniteQueryOptions< TQueryFnData, TError, TData, TQueryKey, TPageParam >, queryClient?: QueryClient, ): UseInfiniteQueryReturnType export function useInfiniteQuery( options: UseInfiniteQueryOptions, queryClient?: QueryClient, ) { return useBaseQuery( InfiniteQueryObserver as typeof QueryObserver, options, queryClient, ) } ================================================ FILE: packages/vue-query/src/useIsFetching.ts ================================================ import { getCurrentScope, onScopeDispose, ref, watchEffect } from 'vue-demi' import { useQueryClient } from './useQueryClient' import type { Ref } from 'vue-demi' import type { QueryFilters as QF } from '@tanstack/query-core' import type { MaybeRefDeep } from './types' import type { QueryClient } from './queryClient' export type QueryFilters = MaybeRefDeep export function useIsFetching( fetchingFilters: MaybeRefDeep = {}, queryClient?: QueryClient, ): Ref { if (process.env.NODE_ENV === 'development') { if (!getCurrentScope()) { console.warn( 'vue-query composable like "useQuery()" should only be used inside a "setup()" function or a running effect scope. They might otherwise lead to memory leaks.', ) } } const client = queryClient || useQueryClient() const isFetching = ref() const listener = () => { isFetching.value = client.isFetching(fetchingFilters) } const unsubscribe = client.getQueryCache().subscribe(listener) watchEffect(listener) onScopeDispose(() => { unsubscribe() }) return isFetching } ================================================ FILE: packages/vue-query/src/useMutation.ts ================================================ import { computed, getCurrentScope, onScopeDispose, reactive, readonly, shallowReactive, shallowReadonly, toRefs, watch, } from 'vue-demi' import { MutationObserver, shouldThrowError } from '@tanstack/query-core' import { cloneDeepUnref, updateState } from './utils' import { useQueryClient } from './useQueryClient' import type { ToRefs } from 'vue-demi' import type { DefaultError, DistributiveOmit, MutateFunction, MutateOptions, MutationObserverOptions, MutationObserverResult, } from '@tanstack/query-core' import type { MaybeRefDeep, ShallowOption } from './types' import type { QueryClient } from './queryClient' type MutationResult = DistributiveOmit< MutationObserverResult, 'mutate' | 'reset' > type UseMutationOptionsBase = MutationObserverOptions & ShallowOption export type UseMutationOptions< TData = unknown, TError = DefaultError, TVariables = void, TOnMutateResult = unknown, > = MaybeRefDeep< UseMutationOptionsBase > type MutateSyncFunction< TData = unknown, TError = DefaultError, TVariables = void, TOnMutateResult = unknown, > = ( ...options: Parameters< MutateFunction > ) => void export type UseMutationReturnType< TData, TError, TVariables, TOnMutateResult, TResult = MutationResult, > = ToRefs> & { mutate: MutateSyncFunction mutateAsync: MutateFunction reset: MutationObserverResult< TData, TError, TVariables, TOnMutateResult >['reset'] } export function useMutation< TData = unknown, TError = DefaultError, TVariables = void, TOnMutateResult = unknown, >( mutationOptions: MaybeRefDeep< UseMutationOptionsBase >, queryClient?: QueryClient, ): UseMutationReturnType { if (process.env.NODE_ENV === 'development') { if (!getCurrentScope()) { console.warn( 'vue-query composable like "useQuery()" should only be used inside a "setup()" function or a running effect scope. They might otherwise lead to memory leaks.', ) } } const client = queryClient || useQueryClient() const options = computed(() => { return client.defaultMutationOptions(cloneDeepUnref(mutationOptions)) }) const observer = new MutationObserver(client, options.value) const state = options.value.shallow ? shallowReactive(observer.getCurrentResult()) : reactive(observer.getCurrentResult()) const unsubscribe = observer.subscribe((result) => { updateState(state, result) }) const mutate = ( variables: TVariables, mutateOptions?: MutateOptions, ) => { observer.mutate(variables, mutateOptions).catch(() => { // This is intentional }) } watch(options, () => { observer.setOptions(options.value) }) onScopeDispose(() => { unsubscribe() }) const readonlyState = options.value.shallow ? shallowReadonly(state) : readonly(state) const resultRefs = toRefs(readonlyState) as ToRefs< Readonly> > watch( () => state.error, (error) => { if ( error && shouldThrowError(options.value.throwOnError, [error as TError]) ) { throw error } }, ) return { ...resultRefs, mutate, mutateAsync: state.mutate, reset: state.reset, } } ================================================ FILE: packages/vue-query/src/useMutationState.ts ================================================ import { computed, getCurrentScope, onScopeDispose, shallowReadonly, shallowRef, watch, } from 'vue-demi' import { useQueryClient } from './useQueryClient' import { cloneDeepUnref } from './utils' import type { Ref } from 'vue-demi' import type { MutationFilters as MF, Mutation, MutationState, } from '@tanstack/query-core' import type { QueryClient } from './queryClient' import type { MaybeRefDeep } from './types' import type { MutationCache } from './mutationCache' export type MutationFilters = MaybeRefDeep export function useIsMutating( filters: MutationFilters = {}, queryClient?: QueryClient, ): Ref { if (process.env.NODE_ENV === 'development') { if (!getCurrentScope()) { console.warn( 'vue-query composable like "useQuery()" should only be used inside a "setup()" function or a running effect scope. They might otherwise lead to memory leaks.', ) } } const client = queryClient || useQueryClient() const mutationState = useMutationState( { filters: computed(() => ({ ...cloneDeepUnref(filters), status: 'pending' as const, })), }, client, ) const length = computed(() => mutationState.value.length) return length } export type MutationStateOptions = { filters?: MutationFilters select?: (mutation: Mutation) => TResult } 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 function useMutationState( options: MutationStateOptions = {}, queryClient?: QueryClient, ): Readonly>> { const filters = computed(() => cloneDeepUnref(options.filters)) const mutationCache = (queryClient || useQueryClient()).getMutationCache() const state = shallowRef(getResult(mutationCache, options)) const unsubscribe = mutationCache.subscribe(() => { state.value = getResult(mutationCache, options) }) watch(filters, () => { state.value = getResult(mutationCache, options) }) onScopeDispose(() => { unsubscribe() }) return shallowReadonly(state) } ================================================ FILE: packages/vue-query/src/useQueries.ts ================================================ import { QueriesObserver } from '@tanstack/query-core' import { computed, getCurrentScope, onScopeDispose, readonly, shallowReadonly, shallowRef, unref, watch, } from 'vue-demi' import { useQueryClient } from './useQueryClient' import { cloneDeepUnref } from './utils' import type { Ref } from 'vue-demi' import type { DefaultError, DefinedQueryObserverResult, QueriesObserverOptions, QueryFunction, QueryKey, QueryObserverResult, ThrowOnError, } from '@tanstack/query-core' import type { UseQueryOptions } from './useQuery' import type { QueryClient } from './queryClient' import type { DeepUnwrapRef, MaybeRefDeep, ShallowOption } from './types' // This defines the `UseQueryOptions` that are accepted in `QueriesOptions` & `GetOptions`. // `placeholderData` function does not have a parameter type UseQueryOptionsForUseQueries< TQueryFnData = unknown, TError = unknown, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, > = UseQueryOptions // 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 SkipTokenForUseQueries = symbol type GetUseQueryOptionsForUseQueries = // Part 1: if UseQueryOptions are already being sent through, then just return T T extends UseQueryOptions ? DeepUnwrapRef : // Part 2: 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 } ? UseQueryOptionsForUseQueries : T extends { queryFnData: infer TQueryFnData; error?: infer TError } ? UseQueryOptionsForUseQueries : T extends { data: infer TData; error?: infer TError } ? UseQueryOptionsForUseQueries : // Part 3: responsible for applying explicit type parameter to function arguments, if tuple [TQueryFnData, TError, TData] T extends [infer TQueryFnData, infer TError, infer TData] ? UseQueryOptionsForUseQueries : T extends [infer TQueryFnData, infer TError] ? UseQueryOptionsForUseQueries : T extends [infer TQueryFnData] ? UseQueryOptionsForUseQueries : // Part 4: responsible for inferring and enforcing type if no explicit parameter was provided T extends { queryFn?: | QueryFunction | SkipTokenForUseQueries select?: (data: any) => infer TData throwOnError?: ThrowOnError } ? UseQueryOptionsForUseQueries< TQueryFnData, unknown extends TError ? DefaultError : TError, unknown extends TData ? TQueryFnData : TData, TQueryKey > : T extends { queryFn?: | QueryFunction | SkipTokenForUseQueries throwOnError?: ThrowOnError } ? UseQueryOptionsForUseQueries< TQueryFnData, TError, TQueryFnData, TQueryKey > : // Fallback UseQueryOptionsForUseQueries // A defined initialData setting should return a DefinedQueryObserverResult rather than QueryObserverResult type GetDefinedOrUndefinedQueryResult = T extends { initialData?: infer TInitialData } ? unknown extends TInitialData ? QueryObserverResult : TInitialData extends TData ? DefinedQueryObserverResult : TInitialData extends () => infer TInitialDataResult ? unknown extends TInitialDataResult ? QueryObserverResult : TInitialDataResult extends TData ? DefinedQueryObserverResult : QueryObserverResult : QueryObserverResult : QueryObserverResult type GetUseQueryResult = // Part 1: if using UseQueryOptions then the types are already set T extends UseQueryOptions< infer TQueryFnData, infer TError, infer TData, any, any > ? GetDefinedOrUndefinedQueryResult< T, undefined extends TData ? TQueryFnData : TData, unknown extends TError ? DefaultError : TError > : // Part 2: 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 3: 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 4: responsible for mapping inferred type to results, if no explicit parameter was provided T extends { queryFn?: | QueryFunction | SkipTokenForUseQueries select?: (data: any) => infer TData throwOnError?: ThrowOnError } ? GetDefinedOrUndefinedQueryResult< T, unknown extends TData ? TQueryFnData : TData, unknown extends TError ? DefaultError : TError > : T extends { queryFn?: | QueryFunction | SkipTokenForUseQueries throwOnError?: ThrowOnError } ? GetDefinedOrUndefinedQueryResult< T, TQueryFnData, unknown extends TError ? DefaultError : TError > : // Fallback QueryObserverResult /** * UseQueriesOptions reducer recursively unwraps function arguments to infer/enforce type param */ export type UseQueriesOptions< T extends Array, TResults extends Array = [], TDepth extends ReadonlyArray = [], > = TDepth['length'] extends MAXIMUM_DEPTH ? Array : T extends [] ? [] : T extends [infer Head] ? [...TResults, GetUseQueryOptionsForUseQueries] : T extends [infer Head, ...infer Tails] ? UseQueriesOptions< [...Tails], [...TResults, GetUseQueryOptionsForUseQueries], [...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< UseQueryOptionsForUseQueries< infer TQueryFnData, infer TError, infer TData, infer TQueryKey > > ? Array< UseQueryOptionsForUseQueries< TQueryFnData, TError, TData, TQueryKey > > : // Fallback Array /** * UseQueriesResults reducer recursively maps type param to results */ export type UseQueriesResults< T extends Array, TResults extends Array = [], TDepth extends ReadonlyArray = [], > = TDepth['length'] extends MAXIMUM_DEPTH ? Array : T extends [] ? [] : T extends [infer Head] ? [...TResults, GetUseQueryResult] : T extends [infer Head, ...infer Tails] ? UseQueriesResults< [...Tails], [...TResults, GetUseQueryResult], [...TDepth, 1] > : { [K in keyof T]: GetUseQueryResult } type UseQueriesOptionsArg> = readonly [ ...UseQueriesOptions, ] export function useQueries< T extends Array, TCombinedResult = UseQueriesResults, >( { queries, ...options }: ShallowOption & { queries: | MaybeRefDeep> | MaybeRefDeep< readonly [ ...{ [K in keyof T]: GetUseQueryOptionsForUseQueries }, ] > combine?: (result: UseQueriesResults) => TCombinedResult }, queryClient?: QueryClient, ): Readonly> { if (process.env.NODE_ENV === 'development') { if (!getCurrentScope()) { console.warn( 'vue-query composable like "useQuery()" should only be used inside a "setup()" function or a running effect scope. They might otherwise lead to memory leaks.', ) } } const client = queryClient || useQueryClient() const defaultedQueries = computed(() => { // Only unref the top level array. const queriesRaw = unref(queries) as ReadonlyArray // Unref the rest for each element in the top level array. return queriesRaw.map((queryOptions) => { const clonedOptions = cloneDeepUnref(queryOptions) if (typeof clonedOptions.enabled === 'function') { clonedOptions.enabled = queryOptions.enabled() } const defaulted = client.defaultQueryOptions(clonedOptions) defaulted._optimisticResults = client.isRestoring?.value ? 'isRestoring' : 'optimistic' return defaulted }) }) const observer = new QueriesObserver( client, defaultedQueries.value, options as QueriesObserverOptions, ) const getOptimisticResult = () => { const [results, getCombinedResult] = observer.getOptimisticResult( defaultedQueries.value, (options as QueriesObserverOptions).combine, ) return getCombinedResult( results.map((result, index) => { return { ...result, refetch: async (...args: Array) => { const [{ [index]: query }] = observer.getOptimisticResult( defaultedQueries.value, (options as QueriesObserverOptions).combine, ) return query!.refetch(...args) }, } }), ) } const state = shallowRef(getOptimisticResult()) let unsubscribe = () => { // noop } if (client.isRestoring) { watch( client.isRestoring, (isRestoring) => { if (!isRestoring) { unsubscribe() unsubscribe = observer.subscribe(() => { state.value = getOptimisticResult() }) state.value = getOptimisticResult() } }, { immediate: true }, ) } watch(defaultedQueries, (queriesValue) => { observer.setQueries( queriesValue, options as QueriesObserverOptions, ) state.value = getOptimisticResult() }) onScopeDispose(() => { unsubscribe() }) return options.shallow ? shallowReadonly(state) : (readonly(state) as Readonly>) } ================================================ FILE: packages/vue-query/src/useQuery.ts ================================================ import { QueryObserver } from '@tanstack/query-core' import { useBaseQuery } from './useBaseQuery' import type { DefaultError, DefinedQueryObserverResult, Enabled, InitialDataFunction, NonUndefinedGuard, QueryKey, QueryObserverOptions, } from '@tanstack/query-core' import type { UseBaseQueryReturnType } from './useBaseQuery' import type { DeepUnwrapRef, MaybeRef, MaybeRefDeep, MaybeRefOrGetter, ShallowOption, } from './types' import type { QueryClient } from './queryClient' export type UseQueryOptions< TQueryFnData = unknown, TError = DefaultError, TData = TQueryFnData, TQueryData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, > = MaybeRef< { [Property in keyof QueryObserverOptions< TQueryFnData, TError, TData, TQueryData, TQueryKey >]: Property extends 'enabled' ? | MaybeRefOrGetter | (() => Enabled< TQueryFnData, TError, TQueryData, DeepUnwrapRef >) : MaybeRefDeep< QueryObserverOptions< TQueryFnData, TError, TData, TQueryData, DeepUnwrapRef >[Property] > } & ShallowOption > export type UndefinedInitialQueryOptions< TQueryFnData = unknown, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, > = UseQueryOptions & { initialData?: | undefined | InitialDataFunction> | NonUndefinedGuard } export type DefinedInitialQueryOptions< TQueryFnData = unknown, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, > = UseQueryOptions & { initialData: | NonUndefinedGuard | (() => NonUndefinedGuard) } export type UseQueryReturnType = UseBaseQueryReturnType< TData, TError > export type UseQueryDefinedReturnType = UseBaseQueryReturnType< TData, TError, DefinedQueryObserverResult > export function useQuery< TQueryFnData = unknown, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, >( options: DefinedInitialQueryOptions, queryClient?: QueryClient, ): UseQueryDefinedReturnType export function useQuery< TQueryFnData = unknown, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, >( options: UndefinedInitialQueryOptions, queryClient?: QueryClient, ): UseQueryReturnType export function useQuery< TQueryFnData = unknown, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, >( options: UseQueryOptions< TQueryFnData, TError, TData, TQueryFnData, TQueryKey >, queryClient?: QueryClient, ): UseQueryReturnType export function useQuery< TQueryFnData, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, >( options: UseQueryOptions< TQueryFnData, TError, TData, TQueryFnData, TQueryKey >, queryClient?: QueryClient, ): | UseQueryReturnType | UseQueryDefinedReturnType { return useBaseQuery(QueryObserver, options, queryClient) } ================================================ FILE: packages/vue-query/src/useQueryClient.ts ================================================ import { hasInjectionContext, inject } from 'vue-demi' import { getClientKey } from './utils' import type { QueryClient } from './queryClient' export function useQueryClient(id = ''): QueryClient { // ensures that `inject()` can be used if (!hasInjectionContext()) { throw new Error( 'vue-query hooks can only be used inside setup() function or functions that support injection context.', ) } const key = getClientKey(id) const queryClient = inject(key) if (!queryClient) { throw new Error( "No 'queryClient' found in Vue context, use 'VueQueryPlugin' to properly initialize the library.", ) } return queryClient } ================================================ FILE: packages/vue-query/src/utils.ts ================================================ import { isRef, unref } from 'vue-demi' import type { MaybeRefDeep } from './types' export const VUE_QUERY_CLIENT = 'VUE_QUERY_CLIENT' export function getClientKey(key?: string) { const suffix = key ? `:${key}` : '' return `${VUE_QUERY_CLIENT}${suffix}` } export function updateState( state: Record, update: Record, ): void { Object.keys(state).forEach((key) => { state[key] = update[key] }) } // Helper function for cloning deep objects where // the level and key is provided to the callback function. function _cloneDeep( value: MaybeRefDeep, customize?: ( val: MaybeRefDeep, key: string, level: number, ) => T | undefined, currentKey: string = '', currentLevel: number = 0, ): T { if (customize) { const result = customize(value, currentKey, currentLevel) if (result === undefined && isRef(value)) { return result as T } if (result !== undefined) { return result } } if (Array.isArray(value)) { return value.map((val, index) => _cloneDeep(val, customize, String(index), currentLevel + 1), ) as unknown as T } if (typeof value === 'object' && isPlainObject(value)) { const entries = Object.entries(value).map(([key, val]) => [ key, _cloneDeep(val, customize, key, currentLevel + 1), ]) return Object.fromEntries(entries) } return value as T } export function cloneDeep( value: MaybeRefDeep, customize?: ( val: MaybeRefDeep, key: string, level: number, ) => T | undefined, ): T { return _cloneDeep(value, customize) } export function cloneDeepUnref( obj: MaybeRefDeep, unrefGetters = false, ): T { return cloneDeep(obj, (val, key, level) => { // Check if we're at the top level and the key is 'queryKey' // // If so, take the recursive descent where we resolve // getters to values as well as refs. if (level === 1 && key === 'queryKey') { return cloneDeepUnref(val, true) } // Resolve getters to values if specified. if (unrefGetters && isFunction(val)) { // Cast due to older TS versions not allowing calling // on certain intersection types. return cloneDeepUnref((val as Function)(), unrefGetters) } // Unref refs and continue to recurse into the value. if (isRef(val)) { return cloneDeepUnref(unref(val), unrefGetters) } return undefined }) } // eslint-disable-next-line @typescript-eslint/no-wrapper-object-types function isPlainObject(value: unknown): value is Object { if (Object.prototype.toString.call(value) !== '[object Object]') { return false } const prototype = Object.getPrototypeOf(value) return prototype === null || prototype === Object.prototype } function isFunction(value: unknown): value is Function { return typeof value === 'function' } ================================================ FILE: packages/vue-query/src/vueQueryPlugin.ts ================================================ import { isVue2 } from 'vue-demi' import { isServer } from '@tanstack/query-core' import { QueryClient } from './queryClient' import { getClientKey } from './utils' import { setupDevtools } from './devtools/devtools' import type { QueryClientConfig } from './types' type ClientPersister = (client: QueryClient) => [() => void, Promise] interface CommonOptions { enableDevtoolsV6Plugin?: boolean queryClientKey?: string clientPersister?: ClientPersister clientPersisterOnSuccess?: (client: QueryClient) => void } interface ConfigOptions extends CommonOptions { queryClientConfig?: QueryClientConfig } interface ClientOptions extends CommonOptions { queryClient?: QueryClient } export type VueQueryPluginOptions = ConfigOptions | ClientOptions export const VueQueryPlugin = { install: (app: any, options: VueQueryPluginOptions = {}) => { const clientKey = getClientKey(options.queryClientKey) let client: QueryClient if ('queryClient' in options && options.queryClient) { client = options.queryClient } else { const clientConfig = 'queryClientConfig' in options ? options.queryClientConfig : undefined client = new QueryClient(clientConfig) } if (!isServer) { client.mount() } let persisterUnmount = () => { // noop } if (options.clientPersister) { if (client.isRestoring) { client.isRestoring.value = true } const [unmount, promise] = options.clientPersister(client) persisterUnmount = unmount promise.then(() => { if (client.isRestoring) { client.isRestoring.value = false } options.clientPersisterOnSuccess?.(client) }) } const cleanup = () => { client.unmount() persisterUnmount() } if (app.onUnmount) { app.onUnmount(cleanup) } else { const originalUnmount = app.unmount app.unmount = function vueQueryUnmount() { cleanup() originalUnmount() } } if (isVue2) { app.mixin({ beforeCreate() { // HACK: taken from provide(): https://github.com/vuejs/composition-api/blob/master/src/apis/inject.ts#L30 if (!this._provided) { const provideCache = {} Object.defineProperty(this, '_provided', { get: () => provideCache, set: (v) => Object.assign(provideCache, v), }) } this._provided[clientKey] = client if (process.env.NODE_ENV === 'development') { if (this === this.$root && options.enableDevtoolsV6Plugin) { setupDevtools(this, client) } } }, }) } else { app.provide(clientKey, client) if (process.env.NODE_ENV === 'development') { if (options.enableDevtoolsV6Plugin) { setupDevtools(app, client) } } } }, } ================================================ FILE: packages/vue-query/src/__mocks__/useBaseQuery.ts ================================================ import { vi } from 'vitest' import type { Mock } from 'vitest' const { useBaseQuery: originImpl, unrefQueryArgs: originalParse } = (await vi.importActual('../useBaseQuery')) as any export const useBaseQuery: Mock<(...args: Array) => any> = vi.fn(originImpl) export const unrefQueryArgs = originalParse ================================================ FILE: packages/vue-query/src/__mocks__/useQueryClient.ts ================================================ import { vi } from 'vitest' import { QueryClient } from '../queryClient' import type { Mock } from 'vitest' const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false, gcTime: Infinity }, }, }) export const useQueryClient: Mock<() => QueryClient> = vi.fn(() => queryClient) ================================================ FILE: packages/vue-query/src/__tests__/infiniteQueryOptions.test-d.ts ================================================ import { assertType, describe, expectTypeOf, it } from 'vitest' import { QueryClient, dataTagSymbol } from '@tanstack/query-core' import { reactive } from 'vue-demi' import { infiniteQueryOptions } from '../infiniteQueryOptions' import { useInfiniteQuery } from '../useInfiniteQuery' import type { InfiniteData } 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 } = reactive(useInfiniteQuery(options)) expectTypeOf(data).toEqualTypeOf< InfiniteData | undefined >() }) 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 >() }) }) ================================================ FILE: packages/vue-query/src/__tests__/mutationCache.test.ts ================================================ import { beforeAll, describe, expect, test, vi } from 'vitest' import { ref } from 'vue-demi' import { MutationCache as MutationCacheOrigin } from '@tanstack/query-core' import { MutationCache } from '../mutationCache' describe('MutationCache', () => { beforeAll(() => { vi.spyOn(MutationCacheOrigin.prototype, 'find') vi.spyOn(MutationCacheOrigin.prototype, 'findAll') }) describe('find', () => { test('should properly unwrap parameters', () => { const mutationCache = new MutationCache() mutationCache.find({ mutationKey: ref(['baz']), }) expect(MutationCacheOrigin.prototype.find).toBeCalledWith({ mutationKey: ['baz'], }) }) }) describe('findAll', () => { test('should properly unwrap parameters', () => { const mutationCache = new MutationCache() mutationCache.findAll({ mutationKey: ref(['baz']), }) expect(MutationCacheOrigin.prototype.findAll).toBeCalledWith({ mutationKey: ['baz'], }) }) }) }) ================================================ FILE: packages/vue-query/src/__tests__/queryCache.test.ts ================================================ import { beforeAll, describe, expect, test, vi } from 'vitest' import { ref } from 'vue-demi' import { QueryCache as QueryCacheOrigin } from '@tanstack/query-core' import { QueryCache } from '../queryCache' describe('QueryCache', () => { beforeAll(() => { vi.spyOn(QueryCacheOrigin.prototype, 'find') vi.spyOn(QueryCacheOrigin.prototype, 'findAll') }) describe('find', () => { test('should properly unwrap parameters', () => { const queryCache = new QueryCache() queryCache.find({ queryKey: ['foo', ref('bar')], }) expect(QueryCacheOrigin.prototype.find).toBeCalledWith({ queryKey: ['foo', 'bar'], }) }) }) describe('findAll', () => { test('should properly unwrap two parameters', () => { const queryCache = new QueryCache() queryCache.findAll({ queryKey: ['foo', ref('bar')], }) expect(QueryCacheOrigin.prototype.findAll).toBeCalledWith({ queryKey: ['foo', 'bar'], }) }) test('should default to empty filters', () => { const queryCache = new QueryCache() queryCache.findAll() expect(QueryCacheOrigin.prototype.findAll).toBeCalledWith({}) }) }) }) ================================================ FILE: packages/vue-query/src/__tests__/queryClient.test-d.ts ================================================ import { assertType, describe, expectTypeOf, it } from 'vitest' import { QueryClient } from '../queryClient' import type { DataTag, InfiniteData } from '@tanstack/query-core' describe('getQueryData', () => { it('should be typed if key is tagged', () => { const queryKey = ['key'] as DataTag, number> const queryClient = new QueryClient() const data = queryClient.getQueryData(queryKey) expectTypeOf(data).toEqualTypeOf() }) it('should infer unknown if key is not tagged', () => { const queryKey = ['key'] as const const queryClient = new QueryClient() const data = queryClient.getQueryData(queryKey) expectTypeOf(data).toEqualTypeOf() }) it('should infer passed generic if passed', () => { const queryKey = ['key'] as const const queryClient = new QueryClient() const data = queryClient.getQueryData(queryKey) expectTypeOf(data).toEqualTypeOf() }) it('should only allow Arrays to be passed', () => { assertType>([ // @ts-expect-error TS2345: Argument of type 'string' is not assignable to parameter of type 'QueryKey' { queryKey: 'key' }, ]) }) }) describe('setQueryData', () => { it('updater should be typed if key is tagged', () => { const queryKey = ['key'] as DataTag, number> const queryClient = new QueryClient() const data = queryClient.setQueryData(queryKey, (prev) => { expectTypeOf(prev).toEqualTypeOf() return prev }) expectTypeOf(data).toEqualTypeOf() }) it('value should be typed if key is tagged', () => { const queryKey = ['key'] as DataTag, number> const queryClient = new QueryClient() // @ts-expect-error value should be a number queryClient.setQueryData(queryKey, '1') // @ts-expect-error value should be a number queryClient.setQueryData(queryKey, () => '1') const data = queryClient.setQueryData(queryKey, 1) expectTypeOf(data).toEqualTypeOf() }) it('should infer unknown for updater if key is not tagged', () => { const queryKey = ['key'] as const const queryClient = new QueryClient() const data = queryClient.setQueryData(queryKey, (prev) => { expectTypeOf(prev).toEqualTypeOf() return prev }) expectTypeOf(data).toEqualTypeOf() }) it('should infer unknown for value if key is not tagged', () => { const queryKey = ['key'] as const const queryClient = new QueryClient() const data = queryClient.setQueryData(queryKey, 'foo') expectTypeOf(data).toEqualTypeOf() }) it('should infer passed generic if passed', () => { const queryKey = ['key'] as const const queryClient = new QueryClient() const data = queryClient.setQueryData(queryKey, (prev) => { expectTypeOf(prev).toEqualTypeOf() return prev }) expectTypeOf(data).toEqualTypeOf() }) it('should infer passed generic for value', () => { const queryKey = ['key'] as const const queryClient = new QueryClient() const data = queryClient.setQueryData(queryKey, 'foo') expectTypeOf(data).toEqualTypeOf() }) it('should preserve updater parameter type inference when used in functions with explicit return types', () => { const queryKey = ['key'] as DataTag, number> const queryClient = new QueryClient() // Simulate usage inside a function with explicit return type // The outer function returns 'unknown' but this shouldn't affect the updater's type inference ;(() => queryClient.setQueryData(queryKey, (data) => { expectTypeOf(data).toEqualTypeOf() return data })) satisfies () => unknown }) }) describe('fetchInfiniteQuery', () => { it('should allow passing pages', async () => { const data = await new QueryClient().fetchInfiniteQuery({ queryKey: ['key'], queryFn: () => Promise.resolve('string'), getNextPageParam: () => 1, initialPageParam: 1, pages: 5, }) expectTypeOf(data).toEqualTypeOf>() }) it('should not allow passing getNextPageParam without pages', () => { assertType>([ { queryKey: ['key'], queryFn: () => Promise.resolve('string'), initialPageParam: 1, getNextPageParam: () => 1, }, ]) }) it('should not allow passing pages without getNextPageParam', () => { assertType>([ // @ts-expect-error Property 'getNextPageParam' is missing { queryKey: ['key'], queryFn: () => Promise.resolve('string'), initialPageParam: 1, pages: 5, }, ]) }) }) ================================================ FILE: packages/vue-query/src/__tests__/queryClient.test.ts ================================================ import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest' import { ref } from 'vue-demi' import { QueryClient as QueryClientOrigin } from '@tanstack/query-core' import { QueryClient } from '../queryClient' import { infiniteQueryOptions } from '../infiniteQueryOptions' vi.mock('@tanstack/query-core') const queryKeyRef = ['foo', ref('bar')] const queryKeyUnref = ['foo', 'bar'] const fn = () => 'mock' describe('QueryCache', () => { beforeEach(() => { vi.useFakeTimers() }) afterEach(() => { vi.useRealTimers() }) describe('isFetching', () => { test('should properly unwrap 1 parameter', () => { const queryClient = new QueryClient() queryClient.isFetching({ queryKey: queryKeyRef, }) expect(QueryClientOrigin.prototype.isFetching).toBeCalledWith({ queryKey: queryKeyUnref, }) }) }) describe('isMutating', () => { test('should properly unwrap 1 parameter', () => { const queryClient = new QueryClient() queryClient.isMutating({ mutationKey: queryKeyRef, }) expect(QueryClientOrigin.prototype.isMutating).toBeCalledWith({ mutationKey: queryKeyUnref, }) }) }) describe('getQueryData', () => { test('should properly unwrap 1 parameter', () => { const queryClient = new QueryClient() queryClient.getQueryData(queryKeyRef) expect(QueryClientOrigin.prototype.getQueryData).toBeCalledWith( queryKeyUnref, ) }) }) describe('getQueriesData', () => { test('should properly unwrap queryKey param', () => { const queryClient = new QueryClient() queryClient.getQueriesData({ queryKey: queryKeyRef }) expect(QueryClientOrigin.prototype.getQueriesData).toBeCalledWith({ queryKey: queryKeyUnref, }) }) test('should properly unwrap filters param', () => { const queryClient = new QueryClient() queryClient.getQueriesData({ queryKey: queryKeyRef }) expect(QueryClientOrigin.prototype.getQueriesData).toBeCalledWith({ queryKey: queryKeyUnref, }) }) }) describe('setQueryData', () => { test('should properly unwrap 3 parameter', () => { const queryClient = new QueryClient() queryClient.setQueryData(queryKeyRef, fn, { updatedAt: ref(3), }) expect(QueryClientOrigin.prototype.setQueryData).toBeCalledWith( queryKeyUnref, fn, { updatedAt: 3 }, ) }) }) describe('setQueriesData', () => { test('should properly unwrap params with queryKey', () => { const queryClient = new QueryClient() queryClient.setQueriesData({ queryKey: queryKeyRef }, fn, { updatedAt: ref(3), }) expect(QueryClientOrigin.prototype.setQueriesData).toBeCalledWith( { queryKey: queryKeyUnref }, fn, { updatedAt: 3 }, ) }) test('should properly unwrap params with filters', () => { const queryClient = new QueryClient() queryClient.setQueriesData({ queryKey: queryKeyRef }, fn, { updatedAt: ref(3), }) expect(QueryClientOrigin.prototype.setQueriesData).toBeCalledWith( { queryKey: queryKeyUnref }, fn, { updatedAt: 3 }, ) }) }) describe('getQueryState', () => { test('should properly unwrap 1 parameter', () => { const queryClient = new QueryClient() queryClient.getQueryState(queryKeyRef) expect(QueryClientOrigin.prototype.getQueryState).toBeCalledWith( queryKeyUnref, ) }) }) describe('removeQueries', () => { test('should properly unwrap 1 parameter', () => { const queryClient = new QueryClient() queryClient.removeQueries({ queryKey: queryKeyRef, }) expect(QueryClientOrigin.prototype.removeQueries).toBeCalledWith({ queryKey: queryKeyUnref, }) }) }) describe('resetQueries', () => { test('should properly unwrap 2 parameter', () => { const queryClient = new QueryClient() queryClient.resetQueries( { queryKey: queryKeyRef, }, { cancelRefetch: ref(false) }, ) expect(QueryClientOrigin.prototype.resetQueries).toBeCalledWith( { queryKey: queryKeyUnref, }, { cancelRefetch: false }, ) }) }) describe('cancelQueries', () => { test('should properly unwrap 2 parameter', () => { const queryClient = new QueryClient() queryClient.cancelQueries( { queryKey: queryKeyRef, }, { revert: ref(false) }, ) expect(QueryClientOrigin.prototype.cancelQueries).toBeCalledWith( { queryKey: queryKeyUnref, }, { revert: false }, ) }) }) describe('invalidateQueries', () => { test('should properly unwrap 2 parameter', () => { const queryClient = new QueryClient() queryClient.invalidateQueries( { queryKey: queryKeyRef, }, { cancelRefetch: ref(false) }, ) expect(QueryClientOrigin.prototype.invalidateQueries).toBeCalledWith( { queryKey: queryKeyUnref, refetchType: 'none', }, { cancelRefetch: false }, ) }) // #7694 test('should call invalidateQueries immediately and refetchQueries after sleep', async () => { const invalidateQueries = vi.spyOn( QueryClientOrigin.prototype, 'invalidateQueries', ) const refetchQueries = vi.spyOn( QueryClientOrigin.prototype, 'refetchQueries', ) const queryClient = new QueryClient() queryClient.invalidateQueries({ queryKey: queryKeyRef, }) expect(invalidateQueries).toBeCalled() expect(refetchQueries).not.toBeCalled() await vi.advanceTimersByTimeAsync(0) expect(refetchQueries).toBeCalled() }) test('should call invalidateQueries immediately and not call refetchQueries', async () => { const invalidateQueries = vi.spyOn( QueryClientOrigin.prototype, 'invalidateQueries', ) const refetchQueries = vi.spyOn( QueryClientOrigin.prototype, 'refetchQueries', ) const queryClient = new QueryClient() queryClient.invalidateQueries({ queryKey: queryKeyRef, refetchType: 'none', }) expect(invalidateQueries).toBeCalled() expect(refetchQueries).not.toBeCalled() await vi.advanceTimersByTimeAsync(0) expect(refetchQueries).not.toBeCalled() }) }) describe('refetchQueries', () => { test('should properly unwrap 2 parameter', () => { const queryClient = new QueryClient() queryClient.refetchQueries( { queryKey: queryKeyRef, }, { cancelRefetch: ref(false) }, ) expect(QueryClientOrigin.prototype.refetchQueries).toBeCalledWith( { queryKey: queryKeyUnref, }, { cancelRefetch: false }, ) }) }) describe('fetchQuery', () => { test('should properly unwrap parameter', () => { const queryClient = new QueryClient() queryClient.fetchQuery({ queryKey: queryKeyRef, }) expect(QueryClientOrigin.prototype.fetchQuery).toBeCalledWith({ queryKey: queryKeyUnref, }) }) }) describe('prefetchQuery', () => { test('should properly unwrap parameters', () => { const queryClient = new QueryClient() queryClient.prefetchQuery({ queryKey: queryKeyRef, queryFn: fn }) expect(QueryClientOrigin.prototype.prefetchQuery).toBeCalledWith({ queryKey: queryKeyUnref, queryFn: fn, }) }) }) describe('fetchInfiniteQuery', () => { test('should properly unwrap parameter', () => { const queryClient = new QueryClient() queryClient.fetchInfiniteQuery({ queryKey: queryKeyRef, initialPageParam: 0, }) expect(QueryClientOrigin.prototype.fetchInfiniteQuery).toBeCalledWith({ initialPageParam: 0, queryKey: queryKeyUnref, }) }) test('should properly unwrap parameter using infiniteQueryOptions with unref', () => { const queryClient = new QueryClient() const options = infiniteQueryOptions({ queryKey: queryKeyUnref, initialPageParam: 0, getNextPageParam: () => 12, }) queryClient.fetchInfiniteQuery(options) expect(QueryClientOrigin.prototype.fetchInfiniteQuery).toBeCalledWith({ initialPageParam: 0, queryKey: queryKeyUnref, }) }) }) describe('prefetchInfiniteQuery', () => { test('should properly unwrap parameters', () => { const queryClient = new QueryClient() queryClient.prefetchInfiniteQuery({ queryKey: queryKeyRef, queryFn: fn, initialPageParam: 0, }) expect(QueryClientOrigin.prototype.prefetchInfiniteQuery).toBeCalledWith({ initialPageParam: 0, queryKey: queryKeyUnref, queryFn: fn, }) }) }) describe('setDefaultOptions', () => { test('should properly unwrap parameters', () => { const queryClient = new QueryClient() queryClient.setDefaultOptions({ queries: { enabled: ref(false), }, }) expect(QueryClientOrigin.prototype.setDefaultOptions).toBeCalledWith({ queries: { enabled: false, }, }) }) }) describe('setQueryDefaults', () => { test('should properly unwrap parameters', () => { const queryClient = new QueryClient() queryClient.setQueryDefaults(queryKeyRef, { enabled: ref(false), }) expect(QueryClientOrigin.prototype.setQueryDefaults).toBeCalledWith( queryKeyUnref, { enabled: false, }, ) }) }) describe('getQueryDefaults', () => { test('should properly unwrap parameters', () => { const queryClient = new QueryClient() queryClient.getQueryDefaults(queryKeyRef) expect(QueryClientOrigin.prototype.getQueryDefaults).toBeCalledWith( queryKeyUnref, ) }) }) describe('setMutationDefaults', () => { test('should properly unwrap parameters', () => { const queryClient = new QueryClient() queryClient.setMutationDefaults(queryKeyRef, { mutationKey: queryKeyRef, }) expect(QueryClientOrigin.prototype.setMutationDefaults).toBeCalledWith( queryKeyUnref, { mutationKey: queryKeyUnref, }, ) }) }) describe('getMutationDefaults', () => { test('should properly unwrap parameters', () => { const queryClient = new QueryClient() queryClient.getMutationDefaults(queryKeyRef) expect(QueryClientOrigin.prototype.getMutationDefaults).toBeCalledWith( queryKeyUnref, ) }) }) }) ================================================ FILE: packages/vue-query/src/__tests__/queryOptions.test-d.ts ================================================ import { assertType, describe, expectTypeOf, it } from 'vitest' import { reactive, ref } from 'vue-demi' import { dataTagSymbol } from '@tanstack/query-core' import { QueryClient } from '../queryClient' import { queryOptions } from '../queryOptions' import { useQuery } from '../useQuery' describe('queryOptions', () => { it('should not allow excess properties', () => { assertType( queryOptions({ queryKey: ['key'], queryFn: () => Promise.resolve(5), // @ts-expect-error this is a good error, because stallTime does not exist! stallTime: 1000, }), ) }) it('should infer types for callbacks', () => { queryOptions({ queryKey: ['key'], queryFn: () => Promise.resolve(5), staleTime: 1000, select: (data) => { expectTypeOf(data).toEqualTypeOf() }, }) }) it('should work when passed to useQuery', () => { const options = queryOptions({ queryKey: ['key'], queryFn: () => Promise.resolve(5), }) const { data } = reactive(useQuery(options)) expectTypeOf(data).toEqualTypeOf() }) it('should tag the queryKey with the result type of the QueryFn', () => { const { queryKey } = queryOptions({ queryKey: ['key'], queryFn: () => Promise.resolve(5), }) expectTypeOf(queryKey[dataTagSymbol]).toEqualTypeOf() }) it('should tag the queryKey even if no promise is returned', () => { const { queryKey } = queryOptions({ queryKey: ['key'], queryFn: () => 5, }) expectTypeOf(queryKey[dataTagSymbol]).toEqualTypeOf() }) it('should tag the queryKey with unknown if there is no queryFn', () => { const { queryKey } = queryOptions({ queryKey: ['key'], }) expectTypeOf(queryKey[dataTagSymbol]).toEqualTypeOf() }) it('should tag the queryKey with the result type of the QueryFn if select is used', () => { const { queryKey } = queryOptions({ queryKey: ['key'], queryFn: () => Promise.resolve(5), select: (data) => data.toString(), }) expectTypeOf(queryKey[dataTagSymbol]).toEqualTypeOf() }) it('should return the proper type when passed to getQueryData', () => { const { queryKey } = queryOptions({ queryKey: ['key'], queryFn: () => Promise.resolve(5), }) const queryClient = new QueryClient() const data = queryClient.getQueryData(queryKey) expectTypeOf(data).toEqualTypeOf() }) it('should properly type updaterFn when passed to setQueryData', () => { const { queryKey } = queryOptions({ queryKey: ['key'], queryFn: () => Promise.resolve(5), }) const queryClient = new QueryClient() const data = queryClient.setQueryData(queryKey, (prev) => { expectTypeOf(prev).toEqualTypeOf() return prev }) expectTypeOf(data).toEqualTypeOf() }) it('should properly type value when passed to setQueryData', () => { const { queryKey } = queryOptions({ queryKey: ['key'], queryFn: () => Promise.resolve(5), }) const queryClient = new QueryClient() // @ts-expect-error value should be a number queryClient.setQueryData(queryKey, '5') // @ts-expect-error value should be a number queryClient.setQueryData(queryKey, () => '5') const data = queryClient.setQueryData(queryKey, 5) expectTypeOf(data).toEqualTypeOf() }) it('should allow to be passed to QueryClient methods while containing ref in queryKey', () => { const options = queryOptions({ queryKey: ['key', ref(1), { nested: ref(2) }], queryFn: () => Promise.resolve(5), }) const queryClient = new QueryClient() // Should not error const data = queryClient.invalidateQueries(options) // Should not error const data2 = queryClient.fetchQuery(options) expectTypeOf(data).toEqualTypeOf>() expectTypeOf(data2).toEqualTypeOf>() }) it('TData should always be defined when initialData is provided as a function which ALWAYS returns the data', () => { const { data } = reactive( useQuery( queryOptions({ queryKey: ['key'], queryFn: () => { return { wow: true, } }, initialData: () => ({ wow: true, }), }), ), ) expectTypeOf(data).toEqualTypeOf<{ wow: boolean }>() }) it('TData should have undefined in the union when initialData is NOT provided', () => { const { data } = reactive( useQuery( queryOptions({ queryKey: ['key'], queryFn: () => { return { wow: true, } }, }), ), ) expectTypeOf(data).toEqualTypeOf<{ wow: boolean } | undefined>() }) it('TData should have undefined in the union when initialData is provided as a function which can return undefined', () => { const { data } = reactive( useQuery( queryOptions({ queryKey: ['key'], queryFn: () => { return { wow: true, } }, initialData: () => undefined as { wow: boolean } | undefined, }), ), ) expectTypeOf(data).toEqualTypeOf<{ wow: boolean } | undefined>() }) it('TData should be narrowed after an isSuccess check when initialData is provided as a function which can return undefined', () => { const { data, isSuccess } = reactive( useQuery( queryOptions({ queryKey: ['key'], queryFn: () => { return { wow: true, } }, initialData: () => undefined as { wow: boolean } | undefined, }), ), ) if (isSuccess) { expectTypeOf(data).toEqualTypeOf<{ wow: boolean }>() } }) it('data should not have undefined when initialData is provided', () => { const { data } = reactive( useQuery( queryOptions({ queryKey: ['query-key'], initialData: 42, }), ), ) expectTypeOf(data).toEqualTypeOf() }) }) ================================================ FILE: packages/vue-query/src/__tests__/useInfiniteQuery.test-d.tsx ================================================ import { describe, expectTypeOf, it } from 'vitest' import { computed, reactive } from 'vue-demi' import { sleep } from '@tanstack/query-test-utils' import { useInfiniteQuery } from '../useInfiniteQuery' import type { InfiniteData } from '@tanstack/query-core' describe('Discriminated union return type', () => { it('data should be possibly undefined by default', () => { const query = reactive( useInfiniteQuery({ queryKey: ['infiniteQuery'], queryFn: () => sleep(0).then(() => 'Some data'), getNextPageParam: () => undefined, initialPageParam: 0, }), ) // TODO: Order of generics prevents pageParams to be typed correctly. Using `unknown` for now expectTypeOf(query.data).toEqualTypeOf< InfiniteData | undefined >() }) it('data should be defined when query is success', () => { const query = reactive( useInfiniteQuery({ queryKey: ['infiniteQuery'], queryFn: () => sleep(0).then(() => 'Some data'), getNextPageParam: () => undefined, initialPageParam: 0, }), ) if (query.isSuccess) { // TODO: Order of generics prevents pageParams to be typed correctly. Using `unknown` for now expectTypeOf(query.data).toEqualTypeOf>() } }) it('error should be null when query is success', () => { const query = reactive( useInfiniteQuery({ queryKey: ['infiniteQuery'], queryFn: () => sleep(0).then(() => 'Some data'), getNextPageParam: () => undefined, initialPageParam: 0, }), ) if (query.isSuccess) { expectTypeOf(query.error).toEqualTypeOf() } }) it('data should be undefined when query is pending', () => { const query = reactive( useInfiniteQuery({ queryKey: ['infiniteQuery'], queryFn: () => sleep(0).then(() => 'Some data'), getNextPageParam: () => undefined, initialPageParam: 0, }), ) if (query.isPending) { expectTypeOf(query.data).toEqualTypeOf() } }) it('error should be defined when query is error', () => { const query = reactive( useInfiniteQuery({ queryKey: ['infiniteQuery'], queryFn: () => sleep(0).then(() => 'Some data'), getNextPageParam: () => undefined, initialPageParam: 0, }), ) if (query.isError) { expectTypeOf(query.error).toEqualTypeOf() } }) it('should accept computed options', () => { const options = computed(() => ({ queryKey: ['infiniteQuery'], queryFn: () => sleep(0).then(() => 'Some data'), getNextPageParam: () => undefined, initialPageParam: 0, })) const query = reactive(useInfiniteQuery(options)) if (query.isSuccess) { expectTypeOf(query.data).toEqualTypeOf>() } }) }) ================================================ FILE: packages/vue-query/src/__tests__/useInfiniteQuery.test.ts ================================================ import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest' import { sleep } from '@tanstack/query-test-utils' import { useInfiniteQuery } from '../useInfiniteQuery' import { infiniteQueryOptions } from '../infiniteQueryOptions' vi.mock('../useQueryClient') describe('useQuery', () => { beforeEach(() => { vi.useFakeTimers() }) afterEach(() => { vi.useRealTimers() }) test('should properly execute infinite query', async () => { const { data, fetchNextPage, status } = useInfiniteQuery({ queryKey: ['infiniteQuery'], queryFn: ({ pageParam }) => sleep(0).then(() => 'data on page ' + pageParam), initialPageParam: 0, getNextPageParam: () => 12, }) expect(data.value).toStrictEqual(undefined) expect(status.value).toStrictEqual('pending') await vi.advanceTimersByTimeAsync(0) expect(data.value).toStrictEqual({ pageParams: [0], pages: ['data on page 0'], }) expect(status.value).toStrictEqual('success') fetchNextPage() await vi.advanceTimersByTimeAsync(0) expect(data.value).toStrictEqual({ pageParams: [0, 12], pages: ['data on page 0', 'data on page 12'], }) expect(status.value).toStrictEqual('success') }) test('should properly execute infinite query using infiniteQueryOptions', async () => { const options = infiniteQueryOptions({ queryKey: ['infiniteQueryOptions'], queryFn: ({ pageParam }) => sleep(0).then(() => 'data on page ' + pageParam), initialPageParam: 0, getNextPageParam: () => 12, }) const { data, fetchNextPage, status } = useInfiniteQuery(options) expect(data.value).toStrictEqual(undefined) expect(status.value).toStrictEqual('pending') await vi.advanceTimersByTimeAsync(0) expect(data.value).toStrictEqual({ pageParams: [0], pages: ['data on page 0'], }) expect(status.value).toStrictEqual('success') fetchNextPage() await vi.advanceTimersByTimeAsync(0) expect(data.value).toStrictEqual({ pageParams: [0, 12], pages: ['data on page 0', 'data on page 12'], }) expect(status.value).toStrictEqual('success') }) }) ================================================ FILE: packages/vue-query/src/__tests__/useIsFetching.test.ts ================================================ import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest' import { onScopeDispose, reactive } from 'vue-demi' import { sleep } from '@tanstack/query-test-utils' import { useQuery } from '../useQuery' import { useIsFetching } from '../useIsFetching' import type { MockedFunction } from 'vitest' vi.mock('../useQueryClient') describe('useIsFetching', () => { beforeEach(() => { vi.useFakeTimers() }) afterEach(() => { vi.useRealTimers() }) test('should properly return isFetching state', async () => { const { isFetching: isFetchingQuery } = useQuery({ queryKey: ['isFetching1'], queryFn: () => sleep(0).then(() => 'Some data'), }) useQuery({ queryKey: ['isFetching2'], queryFn: () => sleep(0).then(() => 'Some data'), }) const isFetching = useIsFetching() expect(isFetchingQuery.value).toStrictEqual(true) expect(isFetching.value).toStrictEqual(2) await vi.advanceTimersByTimeAsync(0) expect(isFetchingQuery.value).toStrictEqual(false) expect(isFetching.value).toStrictEqual(0) }) test('should stop listening to changes on onScopeDispose', async () => { const onScopeDisposeMock = onScopeDispose as MockedFunction< typeof onScopeDispose > onScopeDisposeMock.mockImplementation((fn) => fn()) const { status } = useQuery({ queryKey: ['onScopeDispose'], queryFn: () => sleep(0).then(() => 'Some data'), }) const isFetching = useIsFetching() expect(status.value).toStrictEqual('pending') expect(isFetching.value).toStrictEqual(1) await vi.advanceTimersByTimeAsync(0) expect(status.value).toStrictEqual('pending') expect(isFetching.value).toStrictEqual(1) await vi.advanceTimersByTimeAsync(0) expect(status.value).toStrictEqual('pending') expect(isFetching.value).toStrictEqual(1) onScopeDisposeMock.mockReset() }) test('should properly update filters', async () => { const filter = reactive({ stale: false }) useQuery({ queryKey: ['isFetching'], queryFn: () => new Promise((resolve) => { setTimeout(() => { return resolve('Some data') }, 100) }), }) const isFetching = useIsFetching(filter) expect(isFetching.value).toStrictEqual(0) filter.stale = true await vi.advanceTimersByTimeAsync(0) expect(isFetching.value).toStrictEqual(1) }) }) ================================================ FILE: packages/vue-query/src/__tests__/useIsMutating.test.ts ================================================ import { afterEach, beforeEach, describe, expect, it, test, vi } from 'vitest' import { onScopeDispose, reactive } from 'vue-demi' import { sleep } from '@tanstack/query-test-utils' import { useMutation } from '../useMutation' import { useIsMutating, useMutationState } from '../useMutationState' import { useQueryClient } from '../useQueryClient' import type { MockedFunction } from 'vitest' vi.mock('../useQueryClient') describe('useIsMutating', () => { beforeEach(() => { vi.useFakeTimers() }) afterEach(() => { vi.useRealTimers() }) test('should properly return isMutating state', async () => { const mutation = useMutation({ mutationFn: (params: string) => sleep(10).then(() => params), }) const mutation2 = useMutation({ mutationFn: (params: string) => sleep(10).then(() => params), }) const isMutating = useIsMutating() expect(isMutating.value).toStrictEqual(0) mutation.mutateAsync('a') mutation2.mutateAsync('b') await vi.advanceTimersByTimeAsync(0) expect(isMutating.value).toStrictEqual(2) await vi.advanceTimersByTimeAsync(10) expect(isMutating.value).toStrictEqual(0) }) test('should stop listening to changes on onScopeDispose', async () => { const onScopeDisposeMock = onScopeDispose as MockedFunction< typeof onScopeDispose > onScopeDisposeMock.mockImplementation((fn) => fn()) const mutation = useMutation({ mutationFn: (params: string) => sleep(0).then(() => params), }) const mutation2 = useMutation({ mutationFn: (params: string) => sleep(0).then(() => params), }) const isMutating = useIsMutating() expect(isMutating.value).toStrictEqual(0) mutation.mutateAsync('a') mutation2.mutateAsync('b') await vi.advanceTimersByTimeAsync(0) expect(isMutating.value).toStrictEqual(0) await vi.advanceTimersByTimeAsync(0) expect(isMutating.value).toStrictEqual(0) onScopeDisposeMock.mockReset() }) test('should properly update filters', async () => { const filter = reactive({ mutationKey: ['foo'] }) const { mutate } = useMutation({ mutationKey: ['isMutating'], mutationFn: (params: string) => sleep(10).then(() => params), }) mutate('foo') const isMutating = useIsMutating(filter) expect(isMutating.value).toStrictEqual(0) filter.mutationKey = ['isMutating'] await vi.advanceTimersByTimeAsync(0) expect(isMutating.value).toStrictEqual(1) }) }) describe('useMutationState', () => { it('should return variables after calling mutate 1', () => { const mutationKey = ['mutation'] const variables = 'foo123' const { mutate } = useMutation({ mutationKey: mutationKey, mutationFn: (params: string) => sleep(0).then(() => params), }) mutate(variables) const mutationState = useMutationState({ filters: { mutationKey, status: 'pending' }, select: (mutation) => mutation.state.variables, }) expect(mutationState.value).toEqual([variables]) }) it('should return variables after calling mutate 2', () => { const queryClient = useQueryClient() queryClient.clear() const mutationKey = ['mutation'] const variables = 'bar234' const { mutate } = useMutation({ mutationKey: mutationKey, mutationFn: (params: string) => sleep(0).then(() => params), }) mutate(variables) const mutationState = useMutationState() expect(mutationState.value[0]?.variables).toEqual(variables) }) }) ================================================ FILE: packages/vue-query/src/__tests__/useMutation.test-d.tsx ================================================ import { describe, expectTypeOf, it } from 'vitest' import { reactive } from 'vue-demi' import { sleep } from '@tanstack/query-test-utils' import { useMutation } from '../useMutation' describe('Discriminated union return type', () => { it('data should be possibly undefined by default', () => { const mutation = reactive( useMutation({ mutationFn: (params: string) => sleep(0).then(() => params), }), ) expectTypeOf(mutation.data).toEqualTypeOf() }) it('data should be defined when mutation is success', () => { const mutation = reactive( useMutation({ mutationFn: (params: string) => sleep(0).then(() => params), }), ) if (mutation.isSuccess) { expectTypeOf(mutation.data).toEqualTypeOf() } }) it('error should be null when mutation is success', () => { const mutation = reactive( useMutation({ mutationFn: (params: string) => sleep(0).then(() => params), }), ) if (mutation.isSuccess) { expectTypeOf(mutation.error).toEqualTypeOf() } }) it('data should be undefined when mutation is pending', () => { const mutation = reactive( useMutation({ mutationFn: (params: string) => sleep(0).then(() => params), }), ) if (mutation.isPending) { expectTypeOf(mutation.data).toEqualTypeOf() } }) it('error should be defined when mutation is error', () => { const mutation = reactive( useMutation({ mutationFn: (params: string) => sleep(0).then(() => params), }), ) if (mutation.isError) { expectTypeOf(mutation.error).toEqualTypeOf() } }) it('should narrow variables', () => { const mutation = reactive( useMutation({ mutationFn: (params: string) => sleep(0).then(() => params), }), ) if (mutation.isIdle) { expectTypeOf(mutation.variables).toEqualTypeOf() return } if (mutation.isPending) { expectTypeOf(mutation.variables).toEqualTypeOf() return } if (mutation.isSuccess) { expectTypeOf(mutation.variables).toEqualTypeOf() return } expectTypeOf(mutation.variables).toEqualTypeOf() }) }) ================================================ FILE: packages/vue-query/src/__tests__/useMutation.test.ts ================================================ import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest' import { reactive, ref } from 'vue-demi' import { sleep } from '@tanstack/query-test-utils' import { useMutation } from '../useMutation' import { useQueryClient } from '../useQueryClient' vi.mock('../useQueryClient') describe('useMutation', () => { beforeEach(() => { vi.useFakeTimers() }) afterEach(() => { vi.useRealTimers() }) test('should be in idle state initially', () => { const mutation = useMutation({ mutationFn: (params) => sleep(0).then(() => params), }) expect(mutation).toMatchObject({ isIdle: { value: true }, isPending: { value: false }, isError: { value: false }, isSuccess: { value: false }, }) }) test('should change state after invoking mutate', () => { const result = 'Mock data' const mutation = useMutation({ mutationFn: (params: string) => sleep(0).then(() => params), }) mutation.mutate(result) expect(mutation).toMatchObject({ isIdle: { value: false }, isPending: { value: true }, isError: { value: false }, isSuccess: { value: false }, data: { value: undefined }, error: { value: null }, }) }) test('should return error when request fails', async () => { const mutation = useMutation({ mutationFn: () => sleep(10).then(() => Promise.reject(new Error('Some error'))), }) mutation.mutate() await vi.advanceTimersByTimeAsync(10) expect(mutation).toMatchObject({ isIdle: { value: false }, isPending: { value: false }, isError: { value: true }, isSuccess: { value: false }, data: { value: undefined }, error: { value: Error('Some error') }, }) }) test('should return data when request succeeds', async () => { const result = 'Mock data' const mutation = useMutation({ mutationFn: (params: string) => sleep(10).then(() => params), }) mutation.mutate(result) await vi.advanceTimersByTimeAsync(10) expect(mutation).toMatchObject({ isIdle: { value: false }, isPending: { value: false }, isError: { value: false }, isSuccess: { value: true }, data: { value: 'Mock data' }, error: { value: null }, }) }) test('should update reactive options', async () => { const queryClient = useQueryClient() const mutationCache = queryClient.getMutationCache() const options = reactive({ mutationKey: ['foo'], mutationFn: (params: string) => sleep(10).then(() => params), }) const mutation = useMutation(options) options.mutationKey = ['bar'] await vi.advanceTimersByTimeAsync(10) mutation.mutate('xyz') await vi.advanceTimersByTimeAsync(10) const mutations = mutationCache.find({ mutationKey: ['bar'] }) expect(mutations?.options.mutationKey).toEqual(['bar']) }) test('should update reactive options deeply', async () => { type MutationKeyTest = { entity: string otherObject: { name: string } } const mutationKey = ref>([ { entity: 'test', otherObject: { name: 'objectName' }, }, ]) const queryClient = useQueryClient() const mutationCache = queryClient.getMutationCache() const options = reactive({ mutationKey, mutationFn: (params: string) => sleep(10).then(() => params), }) const mutation = useMutation(options) mutationKey.value[0]!.otherObject.name = 'someOtherObjectName' await vi.advanceTimersByTimeAsync(10) mutation.mutate('xyz') await vi.advanceTimersByTimeAsync(10) const mutations = mutationCache.getAll() const relevantMutation = mutations.find((m) => { return ( Array.isArray(m.options.mutationKey) && !!m.options.mutationKey[0].otherObject ) }) expect( (relevantMutation?.options.mutationKey as Array)[0] ?.otherObject.name === 'someOtherObjectName', ).toBe(true) }) test('should allow for non-options object (mutationFn or mutationKey) passed as arg1 & arg2 to trigger reactive updates', async () => { const mutationKey = ref>(['foo2']) const mutationFn = ref((params: string) => sleep(0).then(() => params)) const queryClient = useQueryClient() const mutationCache = queryClient.getMutationCache() const mutation = useMutation({ mutationKey, mutationFn }) mutationKey.value = ['bar2'] let proof = false mutationFn.value = (params: string) => { proof = true return sleep(10).then(() => params) } await vi.advanceTimersByTimeAsync(10) mutation.mutate('xyz') await vi.advanceTimersByTimeAsync(10) const mutations = mutationCache.find({ mutationKey: ['bar2'] }) expect(mutations?.options.mutationKey).toEqual(['bar2']) expect(proof).toEqual(true) }) test('should reset state after invoking mutation.reset', async () => { const mutation = useMutation({ mutationFn: () => sleep(10).then(() => Promise.reject(new Error('Some error'))), }) mutation.mutate() await vi.advanceTimersByTimeAsync(10) mutation.reset() expect(mutation).toMatchObject({ isIdle: { value: true }, isPending: { value: false }, isError: { value: false }, isSuccess: { value: false }, data: { value: undefined }, error: { value: null }, }) }) describe('side effects', () => { beforeEach(() => { vi.clearAllMocks() }) test('should call onMutate when passed as an option', async () => { const onMutate = vi.fn() const mutation = useMutation({ mutationFn: (params: string) => sleep(10).then(() => params), onMutate, }) mutation.mutate('') await vi.advanceTimersByTimeAsync(10) expect(onMutate).toHaveBeenCalledTimes(1) }) test('should call onError when passed as an option', async () => { const onError = vi.fn() const mutation = useMutation({ mutationFn: () => sleep(10).then(() => Promise.reject(new Error('Some error'))), onError, }) mutation.mutate('') await vi.advanceTimersByTimeAsync(10) expect(onError).toHaveBeenCalledTimes(1) }) test('should call onSuccess when passed as an option', async () => { const onSuccess = vi.fn() const mutation = useMutation({ mutationFn: (params: string) => sleep(10).then(() => params), onSuccess, }) mutation.mutate('') await vi.advanceTimersByTimeAsync(10) expect(onSuccess).toHaveBeenCalledTimes(1) }) test('should call onSettled when passed as an option', async () => { const onSettled = vi.fn() const mutation = useMutation({ mutationFn: (params: string) => sleep(10).then(() => params), onSettled, }) mutation.mutate('') await vi.advanceTimersByTimeAsync(10) expect(onSettled).toHaveBeenCalledTimes(1) }) test('should call onError when passed as an argument of mutate function', async () => { const onError = vi.fn() const mutation = useMutation({ mutationFn: () => sleep(10).then(() => Promise.reject(new Error('Some error'))), }) mutation.mutate(undefined, { onError }) await vi.advanceTimersByTimeAsync(10) expect(onError).toHaveBeenCalledTimes(1) }) test('should call onSuccess when passed as an argument of mutate function', async () => { const onSuccess = vi.fn() const mutation = useMutation({ mutationFn: (params: string) => sleep(10).then(() => params), }) mutation.mutate('', { onSuccess }) await vi.advanceTimersByTimeAsync(10) expect(onSuccess).toHaveBeenCalledTimes(1) }) test('should call onSettled when passed as an argument of mutate function', async () => { const onSettled = vi.fn() const mutation = useMutation({ mutationFn: (params: string) => sleep(10).then(() => params), }) mutation.mutate('', { onSettled }) await vi.advanceTimersByTimeAsync(10) expect(onSettled).toHaveBeenCalledTimes(1) }) test('should fire both onSettled functions', async () => { const onSettled = vi.fn() const onSettledOnFunction = vi.fn() const mutation = useMutation({ mutationFn: (params: string) => sleep(10).then(() => params), onSettled, }) mutation.mutate('', { onSettled: onSettledOnFunction }) await vi.advanceTimersByTimeAsync(10) expect(onSettled).toHaveBeenCalledTimes(1) expect(onSettledOnFunction).toHaveBeenCalledTimes(1) }) }) describe('async', () => { beforeEach(() => { vi.clearAllMocks() }) test('should resolve properly', async () => { const result = 'Mock data' const mutation = useMutation({ mutationFn: (params: string) => sleep(10).then(() => params), }) await vi.waitFor(() => expect(mutation.mutateAsync(result)).resolves.toBe(result), ) expect(mutation).toMatchObject({ isIdle: { value: false }, isPending: { value: false }, isError: { value: false }, isSuccess: { value: true }, data: { value: 'Mock data' }, error: { value: null }, }) }) test('should throw on error', async () => { const mutation = useMutation({ mutationFn: () => sleep(10).then(() => Promise.reject(new Error('Some error'))), }) await vi.waitFor(() => expect(mutation.mutateAsync()).rejects.toThrowError('Some error'), ) expect(mutation).toMatchObject({ isIdle: { value: false }, isPending: { value: false }, isError: { value: true }, isSuccess: { value: false }, data: { value: undefined }, error: { value: Error('Some error') }, }) }) }) describe('throwOnError', () => { test('should evaluate throwOnError when mutation is expected to throw', async () => { const err = new Error('Expected mock error. All is well!') const boundaryFn = vi.fn() const { mutate } = useMutation({ mutationFn: () => sleep(10).then(() => Promise.reject(err)), throwOnError: boundaryFn, }) mutate() await vi.advanceTimersByTimeAsync(10) expect(boundaryFn).toHaveBeenCalledTimes(1) expect(boundaryFn).toHaveBeenCalledWith(err) }) }) }) ================================================ FILE: packages/vue-query/src/__tests__/useQueries.test-d.ts ================================================ import { describe, expectTypeOf, it } from 'vitest' import { reactive } from 'vue' import { skipToken, useQueries } from '..' import { queryOptions } from '../queryOptions' import type { OmitKeyof, QueryObserverResult } from '..' import type { UseQueryOptions } from '../useQuery' describe('UseQueries config object overload', () => { it('TData should always be defined when initialData is provided as an object', () => { const query1 = { queryKey: ['key1'], queryFn: () => { return { wow: true, } }, initialData: { wow: false, }, } const query2 = queryOptions({ queryKey: ['key2'], queryFn: () => 'Query Data', initialData: 'initial data', }) const query3 = { queryKey: ['key2'], queryFn: () => 'Query Data', } const { value: queriesState } = useQueries({ queries: [query1, query2, query3], }) expectTypeOf(queriesState[0].data).toEqualTypeOf<{ wow: boolean }>() expectTypeOf(queriesState[1].data).toEqualTypeOf() expectTypeOf(queriesState[2].data).toEqualTypeOf() }) it('TData should be defined when passed through queryOptions', () => { const options = queryOptions({ queryKey: ['key'], queryFn: () => { return { wow: true, } }, initialData: { wow: true, }, }) const { value: queriesState } = useQueries({ queries: [options] }) expectTypeOf(queriesState[0].data).toEqualTypeOf<{ wow: boolean }>() }) it('should be possible to define a different TData than TQueryFnData using select with queryOptions spread into useQueries', () => { const query1 = queryOptions({ queryKey: ['key'], queryFn: () => Promise.resolve(1), select: (data) => data > 1, }) const query2 = { queryKey: ['key'], queryFn: () => Promise.resolve(1), select: (data: any) => data > 1, } const queriesState = reactive(useQueries({ queries: [query1, query2] })) expectTypeOf(queriesState.value[0].data).toEqualTypeOf< boolean | undefined >() expectTypeOf(queriesState.value[1].data).toEqualTypeOf< boolean | undefined >() }) it('TData should have undefined in the union when initialData is provided as a function which can return undefined', () => { const { value: queriesState } = useQueries({ queries: [ { queryKey: ['key'], queryFn: () => { return { wow: true, } }, initialData: () => undefined as { wow: boolean } | undefined, }, ], }) expectTypeOf(queriesState[0].data).toEqualTypeOf< { wow: boolean } | undefined >() }) it('TData should have correct type when conditional skipToken is passed', () => { const { value: queriesState } = useQueries({ queries: [ queryOptions({ queryKey: ['key'], queryFn: Math.random() > 0.5 ? skipToken : () => Promise.resolve(5), }), ], }) const firstResult = queriesState[0] expectTypeOf(firstResult).toEqualTypeOf< QueryObserverResult >() expectTypeOf(firstResult.data).toEqualTypeOf() }) describe('custom hook', () => { it('should allow custom hooks using UseQueryOptions', () => { const useCustomQueries = ( options?: OmitKeyof< UseQueryOptions, 'queryKey' | 'queryFn', 'safely' >, ) => useQueries({ queries: [ { ...options, queryKey: ['todos-key'], queryFn: () => Promise.resolve('data'), }, ], }) const { value: queriesState } = useCustomQueries() expectTypeOf(queriesState[0].data).toEqualTypeOf() }) }) // Fix #7270 it('should have proper type inference with different options provided', () => { const numbers = [1, 2, 3] const queryKey = (n: number) => [n] const queryFn = (n: number) => () => Promise.resolve(n) const select = (data: number) => data.toString() const queries = numbers.map((n) => ({ queryKey: [n], queryFn: () => Promise.resolve(n), select: (data: number) => data.toString(), })) const queriesWithoutSelect = numbers.map((n) => ({ queryKey: queryKey(n), queryFn: queryFn(n), })) const queriesWithQueryOptions = numbers.map((n) => queryOptions({ queryKey: queryKey(n), queryFn: queryFn(n), select, }), ) const queriesWithQueryOptionsWithoutSelect = numbers.map((n) => queryOptions({ queryKey: queryKey(n), queryFn: queryFn(n), }), ) const query1 = useQueries({ queries: queries }) expectTypeOf(query1.value).toEqualTypeOf< Array> >() const query2 = useQueries({ queries: queriesWithoutSelect }) expectTypeOf(query2.value).toEqualTypeOf< Array> >() const query3 = useQueries({ queries: queriesWithQueryOptions }) expectTypeOf(query3.value).toEqualTypeOf< Array> >() const query4 = useQueries({ queries: queriesWithQueryOptionsWithoutSelect }) expectTypeOf(query4.value).toEqualTypeOf< Array> >() const queryCombine = useQueries({ queries: queries, combine: (data) => { return data.reduce((acc, i) => { acc.push(i.data ?? '') return acc }, [] as Array) }, }) expectTypeOf(queryCombine.value).toEqualTypeOf>() const queryCombineWithoutSelect = useQueries({ queries: queriesWithoutSelect, combine: (data) => { return data.reduce((acc, i) => { acc.push(i.data ?? 0) return acc }, [] as Array) }, }) expectTypeOf(queryCombineWithoutSelect.value).toEqualTypeOf>() }) it('should return correct data for dynamic queries with mixed result types', () => { const Queries1 = { get: () => queryOptions({ queryKey: ['key1'], queryFn: () => Promise.resolve(1), }), } const Queries2 = { get: () => queryOptions({ queryKey: ['key2'], queryFn: () => Promise.resolve(true), }), } const queries1List = [1, 2, 3].map(() => ({ ...Queries1.get() })) const { value: queriesState } = useQueries({ queries: [...queries1List, { ...Queries2.get() }], }) expectTypeOf(queriesState).toEqualTypeOf< [ ...Array>, QueryObserverResult, ] >() expectTypeOf(queriesState[0].data).toEqualTypeOf< number | boolean | undefined >() }) }) ================================================ FILE: packages/vue-query/src/__tests__/useQueries.test.ts ================================================ import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest' import { onScopeDispose, ref } from 'vue-demi' import { sleep } from '@tanstack/query-test-utils' import { useQueries } from '../useQueries' import { useQueryClient } from '../useQueryClient' import { QueryClient } from '../queryClient' import type { MockedFunction } from 'vitest' vi.mock('../useQueryClient') describe('useQueries', () => { beforeEach(() => { vi.restoreAllMocks() vi.useFakeTimers() }) afterEach(() => { vi.useRealTimers() }) test('should return result for each query', () => { const queries = [ { queryKey: ['key1'], queryFn: () => sleep(0).then(() => 'Some data'), }, { queryKey: ['key2'], queryFn: () => sleep(0).then(() => 'Some data'), }, ] const queriesState = useQueries({ queries }) expect(queriesState.value).toMatchObject([ { status: 'pending', isPending: true, isFetching: true, isStale: true, }, { status: 'pending', isPending: true, isFetching: true, isStale: true, }, ]) }) test('should resolve to success and update reactive state', async () => { const queries = [ { queryKey: ['key11'], queryFn: () => sleep(0).then(() => 'Some data'), }, { queryKey: ['key12'], queryFn: () => sleep(0).then(() => 'Some data'), }, ] const queriesState = useQueries({ queries }) await vi.advanceTimersByTimeAsync(0) expect(queriesState.value).toMatchObject([ { status: 'success', isPending: false, isFetching: false, isStale: true, }, { status: 'success', isPending: false, isFetching: false, isStale: true, }, ]) }) test('should reject one of the queries and update reactive state', async () => { const queries = [ { queryKey: ['key21'], queryFn: () => sleep(0).then(() => Promise.reject(new Error('Some error'))), }, { queryKey: ['key22'], queryFn: () => sleep(0).then(() => 'Some data'), }, ] const queriesState = useQueries({ queries }) await vi.advanceTimersByTimeAsync(0) expect(queriesState.value).toMatchObject([ { status: 'error', isPending: false, isFetching: false, isStale: true, }, { status: 'success', isPending: false, isFetching: false, isStale: true, }, ]) }) test('should return state for new queries', async () => { const queries = ref([ { queryKey: ['key31'], queryFn: () => sleep(0).then(() => 'value31'), }, { queryKey: ['key32'], queryFn: () => sleep(0).then(() => 'value32'), }, { queryKey: ['key33'], queryFn: () => sleep(0).then(() => 'value33'), }, ]) const queriesState = useQueries({ queries }) await vi.advanceTimersByTimeAsync(0) queries.value.splice( 0, queries.value.length, { queryKey: ['key31'], queryFn: () => sleep(0).then(() => 'value31'), }, { queryKey: ['key34'], queryFn: () => sleep(0).then(() => 'value34'), }, ) await vi.advanceTimersByTimeAsync(0) await vi.advanceTimersByTimeAsync(0) expect(queriesState.value.length).toEqual(2) expect(queriesState.value).toMatchObject([ { data: 'value31', status: 'success', isPending: false, isFetching: false, isStale: true, }, { data: 'value34', status: 'success', isPending: false, isFetching: false, isStale: true, }, ]) }) test('should stop listening to changes on onScopeDispose', async () => { const onScopeDisposeMock = onScopeDispose as MockedFunction< typeof onScopeDispose > onScopeDisposeMock.mockImplementationOnce((fn) => fn()) const queries = [ { queryKey: ['key41'], queryFn: () => sleep(0).then(() => 'Some data'), }, { queryKey: ['key42'], queryFn: () => sleep(0).then(() => 'Some data'), }, ] const queriesState = useQueries({ queries }) await vi.advanceTimersByTimeAsync(0) expect(queriesState.value).toMatchObject([ { status: 'pending', isPending: true, isFetching: true, isStale: true, }, { status: 'pending', isPending: true, isFetching: true, isStale: true, }, ]) }) test('should use queryClient provided via options', async () => { const queryClient = new QueryClient() const queries = [ { queryKey: ['key41'], queryFn: () => sleep(0).then(() => 'Some data'), }, { queryKey: ['key42'], queryFn: () => sleep(0).then(() => 'Some data'), }, ] useQueries({ queries }, queryClient) await vi.advanceTimersByTimeAsync(0) expect(useQueryClient).toHaveBeenCalledTimes(0) }) test('should combine queries', async () => { const firstResult = 'first result' const secondResult = 'second result' const queryClient = new QueryClient() const queries = [ { queryKey: ['key41'], queryFn: () => sleep(0).then(() => firstResult), }, { queryKey: ['key42'], queryFn: () => sleep(0).then(() => secondResult), }, ] const queriesResult = useQueries( { queries, combine: (results) => { return { combined: true, res: results.map((res) => res.data), } }, }, queryClient, ) await vi.advanceTimersByTimeAsync(0) expect(queriesResult.value).toMatchObject({ combined: true, res: [firstResult, secondResult], }) }) test('should be `enabled` to accept getter function', async () => { const fetchFn = vi.fn() const checked = ref(false) useQueries({ queries: [ { queryKey: ['enabled'], queryFn: fetchFn, enabled: () => checked.value, }, ], }) expect(fetchFn).not.toHaveBeenCalled() checked.value = true await vi.advanceTimersByTimeAsync(0) expect(fetchFn).toHaveBeenCalled() }) test('should allow getters for query keys', async () => { const fetchFn = vi.fn() const key1 = ref('key1') const key2 = ref('key2') useQueries({ queries: [ { queryKey: ['key', () => key1.value, () => key2.value], queryFn: fetchFn, }, ], }) expect(fetchFn).toHaveBeenCalledTimes(1) key1.value = 'key3' await vi.advanceTimersByTimeAsync(0) expect(fetchFn).toHaveBeenCalledTimes(2) key2.value = 'key4' await vi.advanceTimersByTimeAsync(0) expect(fetchFn).toHaveBeenCalledTimes(3) }) test('should allow arbitrarily nested getters for query keys', async () => { const fetchFn = vi.fn() const key1 = ref('key1') const key2 = ref('key2') const key3 = ref('key3') const key4 = ref('key4') const key5 = ref('key5') useQueries({ queries: [ { queryKey: [ 'key', key1, () => key2.value, { key: () => key3.value }, [{ foo: { bar: () => key4.value } }], () => ({ foo: { bar: { baz: () => key5.value, }, }, }), ], queryFn: fetchFn, }, ], }) expect(fetchFn).toHaveBeenCalledTimes(1) key1.value = 'key1-updated' await vi.advanceTimersByTimeAsync(0) expect(fetchFn).toHaveBeenCalledTimes(2) key2.value = 'key2-updated' await vi.advanceTimersByTimeAsync(0) expect(fetchFn).toHaveBeenCalledTimes(3) key3.value = 'key3-updated' await vi.advanceTimersByTimeAsync(0) expect(fetchFn).toHaveBeenCalledTimes(4) key4.value = 'key4-updated' await vi.advanceTimersByTimeAsync(0) expect(fetchFn).toHaveBeenCalledTimes(5) key5.value = 'key5-updated' await vi.advanceTimersByTimeAsync(0) expect(fetchFn).toHaveBeenCalledTimes(6) }) }) ================================================ FILE: packages/vue-query/src/__tests__/useQuery.test-d.ts ================================================ import { describe, expectTypeOf, it } from 'vitest' import { computed, reactive, ref } from 'vue-demi' import { sleep } from '@tanstack/query-test-utils' import { queryOptions, useQuery } from '..' import type { OmitKeyof, UseQueryOptions } from '..' describe('useQuery', () => { describe('Config object overload', () => { it('TData should always be defined when initialData is provided as an object', () => { const { data } = reactive( useQuery({ queryKey: ['key'], queryFn: () => { return { wow: true, } }, initialData: { wow: true, }, }), ) expectTypeOf(data).toEqualTypeOf<{ wow: boolean }>() }) it('TData should be defined when passed through queryOptions', () => { const options = queryOptions({ queryKey: ['key'], queryFn: () => { return { wow: true, } }, initialData: { wow: true, }, }) const { data } = reactive(useQuery(options)) expectTypeOf(data).toEqualTypeOf<{ wow: boolean }>() }) it('should be possible to define a different TData than TQueryFnData using select with queryOptions spread into useQuery', () => { const options = queryOptions({ queryKey: ['key'], queryFn: () => Promise.resolve(1), }) const query = reactive( useQuery({ ...options, select: (data) => data > 1, }), ) expectTypeOf(query.data).toEqualTypeOf() }) it('TData should always be defined when initialData is provided as a function which ALWAYS returns the data', () => { const { data } = reactive( useQuery({ queryKey: ['key'], queryFn: () => { return { wow: true, } }, initialData: () => ({ wow: true, }), }), ) expectTypeOf(data).toEqualTypeOf<{ wow: boolean }>() }) it('TData should have undefined in the union when initialData is NOT provided', () => { const { data } = reactive( useQuery({ queryKey: ['key'], queryFn: () => { return { wow: true, } }, }), ) expectTypeOf(data).toEqualTypeOf<{ wow: boolean } | undefined>() }) it('TData should have undefined in the union when initialData is provided as a function which can return undefined', () => { const { data } = reactive( useQuery({ queryKey: ['key'], queryFn: () => { return { wow: true, } }, initialData: () => undefined as { wow: boolean } | undefined, }), ) expectTypeOf(data).toEqualTypeOf<{ wow: boolean } | undefined>() }) it('TData should be narrowed after an isSuccess check when initialData is provided as a function which can return undefined', () => { const { data, isSuccess } = reactive( useQuery({ queryKey: ['key'], queryFn: () => { return { wow: true, } }, initialData: () => undefined as { wow: boolean } | undefined, }), ) if (isSuccess) { expectTypeOf(data).toEqualTypeOf<{ wow: boolean }>() } }) it('data should not have undefined when initialData is provided', () => { const { data } = reactive( useQuery({ queryKey: ['query-key'], initialData: 42, }), ) expectTypeOf(data).toEqualTypeOf() }) }) describe('custom composable', () => { it('should allow custom composable using UseQueryOptions', () => { const useCustomQuery = ( options?: OmitKeyof< UseQueryOptions, 'queryKey' | 'queryFn', 'safely' >, ) => { return useQuery({ ...options, queryKey: ['todos-key'], queryFn: () => Promise.resolve('data'), }) } const { data } = reactive(useCustomQuery()) expectTypeOf(data).toEqualTypeOf() }) }) describe('structuralSharing', () => { it('should be able to use structuralSharing with unknown types', () => { // https://github.com/TanStack/query/issues/6525#issuecomment-1938411343 useQuery({ queryKey: ['key'], queryFn: () => 5, structuralSharing: (oldData, newData) => { expectTypeOf(oldData).toBeUnknown() expectTypeOf(newData).toBeUnknown() return newData }, }) }) }) describe('Discriminated union return type', () => { it('data should be possibly undefined by default', () => { const query = reactive( useQuery({ queryKey: ['key'], queryFn: () => sleep(0).then(() => 'Some data'), }), ) expectTypeOf(query.data).toEqualTypeOf() }) it('data should be defined when query is success', () => { const query = reactive( useQuery({ queryKey: ['key'], queryFn: () => sleep(0).then(() => 'Some data'), }), ) if (query.isSuccess) { expectTypeOf(query.data).toEqualTypeOf() } }) it('error should be null when query is success', () => { const query = reactive( useQuery({ queryKey: ['key'], queryFn: () => sleep(0).then(() => 'Some data'), }), ) if (query.isSuccess) { expectTypeOf(query.error).toEqualTypeOf() } }) it('data should be undefined when query is pending', () => { const query = reactive( useQuery({ queryKey: ['key'], queryFn: () => sleep(0).then(() => 'Some data'), }), ) if (query.isPending) { expectTypeOf(query.data).toEqualTypeOf() } }) it('error should be defined when query is error', () => { const query = reactive( useQuery({ queryKey: ['key'], queryFn: () => sleep(0).then(() => 'Some data'), }), ) if (query.isError) { expectTypeOf(query.error).toEqualTypeOf() } }) }) describe('accept ref options', () => { it('should accept ref options', () => { const options = ref({ queryKey: ['key'], queryFn: () => sleep(0).then(() => 'Some data'), }) const query = reactive(useQuery(options)) if (query.isSuccess) { expectTypeOf(query.data).toEqualTypeOf() } }) it('should accept computed options', () => { const options = computed(() => ({ queryKey: ['key'], queryFn: () => sleep(0).then(() => 'Some data'), })) const query = reactive(useQuery(options)) if (query.isSuccess) { expectTypeOf(query.data).toEqualTypeOf() } }) it('should accept computed query options', () => { const options = computed(() => queryOptions({ queryKey: ['key'], queryFn: () => sleep(0).then(() => 'Some data'), }), ) const query = reactive(useQuery(options)) if (query.isSuccess) { expectTypeOf(query.data).toEqualTypeOf() } }) }) }) ================================================ FILE: packages/vue-query/src/__tests__/useQuery.test.ts ================================================ import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest' import { computed, getCurrentInstance, onScopeDispose, reactive, ref, } from 'vue-demi' import { QueryObserver } from '@tanstack/query-core' import { sleep } from '@tanstack/query-test-utils' import { useQuery } from '../useQuery' import { useBaseQuery } from '../useBaseQuery' import type { Mock, MockedFunction } from 'vitest' vi.mock('../useQueryClient') vi.mock('../useBaseQuery') describe('useQuery', () => { beforeEach(() => { vi.useFakeTimers() }) afterEach(() => { vi.useRealTimers() }) test('should properly execute query', () => { const queryFn = () => sleep(0).then(() => 'Some data') useQuery({ queryKey: ['key0'], queryFn, staleTime: 1000, }) expect(useBaseQuery).toBeCalledWith( QueryObserver, { queryKey: ['key0'], queryFn, staleTime: 1000, }, undefined, ) }) test('should return pending status initially', () => { const query = useQuery({ queryKey: ['key1'], queryFn: () => sleep(0).then(() => 'Some data'), }) expect(query).toMatchObject({ status: { value: 'pending' }, isPending: { value: true }, isFetching: { value: true }, isStale: { value: true }, }) }) test('should resolve to success and update reactive state: useQuery(key, dataFn)', async () => { const query = useQuery({ queryKey: ['key2'], queryFn: () => sleep(0).then(() => 'result2'), }) await vi.advanceTimersByTimeAsync(0) expect(query).toMatchObject({ status: { value: 'success' }, data: { value: 'result2' }, isPending: { value: false }, isFetching: { value: false }, isFetched: { value: true }, isSuccess: { value: true }, }) }) test('should resolve to success and update reactive state: useQuery(optionsObj)', async () => { const query = useQuery({ queryKey: ['key31'], queryFn: () => sleep(0).then(() => 'result31'), enabled: true, }) await vi.advanceTimersByTimeAsync(0) expect(query).toMatchObject({ status: { value: 'success' }, data: { value: 'result31' }, isPending: { value: false }, isFetching: { value: false }, isFetched: { value: true }, isSuccess: { value: true }, }) }) test('should resolve to success and update reactive state: useQuery(key, optionsObj)', async () => { const query = useQuery({ queryKey: ['key32'], queryFn: () => sleep(0).then(() => 'result32'), enabled: true, }) await vi.advanceTimersByTimeAsync(0) expect(query).toMatchObject({ status: { value: 'success' }, data: { value: 'result32' }, isPending: { value: false }, isFetching: { value: false }, isFetched: { value: true }, isSuccess: { value: true }, }) }) test('should reject and update reactive state', async () => { const query = useQuery({ queryKey: ['key3'], queryFn: () => sleep(0).then(() => Promise.reject(new Error('Some error'))), }) await vi.advanceTimersByTimeAsync(0) expect(query).toMatchObject({ status: { value: 'error' }, data: { value: undefined }, error: { value: { message: 'Some error' } }, isPending: { value: false }, isFetching: { value: false }, isFetched: { value: true }, isError: { value: true }, failureCount: { value: 1 }, failureReason: { value: { message: 'Some error' } }, }) }) test('should update query on reactive (Ref) key change', async () => { const secondKeyRef = ref('key7') const query = useQuery({ queryKey: ['key6', secondKeyRef], queryFn: () => sleep(10).then(() => 'Some data'), }) await vi.advanceTimersByTimeAsync(10) expect(query).toMatchObject({ status: { value: 'success' }, }) secondKeyRef.value = 'key8' await vi.advanceTimersByTimeAsync(0) expect(query).toMatchObject({ status: { value: 'pending' }, data: { value: undefined }, }) await vi.advanceTimersByTimeAsync(10) expect(query).toMatchObject({ status: { value: 'success' }, }) }) test("should update query when an option is passed as Ref and it's changed", async () => { const enabled = ref(false) const query = useQuery({ queryKey: ['key9'], queryFn: () => sleep(10).then(() => 'Some data'), enabled, }) await vi.advanceTimersByTimeAsync(0) expect(query).toMatchObject({ fetchStatus: { value: 'idle' }, data: { value: undefined }, }) enabled.value = true await vi.advanceTimersByTimeAsync(0) expect(query).toMatchObject({ fetchStatus: { value: 'fetching' }, data: { value: undefined }, }) await vi.advanceTimersByTimeAsync(10) expect(query).toMatchObject({ status: { value: 'success' }, }) }) test('should properly execute dependant queries', async () => { const { data } = useQuery({ queryKey: ['dependant1'], queryFn: () => sleep(0).then(() => 'Some data'), }) const enabled = computed(() => !!data.value) const dependentQueryFn = vi .fn() .mockImplementation(() => sleep(10).then(() => 'Some data')) const { fetchStatus, status } = useQuery( reactive({ queryKey: ['dependant2'], queryFn: dependentQueryFn, enabled, }), ) expect(data.value).toStrictEqual(undefined) expect(fetchStatus.value).toStrictEqual('idle') expect(dependentQueryFn).not.toHaveBeenCalled() await vi.advanceTimersByTimeAsync(0) expect(data.value).toStrictEqual('Some data') expect(fetchStatus.value).toStrictEqual('fetching') await vi.advanceTimersByTimeAsync(10) expect(fetchStatus.value).toStrictEqual('idle') expect(status.value).toStrictEqual('success') expect(dependentQueryFn).toHaveBeenCalledTimes(1) expect(dependentQueryFn).toHaveBeenCalledWith( expect.objectContaining({ queryKey: ['dependant2'] }), ) }) test('should stop listening to changes on onScopeDispose', async () => { const onScopeDisposeMock = onScopeDispose as MockedFunction< typeof onScopeDispose > onScopeDisposeMock.mockImplementationOnce((fn) => fn()) const { status } = useQuery({ queryKey: ['onScopeDispose'], queryFn: () => sleep(0).then(() => 'Some data'), }) expect(status.value).toStrictEqual('pending') await vi.advanceTimersByTimeAsync(0) expect(status.value).toStrictEqual('pending') await vi.advanceTimersByTimeAsync(0) expect(status.value).toStrictEqual('pending') }) test('should use the current value for the queryKey when refetch is called', async () => { const fetchFn = vi.fn() const keyRef = ref('key11') const query = useQuery({ queryKey: ['key10', keyRef], queryFn: fetchFn, enabled: false, }) expect(fetchFn).not.toHaveBeenCalled() await query.refetch() expect(fetchFn).toHaveBeenCalledTimes(1) expect(fetchFn).toHaveBeenCalledWith( expect.objectContaining({ queryKey: ['key10', 'key11'], }), ) keyRef.value = 'key12' await query.refetch() expect(fetchFn).toHaveBeenCalledTimes(2) expect(fetchFn).toHaveBeenCalledWith( expect.objectContaining({ queryKey: ['key10', 'key12'], }), ) }) test('should be `enabled` to accept getter function', async () => { const fetchFn = vi.fn() const checked = ref(false) useQuery({ queryKey: ['enabled'], queryFn: fetchFn, enabled: () => checked.value, }) expect(fetchFn).not.toHaveBeenCalled() checked.value = true await vi.advanceTimersByTimeAsync(0) expect(fetchFn).toHaveBeenCalled() }) test('should allow getters for query keys', async () => { const fetchFn = vi.fn() const key1 = ref('key1') const key2 = ref('key2') useQuery({ queryKey: ['key', () => key1.value, () => key2.value], queryFn: fetchFn, }) expect(fetchFn).toHaveBeenCalledTimes(1) key1.value = 'key3' await vi.advanceTimersByTimeAsync(0) expect(fetchFn).toHaveBeenCalledTimes(2) key2.value = 'key4' await vi.advanceTimersByTimeAsync(0) expect(fetchFn).toHaveBeenCalledTimes(3) }) test('should allow arbitrarily nested getters for query keys', async () => { const fetchFn = vi.fn() const key1 = ref('key1') const key2 = ref('key2') const key3 = ref('key3') const key4 = ref('key4') const key5 = ref('key5') useQuery({ queryKey: [ 'key', key1, () => key2.value, { key: () => key3.value }, [{ foo: { bar: () => key4.value } }], () => ({ foo: { bar: { baz: () => key5.value, }, }, }), ], queryFn: fetchFn, }) expect(fetchFn).toHaveBeenCalledTimes(1) key1.value = 'key1-updated' await vi.advanceTimersByTimeAsync(0) expect(fetchFn).toHaveBeenCalledTimes(2) key2.value = 'key2-updated' await vi.advanceTimersByTimeAsync(0) expect(fetchFn).toHaveBeenCalledTimes(3) key3.value = 'key3-updated' await vi.advanceTimersByTimeAsync(0) expect(fetchFn).toHaveBeenCalledTimes(4) key4.value = 'key4-updated' await vi.advanceTimersByTimeAsync(0) expect(fetchFn).toHaveBeenCalledTimes(5) key5.value = 'key5-updated' await vi.advanceTimersByTimeAsync(0) expect(fetchFn).toHaveBeenCalledTimes(6) }) describe('throwOnError', () => { test('should evaluate throwOnError when query is expected to throw', async () => { const boundaryFn = vi.fn() useQuery({ queryKey: ['key'], queryFn: () => sleep(0).then(() => Promise.reject(new Error('Some error'))), retry: false, throwOnError: boundaryFn, }) await vi.advanceTimersByTimeAsync(0) expect(boundaryFn).toHaveBeenCalledTimes(1) expect(boundaryFn).toHaveBeenCalledWith( Error('Some error'), expect.objectContaining({ state: expect.objectContaining({ status: 'error' }), }), ) }) }) describe('suspense', () => { test('should return a Promise', () => { const getCurrentInstanceSpy = getCurrentInstance as Mock getCurrentInstanceSpy.mockImplementation(() => ({ suspense: {} })) const query = useQuery({ queryKey: ['suspense'], queryFn: () => sleep(0).then(() => 'Some data'), }) const result = query.suspense() expect(result).toBeInstanceOf(Promise) }) test('should resolve after being enabled', async () => { const getCurrentInstanceSpy = getCurrentInstance as Mock getCurrentInstanceSpy.mockImplementation(() => ({ suspense: {} })) let afterTimeout = false const isEnabled = ref(false) const query = useQuery({ queryKey: ['suspense2'], queryFn: () => sleep(0).then(() => 'Some data'), enabled: isEnabled, }) setTimeout(() => { afterTimeout = true isEnabled.value = true }, 200) query.suspense() await vi.advanceTimersByTimeAsync(200) expect(afterTimeout).toBe(true) }) test('should resolve immediately when stale without refetching', () => { const getCurrentInstanceSpy = getCurrentInstance as Mock getCurrentInstanceSpy.mockImplementation(() => ({ suspense: {} })) const fetcherSpy = vi.fn(() => sleep(0).then(() => 'Some data')) // let afterTimeout = false; const query = useQuery({ queryKey: ['suspense3'], queryFn: () => sleep(0).then(() => 'Some data'), staleTime: 10000, initialData: 'foo', }) return query.suspense().then(() => { expect(fetcherSpy).toHaveBeenCalledTimes(0) }) }) test('should not throw from suspense by default', async () => { const getCurrentInstanceSpy = getCurrentInstance as Mock getCurrentInstanceSpy.mockImplementation(() => ({ suspense: {} })) const query = useQuery({ queryKey: ['suspense4'], queryFn: () => sleep(0).then(() => Promise.reject(new Error('Some error'))), staleTime: 10000, }) await vi.advanceTimersByTimeAsync(0) expect(query).toMatchObject({ status: { value: 'error' }, isError: { value: true }, }) }) test('should throw from suspense when throwOnError is true', async () => { const getCurrentInstanceSpy = getCurrentInstance as Mock getCurrentInstanceSpy.mockImplementation(() => ({ suspense: {} })) const boundaryFn = vi.fn() const query = useQuery({ queryKey: ['suspense5'], queryFn: () => sleep(0).then(() => Promise.reject(new Error('Some error'))), staleTime: 10000, throwOnError: boundaryFn, }) query.suspense() await vi.advanceTimersByTimeAsync(10000) expect(boundaryFn).toHaveBeenCalledTimes(2) expect(boundaryFn).toHaveBeenNthCalledWith( 1, Error('Some error'), expect.objectContaining({ state: expect.objectContaining({ status: 'error' }), }), ) expect(boundaryFn).toHaveBeenNthCalledWith( 2, Error('Some error'), expect.objectContaining({ state: expect.objectContaining({ status: 'error' }), }), ) }) }) }) ================================================ FILE: packages/vue-query/src/__tests__/useQueryClient.test.ts ================================================ import { beforeEach, describe, expect, test, vi } from 'vitest' import { hasInjectionContext, inject } from 'vue-demi' import { useQueryClient } from '../useQueryClient' import { VUE_QUERY_CLIENT } from '../utils' import type { Mock } from 'vitest' describe('useQueryClient', () => { const injectSpy = inject as Mock const hasInjectionContextSpy = hasInjectionContext as Mock beforeEach(() => { vi.restoreAllMocks() }) test('should return queryClient when it is provided in the context', () => { const queryClientMock = { name: 'Mocked client' } injectSpy.mockReturnValueOnce(queryClientMock) const queryClient = useQueryClient() expect(queryClient).toStrictEqual(queryClientMock) expect(injectSpy).toHaveBeenCalledTimes(1) expect(injectSpy).toHaveBeenCalledWith(VUE_QUERY_CLIENT) }) test('should throw an error when queryClient does not exist in the context', () => { injectSpy.mockReturnValueOnce(undefined) expect(useQueryClient).toThrowError() expect(injectSpy).toHaveBeenCalledTimes(1) expect(injectSpy).toHaveBeenCalledWith(VUE_QUERY_CLIENT) }) test('should throw an error when used outside of setup function', () => { hasInjectionContextSpy.mockReturnValueOnce(false) expect(useQueryClient).toThrowError() expect(hasInjectionContextSpy).toHaveBeenCalledTimes(1) }) test('should call inject with a custom key as a suffix', () => { const queryClientKey = 'foo' const expectedKeyParameter = `${VUE_QUERY_CLIENT}:${queryClientKey}` const queryClientMock = { name: 'Mocked client' } injectSpy.mockReturnValueOnce(queryClientMock) useQueryClient(queryClientKey) expect(injectSpy).toHaveBeenCalledWith(expectedKeyParameter) }) }) ================================================ FILE: packages/vue-query/src/__tests__/utils.test.ts ================================================ import { describe, expect, test } from 'vitest' import { reactive, ref } from 'vue-demi' import { cloneDeep, cloneDeepUnref, updateState } from '../utils' describe('utils', () => { describe('updateState', () => { test('should update first object with values from the second one', () => { const origin = { option1: 'a', option2: 'b', option3: 'c' } const update = { option1: 'x', option2: 'y', option3: 'z' } const expected = { option1: 'x', option2: 'y', option3: 'z' } updateState(origin, update) expect(origin).toEqual(expected) }) test('should update only existing keys', () => { const origin = { option1: 'a', option2: 'b' } const update = { option1: 'x', option2: 'y', option3: 'z' } const expected = { option1: 'x', option2: 'y' } updateState(origin, update) expect(origin).toEqual(expected) }) test('should remove non existing keys', () => { const origin = { option1: 'a', option2: 'b', option3: 'c' } const update = { option1: 'x', option2: 'y' } const expected = { option1: 'x', option2: 'y' } updateState(origin, update) expect(origin).toEqual(expected) }) }) describe('cloneDeep', () => { test('should copy primitives and functions AS-IS', () => { expect(cloneDeep(3456)).toBe(3456) expect(cloneDeep('theString')).toBe('theString') expect(cloneDeep(null)).toBe(null) }) test('should copy Maps and Sets AS-IS', () => { const setVal = new Set([3, 4, 5]) const setValCopy = cloneDeep(setVal) expect(setValCopy).toBe(setVal) expect(setValCopy).toStrictEqual(new Set([3, 4, 5])) const mapVal = new Map([ ['a', 'aVal'], ['b', 'bVal'], ]) const mapValCopy = cloneDeep(mapVal) expect(mapValCopy).toBe(mapVal) expect(mapValCopy).toStrictEqual( new Map([ ['a', 'aVal'], ['b', 'bVal'], ]), ) }) test('should deeply copy arrays', () => { const val = [ 25, 'str', null, new Set([3, 4]), [5, 6, { a: 1 }], undefined, ] const cp = cloneDeep(val) expect(cp).toStrictEqual([ 25, 'str', null, new Set([3, 4]), [5, 6, { a: 1 }], undefined, ]) expect(cp).not.toBe(val) expect(cp[3]).toBe(val[3]) // Set([3, 4]) expect(cp[4]).not.toBe(val[4]) // [5, 6, { a: 1 }] expect((cp[4] as Array)[2]).not.toBe((val[4] as Array)[2]) // { a : 1 } }) test('should deeply copy object', () => { const val = reactive({ a: 25, b: 'str', c: null, d: undefined, e: new Set([5, 6]), f: [3, 4], g: { fa: 26 }, }) const cp = cloneDeep(val) expect(cp).toStrictEqual({ a: 25, b: 'str', c: null, d: undefined, e: new Set([5, 6]), f: [3, 4], g: { fa: 26 }, }) expect(cp.e).toBe(val.e) // Set expect(cp.f).not.toBe(val.f) // [] expect(cp.g).not.toBe(val.g) // {} }) }) describe('cloneDeepUnref', () => { test('should unref primitives', () => { expect(cloneDeepUnref(ref(34))).toBe(34) expect(cloneDeepUnref(ref('myStr'))).toBe('myStr') }) test('should deeply unref arrays', () => { const val = ref([2, 3, ref(4), ref('5'), { a: ref(6) }, [ref(7)]]) const cp = cloneDeepUnref(val) expect(cp).toStrictEqual([2, 3, 4, '5', { a: 6 }, [7]]) }) test('should deeply unref objects', () => { const val = ref({ a: 1, b: ref(2), c: [ref('c1'), ref(['c2'])], d: { e: ref('e'), }, }) const cp = cloneDeepUnref(val) expect(cp).toEqual({ a: 1, b: 2, c: ['c1', ['c2']], d: { e: 'e' }, }) }) test('should clone getters returning values in queryKey', () => { const val = ref({ queryKey: [1, 2, () => '3'] }) const cp = cloneDeepUnref(val) expect(cp).toStrictEqual({ queryKey: [1, 2, '3'] }) }) test('should unref undefined', () => { expect(cloneDeepUnref(ref(undefined))).toBe(undefined) }) }) }) ================================================ FILE: packages/vue-query/src/__tests__/vueQueryPlugin.test.ts ================================================ import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest' import { isVue2, isVue3, ref } from 'vue-demi' import { QueryClient } from '../queryClient' import { VueQueryPlugin } from '../vueQueryPlugin' import { VUE_QUERY_CLIENT } from '../utils' import { setupDevtools } from '../devtools/devtools' import { useQuery } from '../useQuery' import { useQueries } from '../useQueries' import type { App, ComponentOptions } from 'vue' import type { Mock } from 'vitest' vi.mock('../devtools/devtools') vi.mock('../useQueryClient') vi.mock('../useBaseQuery') type UnmountCallback = () => void interface TestApp extends App { onUnmount: UnmountCallback _unmount: UnmountCallback _mixin: ComponentOptions _provided: Record $root: TestApp } const testIf = (condition: boolean) => (condition ? test : test.skip) function getAppMock(withUnmountHook = false): TestApp { const mock = { provide: vi.fn(), unmount: vi.fn(), onUnmount: withUnmountHook ? vi.fn((u: UnmountCallback) => { mock._unmount = u }) : undefined, mixin: (m: ComponentOptions) => { mock._mixin = m }, } as unknown as TestApp return mock } describe('VueQueryPlugin', () => { beforeEach(() => { vi.useFakeTimers() }) afterEach(() => { vi.useRealTimers() }) describe('devtools', () => { test('should NOT setup devtools', () => { const setupDevtoolsMock = setupDevtools as Mock const appMock = getAppMock() VueQueryPlugin.install(appMock) expect(setupDevtoolsMock).toHaveBeenCalledTimes(0) }) testIf(isVue2)('should NOT setup devtools by default', () => { const envCopy = process.env.NODE_ENV process.env.NODE_ENV = 'development' const setupDevtoolsMock = setupDevtools as Mock const appMock = getAppMock() VueQueryPlugin.install(appMock) appMock.$root = appMock appMock._mixin.beforeCreate?.call(appMock) process.env.NODE_ENV = envCopy expect(setupDevtoolsMock).toHaveBeenCalledTimes(0) }) testIf(isVue2)('should setup devtools', () => { const envCopy = process.env.NODE_ENV process.env.NODE_ENV = 'development' const setupDevtoolsMock = setupDevtools as Mock const appMock = getAppMock() VueQueryPlugin.install(appMock, { enableDevtoolsV6Plugin: true }) appMock.$root = appMock appMock._mixin.beforeCreate?.call(appMock) process.env.NODE_ENV = envCopy expect(setupDevtoolsMock).toHaveBeenCalledTimes(1) }) testIf(isVue3)('should NOT setup devtools by default', () => { const envCopy = process.env.NODE_ENV process.env.NODE_ENV = 'development' const setupDevtoolsMock = setupDevtools as Mock const appMock = getAppMock() VueQueryPlugin.install(appMock) process.env.NODE_ENV = envCopy expect(setupDevtoolsMock).toHaveBeenCalledTimes(0) }) testIf(isVue3)('should setup devtools', () => { const envCopy = process.env.NODE_ENV process.env.NODE_ENV = 'development' const setupDevtoolsMock = setupDevtools as Mock const appMock = getAppMock() VueQueryPlugin.install(appMock, { enableDevtoolsV6Plugin: true }) process.env.NODE_ENV = envCopy expect(setupDevtoolsMock).toHaveBeenCalledTimes(1) }) }) describe('when app unmounts', () => { test('should call unmount on each client when onUnmount is missing', () => { const appMock = getAppMock() const customClient = { mount: vi.fn(), unmount: vi.fn(), } as unknown as QueryClient const originalUnmount = appMock.unmount VueQueryPlugin.install(appMock, { queryClient: customClient, }) appMock.unmount() expect(appMock.unmount).not.toEqual(originalUnmount) expect(customClient.unmount).toHaveBeenCalledTimes(1) expect(originalUnmount).toHaveBeenCalledTimes(1) }) test('should call onUnmount if present', () => { const appMock = getAppMock(true) const customClient = { mount: vi.fn(), unmount: vi.fn(), } as unknown as QueryClient const originalUnmount = appMock.unmount VueQueryPlugin.install(appMock, { queryClient: customClient }) appMock._unmount() expect(appMock.unmount).toEqual(originalUnmount) expect(customClient.unmount).toHaveBeenCalledTimes(1) }) }) describe('when called without additional options', () => { testIf(isVue2)('should provide a client with default clientKey', () => { const appMock = getAppMock() VueQueryPlugin.install(appMock) appMock._mixin.beforeCreate?.call(appMock) expect(appMock._provided).toMatchObject({ VUE_QUERY_CLIENT: expect.any(QueryClient), }) }) testIf(isVue3)('should provide a client with default clientKey', () => { const appMock = getAppMock() VueQueryPlugin.install(appMock) expect(appMock.provide).toHaveBeenCalledWith( VUE_QUERY_CLIENT, expect.any(QueryClient), ) }) }) describe('when called with custom clientKey', () => { testIf(isVue2)('should provide a client with customized clientKey', () => { const appMock = getAppMock() VueQueryPlugin.install(appMock, { queryClientKey: 'CUSTOM' }) appMock._mixin.beforeCreate?.call(appMock) expect(appMock._provided).toMatchObject({ [VUE_QUERY_CLIENT + ':CUSTOM']: expect.any(QueryClient), }) }) testIf(isVue3)('should provide a client with customized clientKey', () => { const appMock = getAppMock() VueQueryPlugin.install(appMock, { queryClientKey: 'CUSTOM' }) expect(appMock.provide).toHaveBeenCalledWith( VUE_QUERY_CLIENT + ':CUSTOM', expect.any(QueryClient), ) }) }) describe('when called with custom client', () => { testIf(isVue2)('should provide that custom client', () => { const appMock = getAppMock() const customClient = { mount: vi.fn() } as unknown as QueryClient VueQueryPlugin.install(appMock, { queryClient: customClient }) appMock._mixin.beforeCreate?.call(appMock) expect(customClient.mount).toHaveBeenCalled() expect(appMock._provided).toMatchObject({ VUE_QUERY_CLIENT: customClient, }) }) testIf(isVue3)('should provide that custom client', () => { const appMock = getAppMock() const customClient = { mount: vi.fn() } as unknown as QueryClient VueQueryPlugin.install(appMock, { queryClient: customClient }) expect(customClient.mount).toHaveBeenCalled() expect(appMock.provide).toHaveBeenCalledWith( VUE_QUERY_CLIENT, customClient, ) }) }) describe('when called with custom client config', () => { testIf(isVue2)( 'should instantiate a client with the provided config', () => { const appMock = getAppMock() const config = { defaultOptions: { queries: { enabled: true } }, } VueQueryPlugin.install(appMock, { queryClientConfig: config, }) appMock._mixin.beforeCreate?.call(appMock) const client = appMock._provided.VUE_QUERY_CLIENT as QueryClient const defaultOptions = client.getDefaultOptions() expect(defaultOptions).toEqual(config.defaultOptions) }, ) testIf(isVue3)( 'should instantiate a client with the provided config', () => { const appMock = getAppMock() const config = { defaultOptions: { queries: { enabled: true } }, } VueQueryPlugin.install(appMock, { queryClientConfig: config, }) const client = (appMock.provide as Mock).mock.calls[0]?.[1] const defaultOptions = client.getDefaultOptions() expect(defaultOptions).toEqual(config.defaultOptions) }, ) }) describe('when persister is provided', () => { test('should properly modify isRestoring flag on queryClient', async () => { const appMock = getAppMock() const customClient = { mount: vi.fn(), isRestoring: ref(false), } as unknown as QueryClient VueQueryPlugin.install(appMock, { queryClient: customClient, clientPersister: () => [ vi.fn(), new Promise((resolve) => { resolve() }), ], }) expect(customClient.isRestoring?.value).toBeTruthy() await vi.advanceTimersByTimeAsync(0) expect(customClient.isRestoring?.value).toBeFalsy() }) test('should delay useQuery subscription and not call fetcher if data is not stale', async () => { const appMock = getAppMock() const customClient = new QueryClient({ defaultOptions: { queries: { staleTime: 1000 * 60 * 60, }, }, }) VueQueryPlugin.install(appMock, { queryClient: customClient, clientPersister: (client) => [ vi.fn(), new Promise((resolve) => { setTimeout(() => { client.setQueryData(['persist'], () => ({ foo: 'bar', })) resolve() }, 0) }), ], }) const fnSpy = vi.fn() const query = useQuery( { queryKey: ['persist'], queryFn: fnSpy, }, customClient, ) expect(customClient.isRestoring?.value).toBeTruthy() expect(query.isFetching.value).toBeFalsy() expect(query.data.value).toStrictEqual(undefined) expect(fnSpy).toHaveBeenCalledTimes(0) await vi.advanceTimersByTimeAsync(0) expect(customClient.isRestoring?.value).toBeFalsy() expect(query.data.value).toStrictEqual({ foo: 'bar' }) expect(fnSpy).toHaveBeenCalledTimes(0) }) test('should delay useQueries subscription and not call fetcher if data is not stale', async () => { const appMock = getAppMock() const customClient = new QueryClient({ defaultOptions: { queries: { staleTime: 1000 * 60 * 60, }, }, }) VueQueryPlugin.install(appMock, { queryClient: customClient, clientPersister: (client) => [ vi.fn(), new Promise((resolve) => { setTimeout(() => { client.setQueryData(['persist1'], () => ({ foo1: 'bar1', })) client.setQueryData(['persist2'], () => ({ foo2: 'bar2', })) resolve() }, 0) }), ], }) const fnSpy = vi.fn() const query = useQuery( { queryKey: ['persist1'], queryFn: fnSpy, }, customClient, ) const queries = useQueries( { queries: [ { queryKey: ['persist2'], queryFn: fnSpy, }, ], }, customClient, ) expect(customClient.isRestoring?.value).toBeTruthy() expect(query.isFetching.value).toBeFalsy() expect(query.data.value).toStrictEqual(undefined) expect(queries.value[0].isFetching).toBeFalsy() expect(queries.value[0].data).toStrictEqual(undefined) expect(fnSpy).toHaveBeenCalledTimes(0) await vi.advanceTimersByTimeAsync(0) expect(customClient.isRestoring?.value).toBeFalsy() expect(query.data.value).toStrictEqual({ foo1: 'bar1' }) expect(queries.value[0].data).toStrictEqual({ foo2: 'bar2' }) expect(fnSpy).toHaveBeenCalledTimes(0) }) }) }) ================================================ FILE: packages/vue-query/src/devtools/devtools.ts ================================================ import { setupDevtoolsPlugin } from '@vue/devtools-api' import { rankItem } from '@tanstack/match-sorter-utils' import { onlineManager } from '@tanstack/query-core' import { getQueryStateLabel, getQueryStatusBg, getQueryStatusFg, sortFns, } from './utils' import type { CustomInspectorNode } from '@vue/devtools-api' import type { Query, QueryCacheNotifyEvent } from '@tanstack/query-core' import type { QueryClient } from '../queryClient' const pluginId = 'vue-query' const pluginName = 'Vue Query' export function setupDevtools(app: any, queryClient: QueryClient) { setupDevtoolsPlugin( { id: pluginId, label: pluginName, packageName: 'vue-query', homepage: 'https://tanstack.com/query/latest', logo: 'https://raw.githubusercontent.com/TanStack/query/main/packages/vue-query/media/vue-query.svg', app, settings: { baseSort: { type: 'choice', component: 'button-group', label: 'Sort Cache Entries', options: [ { label: 'ASC', value: 1, }, { label: 'DESC', value: -1, }, ], defaultValue: 1, }, sortFn: { type: 'choice', label: 'Sort Function', options: Object.keys(sortFns).map((key) => ({ label: key, value: key, })), defaultValue: Object.keys(sortFns)[0]!, }, onlineMode: { type: 'choice', component: 'button-group', label: 'Online mode', options: [ { label: 'Online', value: 1, }, { label: 'Offline', value: 0, }, ], defaultValue: 1, }, }, }, (api) => { const initialSettings = api.getSettings() onlineManager.setOnline(Boolean(initialSettings.onlineMode.valueOf())) const queryCache = queryClient.getQueryCache() api.addInspector({ id: pluginId, label: pluginName, icon: 'api', nodeActions: [ { icon: 'file_download', tooltip: 'Refetch', action: (queryHash: string) => { queryCache.get(queryHash)?.fetch() }, }, { icon: 'alarm', tooltip: 'Invalidate', action: (queryHash: string) => { const query = queryCache.get(queryHash) as Query queryClient.invalidateQueries(query) }, }, { icon: 'settings_backup_restore', tooltip: 'Reset', action: (queryHash: string) => { queryCache.get(queryHash)?.reset() }, }, { icon: 'delete', tooltip: 'Remove', action: (queryHash: string) => { const query = queryCache.get(queryHash) as Query queryCache.remove(query) }, }, { icon: 'hourglass_empty', tooltip: 'Force loading', action: (queryHash: string) => { const query = queryCache.get(queryHash) as Query query.setState({ data: undefined, status: 'pending', }) }, }, { icon: 'error_outline', tooltip: 'Force error', action: (queryHash: string) => { const query = queryCache.get(queryHash) as Query query.setState({ data: undefined, status: 'error', error: new Error('Unknown error from devtools'), }) }, }, ], }) api.addTimelineLayer({ id: pluginId, label: pluginName, color: 0xffd94c, }) queryCache.subscribe((event) => { api.sendInspectorTree(pluginId) api.sendInspectorState(pluginId) const queryEvents: Array = [ 'added', 'removed', 'updated', ] if (queryEvents.includes(event.type)) { api.addTimelineEvent({ layerId: pluginId, event: { title: event.type, subtitle: event.query.queryHash, time: api.now(), data: { queryHash: event.query.queryHash, ...event, }, }, }) } }) api.on.setPluginSettings((payload) => { if (payload.key === 'onlineMode') { onlineManager.setOnline(Boolean(payload.newValue)) } }) api.on.getInspectorTree((payload) => { if (payload.inspectorId === pluginId) { const queries = queryCache.getAll() const settings = api.getSettings() const filtered = payload.filter ? queries.filter( (item) => rankItem(item.queryHash, payload.filter).passed, ) : [...queries] const sorted = filtered.sort( (a, b) => sortFns[settings.sortFn]!(a, b) * settings.baseSort, ) const nodes: Array = sorted.map((query) => { const stateLabel = getQueryStateLabel(query) return { id: query.queryHash, label: query.queryHash, tags: [ { label: `${stateLabel} [${query.getObserversCount()}]`, textColor: getQueryStatusFg(query), backgroundColor: getQueryStatusBg(query), }, ], } }) payload.rootNodes = nodes } }) api.on.getInspectorState((payload) => { if (payload.inspectorId === pluginId) { const query = queryCache.get(payload.nodeId) if (!query) { return } payload.state = { ' Query Details': [ { key: 'Query key', value: query.queryHash, }, { key: 'Query status', value: getQueryStateLabel(query), }, { key: 'Observers', value: query.getObserversCount(), }, { key: 'Last Updated', value: new Date(query.state.dataUpdatedAt).toLocaleTimeString(), }, ], 'Data Explorer': [ { key: 'Data', value: query.state.data, }, ], 'Query Explorer': [ { key: 'Query', value: query, }, ], } } }) }, ) } ================================================ FILE: packages/vue-query/src/devtools/utils.ts ================================================ import type { Query } from '@tanstack/query-core' type SortFn = (a: Query, b: Query) => number enum QueryState { Fetching = 0, Fresh, Stale, Inactive, Paused, } export function getQueryState(query: Query): QueryState { if (query.state.fetchStatus === 'fetching') { return QueryState.Fetching } if (query.state.fetchStatus === 'paused') { return QueryState.Paused } if (!query.getObserversCount()) { return QueryState.Inactive } if (query.isStale()) { return QueryState.Stale } return QueryState.Fresh } export function getQueryStateLabel(query: Query): string { const queryState = getQueryState(query) if (queryState === QueryState.Fetching) { return 'fetching' } if (queryState === QueryState.Paused) { return 'paused' } if (queryState === QueryState.Stale) { return 'stale' } if (queryState === QueryState.Inactive) { return 'inactive' } return 'fresh' } export function getQueryStatusFg(query: Query): number { const queryState = getQueryState(query) if (queryState === QueryState.Stale) { return 0x000000 } return 0xffffff } export function getQueryStatusBg(query: Query): number { const queryState = getQueryState(query) if (queryState === QueryState.Fetching) { return 0x006bff } if (queryState === QueryState.Paused) { return 0x8c49eb } if (queryState === QueryState.Stale) { return 0xffb200 } if (queryState === QueryState.Inactive) { return 0x3f4e60 } return 0x008327 } const queryHashSort: SortFn = (a, b) => a.queryHash.localeCompare(b.queryHash) const dateSort: SortFn = (a, b) => a.state.dataUpdatedAt < b.state.dataUpdatedAt ? 1 : -1 const statusAndDateSort: SortFn = (a, b) => { if (getQueryState(a) === getQueryState(b)) { return dateSort(a, b) } return getQueryState(a) > getQueryState(b) ? 1 : -1 } export const sortFns: Record = { 'Status > Last Updated': statusAndDateSort, 'Query Hash': queryHashSort, 'Last Updated': dateSort, } ================================================ FILE: packages/vue-query-devtools/eslint.config.js ================================================ // @ts-check // @ts-expect-error import pluginVue from 'eslint-plugin-vue' import rootConfig from './root.eslint.config.js' export default [...rootConfig, ...pluginVue.configs['flat/base']] ================================================ FILE: packages/vue-query-devtools/package.json ================================================ { "name": "@tanstack/vue-query-devtools", "version": "5.89.0", "description": "Developer tools to interact with and visualize the TanStack/vue-query cache", "author": "tannerlinsley", "license": "MIT", "repository": { "type": "git", "url": "https://github.com/TanStack/query.git", "directory": "packages/vue-query-devtools" }, "homepage": "https://tanstack.com/query", "funding": { "type": "github", "url": "https://github.com/sponsors/tannerlinsley" }, "scripts": { "clean": "premove ./build ./coverage ./dist-ts", "compile": "vue-tsc --build", "test:eslint": "eslint --concurrency=auto ./src", "test:types": "vue-tsc --build", "test:build": "publint --strict && attw --pack", "build": "pnpm run compile && vite build" }, "type": "module", "types": "dist/esm/index.d.ts", "module": "dist/esm/index.js", "main": "dist/esm/index.js", "exports": { ".": { "@tanstack/custom-condition": "./src/index.ts", "types": "./dist/esm/index.d.ts", "default": "./dist/esm/index.js" }, "./production": { "@tanstack/custom-condition": "./src/production.ts", "types": "./dist/esm/production.d.ts", "default": "./dist/esm/production.js" }, "./dist/production.js": { "types": "./dist/esm/production.d.ts", "default": "./dist/esm/production.js" }, "./package.json": "./package.json" }, "sideEffects": false, "files": [ "dist", "src", "!src/__tests__" ], "dependencies": { "@tanstack/query-devtools": "workspace:*" }, "devDependencies": { "@tanstack/vue-query": "workspace:*", "@vitejs/plugin-vue": "^5.2.4", "eslint-plugin-vue": "^9.27.0", "typescript": "5.8.3", "vite": "^6.3.6", "vue": "^3.4.27", "vue-tsc": "^2.2.8" }, "peerDependencies": { "@tanstack/vue-query": "workspace:^", "vue": "^3.3.0" } } ================================================ FILE: packages/vue-query-devtools/tsconfig.json ================================================ { "extends": "../../tsconfig.json", "compilerOptions": { "outDir": "./dist-ts", "rootDir": "." }, "include": ["src", "*.config.*", "package.json"], "references": [{ "path": "../vue-query" }, { "path": "../query-devtools" }] } ================================================ FILE: packages/vue-query-devtools/vite.config.ts ================================================ import { defineConfig, mergeConfig } from 'vite' import vue from '@vitejs/plugin-vue' import { tanstackViteConfig } from '@tanstack/config/vite' const config = defineConfig({ plugins: [vue()], // fix from https://github.com/vitest-dev/vitest/issues/6992#issuecomment-2509408660 resolve: { conditions: ['@tanstack/custom-condition'], }, environments: { ssr: { resolve: { conditions: ['@tanstack/custom-condition'], }, }, }, }) export default mergeConfig( config, tanstackViteConfig({ entry: ['src/index.ts', 'src/production.ts'], srcDir: 'src', }), ) ================================================ FILE: packages/vue-query-devtools/.attw.json ================================================ { "ignoreRules": [ "cjs-resolves-to-esm", "internal-resolution-error", "no-resolution" ] } ================================================ SYMLINK: packages/vue-query-devtools/root.eslint.config.js -> eslint.config.js ================================================ ================================================ FILE: packages/vue-query-devtools/src/devtools.vue ================================================ ================================================ FILE: packages/vue-query-devtools/src/index.ts ================================================ import devtools from './devtools.vue' import type { DefineComponent } from 'vue' import type { DevtoolsOptions } from './types' export const VueQueryDevtools = ( process.env.NODE_ENV !== 'development' ? function () { return null } : devtools ) as DefineComponent ================================================ FILE: packages/vue-query-devtools/src/production.ts ================================================ import devtools from './devtools.vue' export default devtools ================================================ FILE: packages/vue-query-devtools/src/types.ts ================================================ import type { DevtoolsButtonPosition, DevtoolsErrorType, DevtoolsPosition, } from '@tanstack/query-devtools' import type { QueryClient } from '@tanstack/vue-query' export interface DevtoolsOptions { /** * Set this true if you want the dev tools to default to being open */ initialIsOpen?: boolean /** * The position of the React Query logo to open and close the devtools panel. * 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' * Defaults to 'bottom-right'. */ buttonPosition?: DevtoolsButtonPosition /** * The position of the React Query devtools panel. * 'top' | 'bottom' | 'left' | 'right' * Defaults to 'bottom'. */ position?: DevtoolsPosition /** * Custom instance of QueryClient */ client?: QueryClient /** * Use this so you can define custom errors that can be shown in the devtools. */ errorTypes?: Array /** * Use this to pass a nonce to the style tag that is added to the document head. This is useful if you are using a Content Security Policy (CSP) nonce to allow inline styles. */ styleNonce?: string /** * Use this so you can attach the devtool's styles to specific element in the DOM. */ shadowDOMTarget?: ShadowRoot /** * Set this to true to hide disabled queries from the devtools panel. */ hideDisabledQueries?: boolean } ================================================ FILE: scripts/generate-labeler-config.ts ================================================ import * as fs from 'node:fs' import * as path from 'node:path' import * as prettier from 'prettier' /** Pairs of package labels and their corresponding paths */ type LabelerPair = [string, string] function readPairsFromFs(): Array { const ignored = new Set(['.DS_Store']) const pairs: Array = [] // Add subfolders in the packages folder, i.e. packages/** fs.readdirSync(path.resolve('packages')) .filter((folder) => !ignored.has(folder)) .forEach((folder) => { // Check if package.json exists for the folder before adding it if ( fs.existsSync( path.resolve(path.join('packages', folder, 'package.json')), ) ) { pairs.push([`package: ${folder}`, `packages/${folder}/**/*`]) } else { console.info( `Skipping \`${folder}\` as it does not have a \`package.json\` file.`, ) } }) // Sort by package name in alphabetical order pairs.sort((a, b) => a[0].localeCompare(b[0])) return pairs } async function generateLabelerYaml(pairs: Array): Promise { function s(n = 1) { return ' '.repeat(n) } // Convert the pairs into valid yaml const formattedPairs = pairs .map(([packageLabel, packagePath]) => { const result = [ `'${packageLabel}':`, `${s(2)}-${s(1)}changed-files:`, `${s(4)}-${s(1)}any-glob-to-any-file:${s(1)}'${packagePath}'`, ].join('\n') return result }) .join('\n') // Get the location of the Prettier config file const prettierConfigPath = await prettier.resolveConfigFile() if (!prettierConfigPath) { throw new Error( 'No Prettier config file found. Please ensure you have a Prettier config file in your project.', ) } console.info('using prettier config file at:', prettierConfigPath) // Resolve the Prettier config const prettierConfig = await prettier.resolveConfig(prettierConfigPath) console.info('using resolved prettier config:', prettierConfig) // Format the YAML string using Prettier const formattedStr = await prettier.format(formattedPairs, { parser: 'yaml', ...prettierConfig, }) return formattedStr } async function run() { console.info('Generating labeler config...') // Generate the pairs of package labels and their corresponding paths const pairs = readPairsFromFs() // Always add the docs folder pairs.push(['documentation', 'docs/**/*']) // Convert the pairs into valid yaml const yamlStr = await generateLabelerYaml(pairs) // Write to 'labeler-config.yml' const configPath = path.resolve('labeler-config.yml') fs.writeFileSync(configPath, yamlStr, { encoding: 'utf-8', }) console.info(`Generated labeler config at \`${configPath}\`!`) return } try { run().then(() => { process.exit(0) }) } catch (error) { console.error('Error generating labeler config:', error) process.exit(1) } ================================================ FILE: scripts/generateDocs.ts ================================================ import { resolve } from 'node:path' import { fileURLToPath } from 'node:url' import { readFileSync, writeFileSync } from 'node:fs' import { generateReferenceDocs } from '@tanstack/config/typedoc' import { glob } from 'tinyglobby' const __dirname = fileURLToPath(new URL('.', import.meta.url)) await generateReferenceDocs({ packages: [ { name: 'angular-query-experimental', entryPoints: [ resolve( __dirname, '../packages/angular-query-experimental/src/index.ts', ), ], tsconfig: resolve( __dirname, '../packages/angular-query-experimental/tsconfig.json', ), outputDir: resolve(__dirname, '../docs/framework/angular/reference'), exclude: ['./packages/query-core/**/*'], }, { name: 'svelte-query', entryPoints: [ resolve(__dirname, '../packages/svelte-query/src/index.ts'), ], tsconfig: resolve(__dirname, '../packages/svelte-query/tsconfig.json'), outputDir: resolve(__dirname, '../docs/framework/svelte/reference'), exclude: ['./packages/query-core/**/*'], }, ], }) // Define the pattern to match all generated markdown files const markdownFilesPattern = 'docs/framework/{angular,svelte}/reference/**/*.md' // Find all markdown files matching the pattern const markdownFiles = await glob(markdownFilesPattern) console.log(`Found ${markdownFiles.length} markdown files to process\n`) // Process each markdown file markdownFiles.forEach((file) => { const content = readFileSync(file, 'utf-8') let updatedContent = content updatedContent = updatedContent.replaceAll(/\]\(\.\.\//gm, '](../../') // updatedContent = content.replaceAll(/\]\(\.\//gm, '](../') updatedContent = updatedContent.replaceAll( /\]\((?!https?:\/\/|\/\/|\/|\.\/|\.\.\/|#)([^)]+)\)/gm, (match, p1) => `](../${p1})`, ) // Write the updated content back to the file if (updatedContent !== content) { writeFileSync(file, updatedContent, 'utf-8') console.log(`Processed file: ${file}`) } }) console.log('\n✅ All markdown files have been processed!') process.exit(0) ================================================ FILE: scripts/getTsupConfig.js ================================================ // @ts-check import { esbuildPluginFilePathExtensions } from 'esbuild-plugin-file-path-extensions' /** * @param {Object} opts - Options for building configurations. * @param {string[]} opts.entry - The entry array. * @returns {import('tsup').Options} */ export function modernConfig(opts) { return { entry: opts.entry, format: ['cjs', 'esm'], target: ['chrome91', 'firefox90', 'edge91', 'safari15', 'ios15', 'opera77'], outDir: 'build/modern', dts: true, sourcemap: true, clean: true, esbuildPlugins: [esbuildPluginFilePathExtensions({ esmExtension: 'js' })], } } /** * @param {Object} opts - Options for building configurations. * @param {string[]} opts.entry - The entry array. * @returns {import('tsup').Options} */ export function legacyConfig(opts) { return { entry: opts.entry, format: ['cjs', 'esm'], target: ['es2020', 'node16'], outDir: 'build/legacy', dts: true, sourcemap: true, clean: true, esbuildPlugins: [esbuildPluginFilePathExtensions({ esmExtension: 'js' })], } } ================================================ FILE: scripts/getViteAliases.js ================================================ // @ts-check import path from 'node:path' import ts from 'typescript' const tsconfig = ts.readConfigFile( path.resolve(__dirname, '..', 'tsconfig.json'), ts.sys.readFile, ).config export const dynamicAliases = Object.entries( tsconfig.compilerOptions.paths || {}, ).reduce((aliases, [key, [value]]) => { const aliasKey = key.replace('/*', '') aliases[aliasKey] = path.resolve(value.replace('/*', '')) return aliases }, /** @type {Record} */ ({})) ================================================ FILE: scripts/publish.ts ================================================ import { resolve } from 'node:path' import { fileURLToPath } from 'node:url' import { publish } from '@tanstack/config/publish' const __dirname = fileURLToPath(new URL('.', import.meta.url)) await publish({ packages: [ { name: '@tanstack/eslint-plugin-query', packageDir: 'packages/eslint-plugin-query', }, { name: '@tanstack/query-async-storage-persister', packageDir: 'packages/query-async-storage-persister', }, { name: '@tanstack/query-broadcast-client-experimental', packageDir: 'packages/query-broadcast-client-experimental', }, { name: '@tanstack/query-core', packageDir: 'packages/query-core', }, { name: '@tanstack/query-devtools', packageDir: 'packages/query-devtools', }, { name: '@tanstack/query-persist-client-core', packageDir: 'packages/query-persist-client-core', }, { name: '@tanstack/query-sync-storage-persister', packageDir: 'packages/query-sync-storage-persister', }, { name: '@tanstack/react-query', packageDir: 'packages/react-query', }, { name: '@tanstack/react-query-devtools', packageDir: 'packages/react-query-devtools', }, { name: '@tanstack/react-query-persist-client', packageDir: 'packages/react-query-persist-client', }, { name: '@tanstack/react-query-next-experimental', packageDir: 'packages/react-query-next-experimental', }, { name: '@tanstack/solid-query', packageDir: 'packages/solid-query', }, { name: '@tanstack/solid-query-devtools', packageDir: 'packages/solid-query-devtools', }, { name: '@tanstack/solid-query-persist-client', packageDir: 'packages/solid-query-persist-client', }, { name: '@tanstack/svelte-query', packageDir: 'packages/svelte-query', }, { name: '@tanstack/svelte-query-devtools', packageDir: 'packages/svelte-query-devtools', }, { name: '@tanstack/svelte-query-persist-client', packageDir: 'packages/svelte-query-persist-client', }, { name: '@tanstack/vue-query', packageDir: 'packages/vue-query', }, { name: '@tanstack/vue-query-devtools', packageDir: 'packages/vue-query-devtools', }, { name: '@tanstack/angular-query-experimental', packageDir: 'packages/angular-query-experimental', }, // publish when angular-query is stable // { // name: '@tanstack/angular-query-persist-client', // packageDir: 'packages/angular-persist-query-client', // }, ], branchConfigs: { main: { prerelease: false, }, alpha: { prerelease: true, }, beta: { prerelease: true, }, rc: { prerelease: true, }, v4: { prerelease: false, previousVersion: true, }, }, rootDir: resolve(__dirname, '..'), branch: process.env.BRANCH, tag: process.env.TAG, ghToken: process.env.GH_TOKEN, }) process.exit(0) ================================================ FILE: scripts/tsconfig.json ================================================ { "extends": "../tsconfig.json", "include": ["**/*"] } ================================================ FILE: scripts/verify-links.ts ================================================ import { existsSync, readFileSync, statSync } from 'node:fs' import path, { resolve } from 'node:path' import { glob } from 'tinyglobby' // @ts-ignore Could not find a declaration file for module 'markdown-link-extractor'. import markdownLinkExtractor from 'markdown-link-extractor' function isRelativeLink(link: string) { return ( link && !link.startsWith('http://') && !link.startsWith('https://') && !link.startsWith('//') && !link.startsWith('#') && !link.startsWith('mailto:') ) } function normalizePath(p: string): string { // Remove any trailing .md p = p.replace(`${path.extname(p)}`, '') return p } function fileExistsForLink( link: string, markdownFile: string, errors: Array, ): boolean { // Remove hash if present const filePart = link.split('#')[0] // If the link is empty after removing hash, it's not a file if (!filePart) return false // Normalize the markdown file path markdownFile = normalizePath(markdownFile) // Normalize the path const normalizedPath = normalizePath(filePart) // Resolve the path relative to the markdown file's directory let absPath = resolve(markdownFile, normalizedPath) // Ensure the resolved path is within /docs const docsRoot = resolve('docs') if (!absPath.startsWith(docsRoot)) { errors.push({ link, markdownFile, resolvedPath: absPath, reason: 'navigates above /docs, invalid', }) return false } // Check if this is an example path const isExample = absPath.includes('/examples/') let exists = false if (isExample) { // Transform /docs/framework/{framework}/examples/ to /examples/{framework}/ absPath = absPath.replace( /\/docs\/framework\/([^/]+)\/examples\//, '/examples/$1/', ) // For examples, we want to check if the directory exists exists = existsSync(absPath) && statSync(absPath).isDirectory() } else { // For non-examples, we want to check if the .md file exists if (!absPath.endsWith('.md')) { absPath = `${absPath}.md` } exists = existsSync(absPath) } if (!exists) { errors.push({ link, markdownFile, resolvedPath: absPath, reason: 'not found', }) } return exists } async function findMarkdownLinks() { // Find all markdown files in docs directory const markdownFiles = await glob('docs/**/*.md', { ignore: ['**/node_modules/**'], }) console.log(`Found ${markdownFiles.length} markdown files\n`) const errors: Array = [] // Process each file for (const file of markdownFiles) { const content = readFileSync(file, 'utf-8') const links: Array = markdownLinkExtractor(content) const filteredLinks = links.filter((link: any) => { if (typeof link === 'string') { return isRelativeLink(link) } else if (link && typeof link.href === 'string') { return isRelativeLink(link.href) } return false }) if (filteredLinks.length > 0) { filteredLinks.forEach((link) => { const href = typeof link === 'string' ? link : link.href fileExistsForLink(href, file, errors) }) } } if (errors.length > 0) { console.log(`\n❌ Found ${errors.length} broken links:`) errors.forEach((err) => { console.log( `${err.link}\n in: ${err.markdownFile}\n path: ${err.resolvedPath}\n why: ${err.reason}\n`, ) }) process.exit(1) } else { console.log('\n✅ No broken links found!') } } findMarkdownLinks().catch(console.error) ================================================ FILE: .nx/workflows/dynamic-changesets.yaml ================================================ distribute-on: small-changeset: 3 linux-medium-js medium-changeset: 6 linux-medium-js large-changeset: 10 linux-medium-js