diff --git a/.github/renovate.json b/.github/renovate.json index 3c12db59658..bb444bf9c29 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -3,10 +3,11 @@ "configMigration": true, "extends": [ "config:recommended", + "helpers:pinGitHubActionDigests", "group:allNonMajor", "schedule:weekly", + "security:minimumReleaseAgeNpm", ":approveMajorUpdates", - ":automergeMinor", ":disablePeerDependencies", ":maintainLockFilesMonthly", ":semanticCommits", diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 46ed81cdf2b..4586591e2f8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -25,13 +25,13 @@ If you have been assigned to fix an issue or develop a new feature, please follo pnpm install ``` - - We use [pnpm](https://pnpm.io/) v10 for package management (run in case of pnpm-related issues). + - We use [pnpm](https://pnpm.io/) v11 for package management (run in case of pnpm-related issues). ```bash corepack enable && corepack prepare ``` - - We use [nvm](https://github.com/nvm-sh/nvm) to manage node versions - please make sure to use the version mentioned in `.nvmrc` + - We use [nvm](https://github.com/nvm-sh/nvm) to manage node versions - please make sure to use the version mentioned in [.nvmrc](./.nvmrc) ```bash nvm use @@ -135,26 +135,6 @@ https://github.com/fulopkovacs/form/assets/43729152/9d35a3c3-8153-4e74-9cb2-af27 If you want to run an example without installing dependencies for the whole repo, just follow the instructions from the example's README.md file. It will then be run against the latest TanStack Query release. -## Online one-click setup - -You can use Gitpod (An Online open-source VS Code-like IDE that is free for Open Source) for developing online. With a single click it will start a workspace and automatically: - -- clone the `TanStack/query` repo. -- install all the dependencies in `/` and `/docs`. -- run below in the root(`/`) to Auto-build files. - - ```bash - npm start - ``` - -- run below in `/docs`. - - ```bash - npm run dev - ``` - -[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/TanStack/query) - ## Changesets This repo uses [Changesets](https://github.com/changesets/changesets) to automate releases. If your PR should release a new package version (patch, minor, or major), please run `pnpm changeset` and commit the file. If needed, changeset descriptions can be more descriptive, and will be included in the changelog. If your PR affects docs, examples, styles, etc., you probably don't need to generate a changeset. @@ -179,7 +159,7 @@ To run tests in a local environment, you should use `nx` commands from the root To run tests for **all packages**, run: ```bash -npm run test +pnpm run test ``` ### ✅ Run tests for a specific package @@ -187,13 +167,13 @@ npm run test To run tests for a specific package, use the following command: ```bash -npx nx run @tanstack/{package-name}:test:lib +pnpm nx run @tanstack/{package-name}:test:lib ``` For example: ```bash -npx nx run @tanstack/react-query:test:lib +pnpm nx run @tanstack/react-query:test:lib ``` ### ⚠️ Caution diff --git a/README.md b/README.md index f21aeb51217..3e4b0598ad8 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,21 @@
- TanStack Query + + + + TanStack Query +

diff --git a/docs/community-resources.md b/docs/community-resources.md index 3ce503b41a5..50753a36ad6 100644 --- a/docs/community-resources.md +++ b/docs/community-resources.md @@ -140,6 +140,11 @@ others: url: 'https://www.kubb.dev/', description: 'Generate SDKs for all your APIs', }, + { + title: '@codewithagents/openapi-react-query', + url: 'https://github.com/codewithagents/openapi-zod-ts', + description: 'Generate fully typed TanStack/React Query v5 hooks from an OpenAPI 3.1 spec, with Zod v4 runtime validation and an end-to-end pipeline (client, mocks, server).', + }, { title: 'OpenAPI codegen', url: 'https://github.com/fabien0102/openapi-codegen', diff --git a/docs/config.json b/docs/config.json index 2b1561d691a..a68832565dc 100644 --- a/docs/config.json +++ b/docs/config.json @@ -864,6 +864,10 @@ "label": "Window Focus Refetching", "to": "framework/preact/guides/window-focus-refetching" }, + { + "label": "Polling", + "to": "framework/preact/guides/polling" + }, { "label": "Disabling/Pausing Queries", "to": "framework/preact/guides/disabling-queries" diff --git a/docs/eslint/exhaustive-deps.md b/docs/eslint/exhaustive-deps.md index 3fa56f1a54f..6f6e8725bea 100644 --- a/docs/eslint/exhaustive-deps.md +++ b/docs/eslint/exhaustive-deps.md @@ -3,8 +3,10 @@ id: exhaustive-deps title: Exhaustive dependencies for query keys --- -Query keys should be seen like a dependency array to your query function: Every variable that is used inside the queryFn should be added to the query key. -This makes sure that queries are cached independently and that queries are refetched automatically when the variables changes. +Query keys should contain the serializable values that identify the data returned by your queryFn. +This makes sure that queries are cached independently and that queries are refetched automatically when those values change. + +Function call targets are not query key dependencies. For example, `fetchTodoById(todoId)` needs `todoId` in the query key, but not `fetchTodoById`. Values referenced inside nested callbacks are still dependencies, so `promise.then(() => todoId)` also needs `todoId` in the query key. ## Rule Details @@ -29,7 +31,7 @@ Examples of **correct** code for this rule: const Component = ({ todoId }) => { const todos = useTodos() useQuery({ - queryKey: ['todo', todos, todoId], + queryKey: ['todo', todoId], queryFn: () => todos.getTodo(todoId), }) } @@ -46,24 +48,12 @@ const todoQueries = { ``` ```tsx -// with { allowlist: { variables: ["todos"] }} -const Component = ({ todoId }) => { - const todos = useTodos() - useQuery({ - queryKey: ['todo', todoId], - queryFn: () => todos.getTodo(todoId), - }) -} -``` - -```tsx -// with { allowlist: { types: ["TodosClient"] }} -class TodosClient { ... } -const Component = ({ todoId }) => { - const todos: TodosClient = new TodosClient() +// with { allowlist: { types: ["Config"] }} +class Config { ... } +const Component = ({ todoId, config }: { todoId: string, config: Config }) => { useQuery({ queryKey: ['todo', todoId], - queryFn: () => todos.getTodo(todoId), + queryFn: () => fetchTodo(todoId, config.baseUrl), }) } ``` diff --git a/docs/framework/angular/devtools.md b/docs/framework/angular/devtools.md index c0cfb9141c0..0103213a350 100644 --- a/docs/framework/angular/devtools.md +++ b/docs/framework/angular/devtools.md @@ -5,9 +5,9 @@ title: Devtools > For Chrome, Firefox, and Edge users: Third-party browser extensions are available for debugging TanStack Query directly in browser DevTools. These provide the same functionality as the framework-specific devtools packages: > -> - Chrome logo [Devtools for Chrome](https://chromewebstore.google.com/detail/tanstack-query-devtools/annajfchloimdhceglpgglpeepfghfai) -> - Firefox logo [Devtools for Firefox](https://addons.mozilla.org/en-US/firefox/addon/tanstack-query-devtools/) -> - Edge logo [Devtools for Edge](https://microsoftedge.microsoft.com/addons/detail/tanstack-query-devtools/edmdpkgkacmjopodhfolmphdenmddobj) +> - Chrome logo Devtools for Chrome +> - Firefox logo Devtools for Firefox +> - Edge logo Devtools for Edge ## Enable devtools @@ -137,7 +137,7 @@ export const appConfig: ApplicationConfig = { ### Options returned from the callback -Of these options `loadDevtools`, `client`, `position`, `errorTypes`, `buttonPosition`, and `initialIsOpen` support reactivity through signals. +Of these options `loadDevtools`, `client`, `position`, `errorTypes`, `buttonPosition`, `initialIsOpen`, and `theme` support reactivity through signals. - `loadDevtools?: 'auto' | boolean` - Defaults to `auto`: lazily loads devtools when in development mode. Skips loading in production mode. @@ -162,3 +162,6 @@ Of these options `loadDevtools`, `client`, `position`, `errorTypes`, `buttonPosi - Use this to pass a shadow DOM target to the devtools so that the styles will be applied within the shadow DOM instead of within the head tag in the light DOM. - `hideDisabledQueries?: boolean` - Set this to true to hide disabled queries from the devtools panel. +- `theme?: "light" | "dark" | "system"` + - Defaults to `system`. + - Set this to change the theme of the devtools panel. diff --git a/docs/framework/angular/guides/disabling-queries.md b/docs/framework/angular/guides/disabling-queries.md index 35da0225dec..80b3c0a5237 100644 --- a/docs/framework/angular/guides/disabling-queries.md +++ b/docs/framework/angular/guides/disabling-queries.md @@ -70,7 +70,7 @@ export class TodosComponent { [//]: # 'Example3' ```angular-ts -import { skipToken, injectQuery } from '@tanstack/query-angular' +import { skipToken, injectQuery } from '@tanstack/angular-query-experimental' @Component({ selector: 'todos', diff --git a/docs/framework/angular/guides/query-functions.md b/docs/framework/angular/guides/query-functions.md index ae5f9e6c99c..b151869506f 100644 --- a/docs/framework/angular/guides/query-functions.md +++ b/docs/framework/angular/guides/query-functions.md @@ -8,7 +8,10 @@ ref: docs/framework/react/guides/query-functions.md ```ts injectQuery(() => ({ queryKey: ['todos'], queryFn: fetchAllTodos })) -injectQuery(() => ({ queryKey: ['todos', todoId], queryFn: () => fetchTodoById(todoId) }) +injectQuery(() => ({ + queryKey: ['todos', todoId], + queryFn: () => fetchTodoById(todoId), +})) injectQuery(() => ({ queryKey: ['todos', todoId], queryFn: async () => { diff --git a/docs/framework/angular/guides/query-retries.md b/docs/framework/angular/guides/query-retries.md index 45228d10bbe..74aeef9a45f 100644 --- a/docs/framework/angular/guides/query-retries.md +++ b/docs/framework/angular/guides/query-retries.md @@ -33,7 +33,7 @@ const result = injectQuery(() => ({ import { QueryCache, QueryClient, - QueryClientProvider, + provideTanStackQuery, } from '@tanstack/angular-query-experimental' const queryClient = new QueryClient({ diff --git a/docs/framework/preact/devtools.md b/docs/framework/preact/devtools.md index 4fc60265dc4..509f51637d2 100644 --- a/docs/framework/preact/devtools.md +++ b/docs/framework/preact/devtools.md @@ -9,9 +9,9 @@ When you begin your Preact Query journey, you'll want these devtools by your sid > For Chrome, Firefox, and Edge users: Third-party browser extensions are available for debugging TanStack Query directly in browser DevTools. These provide the same functionality as the framework-specific devtools packages: > -> - Chrome logo [Devtools for Chrome](https://chromewebstore.google.com/detail/tanstack-query-devtools/annajfchloimdhceglpgglpeepfghfai) -> - Firefox logo [Devtools for Firefox](https://addons.mozilla.org/en-US/firefox/addon/tanstack-query-devtools/) -> - Edge logo [Devtools for Edge](https://microsoftedge.microsoft.com/addons/detail/tanstack-query-devtools/edmdpkgkacmjopodhfolmphdenmddobj) +> - Chrome logo Devtools for Chrome +> - Firefox logo Devtools for Firefox +> - Edge logo Devtools for Edge ## Install and Import the Devtools diff --git a/docs/framework/preact/guides/polling.md b/docs/framework/preact/guides/polling.md new file mode 100644 index 00000000000..1421f4c30b1 --- /dev/null +++ b/docs/framework/preact/guides/polling.md @@ -0,0 +1,6 @@ +--- +id: polling +title: Polling +ref: docs/framework/react/guides/polling.md +replace: { '@tanstack/react-query': '@tanstack/preact-query' } +--- diff --git a/docs/framework/react/devtools.md b/docs/framework/react/devtools.md index ccdde019087..4c7b7d6d68b 100644 --- a/docs/framework/react/devtools.md +++ b/docs/framework/react/devtools.md @@ -9,9 +9,9 @@ When you begin your React Query journey, you'll want these devtools by your side > For Chrome, Firefox, and Edge users: Third-party browser extensions are available for debugging TanStack Query directly in browser DevTools. These provide the same functionality as the framework-specific devtools packages: > -> - Chrome logo [Devtools for Chrome](https://chromewebstore.google.com/detail/tanstack-query-devtools/annajfchloimdhceglpgglpeepfghfai) -> - Firefox logo [Devtools for Firefox](https://addons.mozilla.org/en-US/firefox/addon/tanstack-query-devtools/) -> - Edge logo [Devtools for Edge](https://microsoftedge.microsoft.com/addons/detail/tanstack-query-devtools/edmdpkgkacmjopodhfolmphdenmddobj) +> - Chrome logo Devtools for Chrome +> - Firefox logo Devtools for Firefox +> - Edge logo Devtools for Edge > For React Native users: A third-party native macOS app is available for debugging React Query in ANY js-based application. Monitor queries across devices in real-time. Check it out here: [rn-better-dev-tools](https://github.com/LovesWorking/rn-better-dev-tools) diff --git a/docs/framework/react/guides/parallel-queries.md b/docs/framework/react/guides/parallel-queries.md index 5d078b134ea..83e70f46f2b 100644 --- a/docs/framework/react/guides/parallel-queries.md +++ b/docs/framework/react/guides/parallel-queries.md @@ -56,3 +56,8 @@ function App({ users }) { ``` [//]: # 'Example2' +[//]: # 'TypeScriptSelect' + +> When using TypeScript, an inline `select` written on a query object passed to `useQueries` can't infer its `data` argument from that same object's `queryFn` — it falls back to `unknown`. Annotate the `select` parameter explicitly, or define the query with the [`queryOptions`](../reference/queryOptions.md) helper, to keep type inference. See [this known limitation](https://github.com/TanStack/query/issues/6556). + +[//]: # 'TypeScriptSelect' diff --git a/docs/framework/react/installation.md b/docs/framework/react/installation.md index f87d92817ef..d4ff6706f55 100644 --- a/docs/framework/react/installation.md +++ b/docs/framework/react/installation.md @@ -31,6 +31,12 @@ or bun add @tanstack/react-query ``` +or + +```bash +deno add @tanstack/react-query +``` + [//]: # 'Compatibility' React Query is compatible with React v18+ and works with ReactDOM and React Native. diff --git a/docs/framework/react/plugins/createPersister.md b/docs/framework/react/plugins/createPersister.md index 0b59b216ce1..e98b4377064 100644 --- a/docs/framework/react/plugins/createPersister.md +++ b/docs/framework/react/plugins/createPersister.md @@ -106,7 +106,7 @@ If `query` is `expired`, `busted` or `malformed` it would be removed from the st ### `persisterGc(): Promise` -This function can be used to sporadically clean up stoage from `expired`, `busted` or `malformed` entries. +This function can be used to sporadically clean up storage from `expired`, `busted` or `malformed` entries. For this function to work, your storage must expose `entries` method that would return a `key-value tuple array`. For example `Object.entries(localStorage)` for `localStorage` or `entries` from `idb-keyval`. diff --git a/docs/framework/react/reference/useQueries.md b/docs/framework/react/reference/useQueries.md index d41a73a2b1c..1048b532d29 100644 --- a/docs/framework/react/reference/useQueries.md +++ b/docs/framework/react/reference/useQueries.md @@ -65,3 +65,81 @@ The `combine` function will only re-run if: - any of the query results changed This means that an inlined `combine` function, as shown above, will run on every render. To avoid this, you can wrap the `combine` function in `useCallback`, or extract it to a stable function reference if it doesn't have any dependencies. + +## TypeScript: typing the `select` option + +Unlike `useQuery`, `useQueries` cannot infer the `data` argument of an _inline_ `select` from its sibling `queryFn`. Because `useQueries` infers the type of the whole `queries` array at once, the `select` parameter of a query object written inline cannot be contextually typed from that same object's `queryFn`, so it falls back to `unknown`. This is a [known TypeScript limitation](https://github.com/TanStack/query/issues/6556). + +```tsx +useQueries({ + queries: [ + { + queryKey: ['post', 1], + queryFn: () => fetchPost(1), + // ❌ `data` is `unknown` here + select: (data) => data.title, + }, + ], +}) +``` + +There are two supported workarounds: + +1. Annotate the `select` parameter explicitly: + +```tsx +useQueries({ + queries: [ + { + queryKey: ['post', 1], + queryFn: () => fetchPost(1), + // ✅ `data` is `Post` + select: (data: Post) => data.title, + }, + ], +}) +``` + +2. Define the query with the [`queryOptions`](./queryOptions.md) helper, which resolves its types in a single object _before_ it reaches `useQueries`: + +```tsx +const postOptions = (id: number) => + queryOptions({ + queryKey: ['post', id], + queryFn: () => fetchPost(id), + // ✅ `data` is `Post` + select: (data) => data.title, + }) + +useQueries({ queries: [postOptions(1), postOptions(2)] }) +``` + +The same limitation applies when you spread a `queryOptions` result to override its `select` inline — the overriding `select` still falls back to `unknown`: + +```tsx +useQueries({ + queries: [ + { + ...postOptions(1), + // ❌ `data` is `unknown` here + select: (data) => data.title, + }, + ], +}) +``` + +Wrap the spread in `queryOptions` again so the override is resolved before it reaches `useQueries`: + +```tsx +useQueries({ + queries: [ + queryOptions({ + ...postOptions(1), + // ✅ `data` is `Post` + select: (data) => data.title, + }), + ], +}) +``` + +The same applies to [`useSuspenseQueries`](./useSuspenseQueries.md). diff --git a/docs/framework/react/reference/useQuery.md b/docs/framework/react/reference/useQuery.md index b02775a62cf..df9f12596a5 100644 --- a/docs/framework/react/reference/useQuery.md +++ b/docs/framework/react/reference/useQuery.md @@ -8,6 +8,7 @@ const { data, dataUpdatedAt, error, + errorUpdateCount, errorUpdatedAt, failureCount, failureReason, @@ -171,6 +172,8 @@ const { - Defaults to `true` - If set to `false`, this instance of `useQuery` will not be subscribed to the cache. This means it won't trigger the `queryFn` on its own, and it won't receive updates if data gets into cache by other means. - `throwOnError: undefined | boolean | (error: TError, query: Query) => boolean` + - Optional + - Defaults to `false` - Set this to `true` if you want errors to be thrown in the render phase and propagate to the nearest error boundary - Set this to `false` to disable `suspense`'s default behavior of throwing errors to the error boundary. - 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`) diff --git a/docs/framework/react/reference/useSuspenseQueries.md b/docs/framework/react/reference/useSuspenseQueries.md index 702d791ee48..5d106da3e9a 100644 --- a/docs/framework/react/reference/useSuspenseQueries.md +++ b/docs/framework/react/reference/useSuspenseQueries.md @@ -16,6 +16,8 @@ The same as for [useQueries](./useQueries.md), except that each `query` can't ha - `enabled` - `placeholderData` +> The [`select` typing caveat](./useQueries.md#typescript-typing-the-select-option) for `useQueries` applies here as well: annotate the `select` parameter or use the [`queryOptions`](./queryOptions.md) helper to keep type inference. + **Returns** Same structure as [useQueries](./useQueries.md), except that for each `query`: diff --git a/docs/framework/solid/devtools.md b/docs/framework/solid/devtools.md index b6a2cca2480..9f5f0945ce2 100644 --- a/docs/framework/solid/devtools.md +++ b/docs/framework/solid/devtools.md @@ -9,9 +9,9 @@ When you begin your Solid Query journey, you'll want these devtools by your side > For Chrome, Firefox, and Edge users: Third-party browser extensions are available for debugging TanStack Query directly in browser DevTools. These provide the same functionality as the framework-specific devtools packages: > -> - Chrome logo [Devtools for Chrome](https://chromewebstore.google.com/detail/tanstack-query-devtools/annajfchloimdhceglpgglpeepfghfai) -> - Firefox logo [Devtools for Firefox](https://addons.mozilla.org/en-US/firefox/addon/tanstack-query-devtools/) -> - Edge logo [Devtools for Edge](https://microsoftedge.microsoft.com/addons/detail/tanstack-query-devtools/edmdpkgkacmjopodhfolmphdenmddobj) +> - Chrome logo Devtools for Chrome +> - Firefox logo Devtools for Firefox +> - Edge logo Devtools for Edge ## Install and Import the Devtools diff --git a/docs/framework/solid/reference/useQuery.md b/docs/framework/solid/reference/useQuery.md index 876c5c99729..aaca4469971 100644 --- a/docs/framework/solid/reference/useQuery.md +++ b/docs/framework/solid/reference/useQuery.md @@ -8,6 +8,7 @@ const { data, dataUpdatedAt, error, + errorUpdateCount, errorUpdatedAt, failureCount, failureReason, diff --git a/docs/framework/svelte/devtools.md b/docs/framework/svelte/devtools.md index 179690c2a63..e0465a5fab0 100644 --- a/docs/framework/svelte/devtools.md +++ b/docs/framework/svelte/devtools.md @@ -5,9 +5,9 @@ title: Devtools > For Chrome, Firefox, and Edge users: Third-party browser extensions are available for debugging TanStack Query directly in browser DevTools. These provide the same functionality as the framework-specific devtools packages: > -> - Chrome logo [Devtools for Chrome](https://chromewebstore.google.com/detail/tanstack-query-devtools/annajfchloimdhceglpgglpeepfghfai) -> - Firefox logo [Devtools for Firefox](https://addons.mozilla.org/en-US/firefox/addon/tanstack-query-devtools/) -> - Edge logo [Devtools for Edge](https://microsoftedge.microsoft.com/addons/detail/tanstack-query-devtools/edmdpkgkacmjopodhfolmphdenmddobj) +> - Chrome logo Devtools for Chrome +> - Firefox logo Devtools for Firefox +> - Edge logo Devtools for Edge ## Install and Import the Devtools diff --git a/docs/framework/svelte/overview.md b/docs/framework/svelte/overview.md index 8be6de78d34..980718456d1 100644 --- a/docs/framework/svelte/overview.md +++ b/docs/framework/svelte/overview.md @@ -37,7 +37,7 @@ Then call any function (e.g. createQuery) from any component:
- {#if query.isLoading} + {#if query.isPending}

Loading...

{:else if query.isError}

Error: {query.error.message}

diff --git a/docs/framework/vue/devtools.md b/docs/framework/vue/devtools.md index a32a5e0f5ba..cbba672f90f 100644 --- a/docs/framework/vue/devtools.md +++ b/docs/framework/vue/devtools.md @@ -9,9 +9,9 @@ When you begin your Vue Query journey, you'll want these devtools by your side. > For Chrome, Firefox, and Edge users: Third-party browser extensions are available for debugging TanStack Query directly in browser DevTools. These provide the same functionality as the framework-specific devtools packages: > -> - Chrome logo [Devtools for Chrome](https://chromewebstore.google.com/detail/tanstack-query-devtools/annajfchloimdhceglpgglpeepfghfai) -> - Firefox logo [Devtools for Firefox](https://addons.mozilla.org/en-US/firefox/addon/tanstack-query-devtools/) -> - Edge logo [Devtools for Edge](https://microsoftedge.microsoft.com/addons/detail/tanstack-query-devtools/edmdpkgkacmjopodhfolmphdenmddobj) +> - Chrome logo Devtools for Chrome +> - Firefox logo Devtools for Firefox +> - Edge logo Devtools for Edge ## Component based Devtools (Vue 3) diff --git a/docs/framework/vue/plugins/createPersister.md b/docs/framework/vue/plugins/createPersister.md index 49e1f669ba8..abb9d4db33d 100644 --- a/docs/framework/vue/plugins/createPersister.md +++ b/docs/framework/vue/plugins/createPersister.md @@ -103,7 +103,7 @@ If `query` is `expired`, `busted` or `malformed` it would be removed from the st ### `persisterGc(): Promise` -This function can be used to sporadically clean up stoage from `expired`, `busted` or `malformed` entries. +This function can be used to sporadically clean up storage from `expired`, `busted` or `malformed` entries. For this function to work, your storage must expose `entries` method that would return a `key-value tuple array`. For example `Object.entries(localStorage)` for `localStorage` or `entries` from `idb-keyval`. diff --git a/examples/angular/auto-refetching/package.json b/examples/angular/auto-refetching/package.json index ffa0ba4bb5a..9ea3e32df28 100644 --- a/examples/angular/auto-refetching/package.json +++ b/examples/angular/auto-refetching/package.json @@ -13,7 +13,7 @@ "@angular/compiler": "^20.0.0", "@angular/core": "^20.0.0", "@angular/platform-browser": "^20.0.0", - "@tanstack/angular-query-experimental": "^5.101.0", + "@tanstack/angular-query-experimental": "^5.101.4", "rxjs": "^7.8.2", "tslib": "^2.8.1", "zone.js": "0.15.0" diff --git a/examples/angular/basic-persister/package.json b/examples/angular/basic-persister/package.json index b227eed959f..e61e01b2d52 100644 --- a/examples/angular/basic-persister/package.json +++ b/examples/angular/basic-persister/package.json @@ -13,9 +13,9 @@ "@angular/compiler": "^20.0.0", "@angular/core": "^20.0.0", "@angular/platform-browser": "^20.0.0", - "@tanstack/angular-query-experimental": "^5.101.0", - "@tanstack/angular-query-persist-client": "^5.101.0", - "@tanstack/query-async-storage-persister": "^5.101.0", + "@tanstack/angular-query-experimental": "^5.101.4", + "@tanstack/angular-query-persist-client": "^5.101.4", + "@tanstack/query-async-storage-persister": "^5.101.4", "rxjs": "^7.8.2", "tslib": "^2.8.1", "zone.js": "0.15.0" diff --git a/examples/angular/basic/package.json b/examples/angular/basic/package.json index c87b2b5bf7d..d7b4faec85e 100644 --- a/examples/angular/basic/package.json +++ b/examples/angular/basic/package.json @@ -13,7 +13,7 @@ "@angular/compiler": "^20.0.0", "@angular/core": "^20.0.0", "@angular/platform-browser": "^20.0.0", - "@tanstack/angular-query-experimental": "^5.101.0", + "@tanstack/angular-query-experimental": "^5.101.4", "rxjs": "^7.8.2", "tslib": "^2.8.1", "zone.js": "0.15.0" diff --git a/examples/angular/devtools-panel/package.json b/examples/angular/devtools-panel/package.json index 0376ced8449..a6ccaa3cd1d 100644 --- a/examples/angular/devtools-panel/package.json +++ b/examples/angular/devtools-panel/package.json @@ -14,7 +14,7 @@ "@angular/core": "^20.0.0", "@angular/platform-browser": "^20.0.0", "@angular/router": "^20.0.0", - "@tanstack/angular-query-experimental": "^5.101.0", + "@tanstack/angular-query-experimental": "^5.101.4", "rxjs": "^7.8.2", "tslib": "^2.8.1", "zone.js": "0.15.0" diff --git a/examples/angular/infinite-query-with-max-pages/package.json b/examples/angular/infinite-query-with-max-pages/package.json index f7bc3b933cc..538bf951039 100644 --- a/examples/angular/infinite-query-with-max-pages/package.json +++ b/examples/angular/infinite-query-with-max-pages/package.json @@ -13,7 +13,7 @@ "@angular/compiler": "^20.0.0", "@angular/core": "^20.0.0", "@angular/platform-browser": "^20.0.0", - "@tanstack/angular-query-experimental": "^5.101.0", + "@tanstack/angular-query-experimental": "^5.101.4", "rxjs": "^7.8.2", "tslib": "^2.8.1", "zone.js": "0.15.0" diff --git a/examples/angular/optimistic-updates/package.json b/examples/angular/optimistic-updates/package.json index 10024dc6b15..02bb343a647 100644 --- a/examples/angular/optimistic-updates/package.json +++ b/examples/angular/optimistic-updates/package.json @@ -14,7 +14,7 @@ "@angular/core": "^20.0.0", "@angular/forms": "^20.0.0", "@angular/platform-browser": "^20.0.0", - "@tanstack/angular-query-experimental": "^5.101.0", + "@tanstack/angular-query-experimental": "^5.101.4", "rxjs": "^7.8.2", "tslib": "^2.8.1", "zone.js": "0.15.0" diff --git a/examples/angular/pagination/package.json b/examples/angular/pagination/package.json index 8164f6ea323..09f5257d502 100644 --- a/examples/angular/pagination/package.json +++ b/examples/angular/pagination/package.json @@ -13,7 +13,7 @@ "@angular/compiler": "^20.0.0", "@angular/core": "^20.0.0", "@angular/platform-browser": "^20.0.0", - "@tanstack/angular-query-experimental": "^5.101.0", + "@tanstack/angular-query-experimental": "^5.101.4", "rxjs": "^7.8.2", "tslib": "^2.8.1", "zone.js": "0.15.0" diff --git a/examples/angular/pagination/src/app/components/example.component.html b/examples/angular/pagination/src/app/components/example.component.html index b6ffc8c1289..e45484a8922 100644 --- a/examples/angular/pagination/src/app/components/example.component.html +++ b/examples/angular/pagination/src/app/components/example.component.html @@ -2,7 +2,7 @@

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 + suppressed 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.

diff --git a/examples/angular/query-options-from-a-service/package.json b/examples/angular/query-options-from-a-service/package.json index a95aaff2726..f3519257ec6 100644 --- a/examples/angular/query-options-from-a-service/package.json +++ b/examples/angular/query-options-from-a-service/package.json @@ -14,7 +14,7 @@ "@angular/core": "^20.0.0", "@angular/platform-browser": "^20.0.0", "@angular/router": "^20.0.0", - "@tanstack/angular-query-experimental": "^5.101.0", + "@tanstack/angular-query-experimental": "^5.101.4", "rxjs": "^7.8.2", "tslib": "^2.8.1", "zone.js": "0.15.0" diff --git a/examples/angular/router/package.json b/examples/angular/router/package.json index 68d4e07f5d8..925255bea00 100644 --- a/examples/angular/router/package.json +++ b/examples/angular/router/package.json @@ -14,7 +14,7 @@ "@angular/core": "^20.0.0", "@angular/platform-browser": "^20.0.0", "@angular/router": "^20.0.0", - "@tanstack/angular-query-experimental": "^5.101.0", + "@tanstack/angular-query-experimental": "^5.101.4", "rxjs": "^7.8.2", "tslib": "^2.8.1", "zone.js": "0.15.0" diff --git a/examples/angular/rxjs/package.json b/examples/angular/rxjs/package.json index 4859e692f81..5fefcdafddc 100644 --- a/examples/angular/rxjs/package.json +++ b/examples/angular/rxjs/package.json @@ -14,7 +14,7 @@ "@angular/core": "^20.0.0", "@angular/forms": "^20.0.0", "@angular/platform-browser": "^20.0.0", - "@tanstack/angular-query-experimental": "^5.101.0", + "@tanstack/angular-query-experimental": "^5.101.4", "rxjs": "^7.8.2", "tslib": "^2.8.1", "zone.js": "0.15.0" diff --git a/examples/angular/simple/package.json b/examples/angular/simple/package.json index 0e613ca6e45..674c0f51f58 100644 --- a/examples/angular/simple/package.json +++ b/examples/angular/simple/package.json @@ -13,7 +13,7 @@ "@angular/compiler": "^20.0.0", "@angular/core": "^20.0.0", "@angular/platform-browser": "^20.0.0", - "@tanstack/angular-query-experimental": "^5.101.0", + "@tanstack/angular-query-experimental": "^5.101.4", "rxjs": "^7.8.2", "tslib": "^2.8.1", "zone.js": "0.15.0" diff --git a/examples/lit/basic/package.json b/examples/lit/basic/package.json index 63d3825dd10..70e3e015fa2 100644 --- a/examples/lit/basic/package.json +++ b/examples/lit/basic/package.json @@ -8,8 +8,8 @@ "preview": "vite preview" }, "dependencies": { - "@tanstack/lit-query": "^0.2.7", - "@tanstack/query-core": "^5.101.0", + "@tanstack/lit-query": "^0.2.11", + "@tanstack/query-core": "^5.101.4", "lit": "^3.3.1" }, "devDependencies": { diff --git a/examples/lit/pagination/package.json b/examples/lit/pagination/package.json index 782ab70ad70..fe7e79ceba2 100644 --- a/examples/lit/pagination/package.json +++ b/examples/lit/pagination/package.json @@ -8,8 +8,8 @@ "preview": "vite preview" }, "dependencies": { - "@tanstack/lit-query": "^0.2.7", - "@tanstack/query-core": "^5.101.0", + "@tanstack/lit-query": "^0.2.11", + "@tanstack/query-core": "^5.101.4", "lit": "^3.3.1" }, "devDependencies": { diff --git a/examples/lit/ssr/package.json b/examples/lit/ssr/package.json index 846cdd904bd..02c5f35fd8e 100644 --- a/examples/lit/ssr/package.json +++ b/examples/lit/ssr/package.json @@ -8,8 +8,8 @@ }, "dependencies": { "@lit-labs/ssr": "^3.3.0", - "@tanstack/lit-query": "^0.2.7", - "@tanstack/query-core": "^5.101.0", + "@tanstack/lit-query": "^0.2.11", + "@tanstack/query-core": "^5.101.4", "lit": "^3.3.1" }, "devDependencies": { diff --git a/examples/preact/simple/package.json b/examples/preact/simple/package.json index 45aef72c406..146fb2bc674 100644 --- a/examples/preact/simple/package.json +++ b/examples/preact/simple/package.json @@ -8,7 +8,7 @@ "preview": "vite preview" }, "dependencies": { - "@tanstack/preact-query": "^5.101.0", + "@tanstack/preact-query": "^5.101.4", "preact": "^10.28.0" }, "devDependencies": { diff --git a/examples/react/algolia/package.json b/examples/react/algolia/package.json index 24f31274e3e..750045e68f0 100644 --- a/examples/react/algolia/package.json +++ b/examples/react/algolia/package.json @@ -9,13 +9,13 @@ }, "dependencies": { "@algolia/client-search": "5.2.1", - "@tanstack/react-query": "^5.101.0", - "@tanstack/react-query-devtools": "^5.101.0", + "@tanstack/react-query": "^5.101.4", + "@tanstack/react-query-devtools": "^5.101.4", "react": "^19.0.0", "react-dom": "^19.0.0" }, "devDependencies": { - "@tanstack/eslint-plugin-query": "^5.101.0", + "@tanstack/eslint-plugin-query": "^5.101.4", "@types/react": "^18.2.79", "@types/react-dom": "^18.2.25", "@vitejs/plugin-react": "^4.3.4", diff --git a/examples/react/auto-refetching/package.json b/examples/react/auto-refetching/package.json index 128303786d7..0f850a9d061 100644 --- a/examples/react/auto-refetching/package.json +++ b/examples/react/auto-refetching/package.json @@ -8,8 +8,8 @@ "start": "next start" }, "dependencies": { - "@tanstack/react-query": "^5.101.0", - "@tanstack/react-query-devtools": "^5.101.0", + "@tanstack/react-query": "^5.101.4", + "@tanstack/react-query-devtools": "^5.101.4", "next": "^16.0.7", "react": "^19.2.1", "react-dom": "^19.2.1" diff --git a/examples/react/basic-graphql-request/package.json b/examples/react/basic-graphql-request/package.json index bf03398edcc..ecae417c08e 100644 --- a/examples/react/basic-graphql-request/package.json +++ b/examples/react/basic-graphql-request/package.json @@ -8,8 +8,8 @@ "preview": "vite preview" }, "dependencies": { - "@tanstack/react-query": "^5.101.0", - "@tanstack/react-query-devtools": "^5.101.0", + "@tanstack/react-query": "^5.101.4", + "@tanstack/react-query-devtools": "^5.101.4", "graphql": "^16.9.0", "graphql-request": "^7.1.2", "react": "^19.0.0", diff --git a/examples/react/basic/package.json b/examples/react/basic/package.json index 08fe16d9386..ec4b29bb526 100644 --- a/examples/react/basic/package.json +++ b/examples/react/basic/package.json @@ -9,15 +9,15 @@ "test:eslint": "eslint ./src" }, "dependencies": { - "@tanstack/query-async-storage-persister": "^5.101.0", - "@tanstack/react-query": "^5.101.0", - "@tanstack/react-query-devtools": "^5.101.0", - "@tanstack/react-query-persist-client": "^5.101.0", + "@tanstack/query-async-storage-persister": "^5.101.4", + "@tanstack/react-query": "^5.101.4", + "@tanstack/react-query-devtools": "^5.101.4", + "@tanstack/react-query-persist-client": "^5.101.4", "react": "^19.0.0", "react-dom": "^19.0.0" }, "devDependencies": { - "@tanstack/eslint-plugin-query": "^5.101.0", + "@tanstack/eslint-plugin-query": "^5.101.4", "@types/react": "^18.2.79", "@types/react-dom": "^18.2.25", "@vitejs/plugin-react": "^4.3.4", diff --git a/examples/react/chat/package.json b/examples/react/chat/package.json index 1309e81e964..7576acf8c91 100644 --- a/examples/react/chat/package.json +++ b/examples/react/chat/package.json @@ -8,8 +8,8 @@ "preview": "vite preview" }, "dependencies": { - "@tanstack/react-query": "^5.101.0", - "@tanstack/react-query-devtools": "^5.101.0", + "@tanstack/react-query": "^5.101.4", + "@tanstack/react-query-devtools": "^5.101.4", "react": "^19.0.0", "react-dom": "^19.0.0" }, diff --git a/examples/react/default-query-function/package.json b/examples/react/default-query-function/package.json index 2f38d6ba8b4..980bcd11990 100644 --- a/examples/react/default-query-function/package.json +++ b/examples/react/default-query-function/package.json @@ -8,8 +8,8 @@ "preview": "vite preview" }, "dependencies": { - "@tanstack/react-query": "^5.101.0", - "@tanstack/react-query-devtools": "^5.101.0", + "@tanstack/react-query": "^5.101.4", + "@tanstack/react-query-devtools": "^5.101.4", "react": "^19.0.0", "react-dom": "^19.0.0" }, diff --git a/examples/react/devtools-panel/package.json b/examples/react/devtools-panel/package.json index 63af0858e27..f7a41e8f9e3 100644 --- a/examples/react/devtools-panel/package.json +++ b/examples/react/devtools-panel/package.json @@ -8,8 +8,8 @@ "preview": "vite preview" }, "dependencies": { - "@tanstack/react-query": "^5.101.0", - "@tanstack/react-query-devtools": "^5.101.0", + "@tanstack/react-query": "^5.101.4", + "@tanstack/react-query-devtools": "^5.101.4", "react": "^19.0.0", "react-dom": "^19.0.0" }, diff --git a/examples/react/eslint-legacy/package.json b/examples/react/eslint-legacy/package.json index d4bfab89688..a3bdc2d4cc1 100644 --- a/examples/react/eslint-legacy/package.json +++ b/examples/react/eslint-legacy/package.json @@ -9,15 +9,15 @@ "test:eslint": "ESLINT_USE_FLAT_CONFIG=false eslint ./src/**/*.tsx" }, "dependencies": { - "@tanstack/query-async-storage-persister": "^5.101.0", - "@tanstack/react-query": "^5.101.0", - "@tanstack/react-query-devtools": "^5.101.0", - "@tanstack/react-query-persist-client": "^5.101.0", + "@tanstack/query-async-storage-persister": "^5.101.4", + "@tanstack/react-query": "^5.101.4", + "@tanstack/react-query-devtools": "^5.101.4", + "@tanstack/react-query-persist-client": "^5.101.4", "react": "^19.0.0", "react-dom": "^19.0.0" }, "devDependencies": { - "@tanstack/eslint-plugin-query": "^5.101.0", + "@tanstack/eslint-plugin-query": "^5.101.4", "@types/react": "^18.2.79", "@types/react-dom": "^18.2.25", "@vitejs/plugin-react": "^4.3.4", diff --git a/examples/react/eslint-plugin-demo/package.json b/examples/react/eslint-plugin-demo/package.json index d9e9f1241aa..743090decab 100644 --- a/examples/react/eslint-plugin-demo/package.json +++ b/examples/react/eslint-plugin-demo/package.json @@ -6,11 +6,11 @@ "test:eslint": "eslint ./src" }, "dependencies": { - "@tanstack/react-query": "^5.101.0", + "@tanstack/react-query": "^5.101.4", "react": "^19.0.0" }, "devDependencies": { - "@tanstack/eslint-plugin-query": "^5.101.0", + "@tanstack/eslint-plugin-query": "^5.101.4", "eslint": "^9.39.0", "typescript": "5.8.3", "typescript-eslint": "^8.48.0" diff --git a/examples/react/infinite-query-with-max-pages/package.json b/examples/react/infinite-query-with-max-pages/package.json index cd12dbd5474..3dcd817efaa 100644 --- a/examples/react/infinite-query-with-max-pages/package.json +++ b/examples/react/infinite-query-with-max-pages/package.json @@ -8,8 +8,8 @@ "start": "next start" }, "dependencies": { - "@tanstack/react-query": "^5.101.0", - "@tanstack/react-query-devtools": "^5.101.0", + "@tanstack/react-query": "^5.101.4", + "@tanstack/react-query-devtools": "^5.101.4", "next": "^16.0.7", "react": "^19.2.1", "react-dom": "^19.2.1" diff --git a/examples/react/load-more-infinite-scroll/package.json b/examples/react/load-more-infinite-scroll/package.json index 23f943f9bad..323f6923627 100644 --- a/examples/react/load-more-infinite-scroll/package.json +++ b/examples/react/load-more-infinite-scroll/package.json @@ -8,8 +8,8 @@ "start": "next start" }, "dependencies": { - "@tanstack/react-query": "^5.101.0", - "@tanstack/react-query-devtools": "^5.101.0", + "@tanstack/react-query": "^5.101.4", + "@tanstack/react-query-devtools": "^5.101.4", "next": "^16.0.7", "react": "^19.2.1", "react-dom": "^19.2.1", diff --git a/examples/react/nextjs-app-prefetching/package.json b/examples/react/nextjs-app-prefetching/package.json index b48c308d6d1..7b1086cd985 100644 --- a/examples/react/nextjs-app-prefetching/package.json +++ b/examples/react/nextjs-app-prefetching/package.json @@ -8,8 +8,8 @@ "start": "next start" }, "dependencies": { - "@tanstack/react-query": "^5.101.0", - "@tanstack/react-query-devtools": "^5.101.0", + "@tanstack/react-query": "^5.101.4", + "@tanstack/react-query-devtools": "^5.101.4", "next": "^16.0.7", "react": "^19.2.1", "react-dom": "^19.2.1" diff --git a/examples/react/nextjs-suspense-streaming/package.json b/examples/react/nextjs-suspense-streaming/package.json index 0ca611d4b40..21016424053 100644 --- a/examples/react/nextjs-suspense-streaming/package.json +++ b/examples/react/nextjs-suspense-streaming/package.json @@ -8,9 +8,9 @@ "start": "next start" }, "dependencies": { - "@tanstack/react-query": "^5.101.0", - "@tanstack/react-query-devtools": "^5.101.0", - "@tanstack/react-query-next-experimental": "^5.101.0", + "@tanstack/react-query": "^5.101.4", + "@tanstack/react-query-devtools": "^5.101.4", + "@tanstack/react-query-next-experimental": "^5.101.4", "next": "^16.0.7", "react": "^19.2.1", "react-dom": "^19.2.1" diff --git a/examples/react/nextjs/package.json b/examples/react/nextjs/package.json index d5eae449f78..d1170e05fdc 100644 --- a/examples/react/nextjs/package.json +++ b/examples/react/nextjs/package.json @@ -8,8 +8,8 @@ "start": "next start" }, "dependencies": { - "@tanstack/react-query": "^5.101.0", - "@tanstack/react-query-devtools": "^5.101.0", + "@tanstack/react-query": "^5.101.4", + "@tanstack/react-query-devtools": "^5.101.4", "next": "^16.0.7", "react": "^19.2.1", "react-dom": "^19.2.1" diff --git a/examples/react/offline/package.json b/examples/react/offline/package.json index e07fb18b391..73d19703532 100644 --- a/examples/react/offline/package.json +++ b/examples/react/offline/package.json @@ -8,11 +8,11 @@ "preview": "vite preview" }, "dependencies": { - "@tanstack/query-async-storage-persister": "^5.101.0", + "@tanstack/query-async-storage-persister": "^5.101.4", "@tanstack/react-location": "^3.7.4", - "@tanstack/react-query": "^5.101.0", - "@tanstack/react-query-devtools": "^5.101.0", - "@tanstack/react-query-persist-client": "^5.101.0", + "@tanstack/react-query": "^5.101.4", + "@tanstack/react-query-devtools": "^5.101.4", + "@tanstack/react-query-persist-client": "^5.101.4", "msw": "^2.6.6", "react": "^19.0.0", "react-dom": "^19.0.0", diff --git a/examples/react/optimistic-updates-cache/package.json b/examples/react/optimistic-updates-cache/package.json index 6249fa08521..ec228f4d294 100755 --- a/examples/react/optimistic-updates-cache/package.json +++ b/examples/react/optimistic-updates-cache/package.json @@ -8,8 +8,8 @@ "start": "next start" }, "dependencies": { - "@tanstack/react-query": "^5.101.0", - "@tanstack/react-query-devtools": "^5.101.0", + "@tanstack/react-query": "^5.101.4", + "@tanstack/react-query-devtools": "^5.101.4", "next": "^16.0.7", "react": "^19.2.1", "react-dom": "^19.2.1" diff --git a/examples/react/optimistic-updates-ui/package.json b/examples/react/optimistic-updates-ui/package.json index c22ee02e1c0..51e9ba16a8c 100755 --- a/examples/react/optimistic-updates-ui/package.json +++ b/examples/react/optimistic-updates-ui/package.json @@ -8,8 +8,8 @@ "start": "next start" }, "dependencies": { - "@tanstack/react-query": "^5.101.0", - "@tanstack/react-query-devtools": "^5.101.0", + "@tanstack/react-query": "^5.101.4", + "@tanstack/react-query-devtools": "^5.101.4", "next": "^16.0.7", "react": "^19.2.1", "react-dom": "^19.2.1" diff --git a/examples/react/pagination/package.json b/examples/react/pagination/package.json index 02de7bcac16..c44d3855a42 100644 --- a/examples/react/pagination/package.json +++ b/examples/react/pagination/package.json @@ -8,8 +8,8 @@ "start": "next start" }, "dependencies": { - "@tanstack/react-query": "^5.101.0", - "@tanstack/react-query-devtools": "^5.101.0", + "@tanstack/react-query": "^5.101.4", + "@tanstack/react-query-devtools": "^5.101.4", "next": "^16.0.7", "react": "^19.2.1", "react-dom": "^19.2.1" diff --git a/examples/react/pagination/src/pages/index.tsx b/examples/react/pagination/src/pages/index.tsx index 64eec265f0c..dee4d49af67 100644 --- a/examples/react/pagination/src/pages/index.tsx +++ b/examples/react/pagination/src/pages/index.tsx @@ -54,7 +54,7 @@ function Example() {

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 + suppressed 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. diff --git a/examples/react/playground/package.json b/examples/react/playground/package.json index 2d819f980ef..c40e3873578 100644 --- a/examples/react/playground/package.json +++ b/examples/react/playground/package.json @@ -8,8 +8,8 @@ "preview": "vite preview" }, "dependencies": { - "@tanstack/react-query": "^5.101.0", - "@tanstack/react-query-devtools": "^5.101.0", + "@tanstack/react-query": "^5.101.4", + "@tanstack/react-query-devtools": "^5.101.4", "react": "^19.0.0", "react-dom": "^19.0.0" }, diff --git a/examples/react/prefetching/package.json b/examples/react/prefetching/package.json index 073cfa815f8..a9336403b36 100644 --- a/examples/react/prefetching/package.json +++ b/examples/react/prefetching/package.json @@ -8,8 +8,8 @@ "start": "next start" }, "dependencies": { - "@tanstack/react-query": "^5.101.0", - "@tanstack/react-query-devtools": "^5.101.0", + "@tanstack/react-query": "^5.101.4", + "@tanstack/react-query-devtools": "^5.101.4", "next": "^16.0.7", "react": "^19.2.1", "react-dom": "^19.2.1" diff --git a/examples/react/react-native/package.json b/examples/react/react-native/package.json index 735c7ff00b1..4c1e9eddecc 100644 --- a/examples/react/react-native/package.json +++ b/examples/react/react-native/package.json @@ -14,8 +14,8 @@ "@react-native-community/netinfo": "^11.4.1", "@react-navigation/native": "^6.1.18", "@react-navigation/stack": "^6.4.1", - "@tanstack/react-query": "^5.101.0", - "@tanstack/react-query-devtools": "^5.101.0", + "@tanstack/react-query": "^5.101.4", + "@tanstack/react-query-devtools": "^5.101.4", "expo": "^52.0.11", "expo-constants": "^17.0.3", "expo-status-bar": "^2.0.0", diff --git a/examples/react/react-router/package.json b/examples/react/react-router/package.json index 2f071a89287..b04f16814b2 100644 --- a/examples/react/react-router/package.json +++ b/examples/react/react-router/package.json @@ -8,8 +8,8 @@ "preview": "vite preview" }, "dependencies": { - "@tanstack/react-query": "^5.101.0", - "@tanstack/react-query-devtools": "^5.101.0", + "@tanstack/react-query": "^5.101.4", + "@tanstack/react-query-devtools": "^5.101.4", "localforage": "^1.10.0", "match-sorter": "^6.3.4", "react": "^19.0.0", diff --git a/examples/react/rick-morty/package.json b/examples/react/rick-morty/package.json index c9b8bcb9d5e..7c3eb3f4f09 100644 --- a/examples/react/rick-morty/package.json +++ b/examples/react/rick-morty/package.json @@ -8,8 +8,8 @@ "preview": "vite preview" }, "dependencies": { - "@tanstack/react-query": "^5.101.0", - "@tanstack/react-query-devtools": "^5.101.0", + "@tanstack/react-query": "^5.101.4", + "@tanstack/react-query-devtools": "^5.101.4", "react": "^19.0.0", "react-dom": "^19.0.0", "react-router": "^6.25.1", diff --git a/examples/react/shadow-dom/package.json b/examples/react/shadow-dom/package.json index 22bd20372e7..f5d47388ac7 100644 --- a/examples/react/shadow-dom/package.json +++ b/examples/react/shadow-dom/package.json @@ -8,8 +8,8 @@ "preview": "vite preview" }, "dependencies": { - "@tanstack/react-query": "^5.101.0", - "@tanstack/react-query-devtools": "^5.101.0", + "@tanstack/react-query": "^5.101.4", + "@tanstack/react-query-devtools": "^5.101.4", "react": "^19.0.0", "react-dom": "^19.0.0" }, diff --git a/examples/react/simple/package.json b/examples/react/simple/package.json index ae93cfe1847..6960cabdfd3 100644 --- a/examples/react/simple/package.json +++ b/examples/react/simple/package.json @@ -8,8 +8,8 @@ "preview": "vite preview" }, "dependencies": { - "@tanstack/react-query": "^5.101.0", - "@tanstack/react-query-devtools": "^5.101.0", + "@tanstack/react-query": "^5.101.4", + "@tanstack/react-query-devtools": "^5.101.4", "react": "^19.0.0", "react-dom": "^19.0.0" }, diff --git a/examples/react/star-wars/package.json b/examples/react/star-wars/package.json index db76467358c..eb6791dac5e 100644 --- a/examples/react/star-wars/package.json +++ b/examples/react/star-wars/package.json @@ -8,8 +8,8 @@ "preview": "vite preview" }, "dependencies": { - "@tanstack/react-query": "^5.101.0", - "@tanstack/react-query-devtools": "^5.101.0", + "@tanstack/react-query": "^5.101.4", + "@tanstack/react-query-devtools": "^5.101.4", "react": "^19.0.0", "react-dom": "^19.0.0", "react-router": "^6.25.1", diff --git a/examples/react/suspense/package.json b/examples/react/suspense/package.json index d4b105c1c71..1bb90e86486 100644 --- a/examples/react/suspense/package.json +++ b/examples/react/suspense/package.json @@ -8,8 +8,8 @@ "preview": "vite preview" }, "dependencies": { - "@tanstack/react-query": "^5.101.0", - "@tanstack/react-query-devtools": "^5.101.0", + "@tanstack/react-query": "^5.101.4", + "@tanstack/react-query-devtools": "^5.101.4", "font-awesome": "^4.7.0", "react": "^19.0.0", "react-dom": "^19.0.0", diff --git a/examples/solid/offline/package.json b/examples/solid/offline/package.json index e00db0413e8..eeeb9531fcd 100644 --- a/examples/solid/offline/package.json +++ b/examples/solid/offline/package.json @@ -8,7 +8,7 @@ "preview": "vite preview" }, "dependencies": { - "@tanstack/query-async-storage-persister": "^5.101.0", + "@tanstack/query-async-storage-persister": "^5.101.4", "@tanstack/solid-query": "^6.0.0-rc.0", "@tanstack/solid-query-devtools": "^6.0.0-rc.0", "@tanstack/solid-query-persist-client": "^6.0.0-rc.0", diff --git a/examples/solid/simple/package.json b/examples/solid/simple/package.json index 9ee36c974e0..524b8b9707b 100644 --- a/examples/solid/simple/package.json +++ b/examples/solid/simple/package.json @@ -15,7 +15,7 @@ }, "devDependencies": { "@solidjs/vite-plugin": "^3.0.0-next.27", - "@tanstack/eslint-plugin-query": "^5.101.0", + "@tanstack/eslint-plugin-query": "^5.101.4", "typescript": "5.8.3", "vite": "^6.4.1" } diff --git a/examples/solid/solid-start-streaming/src/routes/with-error.tsx b/examples/solid/solid-start-streaming/src/routes/with-error.tsx index e437d9444b5..5de6ca7046e 100644 --- a/examples/solid/solid-start-streaming/src/routes/with-error.tsx +++ b/examples/solid/solid-start-streaming/src/routes/with-error.tsx @@ -13,7 +13,7 @@ export default function Streamed() {

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. + `components/query-boundary.tsx` for one possible approach.

diff --git a/examples/svelte/auto-refetching/package.json b/examples/svelte/auto-refetching/package.json index cb49311f46c..3aadc715709 100644 --- a/examples/svelte/auto-refetching/package.json +++ b/examples/svelte/auto-refetching/package.json @@ -8,8 +8,8 @@ "preview": "vite preview" }, "dependencies": { - "@tanstack/svelte-query": "^6.1.34", - "@tanstack/svelte-query-devtools": "^6.1.34" + "@tanstack/svelte-query": "^6.1.38", + "@tanstack/svelte-query-devtools": "^6.1.38" }, "devDependencies": { "@sveltejs/adapter-auto": "^6.1.0", diff --git a/examples/svelte/basic/package.json b/examples/svelte/basic/package.json index f6d34bd8518..c33732f0a96 100644 --- a/examples/svelte/basic/package.json +++ b/examples/svelte/basic/package.json @@ -8,10 +8,10 @@ "preview": "vite preview" }, "dependencies": { - "@tanstack/query-async-storage-persister": "^5.101.0", - "@tanstack/svelte-query": "^6.1.34", - "@tanstack/svelte-query-devtools": "^6.1.34", - "@tanstack/svelte-query-persist-client": "^6.1.34" + "@tanstack/query-async-storage-persister": "^5.101.4", + "@tanstack/svelte-query": "^6.1.38", + "@tanstack/svelte-query-devtools": "^6.1.38", + "@tanstack/svelte-query-persist-client": "^6.1.38" }, "devDependencies": { "@sveltejs/adapter-auto": "^6.1.0", diff --git a/examples/svelte/load-more-infinite-scroll/package.json b/examples/svelte/load-more-infinite-scroll/package.json index 15074a7de2f..2b418e8ab11 100644 --- a/examples/svelte/load-more-infinite-scroll/package.json +++ b/examples/svelte/load-more-infinite-scroll/package.json @@ -8,8 +8,8 @@ "preview": "vite preview" }, "dependencies": { - "@tanstack/svelte-query": "^6.1.34", - "@tanstack/svelte-query-devtools": "^6.1.34" + "@tanstack/svelte-query": "^6.1.38", + "@tanstack/svelte-query-devtools": "^6.1.38" }, "devDependencies": { "@sveltejs/adapter-auto": "^6.1.0", diff --git a/examples/svelte/optimistic-updates/package.json b/examples/svelte/optimistic-updates/package.json index 6da280bc9c8..4b14b789a8e 100644 --- a/examples/svelte/optimistic-updates/package.json +++ b/examples/svelte/optimistic-updates/package.json @@ -8,8 +8,8 @@ "preview": "vite preview" }, "dependencies": { - "@tanstack/svelte-query": "^6.1.34", - "@tanstack/svelte-query-devtools": "^6.1.34" + "@tanstack/svelte-query": "^6.1.38", + "@tanstack/svelte-query-devtools": "^6.1.38" }, "devDependencies": { "@sveltejs/adapter-auto": "^6.1.0", diff --git a/examples/svelte/playground/package.json b/examples/svelte/playground/package.json index 448b42fb2c0..7bf8ae7ba4e 100644 --- a/examples/svelte/playground/package.json +++ b/examples/svelte/playground/package.json @@ -8,8 +8,8 @@ "preview": "vite preview" }, "dependencies": { - "@tanstack/svelte-query": "^6.1.34", - "@tanstack/svelte-query-devtools": "^6.1.34" + "@tanstack/svelte-query": "^6.1.38", + "@tanstack/svelte-query-devtools": "^6.1.38" }, "devDependencies": { "@sveltejs/adapter-auto": "^6.1.0", diff --git a/examples/svelte/simple/package.json b/examples/svelte/simple/package.json index 8a4ced340f7..a7e5302a989 100644 --- a/examples/svelte/simple/package.json +++ b/examples/svelte/simple/package.json @@ -8,8 +8,8 @@ "preview": "vite preview" }, "dependencies": { - "@tanstack/svelte-query": "^6.1.34", - "@tanstack/svelte-query-devtools": "^6.1.34" + "@tanstack/svelte-query": "^6.1.38", + "@tanstack/svelte-query-devtools": "^6.1.38" }, "devDependencies": { "@sveltejs/vite-plugin-svelte": "^5.1.1", diff --git a/examples/svelte/ssr/package.json b/examples/svelte/ssr/package.json index b7d41464b17..d87efacff82 100644 --- a/examples/svelte/ssr/package.json +++ b/examples/svelte/ssr/package.json @@ -8,8 +8,8 @@ "preview": "vite preview" }, "dependencies": { - "@tanstack/svelte-query": "^6.1.34", - "@tanstack/svelte-query-devtools": "^6.1.34" + "@tanstack/svelte-query": "^6.1.38", + "@tanstack/svelte-query-devtools": "^6.1.38" }, "devDependencies": { "@sveltejs/adapter-auto": "^6.1.0", diff --git a/examples/svelte/star-wars/package.json b/examples/svelte/star-wars/package.json index 6f654eef9f6..87f8491d7b0 100644 --- a/examples/svelte/star-wars/package.json +++ b/examples/svelte/star-wars/package.json @@ -8,8 +8,8 @@ "preview": "vite preview" }, "dependencies": { - "@tanstack/svelte-query": "^6.1.34", - "@tanstack/svelte-query-devtools": "^6.1.34" + "@tanstack/svelte-query": "^6.1.38", + "@tanstack/svelte-query-devtools": "^6.1.38" }, "devDependencies": { "@sveltejs/adapter-auto": "^6.1.0", diff --git a/examples/vue/basic/package.json b/examples/vue/basic/package.json index 81b7be813a1..b4b88c1ba48 100644 --- a/examples/vue/basic/package.json +++ b/examples/vue/basic/package.json @@ -8,8 +8,8 @@ "preview": "vite preview" }, "dependencies": { - "@tanstack/vue-query": "^5.101.0", - "@tanstack/vue-query-devtools": "^6.1.34", + "@tanstack/vue-query": "^5.101.4", + "@tanstack/vue-query-devtools": "^6.1.38", "vue": "^3.4.27" }, "devDependencies": { diff --git a/examples/vue/dependent-queries/package.json b/examples/vue/dependent-queries/package.json index fdfb895062d..2bf7264a605 100644 --- a/examples/vue/dependent-queries/package.json +++ b/examples/vue/dependent-queries/package.json @@ -8,7 +8,7 @@ "preview": "vite preview" }, "dependencies": { - "@tanstack/vue-query": "^5.101.0", + "@tanstack/vue-query": "^5.101.4", "vue": "^3.4.27" }, "devDependencies": { diff --git a/examples/vue/persister/package.json b/examples/vue/persister/package.json index 65c42729c3d..865ce57eacb 100644 --- a/examples/vue/persister/package.json +++ b/examples/vue/persister/package.json @@ -8,10 +8,10 @@ "preview": "vite preview" }, "dependencies": { - "@tanstack/query-core": "^5.101.0", - "@tanstack/query-persist-client-core": "^5.101.0", - "@tanstack/query-sync-storage-persister": "^5.101.0", - "@tanstack/vue-query": "^5.101.0", + "@tanstack/query-core": "^5.101.4", + "@tanstack/query-persist-client-core": "^5.101.4", + "@tanstack/query-sync-storage-persister": "^5.101.4", + "@tanstack/vue-query": "^5.101.4", "idb-keyval": "^6.2.1", "vue": "^3.4.27" }, diff --git a/examples/vue/simple/package.json b/examples/vue/simple/package.json index b41562f8b53..ac1bb536ebb 100644 --- a/examples/vue/simple/package.json +++ b/examples/vue/simple/package.json @@ -8,8 +8,8 @@ "preview": "vite preview" }, "dependencies": { - "@tanstack/vue-query": "^5.101.0", - "@tanstack/vue-query-devtools": "^6.1.34", + "@tanstack/vue-query": "^5.101.4", + "@tanstack/vue-query-devtools": "^6.1.38", "vue": "^3.4.27" }, "devDependencies": { diff --git a/integrations/angular-cli-20/package.json b/integrations/angular-cli-20/package.json index 10e1d3d64a7..6f8d497e655 100644 --- a/integrations/angular-cli-20/package.json +++ b/integrations/angular-cli-20/package.json @@ -14,7 +14,7 @@ "@angular/forms": "^20.0.0", "@angular/platform-browser": "^20.0.0", "@angular/router": "^20.0.0", - "@tanstack/angular-query-experimental": "^5.101.0", + "@tanstack/angular-query-experimental": "^5.101.4", "rxjs": "~7.8.0", "tslib": "^2.3.0", "zone.js": "~0.15.0" diff --git a/media/header_query.png b/media/header_query.png deleted file mode 100644 index 96fe6e78fae..00000000000 Binary files a/media/header_query.png and /dev/null differ diff --git a/package.json b/package.json index 8bf58562848..802a1e1de0c 100644 --- a/package.json +++ b/package.json @@ -5,9 +5,9 @@ "type": "git", "url": "git+https://github.com/TanStack/query.git" }, - "packageManager": "pnpm@11.1.0", + "packageManager": "pnpm@11.9.0", "engines": { - "pnpm": ">=11.0.0" + "pnpm": ">=11.9.0" }, "type": "module", "scripts": { diff --git a/packages/angular-query-experimental/CHANGELOG.md b/packages/angular-query-experimental/CHANGELOG.md index 94055e7f773..b1bdffa47d6 100644 --- a/packages/angular-query-experimental/CHANGELOG.md +++ b/packages/angular-query-experimental/CHANGELOG.md @@ -1,5 +1,33 @@ # @tanstack/angular-query-experimental +## 5.101.4 + +### Patch Changes + +- Updated dependencies []: + - @tanstack/query-core@5.101.4 + +## 5.101.3 + +### Patch Changes + +- Updated dependencies [[`7e3c822`](https://github.com/TanStack/query/commit/7e3c822a10896f41a8f1031c16b85096277af677)]: + - @tanstack/query-core@5.101.3 + +## 5.101.2 + +### Patch Changes + +- Updated dependencies []: + - @tanstack/query-core@5.101.2 + +## 5.101.1 + +### Patch Changes + +- Updated dependencies [[`9eff92e`](https://github.com/TanStack/query/commit/9eff92ed86e284ec0125b3a3539d028688235bd1)]: + - @tanstack/query-core@5.101.1 + ## 5.101.0 ### Patch Changes diff --git a/packages/angular-query-experimental/README.md b/packages/angular-query-experimental/README.md index 6ed2dfa05a8..53fea66deba 100644 --- a/packages/angular-query-experimental/README.md +++ b/packages/angular-query-experimental/README.md @@ -1,4 +1,18 @@ -![TanStack Query Header](https://github.com/TanStack/query/raw/main/media/repo-header.png) + + + + TanStack Angular Query + [![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) diff --git a/packages/angular-query-experimental/package.json b/packages/angular-query-experimental/package.json index a566a21079e..31b6aac7e15 100644 --- a/packages/angular-query-experimental/package.json +++ b/packages/angular-query-experimental/package.json @@ -1,6 +1,6 @@ { "name": "@tanstack/angular-query-experimental", - "version": "5.101.0", + "version": "5.101.4", "description": "Signals for managing, caching and syncing asynchronous and remote data in Angular", "author": "Arnoud de Vries", "license": "MIT", diff --git a/packages/angular-query-experimental/src/__tests__/inject-mutation.test.ts b/packages/angular-query-experimental/src/__tests__/inject-mutation.test.ts index f0c53602b1d..00d5190ce01 100644 --- a/packages/angular-query-experimental/src/__tests__/inject-mutation.test.ts +++ b/packages/angular-query-experimental/src/__tests__/inject-mutation.test.ts @@ -382,8 +382,7 @@ describe('injectMutation', () => { 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']) + expect(mutation?.options.mutationKey).toStrictEqual(['fake', 'value']) }) it('should update options on required signal input change', async () => { diff --git a/packages/angular-query-experimental/src/__tests__/inject-query.test.ts b/packages/angular-query-experimental/src/__tests__/inject-query.test.ts index fb7fd80a7b7..1e7b81ba971 100644 --- a/packages/angular-query-experimental/src/__tests__/inject-query.test.ts +++ b/packages/angular-query-experimental/src/__tests__/inject-query.test.ts @@ -436,7 +436,7 @@ describe('injectQuery', () => { expect(spy).toHaveBeenCalledTimes(2) // should call queryFn with context containing the new queryKey - expect(spy).toHaveBeenCalledWith({ + expect(spy).toHaveBeenNthCalledWith(2, { client: queryClient, meta: undefined, queryKey: key2, @@ -528,7 +528,8 @@ describe('injectQuery', () => { void query.refetch().then(() => { expect(fetchFn).toHaveBeenCalledTimes(1) - expect(fetchFn).toHaveBeenCalledWith( + expect(fetchFn).toHaveBeenNthCalledWith( + 1, expect.objectContaining({ queryKey: [...key, 'key11'], }), @@ -541,7 +542,8 @@ describe('injectQuery', () => { void query.refetch().then(() => { expect(fetchFn).toHaveBeenCalledTimes(2) - expect(fetchFn).toHaveBeenCalledWith( + expect(fetchFn).toHaveBeenNthCalledWith( + 2, expect.objectContaining({ queryKey: [...key, 'key12'], }), diff --git a/packages/angular-query-experimental/src/__tests__/with-devtools.test.ts b/packages/angular-query-experimental/src/__tests__/with-devtools.test.ts index 6907186a928..47ce1173440 100644 --- a/packages/angular-query-experimental/src/__tests__/with-devtools.test.ts +++ b/packages/angular-query-experimental/src/__tests__/with-devtools.test.ts @@ -17,6 +17,7 @@ import type { DevtoolsButtonPosition, DevtoolsErrorType, DevtoolsPosition, + Theme, } from '@tanstack/query-devtools' import type { DevtoolsOptions } from '../devtools' @@ -28,6 +29,7 @@ const mockDevtoolsInstance = { setErrorTypes: vi.fn(), setButtonPosition: vi.fn(), setInitialIsOpen: vi.fn(), + setTheme: vi.fn(), } function MockTanstackQueryDevtools() { @@ -433,6 +435,38 @@ describe('withDevtools feature', () => { expect(mockDevtoolsInstance.setInitialIsOpen).toHaveBeenCalledWith(true) }) + it('should update theme', async () => { + const theme = signal('system') + + TestBed.configureTestingModule({ + providers: [ + provideZonelessChangeDetection(), + provideTanStackQuery( + new QueryClient(), + withDevtools(() => ({ + loadDevtools: true, + theme: theme(), + })), + ), + ], + }) + + TestBed.inject(ENVIRONMENT_INITIALIZER) + await vi.advanceTimersByTimeAsync(0) + await vi.dynamicImportSettled() + + TestBed.tick() + + expect(mockDevtoolsInstance.setTheme).toHaveBeenCalledTimes(0) + + theme.set('dark') + + TestBed.tick() + + expect(mockDevtoolsInstance.setTheme).toHaveBeenCalledTimes(1) + expect(mockDevtoolsInstance.setTheme).toHaveBeenCalledWith('dark') + }) + it('should destroy devtools', async () => { const loadDevtools = signal(true) diff --git a/packages/angular-query-experimental/src/infinite-query-options.ts b/packages/angular-query-experimental/src/infinite-query-options.ts index fc18c0e94dc..0bed440730b 100644 --- a/packages/angular-query-experimental/src/infinite-query-options.ts +++ b/packages/angular-query-experimental/src/infinite-query-options.ts @@ -79,7 +79,7 @@ export type DefinedInitialDataInfiniteOptions< } /** - * Allows to share and re-use infinite query options in a type-safe way. + * Allows sharing and re-using 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`. @@ -110,7 +110,7 @@ export function infiniteQueryOptions< } /** - * Allows to share and re-use infinite query options in a type-safe way. + * Allows sharing and re-using 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`. @@ -141,7 +141,7 @@ export function infiniteQueryOptions< } /** - * Allows to share and re-use infinite query options in a type-safe way. + * Allows sharing and re-using 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`. @@ -172,7 +172,7 @@ export function infiniteQueryOptions< } /** - * Allows to share and re-use infinite query options in a type-safe way. + * Allows sharing and re-using 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`. diff --git a/packages/angular-query-experimental/src/inject-queries.ts b/packages/angular-query-experimental/src/inject-queries.ts index 2f201799e74..d61a937a3c2 100644 --- a/packages/angular-query-experimental/src/inject-queries.ts +++ b/packages/angular-query-experimental/src/inject-queries.ts @@ -159,7 +159,7 @@ export type QueriesOptions< > : 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! + : // If T is *some* array but we couldn't assign unknown[] to it, then it must hold some known/homogeneous type! // use this to infer the param types in the case of Array.map() argument T extends Array< QueryObserverOptionsForCreateQueries< diff --git a/packages/angular-query-experimental/src/mutation-options.ts b/packages/angular-query-experimental/src/mutation-options.ts index ef0c2cc8b26..de59b994296 100644 --- a/packages/angular-query-experimental/src/mutation-options.ts +++ b/packages/angular-query-experimental/src/mutation-options.ts @@ -2,7 +2,7 @@ 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. + * Allows sharing and re-using mutation options in a type-safe way. * * **Example** * @@ -66,7 +66,7 @@ export function mutationOptions< > /** - * Allows to share and re-use mutation options in a type-safe way. + * Allows sharing and re-using mutation options in a type-safe way. * * **Example** * diff --git a/packages/angular-query-experimental/src/providers.ts b/packages/angular-query-experimental/src/providers.ts index 076d76d0c34..7d17f93e984 100644 --- a/packages/angular-query-experimental/src/providers.ts +++ b/packages/angular-query-experimental/src/providers.ts @@ -32,7 +32,7 @@ export function provideQueryClient( /** * Sets up providers necessary to enable TanStack Query functionality for Angular applications. * - * Allows to configure a `QueryClient` and optional features such as developer tools. + * Allows configuring a `QueryClient` and optional features such as developer tools. * * **Example - standalone** * @@ -115,7 +115,7 @@ export function provideTanStackQuery( /** * Sets up providers necessary to enable TanStack Query functionality for Angular applications. * - * Allows to configure a `QueryClient`. + * Allows configuring 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 diff --git a/packages/angular-query-experimental/src/query-options.ts b/packages/angular-query-experimental/src/query-options.ts index 069472b9032..14a50074b06 100644 --- a/packages/angular-query-experimental/src/query-options.ts +++ b/packages/angular-query-experimental/src/query-options.ts @@ -53,7 +53,7 @@ export type DefinedInitialDataOptions< } /** - * Allows to share and re-use query options in a type-safe way. + * Allows sharing and re-using query options in a type-safe way. * * The `queryKey` will be tagged with the type from `queryFn`. * @@ -85,7 +85,7 @@ export function queryOptions< } /** - * Allows to share and re-use query options in a type-safe way. + * Allows sharing and re-using query options in a type-safe way. * * The `queryKey` will be tagged with the type from `queryFn`. * @@ -117,7 +117,7 @@ export function queryOptions< } /** - * Allows to share and re-use query options in a type-safe way. + * Allows sharing and re-using query options in a type-safe way. * * The `queryKey` will be tagged with the type from `queryFn`. * @@ -149,7 +149,7 @@ export function queryOptions< } /** - * Allows to share and re-use query options in a type-safe way. + * Allows sharing and re-using query options in a type-safe way. * * The `queryKey` will be tagged with the type from `queryFn`. * diff --git a/packages/angular-query-persist-client/CHANGELOG.md b/packages/angular-query-persist-client/CHANGELOG.md index e3f3bea04a7..ee472d874dd 100644 --- a/packages/angular-query-persist-client/CHANGELOG.md +++ b/packages/angular-query-persist-client/CHANGELOG.md @@ -1,5 +1,37 @@ # @tanstack/angular-query-persist-client +## 5.101.4 + +### Patch Changes + +- Updated dependencies []: + - @tanstack/angular-query-experimental@5.101.4 + - @tanstack/query-persist-client-core@5.101.4 + +## 5.101.3 + +### Patch Changes + +- Updated dependencies []: + - @tanstack/angular-query-experimental@5.101.3 + - @tanstack/query-persist-client-core@5.101.3 + +## 5.101.2 + +### Patch Changes + +- Updated dependencies []: + - @tanstack/angular-query-experimental@5.101.2 + - @tanstack/query-persist-client-core@5.101.2 + +## 5.101.1 + +### Patch Changes + +- Updated dependencies []: + - @tanstack/angular-query-experimental@5.101.1 + - @tanstack/query-persist-client-core@5.101.1 + ## 5.101.0 ### Patch Changes diff --git a/packages/angular-query-persist-client/package.json b/packages/angular-query-persist-client/package.json index 5b185188179..ed44373b33a 100644 --- a/packages/angular-query-persist-client/package.json +++ b/packages/angular-query-persist-client/package.json @@ -1,7 +1,7 @@ { "name": "@tanstack/angular-query-persist-client", "private": true, - "version": "5.101.0", + "version": "5.101.4", "description": "Angular bindings to work with persisters in TanStack/angular-query", "author": "Omer Gronich", "license": "MIT", diff --git a/packages/angular-query-persist-client/src/__tests__/with-persist-query-client.test.ts b/packages/angular-query-persist-client/src/__tests__/with-persist-query-client.test.ts index 1f9bd61419a..dd8e52ce137 100644 --- a/packages/angular-query-persist-client/src/__tests__/with-persist-query-client.test.ts +++ b/packages/angular-query-persist-client/src/__tests__/with-persist-query-client.test.ts @@ -279,11 +279,11 @@ describe('withPersistQueryClient', () => { class Page { state = injectQuery(() => ({ queryKey: key, - queryFn: async () => { - await sleep(10) - fetched = true - return 'fetched' - }, + queryFn: () => + sleep(10).then(() => { + fetched = true + return 'fetched' + }), staleTime: Infinity, })) diff --git a/packages/eslint-plugin-query/CHANGELOG.md b/packages/eslint-plugin-query/CHANGELOG.md index 3d3ff84aa7e..2b387b903e1 100644 --- a/packages/eslint-plugin-query/CHANGELOG.md +++ b/packages/eslint-plugin-query/CHANGELOG.md @@ -1,5 +1,17 @@ # @tanstack/eslint-plugin-query +## 5.101.4 + +### Patch Changes + +- [#11067](https://github.com/TanStack/query/pull/11067) [`7ac45ed`](https://github.com/TanStack/query/commit/7ac45ed45cbc28a54ac2fcfb1faccfcd87fb7b75) - Relax `exhaustive-deps` so function call targets are not required in query keys while values referenced in nested callbacks are still checked. + +## 5.101.3 + +## 5.101.2 + +## 5.101.1 + ## 5.101.0 ### Minor Changes diff --git a/packages/eslint-plugin-query/package.json b/packages/eslint-plugin-query/package.json index 5efae7fc13c..6f67b9536f9 100644 --- a/packages/eslint-plugin-query/package.json +++ b/packages/eslint-plugin-query/package.json @@ -1,6 +1,6 @@ { "name": "@tanstack/eslint-plugin-query", - "version": "5.101.0", + "version": "5.101.4", "description": "ESLint plugin for TanStack Query", "author": "Eliya Cohen", "license": "MIT", diff --git a/packages/eslint-plugin-query/src/__tests__/ast-utils.test.ts b/packages/eslint-plugin-query/src/__tests__/ast-utils.test.ts new file mode 100644 index 00000000000..63f4fdbd47e --- /dev/null +++ b/packages/eslint-plugin-query/src/__tests__/ast-utils.test.ts @@ -0,0 +1,53 @@ +import { AST_NODE_TYPES } from '@typescript-eslint/utils' +import { describe, expect, it } from 'vitest' +import { ExhaustiveDepsUtils } from '../rules/exhaustive-deps/exhaustive-deps.utils' +import { ASTUtils } from '../utils/ast-utils' +import type { TSESLint, TSESTree } from '@typescript-eslint/utils' + +function createIdentifier(name: string): TSESTree.Identifier { + return { type: AST_NODE_TYPES.Identifier, name } as TSESTree.Identifier +} + +describe('ASTUtils', () => { + it('stops member traversal when a node has no parent', () => { + const identifier = createIdentifier('value') + + expect(ASTUtils.traverseUpMemberExpression(identifier)).toBe(identifier) + }) + + it('handles an external reference without a parent', () => { + const operation = createIdentifier('operation') + const reference = { + identifier: operation, + isRead: () => true, + resolved: null, + } as TSESLint.Scope.Reference + const scope = { + childScopes: [], + references: [reference], + set: new Map(), + } as unknown as TSESLint.Scope.Scope + const scopeManager = { + acquire: () => scope, + } as unknown as TSESLint.Scope.ScopeManager + const sourceCode = { + getText: () => 'operation', + } as unknown as Readonly + + expect( + ASTUtils.getExternalRefs({ + scopeManager, + sourceCode, + node: operation, + }), + ).toEqual([reference]) + }) +}) + +describe('ExhaustiveDepsUtils', () => { + it('does not treat a detached identifier as a function call target', () => { + expect( + ExhaustiveDepsUtils.isFunctionCallTarget(createIdentifier('fetchTodos')), + ).toBe(false) + }) +}) diff --git a/packages/eslint-plugin-query/src/__tests__/exhaustive-deps.test.ts b/packages/eslint-plugin-query/src/__tests__/exhaustive-deps.test.ts index 7e67ae85404..23f3492395c 100644 --- a/packages/eslint-plugin-query/src/__tests__/exhaustive-deps.test.ts +++ b/packages/eslint-plugin-query/src/__tests__/exhaustive-deps.test.ts @@ -48,6 +48,43 @@ ruleTester.run('exhaustive-deps', rule, { } `, }, + { + name: 'should not require a component scoped function call target in queryKey', + code: normalizeIndent` + function Component({ todoId }) { + const fetchTodoById = (id) => Promise.resolve(id) + + return useQuery({ + queryKey: ['todos', todoId], + queryFn: () => fetchTodoById(todoId), + }) + } + `, + }, + { + name: 'should not require a method call receiver in queryKey', + code: normalizeIndent` + function Component({ todoId }) { + const todos = useTodos() + + return useQuery({ + queryKey: ['todo', todoId], + queryFn: () => todos.getTodo(todoId), + }) + } + `, + }, + { + name: 'should not require a data method receiver in queryKey', + code: normalizeIndent` + function Component({ items }) { + useQuery({ + queryKey: ['items'], + queryFn: () => items?.map((item) => item.id), + }) + } + `, + }, { name: 'should pass props.src', code: ` @@ -776,6 +813,28 @@ ruleTester.run('exhaustive-deps', rule, { } `, }, + { + name: 'should pass when optional chaining method call receiver is omitted', + code: normalizeIndent` + function useThing(a) { + return useQuery({ + queryKey: ['thing'], + queryFn: () => a?.foo() + }) + } + `, + }, + { + name: 'should pass when non-null assertion method call receiver is omitted', + code: normalizeIndent` + function useThing(a) { + return useQuery({ + queryKey: ['thing'], + queryFn: () => a!.foo() + }) + } + `, + }, { name: 'should pass when queryKey uses TSAsExpression with array', code: normalizeIndent` @@ -881,80 +940,47 @@ ruleTester.run('exhaustive-deps', rule, { `, }, { - name: 'should pass when queryFn is ternary with both branches having deps in queryKey', + name: 'should pass when sibling member method call receivers are omitted', code: normalizeIndent` - function useThing(condition, a, b) { + function useThing(a) { return useQuery({ - queryKey: ['thing', a, b], - queryFn: condition ? () => fetchA(a) : () => fetchB(b) + queryKey: ['thing'], + queryFn: () => { + a.b.foo() + a.c.bar() + return 1 + } }) } `, }, - ], - invalid: [ { - name: 'should fail when optional chaining method call is missing root', + name: 'should pass when nested member method call receiver is omitted', code: normalizeIndent` function useThing(a) { return useQuery({ queryKey: ['thing'], - queryFn: () => a?.foo() + queryFn: () => { + a.b.foo() + return 1 + } }) } `, - errors: [ - { - messageId: 'missingDeps', - data: { deps: 'a' }, - suggestions: [ - { - messageId: 'fixTo', - output: normalizeIndent` - function useThing(a) { - return useQuery({ - queryKey: ['thing', a], - queryFn: () => a?.foo() - }) - } - `, - }, - ], - }, - ], }, { - name: 'should fail when non-null assertion method call is missing root', + name: 'should pass when queryFn is ternary with both branches having deps in queryKey', code: normalizeIndent` - function useThing(a) { + function useThing(condition, a, b) { return useQuery({ - queryKey: ['thing'], - queryFn: () => a!.foo() + queryKey: ['thing', a, b], + queryFn: condition ? () => fetchA(a) : () => fetchB(b) }) } `, - errors: [ - { - messageId: 'missingDeps', - data: { deps: 'a' }, - suggestions: [ - { - messageId: 'fixTo', - output: normalizeIndent` - function useThing(a) { - return useQuery({ - queryKey: ['thing', a], - queryFn: () => a!.foo() - }) - } - `, - }, - ], - }, - ], }, { - name: 'should fail when alias of props used in queryFn is missing in queryKey', + name: 'should not require a nested method call receiver in queryKey', code: normalizeIndent` function Component(props) { const entities = props.entities; @@ -969,26 +995,32 @@ ruleTester.run('exhaustive-deps', rule, { }); } `, + }, + ], + invalid: [ + { + name: 'should fail when a computed method name is missing in queryKey', + code: normalizeIndent` + function Component({ client, operation }) { + useQuery({ + queryKey: ['data'], + queryFn: () => client[operation](), + }) + } + `, errors: [ { messageId: 'missingDeps', - data: { deps: 'entities' }, + data: { deps: 'operation' }, suggestions: [ { messageId: 'fixTo', - data: { result: "['get-stuff', entities]" }, output: normalizeIndent` - function Component(props) { - const entities = props.entities; - - const q = useQuery({ - queryKey: ['get-stuff', entities], - queryFn: () => { - return api.fetchStuff({ - ids: entities.map((o) => o.id) - }); - } - }); + function Component({ client, operation }) { + useQuery({ + queryKey: ['data', operation], + queryFn: () => client[operation](), + }) } `, }, @@ -1631,80 +1663,6 @@ ruleTester.run('exhaustive-deps', rule, { }, ], }, - { - name: 'should fail when sibling member method calls missing one path', - code: normalizeIndent` - function useThing(a) { - return useQuery({ - queryKey: ['thing', a.b], - queryFn: () => { - a.b.foo() - a.c.bar() - return 1 - } - }) - } - `, - errors: [ - { - messageId: 'missingDeps', - data: { deps: 'a.c' }, - suggestions: [ - { - messageId: 'fixTo', - output: normalizeIndent` - function useThing(a) { - return useQuery({ - queryKey: ['thing', a.b, a.c], - queryFn: () => { - a.b.foo() - a.c.bar() - return 1 - } - }) - } - `, - }, - ], - }, - ], - }, - { - name: 'should fail when single member method call missing path and root', - code: normalizeIndent` - function useThing(a) { - return useQuery({ - queryKey: ['thing'], - queryFn: () => { - a.b.foo() - return 1 - } - }) - } - `, - errors: [ - { - messageId: 'missingDeps', - data: { deps: 'a.b' }, - suggestions: [ - { - messageId: 'fixTo', - output: normalizeIndent` - function useThing(a) { - return useQuery({ - queryKey: ['thing', a.b], - queryFn: () => { - a.b.foo() - return 1 - } - }) - } - `, - }, - ], - }, - ], - }, { name: 'should fail when queryKey has TSAsExpression with missing dep', code: normalizeIndent` @@ -1771,27 +1729,27 @@ ruleTester.run('exhaustive-deps', rule, { name: 'should fail when type allowlist is empty', options: [{ allowlist: { types: [] } }], code: normalizeIndent` - interface Api { fetch: () => void } + interface Api { baseUrl: string } function useThing(api: Api) { return useQuery({ queryKey: ['thing'], - queryFn: () => api.fetch() + queryFn: () => api.baseUrl }) } `, errors: [ { messageId: 'missingDeps', - data: { deps: 'api' }, + data: { deps: 'api.baseUrl' }, suggestions: [ { messageId: 'fixTo', output: normalizeIndent` - interface Api { fetch: () => void } + interface Api { baseUrl: string } function useThing(api: Api) { return useQuery({ - queryKey: ['thing', api], - queryFn: () => api.fetch() + queryKey: ['thing', api.baseUrl], + queryFn: () => api.baseUrl }) } `, @@ -1929,13 +1887,12 @@ ruleTester.run('exhaustive-deps allowlist.types', rule, { name: 'should ignore missing member path when root type is in allowlist.types', options: [{ allowlist: { types: ['Svc'] } }], code: normalizeIndent` - interface Svc { part: { load: (id: string) => void } } + interface Svc { part: { baseUrl: string } } function useThing(svc: Svc, id: string) { return useQuery({ queryKey: ['thing', id], queryFn: () => { - svc.part.load(id) - return id + return { baseUrl: svc.part.baseUrl, id } } }) } @@ -2007,13 +1964,12 @@ ruleTester.run('exhaustive-deps allowlist.types', rule, { name: 'should report missing member path when root type not in allowlist.types', options: [{ allowlist: { types: ['Other'] } }], code: normalizeIndent` - interface Svc { part: { load: (id: string) => void } } + interface Svc { part: { baseUrl: string } } function useThing(svc: Svc, id: string) { return useQuery({ queryKey: ['thing', id], queryFn: () => { - svc.part.load(id) - return id + return { baseUrl: svc.part.baseUrl, id } } }) } @@ -2021,18 +1977,17 @@ ruleTester.run('exhaustive-deps allowlist.types', rule, { errors: [ { messageId: 'missingDeps', - data: { deps: 'svc.part' }, + data: { deps: 'svc.part.baseUrl' }, suggestions: [ { messageId: 'fixTo', output: normalizeIndent` - interface Svc { part: { load: (id: string) => void } } + interface Svc { part: { baseUrl: string } } function useThing(svc: Svc, id: string) { return useQuery({ - queryKey: ['thing', id, svc.part], + queryKey: ['thing', id, svc.part.baseUrl], queryFn: () => { - svc.part.load(id) - return id + return { baseUrl: svc.part.baseUrl, id } } }) } @@ -2046,13 +2001,12 @@ ruleTester.run('exhaustive-deps allowlist.types', rule, { name: 'should report missing member path when variable has type annotation but type not allowlisted', options: [{ allowlist: { types: ['AllowedService'] } }], code: normalizeIndent` - interface MyService { method: () => void } + interface MyService { baseUrl: string } function useData(service: MyService) { return useQuery({ queryKey: ['data'], queryFn: () => { - service.method() - return 'data' + return service.baseUrl } }) } @@ -2060,18 +2014,17 @@ ruleTester.run('exhaustive-deps allowlist.types', rule, { errors: [ { messageId: 'missingDeps', - data: { deps: 'service' }, + data: { deps: 'service.baseUrl' }, suggestions: [ { messageId: 'fixTo', output: normalizeIndent` - interface MyService { method: () => void } + interface MyService { baseUrl: string } function useData(service: MyService) { return useQuery({ - queryKey: ['data', service], + queryKey: ['data', service.baseUrl], queryFn: () => { - service.method() - return 'data' + return service.baseUrl } }) } @@ -2085,20 +2038,19 @@ ruleTester.run('exhaustive-deps allowlist.types', rule, { name: 'should not inherit allowlisted type from outer shadowed binding', options: [{ allowlist: { types: ['AllowedService'] } }], code: normalizeIndent` - interface AllowedService { load: () => void } - interface OtherService { load: () => void } + interface AllowedService { baseUrl: string } + interface OtherService { baseUrl: string } function useThing() { - const svc: AllowedService = { load: () => undefined } + const svc: AllowedService = { baseUrl: 'allowed' } if (true) { - const svc: OtherService = { load: () => undefined } + const svc: OtherService = { baseUrl: 'other' } return useQuery({ queryKey: ['thing'], queryFn: () => { - svc.load() - return 'data' + return svc.baseUrl } }) } @@ -2109,25 +2061,24 @@ ruleTester.run('exhaustive-deps allowlist.types', rule, { errors: [ { messageId: 'missingDeps', - data: { deps: 'svc' }, + data: { deps: 'svc.baseUrl' }, suggestions: [ { messageId: 'fixTo', output: normalizeIndent` - interface AllowedService { load: () => void } - interface OtherService { load: () => void } + interface AllowedService { baseUrl: string } + interface OtherService { baseUrl: string } function useThing() { - const svc: AllowedService = { load: () => undefined } + const svc: AllowedService = { baseUrl: 'allowed' } if (true) { - const svc: OtherService = { load: () => undefined } + const svc: OtherService = { baseUrl: 'other' } return useQuery({ - queryKey: ['thing', svc], + queryKey: ['thing', svc.baseUrl], queryFn: () => { - svc.load() - return 'data' + return svc.baseUrl } }) } @@ -2153,8 +2104,7 @@ ruleTester.run('exhaustive-deps allowlist.variables', rule, { return useQuery({ queryKey: ['thing', id], queryFn: () => { - svc.part.load(id) - return id + return { part: svc.part, id } } }) } @@ -2184,9 +2134,7 @@ ruleTester.run('exhaustive-deps allowlist.variables', rule, { return useQuery({ queryKey: ['thing'], queryFn: () => { - svc.part.load() - other.x.run() - return 1 + return { svcPart: svc.part, otherX: other.x } } }) } @@ -2203,9 +2151,7 @@ ruleTester.run('exhaustive-deps allowlist.variables', rule, { return useQuery({ queryKey: ['thing', other.x], queryFn: () => { - svc.part.load() - other.x.run() - return 1 + return { svcPart: svc.part, otherX: other.x } } }) } @@ -2222,8 +2168,7 @@ ruleTester.run('exhaustive-deps allowlist.variables', rule, { return useQuery({ queryKey: ['thing', id], queryFn: () => { - svc.part.load(id) - return id + return { part: svc.part, id } } }) } @@ -2240,8 +2185,7 @@ ruleTester.run('exhaustive-deps allowlist.variables', rule, { return useQuery({ queryKey: ['thing', id, svc.part], queryFn: () => { - svc.part.load(id) - return id + return { part: svc.part, id } } }) } diff --git a/packages/eslint-plugin-query/src/rules/exhaustive-deps/exhaustive-deps.utils.ts b/packages/eslint-plugin-query/src/rules/exhaustive-deps/exhaustive-deps.utils.ts index 135a5b620d1..4f467aa4548 100644 --- a/packages/eslint-plugin-query/src/rules/exhaustive-deps/exhaustive-deps.utils.ts +++ b/packages/eslint-plugin-query/src/rules/exhaustive-deps/exhaustive-deps.utils.ts @@ -55,11 +55,24 @@ export const ExhaustiveDepsUtils = { return ( reference.identifier.name !== 'undefined' && + !ExhaustiveDepsUtils.isFunctionCallTarget(reference.identifier) && reference.identifier.parent.type !== AST_NODE_TYPES.NewExpression && !ExhaustiveDepsUtils.isInstanceOfKind(reference.identifier.parent) ) }, + isFunctionCallTarget( + identifier: TSESTree.Identifier | TSESTree.JSXIdentifier, + ): boolean { + const callee = ASTUtils.traverseUpMemberExpression(identifier) + + return ( + callee.parent !== undefined && + callee.parent.type === AST_NODE_TYPES.CallExpression && + callee.parent.callee === callee + ) + }, + /** * Given required refs and existing queryKey entries, compute missing dependency paths * respecting allowlisted variables and types. @@ -296,11 +309,7 @@ export const ExhaustiveDepsUtils = { }): { path: string; root: string; coversRootMembers: boolean } | null { const { identifier, sourceCode } = params - const fullChainNode = ASTUtils.traverseUpOnly(identifier, [ - AST_NODE_TYPES.MemberExpression, - AST_NODE_TYPES.TSNonNullExpression, - AST_NODE_TYPES.Identifier, - ]) + const fullChainNode = ASTUtils.traverseUpMemberExpression(identifier) const fullText = ExhaustiveDepsUtils.normalizeChain( sourceCode.getText(fullChainNode), diff --git a/packages/eslint-plugin-query/src/utils/ast-utils.ts b/packages/eslint-plugin-query/src/utils/ast-utils.ts index a44ae864549..b73825432e6 100644 --- a/packages/eslint-plugin-query/src/utils/ast-utils.ts +++ b/packages/eslint-plugin-query/src/utils/ast-utils.ts @@ -143,6 +143,21 @@ export const ASTUtils = { return identifier }, + traverseUpMemberExpression(node: TSESTree.Node): TSESTree.Node { + const parent = node.parent + + if ( + parent !== undefined && + ((parent.type === AST_NODE_TYPES.MemberExpression && + parent.object === node) || + parent.type === AST_NODE_TYPES.ChainExpression || + parent.type === AST_NODE_TYPES.TSNonNullExpression) + ) { + return ASTUtils.traverseUpMemberExpression(parent) + } + + return node + }, isDeclaredInNode(params: { functionNode: TSESTree.Node reference: TSESLint.Scope.Reference @@ -184,10 +199,22 @@ export const ASTUtils = { const references = collectReferences(scope) .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, - ]) + const memberPath = ASTUtils.traverseUpMemberExpression(x.identifier) + const memberExpression = memberPath.parent + const isComputedCallProperty = + memberExpression !== undefined && + memberExpression.type === AST_NODE_TYPES.MemberExpression && + memberExpression.computed && + memberExpression.property === memberPath && + memberExpression.parent.type === AST_NODE_TYPES.CallExpression && + memberExpression.parent.callee === memberExpression + + const referenceNode = isComputedCallProperty + ? memberPath + : ASTUtils.traverseUpOnly(x.identifier, [ + AST_NODE_TYPES.MemberExpression, + AST_NODE_TYPES.Identifier, + ]) return { variable: x, diff --git a/packages/lit-query/CHANGELOG.md b/packages/lit-query/CHANGELOG.md index d9d2eda1f55..b660fe5cb6f 100644 --- a/packages/lit-query/CHANGELOG.md +++ b/packages/lit-query/CHANGELOG.md @@ -1,5 +1,33 @@ # @tanstack/lit-query +## 0.2.11 + +### Patch Changes + +- Updated dependencies []: + - @tanstack/query-core@5.101.4 + +## 0.2.10 + +### Patch Changes + +- Updated dependencies [[`7e3c822`](https://github.com/TanStack/query/commit/7e3c822a10896f41a8f1031c16b85096277af677)]: + - @tanstack/query-core@5.101.3 + +## 0.2.9 + +### Patch Changes + +- Updated dependencies []: + - @tanstack/query-core@5.101.2 + +## 0.2.8 + +### Patch Changes + +- Updated dependencies [[`9eff92e`](https://github.com/TanStack/query/commit/9eff92ed86e284ec0125b3a3539d028688235bd1)]: + - @tanstack/query-core@5.101.1 + ## 0.2.7 ### Patch Changes diff --git a/packages/lit-query/README.md b/packages/lit-query/README.md index c439074d806..49e903ab015 100644 --- a/packages/lit-query/README.md +++ b/packages/lit-query/README.md @@ -1,3 +1,20 @@ +
+ + + + TanStack Lit Query + +
# @tanstack/lit-query Lit adapter for `@tanstack/query-core` using Lit reactive controllers. diff --git a/packages/lit-query/package.json b/packages/lit-query/package.json index 635e5958e79..20d2329faa0 100644 --- a/packages/lit-query/package.json +++ b/packages/lit-query/package.json @@ -1,6 +1,6 @@ { "name": "@tanstack/lit-query", - "version": "0.2.7", + "version": "0.2.11", "description": "Lit adapter for TanStack Query Core", "author": "tannerlinsley", "license": "MIT", diff --git a/packages/lit-query/src/tests/counters-and-state.test.ts b/packages/lit-query/src/tests/counters-and-state.test.ts index d2b14739f4a..e81baa5a751 100644 --- a/packages/lit-query/src/tests/counters-and-state.test.ts +++ b/packages/lit-query/src/tests/counters-and-state.test.ts @@ -272,8 +272,9 @@ describe('useIsFetching/useIsMutating/useMutationState', () => { await waitFor(() => consumer.isMutating() === 0) expect( - explicitClient.getQueryCache().find({ queryKey: consumer.queryKey }), - ).toBeDefined() + explicitClient.getQueryCache().find({ queryKey: consumer.queryKey }) + ?.state.data, + ).toBe('query-ok') expect( providerClient.getQueryCache().find({ queryKey: consumer.queryKey }), ).toBeUndefined() diff --git a/packages/lit-query/src/tests/infinite-and-options.test.ts b/packages/lit-query/src/tests/infinite-and-options.test.ts index 6ef06b6cdc1..85b7022231e 100644 --- a/packages/lit-query/src/tests/infinite-and-options.test.ts +++ b/packages/lit-query/src/tests/infinite-and-options.test.ts @@ -118,8 +118,9 @@ describe('createInfiniteQueryController', () => { await waitFor(() => consumer.infinite().isSuccess) expect(consumer.infinite().data?.pages).toEqual([0]) expect( - explicitClient.getQueryCache().find({ queryKey: consumer.queryKey }), - ).toBeDefined() + explicitClient.getQueryCache().find({ queryKey: consumer.queryKey }) + ?.state.data, + ).toEqual({ pages: [0], pageParams: [0] }) expect( providerClient.getQueryCache().find({ queryKey: consumer.queryKey }), ).toBeUndefined() @@ -364,7 +365,7 @@ describe('createInfiniteQueryController', () => { const nextPageResult = await infinite.fetchNextPage() expect(nextPageResult.isFetchNextPageError).toBe(true) - expect(nextPageResult.error).toBeInstanceOf(Error) + expect(nextPageResult.error).toEqual(new Error('next-page-failed')) await waitFor(() => infinite().isFetchNextPageError) expect(infinite().data?.pages).toEqual([0]) }) diff --git a/packages/lit-query/src/tests/mutation-controller.test.ts b/packages/lit-query/src/tests/mutation-controller.test.ts index e205ac05018..58ad85a5bf1 100644 --- a/packages/lit-query/src/tests/mutation-controller.test.ts +++ b/packages/lit-query/src/tests/mutation-controller.test.ts @@ -164,7 +164,7 @@ describe('createMutationController', () => { await waitFor(() => mutation().isPending) await expect(errorPromise).rejects.toThrow('negative-not-allowed') await waitFor(() => mutation().isError) - expect(mutation().error).toBeInstanceOf(Error) + expect(mutation().error).toEqual(new Error('negative-not-allowed')) }) it('M10: reset clears mutation state back to idle baseline', async () => { @@ -188,7 +188,7 @@ describe('createMutationController', () => { 'reset-target', ) await waitFor(() => mutation().isError) - expect(mutation().error).toBeInstanceOf(Error) + expect(mutation().error).toEqual(new Error('reset-target')) mutation.reset() expect(mutation().isIdle).toBe(true) @@ -221,7 +221,7 @@ describe('createMutationController', () => { expect(() => mutation.mutate(-1)).not.toThrow() await waitFor(() => mutation().isError) - expect(mutation().error).toBeInstanceOf(Error) + expect(mutation().error).toEqual(new Error('negative-not-allowed')) await expect(mutation.mutateAsync(-1)).rejects.toThrow( 'negative-not-allowed', diff --git a/packages/lit-query/src/tests/queries-controller.test.ts b/packages/lit-query/src/tests/queries-controller.test.ts index 67f6c5456bb..f651b2b42a8 100644 --- a/packages/lit-query/src/tests/queries-controller.test.ts +++ b/packages/lit-query/src/tests/queries-controller.test.ts @@ -179,8 +179,9 @@ describe('createQueriesController', () => { ) expect( - explicitClient.getQueryCache().find({ queryKey: consumer.queryKeys[0]! }), - ).toBeDefined() + explicitClient.getQueryCache().find({ queryKey: consumer.queryKeys[0]! }) + ?.state.data, + ).toBe('alpha') expect( providerClient.getQueryCache().find({ queryKey: consumer.queryKeys[0]! }), ).toBeUndefined() diff --git a/packages/lit-query/src/tests/query-controller.test.ts b/packages/lit-query/src/tests/query-controller.test.ts index d8261e585c9..43e0bfb4055 100644 --- a/packages/lit-query/src/tests/query-controller.test.ts +++ b/packages/lit-query/src/tests/query-controller.test.ts @@ -1154,7 +1154,7 @@ describe('createQueryController', () => { await waitFor(() => query().isSuccess) const cacheQuery = client.getQueryCache().find({ queryKey }) - expect(cacheQuery).toBeDefined() + expect(cacheQuery?.state.data).toEqual(['a', 'b']) expect(cacheQuery?.getObserversCount()).toBe(1) host.disconnect() @@ -1196,8 +1196,8 @@ describe('createQueryController', () => { await consumer.updateComplete await waitFor(() => consumer.query().isSuccess) - expect(consumer.query().data).toBeDefined() - expect(consumer.queryCalls).toBeGreaterThan(0) + expect(consumer.queryCalls).toBe(1) + expect(consumer.query().data).toBe('value-1') consumer.query.destroy() provider.remove() @@ -1232,7 +1232,7 @@ describe('createQueryController', () => { await consumer.updateComplete await waitFor(() => consumer.query().isSuccess) - expect(consumer.query().data).toBeDefined() + expect(consumer.query().data).toBe('value-1') consumer.query.destroy() provider.remove() diff --git a/packages/preact-query-devtools/CHANGELOG.md b/packages/preact-query-devtools/CHANGELOG.md index 6e59fe956d5..f7b2bb5d3eb 100644 --- a/packages/preact-query-devtools/CHANGELOG.md +++ b/packages/preact-query-devtools/CHANGELOG.md @@ -1,5 +1,37 @@ # @tanstack/preact-query-devtools +## 5.101.4 + +### Patch Changes + +- Updated dependencies []: + - @tanstack/preact-query@5.101.4 + - @tanstack/query-devtools@5.101.4 + +## 5.101.3 + +### Patch Changes + +- Updated dependencies []: + - @tanstack/preact-query@5.101.3 + - @tanstack/query-devtools@5.101.3 + +## 5.101.2 + +### Patch Changes + +- Updated dependencies [[`f5bf180`](https://github.com/TanStack/query/commit/f5bf180d933d8b8d9d9e7b845e55b26a3a413b07), [`25cdd97`](https://github.com/TanStack/query/commit/25cdd975fed4703d2ca5b600ca5ccd2b600b3dd8), [`ecd89c8`](https://github.com/TanStack/query/commit/ecd89c8faf7acc226f00633ea3a761d3ab842c1d), [`01c7634`](https://github.com/TanStack/query/commit/01c763444e3cf3dfa9744f13911aa1533cac3c29), [`49012db`](https://github.com/TanStack/query/commit/49012dbd5192dfe483d3b108b72ffaa7f2849e0f)]: + - @tanstack/query-devtools@5.101.2 + - @tanstack/preact-query@5.101.2 + +## 5.101.1 + +### Patch Changes + +- Updated dependencies []: + - @tanstack/preact-query@5.101.1 + - @tanstack/query-devtools@5.101.1 + ## 5.101.0 ### Patch Changes diff --git a/packages/preact-query-devtools/package.json b/packages/preact-query-devtools/package.json index 03a27a29388..584857d956b 100644 --- a/packages/preact-query-devtools/package.json +++ b/packages/preact-query-devtools/package.json @@ -1,6 +1,6 @@ { "name": "@tanstack/preact-query-devtools", - "version": "5.101.0", + "version": "5.101.4", "description": "Developer tools to interact with and visualize the TanStack/preact-query cache", "author": "tannerlinsley", "license": "MIT", diff --git a/packages/preact-query-devtools/src/__tests__/PreactQueryDevtools.test.tsx b/packages/preact-query-devtools/src/__tests__/PreactQueryDevtools.test.tsx index cdbd85021d6..75e69860edd 100644 --- a/packages/preact-query-devtools/src/__tests__/PreactQueryDevtools.test.tsx +++ b/packages/preact-query-devtools/src/__tests__/PreactQueryDevtools.test.tsx @@ -2,6 +2,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import { render } from '@testing-library/preact' import { QueryClient, QueryClientProvider } from '@tanstack/preact-query' import { TanstackQueryDevtools } from '@tanstack/query-devtools' +import type { PreactQueryDevtools as PreactQueryDevtoolsComponent } from '../PreactQueryDevtools' const mountMock = vi.fn() const unmountMock = vi.fn() @@ -26,22 +27,22 @@ vi.mock('@tanstack/query-devtools', () => ({ })) describe('PreactQueryDevtools', () => { - beforeEach(() => { + let PreactQueryDevtools: typeof PreactQueryDevtoolsComponent + let queryClient: QueryClient + + beforeEach(async () => { vi.clearAllMocks() + ;({ PreactQueryDevtools } = await import('../PreactQueryDevtools')) + queryClient = new QueryClient() }) - it('should throw an error if no query client has been set', async () => { - const { PreactQueryDevtools } = await import('../PreactQueryDevtools') - + it('should throw an error if no query client has been set', () => { expect(() => render()).toThrow( 'No QueryClient set, use QueryClientProvider to set one', ) }) - it('should not throw an error if query client is provided via context', async () => { - const { PreactQueryDevtools } = await import('../PreactQueryDevtools') - const queryClient = new QueryClient() - + it('should not throw an error if query client is provided via context', () => { expect(() => render( @@ -52,20 +53,14 @@ describe('PreactQueryDevtools', () => { expect(mountMock).toHaveBeenCalled() }) - it('should not throw an error if query client is provided via props', async () => { - const { PreactQueryDevtools } = await import('../PreactQueryDevtools') - const queryClient = new QueryClient() - + it('should not throw an error if query client is provided via props', () => { expect(() => render(), ).not.toThrow() expect(mountMock).toHaveBeenCalled() }) - it('should forward "buttonPosition" to the devtools instance', async () => { - const { PreactQueryDevtools } = await import('../PreactQueryDevtools') - const queryClient = new QueryClient() - + it('should forward "buttonPosition" to the devtools instance', () => { render( , ) @@ -73,36 +68,25 @@ describe('PreactQueryDevtools', () => { expect(setButtonPositionMock).toHaveBeenCalledWith('top-left') }) - it('should forward "position" to the devtools instance', async () => { - const { PreactQueryDevtools } = await import('../PreactQueryDevtools') - const queryClient = new QueryClient() - + it('should forward "position" to the devtools instance', () => { render() expect(setPositionMock).toHaveBeenCalledWith('left') }) - it('should forward "initialIsOpen" to the devtools instance', async () => { - const { PreactQueryDevtools } = await import('../PreactQueryDevtools') - const queryClient = new QueryClient() - + it('should forward "initialIsOpen" to the devtools instance', () => { render() expect(setInitialIsOpenMock).toHaveBeenCalledWith(true) }) - it('should default "initialIsOpen" to "false" when the prop is omitted', async () => { - const { PreactQueryDevtools } = await import('../PreactQueryDevtools') - const queryClient = new QueryClient() - + it('should default "initialIsOpen" to "false" when the prop is omitted', () => { render() expect(setInitialIsOpenMock).toHaveBeenCalledWith(false) }) - it('should forward "errorTypes" to the devtools instance', async () => { - const { PreactQueryDevtools } = await import('../PreactQueryDevtools') - const queryClient = new QueryClient() + it('should forward "errorTypes" to the devtools instance', () => { const errorTypes = [ { name: 'Network', initializer: () => new Error('Network') }, ] @@ -112,37 +96,25 @@ describe('PreactQueryDevtools', () => { expect(setErrorTypesMock).toHaveBeenCalledWith(errorTypes) }) - it('should default "errorTypes" to an empty array when the prop is omitted', async () => { - const { PreactQueryDevtools } = await import('../PreactQueryDevtools') - const queryClient = new QueryClient() - + it('should default "errorTypes" to an empty array when the prop is omitted', () => { render() expect(setErrorTypesMock).toHaveBeenCalledWith([]) }) - it('should forward "theme" to the devtools instance', async () => { - const { PreactQueryDevtools } = await import('../PreactQueryDevtools') - const queryClient = new QueryClient() - + it('should forward "theme" to the devtools instance', () => { render() expect(setThemeMock).toHaveBeenCalledWith('dark') }) - it('should forward the resolved "QueryClient" via "setClient"', async () => { - const { PreactQueryDevtools } = await import('../PreactQueryDevtools') - const queryClient = new QueryClient() - + it('should forward the resolved "QueryClient" via "setClient"', () => { render() expect(setClientMock).toHaveBeenCalledWith(queryClient) }) - it('should forward "styleNonce" to the devtools constructor', async () => { - const { PreactQueryDevtools } = await import('../PreactQueryDevtools') - const queryClient = new QueryClient() - + it('should forward "styleNonce" to the devtools constructor', () => { render() expect(TanstackQueryDevtools).toHaveBeenCalledWith( @@ -150,9 +122,7 @@ describe('PreactQueryDevtools', () => { ) }) - it('should forward "shadowDOMTarget" to the devtools constructor', async () => { - const { PreactQueryDevtools } = await import('../PreactQueryDevtools') - const queryClient = new QueryClient() + it('should forward "shadowDOMTarget" to the devtools constructor', () => { const shadowDOMTarget = document .createElement('div') .attachShadow({ mode: 'open' }) @@ -169,10 +139,7 @@ describe('PreactQueryDevtools', () => { ) }) - it('should forward "hideDisabledQueries" to the devtools constructor', async () => { - const { PreactQueryDevtools } = await import('../PreactQueryDevtools') - const queryClient = new QueryClient() - + it('should forward "hideDisabledQueries" to the devtools constructor', () => { render( , ) @@ -182,10 +149,72 @@ describe('PreactQueryDevtools', () => { ) }) - it('should call "unmount" on the devtools instance when the component unmounts', async () => { - const { PreactQueryDevtools } = await import('../PreactQueryDevtools') - const queryClient = new QueryClient() + it('should forward a "buttonPosition" change to the devtools instance after mount', () => { + const { rerender } = render( + , + ) + setButtonPositionMock.mockClear() + + rerender( + , + ) + + expect(setButtonPositionMock).toHaveBeenCalledWith('top-left') + }) + + it('should forward a "position" change to the devtools instance after mount', () => { + const { rerender } = render( + , + ) + setPositionMock.mockClear() + + rerender() + + expect(setPositionMock).toHaveBeenCalledWith('top') + }) + + it('should forward an "initialIsOpen" change to the devtools instance after mount', () => { + const { rerender } = render( + , + ) + setInitialIsOpenMock.mockClear() + + rerender() + + expect(setInitialIsOpenMock).toHaveBeenCalledWith(true) + }) + + it('should forward an "errorTypes" change to the devtools instance after mount', () => { + const { rerender } = render( + , + ) + setErrorTypesMock.mockClear() + + const errorTypes = [ + { name: 'Network', initializer: () => new Error('Network') }, + ] + rerender( + , + ) + + expect(setErrorTypesMock).toHaveBeenCalledWith(errorTypes) + }) + + it('should forward a "theme" change to the devtools instance after mount', () => { + const { rerender } = render( + , + ) + setThemeMock.mockClear() + + rerender() + + expect(setThemeMock).toHaveBeenCalledWith('dark') + }) + it('should call "unmount" on the devtools instance when the component unmounts', () => { const { unmount } = render() unmount() @@ -197,8 +226,8 @@ describe('PreactQueryDevtools', () => { vi.resetModules() try { - const { PreactQueryDevtools } = await import('..') - expect(PreactQueryDevtools({})).toBeNull() + const { PreactQueryDevtools: ProductionDevtools } = await import('..') + expect(ProductionDevtools({})).toBeNull() } finally { vi.unstubAllEnvs() vi.resetModules() diff --git a/packages/preact-query-devtools/src/__tests__/PreactQueryDevtoolsPanel.test.tsx b/packages/preact-query-devtools/src/__tests__/PreactQueryDevtoolsPanel.test.tsx index 1d54b36bb60..3eedadff41f 100644 --- a/packages/preact-query-devtools/src/__tests__/PreactQueryDevtoolsPanel.test.tsx +++ b/packages/preact-query-devtools/src/__tests__/PreactQueryDevtoolsPanel.test.tsx @@ -2,6 +2,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import { render } from '@testing-library/preact' import { QueryClient, QueryClientProvider } from '@tanstack/preact-query' import { TanstackQueryDevtoolsPanel } from '@tanstack/query-devtools' +import type { PreactQueryDevtoolsPanel as PreactQueryDevtoolsPanelComponent } from '../PreactQueryDevtoolsPanel' const mountMock = vi.fn() const unmountMock = vi.fn() @@ -24,24 +25,23 @@ vi.mock('@tanstack/query-devtools', () => ({ })) describe('PreactQueryDevtoolsPanel', () => { - beforeEach(() => { + let PreactQueryDevtoolsPanel: typeof PreactQueryDevtoolsPanelComponent + let queryClient: QueryClient + + beforeEach(async () => { vi.clearAllMocks() + ;({ PreactQueryDevtoolsPanel } = + await import('../PreactQueryDevtoolsPanel')) + queryClient = new QueryClient() }) - it('should throw an error if no query client has been set', async () => { - const { PreactQueryDevtoolsPanel } = - await import('../PreactQueryDevtoolsPanel') - + it('should throw an error if no query client has been set', () => { expect(() => render()).toThrow( 'No QueryClient set, use QueryClientProvider to set one', ) }) - it('should not throw an error if query client is provided via context', async () => { - const { PreactQueryDevtoolsPanel } = - await import('../PreactQueryDevtoolsPanel') - const queryClient = new QueryClient() - + it('should not throw an error if query client is provided via context', () => { expect(() => render( @@ -52,42 +52,30 @@ describe('PreactQueryDevtoolsPanel', () => { expect(mountMock).toHaveBeenCalled() }) - it('should not throw an error if query client is provided via props', async () => { - const { PreactQueryDevtoolsPanel } = - await import('../PreactQueryDevtoolsPanel') - const queryClient = new QueryClient() - + it('should not throw an error if query client is provided via props', () => { expect(() => render(), ).not.toThrow() expect(mountMock).toHaveBeenCalled() }) - it('should forward "onClose" to the devtools instance', async () => { - const { PreactQueryDevtoolsPanel } = - await import('../PreactQueryDevtoolsPanel') - const queryClient = new QueryClient() + it('should forward "onClose" to the devtools instance', () => { const onClose = vi.fn() render() - expect(setOnCloseMock).toHaveBeenCalledWith(expect.any(Function)) + expect(setOnCloseMock).toHaveBeenCalledWith(onClose) }) - it('should default "onClose" to a no-op function when the prop is omitted', async () => { - const { PreactQueryDevtoolsPanel } = - await import('../PreactQueryDevtoolsPanel') - const queryClient = new QueryClient() - + it('should default "onClose" to a no-op function when the prop is omitted', () => { render() - expect(setOnCloseMock).toHaveBeenCalledWith(expect.any(Function)) + const forwarded = setOnCloseMock.mock.calls[0]?.[0] + expect(forwarded).toBeInstanceOf(Function) + expect(forwarded()).toBeUndefined() }) - it('should forward "errorTypes" to the devtools instance', async () => { - const { PreactQueryDevtoolsPanel } = - await import('../PreactQueryDevtoolsPanel') - const queryClient = new QueryClient() + it('should forward "errorTypes" to the devtools instance', () => { const errorTypes = [ { name: 'Network', initializer: () => new Error('Network') }, ] @@ -99,41 +87,25 @@ describe('PreactQueryDevtoolsPanel', () => { expect(setErrorTypesMock).toHaveBeenCalledWith(errorTypes) }) - it('should default "errorTypes" to an empty array when the prop is omitted', async () => { - const { PreactQueryDevtoolsPanel } = - await import('../PreactQueryDevtoolsPanel') - const queryClient = new QueryClient() - + it('should default "errorTypes" to an empty array when the prop is omitted', () => { render() expect(setErrorTypesMock).toHaveBeenCalledWith([]) }) - it('should forward "theme" to the devtools instance', async () => { - const { PreactQueryDevtoolsPanel } = - await import('../PreactQueryDevtoolsPanel') - const queryClient = new QueryClient() - + it('should forward "theme" to the devtools instance', () => { render() expect(setThemeMock).toHaveBeenCalledWith('dark') }) - it('should forward the resolved "QueryClient" via "setClient"', async () => { - const { PreactQueryDevtoolsPanel } = - await import('../PreactQueryDevtoolsPanel') - const queryClient = new QueryClient() - + it('should forward the resolved "QueryClient" via "setClient"', () => { render() expect(setClientMock).toHaveBeenCalledWith(queryClient) }) - it('should forward "styleNonce" to the devtools constructor', async () => { - const { PreactQueryDevtoolsPanel } = - await import('../PreactQueryDevtoolsPanel') - const queryClient = new QueryClient() - + it('should forward "styleNonce" to the devtools constructor', () => { render() expect(TanstackQueryDevtoolsPanel).toHaveBeenCalledWith( @@ -141,10 +113,7 @@ describe('PreactQueryDevtoolsPanel', () => { ) }) - it('should forward "shadowDOMTarget" to the devtools constructor', async () => { - const { PreactQueryDevtoolsPanel } = - await import('../PreactQueryDevtoolsPanel') - const queryClient = new QueryClient() + it('should forward "shadowDOMTarget" to the devtools constructor', () => { const shadowDOMTarget = document .createElement('div') .attachShadow({ mode: 'open' }) @@ -161,11 +130,7 @@ describe('PreactQueryDevtoolsPanel', () => { ) }) - it('should forward "hideDisabledQueries" to the devtools constructor', async () => { - const { PreactQueryDevtoolsPanel } = - await import('../PreactQueryDevtoolsPanel') - const queryClient = new QueryClient() - + it('should forward "hideDisabledQueries" to the devtools constructor', () => { render( { ) }) - it('should preserve the default container height when "style" omits "height"', async () => { - const { PreactQueryDevtoolsPanel } = - await import('../PreactQueryDevtoolsPanel') - const queryClient = new QueryClient() - + it('should preserve the default container height when "style" omits "height"', () => { const { container } = render( { }) }) - it('should let "style" override the default container height on the rendered element', async () => { - const { PreactQueryDevtoolsPanel } = - await import('../PreactQueryDevtoolsPanel') - const queryClient = new QueryClient() - + it('should let "style" override the default container height on the rendered element', () => { const { container } = render( { }) }) - it('should call "unmount" on the devtools instance when the component unmounts', async () => { - const { PreactQueryDevtoolsPanel } = - await import('../PreactQueryDevtoolsPanel') - const queryClient = new QueryClient() - + it('should call "unmount" on the devtools instance when the component unmounts', () => { const { unmount } = render( , ) @@ -232,8 +185,9 @@ describe('PreactQueryDevtoolsPanel', () => { vi.resetModules() try { - const { PreactQueryDevtoolsPanel } = await import('..') - expect(PreactQueryDevtoolsPanel({})).toBeNull() + const { PreactQueryDevtoolsPanel: ProductionDevtoolsPanel } = + await import('..') + expect(ProductionDevtoolsPanel({})).toBeNull() } finally { vi.unstubAllEnvs() vi.resetModules() diff --git a/packages/preact-query-persist-client/CHANGELOG.md b/packages/preact-query-persist-client/CHANGELOG.md index 558fd2d15a1..6cdd8e44de8 100644 --- a/packages/preact-query-persist-client/CHANGELOG.md +++ b/packages/preact-query-persist-client/CHANGELOG.md @@ -1,5 +1,37 @@ # @tanstack/preact-query-persist-client +## 5.101.4 + +### Patch Changes + +- Updated dependencies []: + - @tanstack/preact-query@5.101.4 + - @tanstack/query-persist-client-core@5.101.4 + +## 5.101.3 + +### Patch Changes + +- Updated dependencies []: + - @tanstack/preact-query@5.101.3 + - @tanstack/query-persist-client-core@5.101.3 + +## 5.101.2 + +### Patch Changes + +- Updated dependencies []: + - @tanstack/preact-query@5.101.2 + - @tanstack/query-persist-client-core@5.101.2 + +## 5.101.1 + +### Patch Changes + +- Updated dependencies []: + - @tanstack/preact-query@5.101.1 + - @tanstack/query-persist-client-core@5.101.1 + ## 5.101.0 ### Patch Changes diff --git a/packages/preact-query-persist-client/package.json b/packages/preact-query-persist-client/package.json index 1a3a980080d..41bbfc6c244 100644 --- a/packages/preact-query-persist-client/package.json +++ b/packages/preact-query-persist-client/package.json @@ -1,6 +1,6 @@ { "name": "@tanstack/preact-query-persist-client", - "version": "5.101.0", + "version": "5.101.4", "description": "Preact bindings to work with persisters in TanStack/preact-query", "author": "tannerlinsley", "license": "MIT", diff --git a/packages/preact-query/CHANGELOG.md b/packages/preact-query/CHANGELOG.md index 5190be09fcc..93b3887e914 100644 --- a/packages/preact-query/CHANGELOG.md +++ b/packages/preact-query/CHANGELOG.md @@ -1,5 +1,33 @@ # @tanstack/preact-query +## 5.101.4 + +### Patch Changes + +- Updated dependencies []: + - @tanstack/query-core@5.101.4 + +## 5.101.3 + +### Patch Changes + +- Updated dependencies [[`7e3c822`](https://github.com/TanStack/query/commit/7e3c822a10896f41a8f1031c16b85096277af677)]: + - @tanstack/query-core@5.101.3 + +## 5.101.2 + +### Patch Changes + +- Updated dependencies []: + - @tanstack/query-core@5.101.2 + +## 5.101.1 + +### Patch Changes + +- Updated dependencies [[`9eff92e`](https://github.com/TanStack/query/commit/9eff92ed86e284ec0125b3a3539d028688235bd1)]: + - @tanstack/query-core@5.101.1 + ## 5.101.0 ### Patch Changes diff --git a/packages/preact-query/README.md b/packages/preact-query/README.md index e9deefa8582..8c105035a3a 100644 --- a/packages/preact-query/README.md +++ b/packages/preact-query/README.md @@ -1,6 +1,20 @@ -![TanStack Query Header](https://github.com/TanStack/query/raw/main/media/repo-header.png) + + + + TanStack Preact Query + Hooks for fetching, caching and updating asynchronous data in Preact diff --git a/packages/preact-query/package.json b/packages/preact-query/package.json index f7cdfc972af..ee2ee4b3776 100644 --- a/packages/preact-query/package.json +++ b/packages/preact-query/package.json @@ -1,6 +1,6 @@ { "name": "@tanstack/preact-query", - "version": "5.101.0", + "version": "5.101.4", "description": "Hooks for managing, caching and syncing asynchronous and remote data in preact", "author": "tannerlinsley", "license": "MIT", diff --git a/packages/preact-query/src/__tests__/QueryClientProvider.test.tsx b/packages/preact-query/src/__tests__/QueryClientProvider.test.tsx index c6ba18d989a..010d3f7e24e 100644 --- a/packages/preact-query/src/__tests__/QueryClientProvider.test.tsx +++ b/packages/preact-query/src/__tests__/QueryClientProvider.test.tsx @@ -47,7 +47,7 @@ describe('QueryClientProvider', () => { await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('test')).toBeInTheDocument() - expect(queryCache.find({ queryKey: key })).toBeDefined() + expect(queryCache.find({ queryKey: key })?.state.data).toBe('test') }) it('allows multiple caches to be partitioned', async () => { @@ -100,10 +100,10 @@ describe('QueryClientProvider', () => { 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() + expect(queryCache1.find({ queryKey: key1 })?.state.data).toBe('test1') + expect(queryCache1.find({ queryKey: key2 })).toBeUndefined() + expect(queryCache2.find({ queryKey: key1 })).toBeUndefined() + expect(queryCache2.find({ queryKey: key2 })?.state.data).toBe('test2') }) it("uses defaultOptions for queries when they don't provide their own config", async () => { @@ -141,7 +141,6 @@ describe('QueryClientProvider', () => { await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('test')).toBeInTheDocument() - expect(queryCache.find({ queryKey: key })).toBeDefined() expect(queryCache.find({ queryKey: key })?.options.gcTime).toBe(Infinity) }) diff --git a/packages/preact-query/src/__tests__/mutationOptions.test.tsx b/packages/preact-query/src/__tests__/mutationOptions.test.tsx index 8b3864b4688..5972fc4d357 100644 --- a/packages/preact-query/src/__tests__/mutationOptions.test.tsx +++ b/packages/preact-query/src/__tests__/mutationOptions.test.tsx @@ -532,6 +532,5 @@ describe('mutationOptions', () => { await vi.advanceTimersByTimeAsync(11) expect(mutationStateArray.length).toEqual(1) expect(mutationStateArray[0]?.data).toEqual('data1') - expect(mutationStateArray[1]).toBeFalsy() }) }) diff --git a/packages/preact-query/src/__tests__/useInfiniteQuery.test.tsx b/packages/preact-query/src/__tests__/useInfiniteQuery.test.tsx index 5a96f0dbf00..55601b5c952 100644 --- a/packages/preact-query/src/__tests__/useInfiniteQuery.test.tsx +++ b/packages/preact-query/src/__tests__/useInfiniteQuery.test.tsx @@ -29,21 +29,6 @@ interface Result { 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', () => { let queryCache: QueryCache let queryClient: QueryClient @@ -1538,12 +1523,18 @@ describe('useInfiniteQuery', () => { refetch, } = useInfiniteQuery({ queryKey: key, - queryFn: ({ pageParam }) => - fetchItems( - pageParam, - fetchCountRef.current++, - pageParam === MAX || (pageParam === MAX - 1 && isRemovedLastPage), - ), + queryFn: ({ pageParam }): Promise => { + const noNext = + pageParam === MAX || (pageParam === MAX - 1 && isRemovedLastPage) + return sleep(10).then(() => ({ + items: [...new Array(10)] + .fill(null) + .map((_, d) => pageParam * pageSize + d), + nextId: noNext ? undefined : pageParam + 1, + prevId: pageParam - 1, + ts: fetchCountRef.current++, + })) + }, getNextPageParam: (lastPage) => lastPage.nextId, initialPageParam: 0, }) diff --git a/packages/preact-query/src/__tests__/useMutation.test.tsx b/packages/preact-query/src/__tests__/useMutation.test.tsx index 4ed75ae8df2..e24f79ddfba 100644 --- a/packages/preact-query/src/__tests__/useMutation.test.tsx +++ b/packages/preact-query/src/__tests__/useMutation.test.tsx @@ -210,7 +210,7 @@ describe('useMutation', () => { function Page() { const { mutateAsync } = useMutation({ - mutationFn: async (_text: string) => + mutationFn: (_text: string) => sleep(10).then(() => { throw new Error('oops') }), @@ -370,7 +370,7 @@ describe('useMutation', () => { function Page() { const { mutateAsync } = useMutation({ - mutationFn: async (_text: string) => + mutationFn: (_text: string) => sleep(10).then(() => { throw new Error('oops') }), @@ -463,15 +463,15 @@ describe('useMutation', () => { expect(getByRole('heading').textContent).toBe('3') expect(onSuccessMock).toHaveBeenCalledTimes(3) - expect(onSuccessMock).toHaveBeenCalledWith(1) - expect(onSuccessMock).toHaveBeenCalledWith(2) - expect(onSuccessMock).toHaveBeenCalledWith(3) + expect(onSuccessMock).toHaveBeenNthCalledWith(1, 1) + expect(onSuccessMock).toHaveBeenNthCalledWith(2, 2) + expect(onSuccessMock).toHaveBeenNthCalledWith(3, 3) expect(onSettledMock).toHaveBeenCalledTimes(3) - expect(onSettledMock).toHaveBeenCalledWith(1) - expect(onSettledMock).toHaveBeenCalledWith(2) - expect(onSettledMock).toHaveBeenCalledWith(3) + expect(onSettledMock).toHaveBeenNthCalledWith(1, 1) + expect(onSettledMock).toHaveBeenNthCalledWith(2, 2) + expect(onSettledMock).toHaveBeenNthCalledWith(3, 3) }) it('should set correct values for `failureReason` and `failureCount` on multiple mutate calls', async () => { @@ -568,24 +568,30 @@ describe('useMutation', () => { await vi.advanceTimersByTimeAsync(0) expect(getByRole('heading').textContent).toBe('3') expect(onErrorMock).toHaveBeenCalledTimes(3) - expect(onErrorMock).toHaveBeenCalledWith( + expect(onErrorMock).toHaveBeenNthCalledWith( + 1, 'Expected mock error. All is well! 1', ) - expect(onErrorMock).toHaveBeenCalledWith( + expect(onErrorMock).toHaveBeenNthCalledWith( + 2, 'Expected mock error. All is well! 2', ) - expect(onErrorMock).toHaveBeenCalledWith( + expect(onErrorMock).toHaveBeenNthCalledWith( + 3, 'Expected mock error. All is well! 3', ) expect(onSettledMock).toHaveBeenCalledTimes(3) - expect(onSettledMock).toHaveBeenCalledWith( + expect(onSettledMock).toHaveBeenNthCalledWith( + 1, 'Expected mock error. All is well! 1', ) - expect(onSettledMock).toHaveBeenCalledWith( + expect(onSettledMock).toHaveBeenNthCalledWith( + 2, 'Expected mock error. All is well! 2', ) - expect(onSettledMock).toHaveBeenCalledWith( + expect(onSettledMock).toHaveBeenNthCalledWith( + 3, 'Expected mock error. All is well! 3', ) }) @@ -734,7 +740,7 @@ describe('useMutation', () => { function Page() { const { mutateAsync } = useMutation({ - mutationFn: async (_text: string) => + mutationFn: (_text: string) => sleep(10).then(() => { throw new Error('oops') }), @@ -854,7 +860,7 @@ describe('useMutation', () => { function Page() { const { mutateAsync } = useMutation({ - mutationFn: async (_text: string) => Promise.reject(new Error('oops')), + mutationFn: (_text: string) => Promise.reject(new Error('oops')), onError: () => { callbacks.push('useMutation.onError') return Promise.resolve() @@ -905,7 +911,7 @@ describe('useMutation', () => { function Page() { const { mutate } = useMutation({ - mutationFn: async (_text: string) => + mutationFn: (_text: string) => sleep(10).then(() => Promise.reject(new Error('oops'))), onError: () => { callbacks.push('useMutation.onError') @@ -1232,13 +1238,13 @@ describe('useMutation', () => { 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')) - }, + mutationFn: (_text: string) => + sleep(10).then(() => { + count++ + return count > 1 + ? Promise.resolve(`data${count}`) + : Promise.reject(new Error('oops')) + }), retry: 1, retryDelay: 5, networkMode: 'offlineFirst', @@ -1802,10 +1808,10 @@ describe('useMutation', () => { function Page() { const mutation = useMutation({ - mutationFn: async (_text: string) => { - await sleep(10) - throw mutateFnError - }, + mutationFn: (_text: string) => + sleep(10).then(() => { + throw mutateFnError + }), onError: () => Promise.reject(error), }) @@ -1848,10 +1854,10 @@ describe('useMutation', () => { function Page() { const mutation = useMutation({ - mutationFn: async (_text: string) => { - await sleep(10) - throw mutateFnError - }, + mutationFn: (_text: string) => + sleep(10).then(() => { + throw mutateFnError + }), onSettled: () => Promise.reject(error), onError, }) @@ -1889,7 +1895,7 @@ describe('useMutation', () => { function Page() { const mutation = useMutation( { - mutationFn: async (text: string) => { + mutationFn: (text: string) => { return Promise.resolve(text) }, }, @@ -2011,13 +2017,13 @@ describe('useMutation', () => { const [message, setMessage] = useState('idle') const { mutate } = useMutation({ - mutationFn: async (shouldFail: boolean) => { - await sleep(10) - if (shouldFail) { - throw new Error('submission failed') - } - return 'submitted successfully' - }, + mutationFn: (shouldFail: boolean) => + sleep(10).then(() => { + if (shouldFail) { + throw new Error('submission failed') + } + return 'submitted successfully' + }), retry: false, }) @@ -2053,13 +2059,13 @@ describe('useMutation', () => { const [message, setMessage] = useState('idle') const { mutate } = useMutation({ - mutationFn: async (shouldFail: boolean) => { - await sleep(10) - if (shouldFail) { - throw new Error('submission failed') - } - return 'submitted successfully' - }, + mutationFn: (shouldFail: boolean) => + sleep(10).then(() => { + if (shouldFail) { + throw new Error('submission failed') + } + return 'submitted successfully' + }), retry: false, }) @@ -2097,14 +2103,14 @@ describe('useMutation', () => { const [message, setMessage] = useState('idle') const { mutate } = useMutation({ - mutationFn: async () => { - await sleep(10) - attempt++ - if (attempt < 2) { - throw new Error('temporary failure') - } - return 'success' - }, + mutationFn: () => + sleep(10).then(() => { + attempt++ + if (attempt < 2) { + throw new Error('temporary failure') + } + return 'success' + }), retry: false, }) @@ -2303,13 +2309,13 @@ describe('useMutation', () => { const [result, setResult] = useState('idle') const { mutateAsync } = useMutation({ - mutationFn: async (file: string) => { - await sleep(10) - if (file === 'file2') { - throw new Error('upload failed') - } - return `uploaded: ${file}` - }, + mutationFn: (file: string) => + sleep(10).then(() => { + if (file === 'file2') { + throw new Error('upload failed') + } + return `uploaded: ${file}` + }), retry: false, }) @@ -2351,13 +2357,13 @@ describe('useMutation', () => { const [result, setResult] = useState('idle') const { mutateAsync } = useMutation({ - mutationFn: async (file: string) => { - await sleep(10) - if (file === 'file2') { - throw new Error('upload failed') - } - return `uploaded: ${file}` - }, + mutationFn: (file: string) => + sleep(10).then(() => { + if (file === 'file2') { + throw new Error('upload failed') + } + return `uploaded: ${file}` + }), retry: false, }) diff --git a/packages/preact-query/src/__tests__/usePrefetchInfiniteQuery.test.tsx b/packages/preact-query/src/__tests__/usePrefetchInfiniteQuery.test.tsx index f7fdc3fd73c..8a6b781919b 100644 --- a/packages/preact-query/src/__tests__/usePrefetchInfiniteQuery.test.tsx +++ b/packages/preact-query/src/__tests__/usePrefetchInfiniteQuery.test.tsx @@ -3,7 +3,6 @@ import { fireEvent } from '@testing-library/preact' import type { VNode } from 'preact' import { Suspense } from 'preact/compat' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import type { Mock } from 'vitest' import { QueryCache, @@ -11,36 +10,8 @@ import { usePrefetchInfiniteQuery, useSuspenseInfiniteQuery, } from '..' -import type { InfiniteData, UseSuspenseInfiniteQueryOptions } from '..' import { renderWithClient } from './utils' -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', () => { let queryCache: QueryCache let queryClient: QueryClient @@ -59,42 +30,36 @@ describe('usePrefetchInfiniteQuery', () => { const Fallback = vi.fn().mockImplementation(() =>
Loading...
) - function Suspended(props: { - queryOpts: UseSuspenseInfiniteQueryOptions< - T, - Error, - InfiniteData, - Array, - any - > - renderPage: (page: T) => VNode - }) { - 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, - }, + 'Do you fetch on render?', + 'Or do you render as you fetch?', + 'Either way, Tanstack Query helps you!', ] const queryOpts = { queryKey: queryKey(), - ...generateInfiniteQueryOptions(data), + queryFn: vi + .fn<(context: { pageParam: number }) => Promise>() + .mockImplementation(({ pageParam }) => + sleep(10).then(() => data[pageParam]!), + ), + initialPageParam: 0, + getNextPageParam: (_lastPage: string, allPages: Array) => + allPages.length < data.length ? allPages.length : undefined, + } + + function Page() { + const state = useSuspenseInfiniteQuery(queryOpts) + + return ( +
+ {state.data.pages.map((page, index) => ( +
data: {page}
+ ))} + +
+ ) } function App() { @@ -102,10 +67,7 @@ describe('usePrefetchInfiniteQuery', () => { return ( }> -
data: {page.data}
} - /> +
) } @@ -127,28 +89,47 @@ describe('usePrefetchInfiniteQuery', () => { }) it('should not display fallback if the query cache is already populated', async () => { + const data = [ + 'Prefetch rocks!', + 'No waterfalls, boy!', + 'Tanstack Query #ftw', + ] + 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 }, - ]), + queryFn: vi + .fn<(context: { pageParam: number }) => Promise>() + .mockImplementation(({ pageParam }) => + sleep(10).then(() => data[pageParam]!), + ), + initialPageParam: 0, + getNextPageParam: (_lastPage: string, allPages: Array) => + allPages.length < data.length ? allPages.length : undefined, } queryClient.prefetchInfiniteQuery({ ...queryOpts, pages: 3 }) await vi.advanceTimersByTimeAsync(30) - ;(queryOpts.queryFn as Mock).mockClear() + queryOpts.queryFn.mockClear() + + function Page() { + const state = useSuspenseInfiniteQuery(queryOpts) + + return ( +
+ {state.data.pages.map((page, index) => ( +
data: {page}
+ ))} + +
+ ) + } function App() { usePrefetchInfiniteQuery(queryOpts) return ( }> -
data: {page.data}
} - /> +
) } @@ -165,13 +146,20 @@ describe('usePrefetchInfiniteQuery', () => { }) it('should not create an endless loop when using inside a suspense boundary', async () => { + const data = ['Infinite Page 1', 'Infinite Page 2', 'Infinite Page 3'] + 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 }, - ]), + queryFn: vi + .fn<(context: { pageParam: number }) => Promise>() + .mockImplementation(({ pageParam }) => + sleep(10).then(() => data[pageParam]!), + ), + initialPageParam: 0, + // always reports another page available, to guard against an endless + // auto-advance loop rather than a bounded pagination sequence + getNextPageParam: (_lastPage: string, allPages: Array) => + allPages.length, } function Prefetch({ children }: { children: VNode }) { @@ -179,14 +167,24 @@ describe('usePrefetchInfiniteQuery', () => { return <>{children} } + function Page() { + const state = useSuspenseInfiniteQuery(queryOpts) + + return ( +
+ {state.data.pages.map((page, index) => ( +
data: {page}
+ ))} + +
+ ) + } + function App() { return ( }> -
data: {page.data}
} - /> +
) diff --git a/packages/preact-query/src/__tests__/usePrefetchQuery.test.tsx b/packages/preact-query/src/__tests__/usePrefetchQuery.test.tsx index 49fd0e682e1..ac6bec8ed1d 100644 --- a/packages/preact-query/src/__tests__/usePrefetchQuery.test.tsx +++ b/packages/preact-query/src/__tests__/usePrefetchQuery.test.tsx @@ -11,15 +11,9 @@ import { useQueryErrorResetBoundary, useSuspenseQuery, } from '..' -import type { UseSuspenseQueryOptions } from '..' import { ErrorBoundary } from './ErrorBoundary' import { renderWithClient } from './utils' -const generateQueryFn = (data: string) => - vi - .fn<(...args: Array) => Promise>() - .mockImplementation(() => sleep(10).then(() => data)) - describe('usePrefetchQuery', () => { let queryCache: QueryCache let queryClient: QueryClient @@ -35,29 +29,20 @@ describe('usePrefetchQuery', () => { vi.useRealTimers() }) - function Suspended(props: { - queryOpts: UseSuspenseQueryOptions> - children?: VNode - }) { - 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'), + queryFn: vi.fn(() => sleep(10).then(() => 'prefetchQuery')), } const componentQueryOpts = { ...queryOpts, - queryFn: generateQueryFn('useSuspenseQuery'), + queryFn: () => sleep(10).then(() => 'useSuspenseQuery'), + } + + function Page() { + const state = useSuspenseQuery(componentQueryOpts) + return
data: {String(state.data)}
} function App() { @@ -65,13 +50,15 @@ describe('usePrefetchQuery', () => { return ( - + ) } const rendered = renderWithClient(queryClient, ) + expect(rendered.getByText('Loading...')).toBeInTheDocument() + await vi.advanceTimersByTimeAsync(10) expect(rendered.getByText('data: prefetchQuery')).toBeInTheDocument() expect(queryOpts.queryFn).toHaveBeenCalledTimes(1) @@ -80,7 +67,14 @@ describe('usePrefetchQuery', () => { it('should not prefetch query if query state exists', async () => { const queryOpts = { queryKey: queryKey(), - queryFn: generateQueryFn('The usePrefetchQuery hook is smart!'), + queryFn: vi.fn(() => + sleep(10).then(() => 'The usePrefetchQuery hook is smart!'), + ), + } + + function Page() { + const state = useSuspenseQuery(queryOpts) + return
data: {String(state.data)}
} function App() { @@ -88,7 +82,7 @@ describe('usePrefetchQuery', () => { return ( - + ) } @@ -108,18 +102,23 @@ describe('usePrefetchQuery', () => { 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 queryFn = vi.fn(() => sleep(10).then(() => 'Not an error')) const queryOpts = { queryKey: queryKey(), queryFn, } - queryFn.mockImplementationOnce(async () => { - await sleep(10) + queryFn.mockImplementationOnce(() => + sleep(10).then(() => { + throw new Error('Oops! Server error!') + }), + ) - throw new Error('Oops! Server error!') - }) + function Page() { + const state = useSuspenseQuery(queryOpts) + return
data: {String(state.data)}
+ } function App() { usePrefetchQuery(queryOpts) @@ -127,7 +126,7 @@ describe('usePrefetchQuery', () => { return (
Oops!
}> - +
) @@ -146,7 +145,7 @@ describe('usePrefetchQuery', () => { }) it('should not create an endless loop when using inside a suspense boundary', async () => { - const queryFn = generateQueryFn('prefetchedQuery') + const queryFn = vi.fn(() => sleep(10).then(() => 'prefetchedQuery')) const queryOpts = { queryKey: queryKey(), @@ -158,11 +157,16 @@ describe('usePrefetchQuery', () => { return <>{children} } + function Page() { + const state = useSuspenseQuery(queryOpts) + return
data: {String(state.data)}
+ } + function App() { return ( }> - + ) @@ -177,18 +181,25 @@ describe('usePrefetchQuery', () => { 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 queryFn = vi.fn(() => + sleep(10).then(() => 'This is fine :dog: :fire:'), + ) const queryOpts = { queryKey: queryKey(), queryFn, } - queryFn.mockImplementationOnce(async () => { - await sleep(10) + queryFn.mockImplementationOnce(() => + sleep(10).then(() => { + throw new Error('Oops! Server error!') + }), + ) - throw new Error('Oops! Server error!') - }) + function Page() { + const state = useSuspenseQuery(queryOpts) + return
data: {String(state.data)}
+ } function App() { const { reset } = useQueryErrorResetBoundary() @@ -205,7 +216,7 @@ describe('usePrefetchQuery', () => { )} > - + ) @@ -230,21 +241,48 @@ describe('usePrefetchQuery', () => { it('should not create a suspense waterfall if prefetch is fired', async () => { const firstQueryOpts = { queryKey: queryKey(), - queryFn: generateQueryFn('Prefetch is nice!'), + queryFn: vi.fn(() => sleep(10).then(() => 'Prefetch is nice!')), } const secondQueryOpts = { queryKey: queryKey(), - queryFn: generateQueryFn('Prefetch is really nice!!'), + queryFn: vi.fn(() => sleep(10).then(() => 'Prefetch is really nice!!')), } const thirdQueryOpts = { queryKey: queryKey(), - queryFn: generateQueryFn('Prefetch does not create waterfalls!!'), + queryFn: vi.fn(() => + sleep(10).then(() => 'Prefetch does not create waterfalls!!'), + ), } const Fallback = vi.fn().mockImplementation(() =>
Loading...
) + function FirstQuery({ children }: { children?: VNode }) { + const state = useSuspenseQuery(firstQueryOpts) + return ( +
+
data: {String(state.data)}
+ {children} +
+ ) + } + + function SecondQuery({ children }: { children?: VNode }) { + const state = useSuspenseQuery(secondQueryOpts) + return ( +
+
data: {String(state.data)}
+ {children} +
+ ) + } + + function ThirdQuery() { + const state = useSuspenseQuery(thirdQueryOpts) + return
data: {String(state.data)}
+ } + function App() { usePrefetchQuery(firstQueryOpts) usePrefetchQuery(secondQueryOpts) @@ -252,11 +290,11 @@ describe('usePrefetchQuery', () => { return ( }> - - - - - + + + + + ) } diff --git a/packages/preact-query/src/__tests__/useQueries.test-d.tsx b/packages/preact-query/src/__tests__/useQueries.test-d.tsx index 85816953ae6..b52b08b05bd 100644 --- a/packages/preact-query/src/__tests__/useQueries.test-d.tsx +++ b/packages/preact-query/src/__tests__/useQueries.test-d.tsx @@ -2,171 +2,871 @@ import { queryKey } from '@tanstack/query-test-utils' import { describe, expectTypeOf, it } from 'vitest' import { skipToken } from '..' -import type { OmitKeyof } from '..' +import type { OmitKeyof, QueryFunction, QueryKey } from '..' import { queryOptions } from '../queryOptions' import type { UseQueryOptions, UseQueryResult } from '../types' import { useQueries } from '../useQueries' +import type { QueryFunctionContext } from '@tanstack/query-core' -describe('UseQueries config object overload', () => { - it('TData should always be defined when initialData is provided as an object', () => { - const query1 = { - queryKey: queryKey(), - queryFn: () => { - return { - wow: true, - } - }, - initialData: { - wow: false, - }, - } - - const query2 = { - queryKey: queryKey(), - queryFn: () => 'Query Data', - initialData: 'initial data', - } - - const query3 = { - queryKey: queryKey(), - 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() - }) +describe('useQueries', () => { + describe('config object overload', () => { + it('TData should always be defined when initialData is provided as an object', () => { + const query1 = { + queryKey: queryKey(), + queryFn: () => { + return { + wow: true, + } + }, + initialData: { + wow: false, + }, + } + + const query2 = { + queryKey: queryKey(), + queryFn: () => 'Query Data', + initialData: 'initial data', + } - it('TData should be defined when passed through queryOptions', () => { - const options = queryOptions({ - queryKey: queryKey(), - queryFn: () => { - return { + const query3 = { + queryKey: queryKey(), + 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: queryKey(), + queryFn: () => { + return { + wow: true, + } + }, + initialData: { wow: true, - } - }, - initialData: { - wow: true, - }, + }, + }) + const queryResults = useQueries({ queries: [options] }) + + const data = queryResults[0].data + + expectTypeOf(data).toEqualTypeOf<{ wow: boolean }>() }) - const queryResults = useQueries({ queries: [options] }) - const data = queryResults[0].data + it('should be possible to define a different TData than TQueryFnData using select with queryOptions spread into useQueries', () => { + const query1 = queryOptions({ + queryKey: queryKey(), + queryFn: () => Promise.resolve(1), + select: (data) => data > 1, + }) - expectTypeOf(data).toEqualTypeOf<{ wow: boolean }>() - }) + const query2 = { + queryKey: queryKey(), + queryFn: () => Promise.resolve(1), + select: (data: number) => data > 1, + } - it('should be possible to define a different TData than TQueryFnData using select with queryOptions spread into useQueries', () => { - const query1 = queryOptions({ - queryKey: queryKey(), - queryFn: () => Promise.resolve(1), - select: (data) => data > 1, + const queryResults = useQueries({ queries: [query1, query2] }) + const query1Data = queryResults[0].data + const query2Data = queryResults[1].data + + expectTypeOf(query1Data).toEqualTypeOf() + expectTypeOf(query2Data).toEqualTypeOf() }) - const query2 = { - queryKey: queryKey(), - queryFn: () => Promise.resolve(1), - select: (data: number) => data > 1, - } + it('TData should have undefined in the union when initialData is provided as a function which can return undefined', () => { + const queryResults = useQueries({ + queries: [ + { + queryKey: queryKey(), + queryFn: () => { + return { + wow: true, + } + }, + initialData: () => undefined as { wow: boolean } | undefined, + }, + ], + }) - const queryResults = useQueries({ queries: [query1, query2] }) - const query1Data = queryResults[0].data - const query2Data = queryResults[1].data + const data = queryResults[0].data - expectTypeOf(query1Data).toEqualTypeOf() - expectTypeOf(query2Data).toEqualTypeOf() - }) + 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: queryKey(), + queryFn: () => Promise.resolve('data'), + }, + ], + }) + } - it('TData should have undefined in the union when initialData is provided as a function which can return undefined', () => { - const queryResults = useQueries({ - queries: [ - { - queryKey: queryKey(), - queryFn: () => { - return { - wow: true, - } + 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: queryKey(), + queryFn: Math.random() > 0.5 ? skipToken : () => Promise.resolve(5), }, - initialData: () => undefined as { wow: boolean } | undefined, - }, - ], + ], + }) + + const firstResult = queryResults[0] + + expectTypeOf(firstResult).toEqualTypeOf>() + expectTypeOf(firstResult.data).toEqualTypeOf() }) - const data = queryResults[0].data + it('should return correct data for dynamic queries with mixed result types', () => { + const Queries1 = { + get: () => + queryOptions({ + queryKey: queryKey(), + queryFn: () => Promise.resolve(1), + }), + } + const Queries2 = { + get: () => + queryOptions({ + queryKey: queryKey(), + queryFn: () => Promise.resolve(true), + }), + } - expectTypeOf(data).toEqualTypeOf<{ wow: boolean } | undefined>() + const queries1List = [1, 2, 3].map(() => ({ ...Queries1.get() })) + const result = useQueries({ + queries: [...queries1List, { ...Queries2.get() }], + }) + + expectTypeOf(result).toEqualTypeOf< + [ + ...Array>, + UseQueryResult, + ] + >() + }) }) - describe('custom hook', () => { - it('should allow custom hooks using UseQueryOptions', () => { - type Data = string + describe('type parameters', () => { + it('should handle type parameter - tuple of tuples', () => { + const key1 = queryKey() + const key2 = queryKey() + const key3 = queryKey() - const useCustomQueries = ( - options?: OmitKeyof, 'queryKey' | 'queryFn'>, - ) => { - return useQueries({ + // @ts-expect-error (Page component is not rendered) + function Page() { + const result1 = useQueries< + [[number], [string], [Array, boolean]] + >({ queries: [ { - ...options, - queryKey: queryKey(), - queryFn: () => Promise.resolve('data'), + queryKey: key1, + queryFn: () => 1, + }, + { + queryKey: key2, + queryFn: () => 'string', + }, + { + queryKey: key3, + queryFn: () => ['string[]'], + }, + ], + }) + expectTypeOf(result1[0]).toEqualTypeOf< + UseQueryResult + >() + expectTypeOf(result1[1]).toEqualTypeOf< + UseQueryResult + >() + 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, 10) + }, + }, + ], + }) + expectTypeOf(result2[0]).toEqualTypeOf< + UseQueryResult + >() + expectTypeOf(result2[1]).toEqualTypeOf< + UseQueryResult + >() + 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, 10) + }, + 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: [], }, ], }) } + }) - const queryResults = useCustomQueries() - const data = queryResults[0].data + it('should handle type parameter - tuple of objects', () => { + const key1 = queryKey() + const key2 = queryKey() + const key3 = queryKey() - expectTypeOf(data).toEqualTypeOf() + // @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< + UseQueryResult + >() + expectTypeOf(result1[1]).toEqualTypeOf< + UseQueryResult + >() + 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, 10) + }, + }, + ], + }) + expectTypeOf(result2[0]).toEqualTypeOf< + UseQueryResult + >() + expectTypeOf(result2[1]).toEqualTypeOf< + UseQueryResult + >() + 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< + UseQueryResult + >() + expectTypeOf(result3[1]).toEqualTypeOf< + UseQueryResult + >() + 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, 10) + }, + 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('TData should have correct type when conditional skipToken is passed', () => { - const queryResults = useQueries({ - queries: [ - { - queryKey: queryKey(), - queryFn: Math.random() > 0.5 ? skipToken : () => Promise.resolve(5), - }, - ], + it('should return correct 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: queryKey(), + queryFn: () => 'string', + select: (a) => { + expectTypeOf(a).toEqualTypeOf() + return a.toLowerCase() + }, + }), + queryOptions({ + queryKey: queryKey(), + queryFn: () => 'string', + select: (a) => { + expectTypeOf(a).toEqualTypeOf() + return parseInt(a, 10) + }, + }), + ], + }) + expectTypeOf(result4[0]).toEqualTypeOf>() + expectTypeOf(result4[1]).toEqualTypeOf>() + expectTypeOf(result4[0].data).toEqualTypeOf() + expectTypeOf(result4[1].data).toEqualTypeOf() + } }) - const firstResult = queryResults[0] + it('should handle array literal without type parameter to infer result type', () => { + const key1 = queryKey() + const key2 = queryKey() + const key3 = queryKey() + const key4 = queryKey() + const key5 = queryKey() - expectTypeOf(firstResult).toEqualTypeOf>() - expectTypeOf(firstResult.data).toEqualTypeOf() - }) + 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, 10), + }, + { + 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, 10), + }, + { + queryKey: key5, + queryFn: () => 'string', + select: (a: string) => parseInt(a, 10), + throwOnError, + }, + ], + }) + expectTypeOf(result4[0]).toEqualTypeOf>() + expectTypeOf(result4[1]).toEqualTypeOf>() + expectTypeOf(result4[2]).toEqualTypeOf>() + expectTypeOf(result4[3]).toEqualTypeOf< + UseQueryResult + >() + + // 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< + UseQueryResult + >() - it('should return correct data for dynamic queries with mixed result types', () => { - const Queries1 = { - get: () => - queryOptions({ - queryKey: queryKey(), - queryFn: () => Promise.resolve(1), - }), - } - const Queries2 = { - get: () => - queryOptions({ - queryKey: queryKey(), - queryFn: () => Promise.resolve(true), - }), - } - - const queries1List = [1, 2, 3].map(() => ({ ...Queries1.get() })) - const result = useQueries({ - queries: [...queries1List, { ...Queries2.get() }], + // 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()), + }, + ], + }) + } }) - expectTypeOf(result).toEqualTypeOf< - [...Array>, UseQueryResult] - >() + it('should handle 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> + >() + } + }) }) }) diff --git a/packages/preact-query/src/__tests__/useQueries.test.tsx b/packages/preact-query/src/__tests__/useQueries.test.tsx index 9eaab9fa811..ccb3b3a34f4 100644 --- a/packages/preact-query/src/__tests__/useQueries.test.tsx +++ b/packages/preact-query/src/__tests__/useQueries.test.tsx @@ -1,32 +1,10 @@ -import type { QueryFunctionContext } from '@tanstack/query-core' import { queryKey, sleep } from '@tanstack/query-test-utils' import { fireEvent, render } from '@testing-library/preact' import { useCallback, useEffect, useState } from 'preact/hooks' -import { - afterEach, - beforeEach, - describe, - expect, - expectTypeOf, - it, - vi, -} from 'vitest' - -import { - IsRestoringProvider, - QueryCache, - QueryClient, - queryOptions, - skipToken, - useQueries, -} from '..' -import type { - QueryFunction, - QueryKey, - QueryObserverResult, - UseQueryOptions, - UseQueryResult, -} from '..' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' + +import { IsRestoringProvider, QueryCache, QueryClient, useQueries } from '..' +import type { QueryObserverResult, UseQueryResult } from '..' import { ErrorBoundary } from './ErrorBoundary' import { renderWithClient } from './utils' @@ -96,11 +74,11 @@ describe('useQueries', () => { queries: [ { queryKey: key1, - queryFn: async () => { - await sleep(10) - count++ - return count - }, + queryFn: () => + sleep(10).then(() => { + count++ + return count + }), }, ], }) @@ -134,672 +112,6 @@ describe('useQueries', () => { 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, 10) - }, - }, - ], - }) - 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, 10) - }, - 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, 10) - }, - }, - ], - }) - 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, 10) - }, - 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: queryKey(), - queryFn: () => 'string', - select: (a) => { - expectTypeOf(a).toEqualTypeOf() - return a.toLowerCase() - }, - }), - queryOptions({ - queryKey: queryKey(), - queryFn: () => 'string', - select: (a) => { - expectTypeOf(a).toEqualTypeOf() - return parseInt(a, 10) - }, - }), - ], - }) - 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, 10), - }, - { - 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, 10), - }, - { - queryKey: key5, - queryFn: () => 'string', - select: (a: string) => parseInt(a, 10), - 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') @@ -833,7 +145,7 @@ describe('useQueries', () => { }, { queryKey: key4, - queryFn: async () => + queryFn: () => Promise.reject( new Error('this should not throw because query#2 already did'), ), @@ -901,7 +213,7 @@ describe('useQueries', () => { }, { queryKey: key4, - queryFn: async () => + queryFn: () => Promise.reject( new Error('this should not throw because query#3 already did'), ), @@ -1457,19 +769,19 @@ describe('useQueries', () => { queries: [ { queryKey: [key1], - queryFn: async () => { - await sleep(10) - queryFns.push('first result') - return 'first result' - }, + queryFn: () => + sleep(10).then(() => { + queryFns.push('first result') + return 'first result' + }), }, { queryKey: [key2], - queryFn: async () => { - await sleep(20) - queryFns.push('second result') - return 'second result' - }, + queryFn: () => + sleep(20).then(() => { + queryFns.push('second result') + return 'second result' + }), }, ], combine: () => 'foo', diff --git a/packages/preact-query/src/__tests__/useQuery.test.tsx b/packages/preact-query/src/__tests__/useQuery.test.tsx index e99de4e3336..3a36725ff09 100644 --- a/packages/preact-query/src/__tests__/useQuery.test.tsx +++ b/packages/preact-query/src/__tests__/useQuery.test.tsx @@ -758,11 +758,11 @@ describe('useQuery', () => { function Page() { const state = useQuery({ queryKey: key, - queryFn: async () => { - await sleep(10) - count++ - return `test${count}` - }, + queryFn: () => + sleep(10).then(() => { + count++ + return `test${count}` + }), }) states.push(state) @@ -877,10 +877,7 @@ describe('useQuery', () => { function Page() { const state = useQuery({ queryKey: key, - queryFn: async () => { - await sleep(10) - return ++count - }, + queryFn: () => sleep(10).then(() => ++count), notifyOnChangeProps: 'all', }) @@ -939,11 +936,11 @@ describe('useQuery', () => { function Page() { const state = useQuery({ queryKey: key, - queryFn: async () => { - await sleep(10) - count++ - return count === 1 ? result1 : result2 - }, + queryFn: () => + sleep(10).then(() => { + count++ + return count === 1 ? result1 : result2 + }), notifyOnChangeProps: 'all', }) @@ -1027,11 +1024,11 @@ describe('useQuery', () => { function Page() { const state = useQuery({ queryKey: key, - queryFn: async () => { - await sleep(10) - count++ - return count - }, + queryFn: () => + sleep(10).then(() => { + count++ + return count + }), staleTime: Infinity, }) @@ -1073,11 +1070,11 @@ describe('useQuery', () => { function Page() { const state = useQuery({ queryKey: key, - queryFn: async () => { - await sleep(10) - count++ - return count - }, + queryFn: () => + sleep(10).then(() => { + count++ + return count + }), enabled: false, }) @@ -1113,11 +1110,11 @@ describe('useQuery', () => { function Page() { const state = useQuery({ queryKey: key, - queryFn: async () => { - await sleep(10) - count++ - return count - }, + queryFn: () => + sleep(10).then(() => { + count++ + return count + }), enabled: false, }) @@ -1272,12 +1269,7 @@ describe('useQuery', () => { const state = useQuery({ queryKey: [key, count], - queryFn: async () => { - await sleep(10) - return { - count, - } - }, + queryFn: () => sleep(10).then(() => ({ count })), select(data) { return data.count }, @@ -1344,12 +1336,7 @@ describe('useQuery', () => { const state = useQuery({ queryKey: [key, count], - queryFn: async () => { - await sleep(10) - return { - count, - } - }, + queryFn: () => sleep(10).then(() => ({ count })), select(data) { return data.count }, @@ -1399,12 +1386,7 @@ describe('useQuery', () => { const state = useQuery({ queryKey: [key, count], - queryFn: async () => { - await sleep(10) - return { - count, - } - }, + queryFn: () => sleep(10).then(() => ({ count })), select(data) { return data.count }, @@ -1483,13 +1465,13 @@ describe('useQuery', () => { 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) - }, + queryFn: () => + sleep(10).then(() => { + if (count === 2) { + throw new Error('Error test') + } + return Promise.resolve(count) + }), retry: false, placeholderData: keepPreviousData, }) @@ -2201,11 +2183,10 @@ describe('useQuery', () => { const state = useQuery({ queryKey: key, - queryFn: async () => { - await sleep(5) - fetchCounterRef.current++ - return `fetch counter: ${fetchCounterRef.current}` - }, + queryFn: () => + sleep(5).then( + () => `fetch counter: ${++fetchCounterRef.current}`, + ), notifyOnChangeProps, }) @@ -2591,10 +2572,7 @@ describe('useQuery', () => { function Page() { const state = useQuery({ queryKey: key, - queryFn: async () => { - await sleep(10) - return count++ - }, + queryFn: () => sleep(10).then(() => count++), staleTime: Infinity, refetchOnWindowFocus: 'always', @@ -2630,10 +2608,7 @@ describe('useQuery', () => { function Page() { const state = useQuery({ queryKey: key, - queryFn: async () => { - await sleep(10) - return count++ - }, + queryFn: () => sleep(10).then(() => count++), staleTime: 0, retry: 0, @@ -3996,16 +3971,15 @@ describe('useQuery', () => { function Page() { const result = useQuery({ queryKey: key, - queryFn: async () => { - await sleep(10) - return ( - queryFn() || { - data: { - nested: true, + queryFn: () => + sleep(10).then( + () => + queryFn() || { + data: { + nested: true, + }, }, - } - ) - }, + ), }) useMemo(() => { @@ -4077,10 +4051,7 @@ describe('useQuery', () => { function Page() { const queryInfo = useQuery({ queryKey: key, - queryFn: async () => { - await sleep(10) - return count++ - }, + queryFn: () => sleep(10).then(() => count++), refetchInterval: ({ state: { data = 0 } }) => (data < 2 ? 10 : false), }) @@ -4715,11 +4686,11 @@ describe('useQuery', () => { function Page() { const state = useQuery({ queryKey: key, - queryFn: async () => { - await sleep(10) - count++ - return count - }, + queryFn: () => + sleep(10).then(() => { + count++ + return count + }), staleTime: Infinity, }) @@ -4786,11 +4757,11 @@ describe('useQuery', () => { function Page() { const state = useQuery({ queryKey: key, - queryFn: async () => { - await sleep(10) - count++ - return count - }, + queryFn: () => + sleep(10).then(() => { + count++ + return count + }), staleTime: Infinity, enabled: false, notifyOnChangeProps: 'all', @@ -4971,13 +4942,13 @@ describe('useQuery', () => { 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')) - } - return 'data' - }, + queryFn: () => + sleep(10).then(() => { + if (id % 2 === 1) { + return Promise.reject(new Error('Error')) + } + return 'data' + }), retry: false, retryOnMount: () => false, refetchOnMount: false, @@ -5092,14 +5063,14 @@ describe('useQuery', () => { function Page() { const state = useQuery({ queryKey: key, - queryFn: async () => { - await sleep(10) - if (count === 0) { - count++ - throw error - } - return 5 - }, + queryFn: () => + sleep(10).then(() => { + if (count === 0) { + count++ + throw error + } + return 5 + }), retry: false, }) diff --git a/packages/preact-query/src/__tests__/useSuspenseQuery.test.tsx b/packages/preact-query/src/__tests__/useSuspenseQuery.test.tsx index d09cc6b8032..8397312bb60 100644 --- a/packages/preact-query/src/__tests__/useSuspenseQuery.test.tsx +++ b/packages/preact-query/src/__tests__/useSuspenseQuery.test.tsx @@ -206,7 +206,7 @@ describe('useSuspenseQuery', () => { expect(rendered.queryByText('loading')).not.toBeInTheDocument() expect(rendered.queryByText('rendered')).not.toBeInTheDocument() - expect(queryCache.find({ queryKey: key })).toBeFalsy() + expect(queryCache.find({ queryKey: key })).toBeUndefined() fireEvent.click(rendered.getByLabelText('toggle')) expect(rendered.getByText('loading')).toBeInTheDocument() @@ -788,6 +788,7 @@ describe('useSuspenseQuery', () => { , ) + expect(rendered.getByText('loading')).toBeInTheDocument() await vi.advanceTimersByTimeAsync(10) expect(rendered.getByText('rendered')).toBeInTheDocument() diff --git a/packages/preact-query/src/useQueries.ts b/packages/preact-query/src/useQueries.ts index be57d2aebd6..7e30658b843 100644 --- a/packages/preact-query/src/useQueries.ts +++ b/packages/preact-query/src/useQueries.ts @@ -162,7 +162,7 @@ export type QueriesOptions< > : 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! + : // If T is *some* array but we couldn't assign unknown[] to it, then it must hold some known/homogeneous type! // use this to infer the param types in the case of Array.map() argument T extends Array< UseQueryOptionsForUseQueries< diff --git a/packages/preact-query/src/useSuspenseQueries.ts b/packages/preact-query/src/useSuspenseQueries.ts index 017c34328c2..191c25458c3 100644 --- a/packages/preact-query/src/useSuspenseQueries.ts +++ b/packages/preact-query/src/useSuspenseQueries.ts @@ -125,7 +125,7 @@ export type SuspenseQueriesOptions< > : 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! + : // If T is *some* array but we couldn't assign unknown[] to it, then it must hold some known/homogeneous type! // use this to infer the param types in the case of Array.map() argument T extends Array< UseSuspenseQueryOptions< diff --git a/packages/query-async-storage-persister/CHANGELOG.md b/packages/query-async-storage-persister/CHANGELOG.md index 36c5b9ba941..45e89a1c329 100644 --- a/packages/query-async-storage-persister/CHANGELOG.md +++ b/packages/query-async-storage-persister/CHANGELOG.md @@ -1,5 +1,37 @@ # @tanstack/query-async-storage-persister +## 5.101.4 + +### Patch Changes + +- Updated dependencies []: + - @tanstack/query-core@5.101.4 + - @tanstack/query-persist-client-core@5.101.4 + +## 5.101.3 + +### Patch Changes + +- Updated dependencies [[`7e3c822`](https://github.com/TanStack/query/commit/7e3c822a10896f41a8f1031c16b85096277af677)]: + - @tanstack/query-core@5.101.3 + - @tanstack/query-persist-client-core@5.101.3 + +## 5.101.2 + +### Patch Changes + +- Updated dependencies []: + - @tanstack/query-core@5.101.2 + - @tanstack/query-persist-client-core@5.101.2 + +## 5.101.1 + +### Patch Changes + +- Updated dependencies [[`9eff92e`](https://github.com/TanStack/query/commit/9eff92ed86e284ec0125b3a3539d028688235bd1)]: + - @tanstack/query-core@5.101.1 + - @tanstack/query-persist-client-core@5.101.1 + ## 5.101.0 ### Patch Changes diff --git a/packages/query-async-storage-persister/package.json b/packages/query-async-storage-persister/package.json index 1a63a95c0f7..6dc6574316a 100644 --- a/packages/query-async-storage-persister/package.json +++ b/packages/query-async-storage-persister/package.json @@ -1,6 +1,6 @@ { "name": "@tanstack/query-async-storage-persister", - "version": "5.101.0", + "version": "5.101.4", "description": "A persister for asynchronous storages, to be used with TanStack/Query", "author": "tannerlinsley", "license": "MIT", diff --git a/packages/query-broadcast-client-experimental/CHANGELOG.md b/packages/query-broadcast-client-experimental/CHANGELOG.md index 9f2a4a56c79..79ddbca7960 100644 --- a/packages/query-broadcast-client-experimental/CHANGELOG.md +++ b/packages/query-broadcast-client-experimental/CHANGELOG.md @@ -1,5 +1,33 @@ # @tanstack/query-broadcast-client-experimental +## 5.101.4 + +### Patch Changes + +- Updated dependencies []: + - @tanstack/query-core@5.101.4 + +## 5.101.3 + +### Patch Changes + +- Updated dependencies [[`7e3c822`](https://github.com/TanStack/query/commit/7e3c822a10896f41a8f1031c16b85096277af677)]: + - @tanstack/query-core@5.101.3 + +## 5.101.2 + +### Patch Changes + +- Updated dependencies []: + - @tanstack/query-core@5.101.2 + +## 5.101.1 + +### Patch Changes + +- Updated dependencies [[`9eff92e`](https://github.com/TanStack/query/commit/9eff92ed86e284ec0125b3a3539d028688235bd1)]: + - @tanstack/query-core@5.101.1 + ## 5.101.0 ### Patch Changes diff --git a/packages/query-broadcast-client-experimental/package.json b/packages/query-broadcast-client-experimental/package.json index 3271f4299df..07f48f1ed05 100644 --- a/packages/query-broadcast-client-experimental/package.json +++ b/packages/query-broadcast-client-experimental/package.json @@ -1,6 +1,6 @@ { "name": "@tanstack/query-broadcast-client-experimental", - "version": "5.101.0", + "version": "5.101.4", "description": "An experimental plugin to for broadcasting the state of your queryClient between browser tabs/windows", "author": "tannerlinsley", "license": "MIT", diff --git a/packages/query-core/CHANGELOG.md b/packages/query-core/CHANGELOG.md index 6f0871c021b..211c6a2ae7a 100644 --- a/packages/query-core/CHANGELOG.md +++ b/packages/query-core/CHANGELOG.md @@ -1,5 +1,21 @@ # @tanstack/query-core +## 5.101.4 + +## 5.101.3 + +### Patch Changes + +- [#11084](https://github.com/TanStack/query/pull/11084) [`7e3c822`](https://github.com/TanStack/query/commit/7e3c822a10896f41a8f1031c16b85096277af677) - Improve `partialMatchKey` performance in query-core. + +## 5.101.2 + +## 5.101.1 + +### Patch Changes + +- [#10610](https://github.com/TanStack/query/pull/10610) [`9eff92e`](https://github.com/TanStack/query/commit/9eff92ed86e284ec0125b3a3539d028688235bd1) - fix missing `dataUpdatedAt` for streamed queries that resolve before hydration + ## 5.101.0 ## 5.100.14 diff --git a/packages/query-core/package.json b/packages/query-core/package.json index 80f9f479f82..03652df3182 100644 --- a/packages/query-core/package.json +++ b/packages/query-core/package.json @@ -1,6 +1,6 @@ { "name": "@tanstack/query-core", - "version": "5.101.0", + "version": "5.101.4", "description": "The framework agnostic core that powers TanStack Query", "author": "tannerlinsley", "license": "MIT", diff --git a/packages/query-core/src/__tests__/focusManager.test.tsx b/packages/query-core/src/__tests__/focusManager.test.tsx index 03e60d5d7f5..842e1b61ce5 100644 --- a/packages/query-core/src/__tests__/focusManager.test.tsx +++ b/packages/query-core/src/__tests__/focusManager.test.tsx @@ -39,7 +39,7 @@ describe('focusManager', () => { vi.advanceTimersByTime(20) expect(count).toEqual(1) - expect(focusManager.isFocused()).toBeTruthy() + expect(focusManager.isFocused()).toBe(true) }) it('should return true for isFocused if document is undefined', () => { @@ -49,7 +49,7 @@ describe('focusManager', () => { delete globalThis.document focusManager.setFocused() - expect(focusManager.isFocused()).toBeTruthy() + expect(focusManager.isFocused()).toBe(true) globalThis.document = document }) diff --git a/packages/query-core/src/__tests__/hydration.test.tsx b/packages/query-core/src/__tests__/hydration.test.tsx index c64cb10da53..389226bd5b6 100644 --- a/packages/query-core/src/__tests__/hydration.test.tsx +++ b/packages/query-core/src/__tests__/hydration.test.tsx @@ -155,7 +155,7 @@ describe('dehydration and rehydration', () => { hydrate(hydrationClient, parsed) expect(hydrationCache.find({ queryKey: key })?.state.data).toBe('string') await vi.advanceTimersByTimeAsync(100) - expect(hydrationCache.find({ queryKey: key })).toBeTruthy() + expect(hydrationCache.find({ queryKey: key })?.state.data).toBe('string') queryClient.clear() hydrationClient.clear() @@ -330,9 +330,11 @@ describe('dehydration and rehydration', () => { const hydrationClient = new QueryClient({ queryCache: hydrationCache }) hydrate(hydrationClient, parsed) - expect(hydrationCache.find({ queryKey: successKey })).toBeTruthy() - expect(hydrationCache.find({ queryKey: loadingKey })).toBeFalsy() - expect(hydrationCache.find({ queryKey: errorKey })).toBeFalsy() + expect(hydrationCache.find({ queryKey: successKey })?.state.data).toBe( + 'success', + ) + expect(hydrationCache.find({ queryKey: loadingKey })).toBeUndefined() + expect(hydrationCache.find({ queryKey: errorKey })).toBeUndefined() queryClient.clear() hydrationClient.clear() @@ -1387,29 +1389,30 @@ describe('dehydration and rehydration', () => { }) it('should preserve queryType for infinite queries during hydration', async () => { + const key = queryKey() const queryCache = new QueryCache() const queryClient = new QueryClient({ queryCache }) - await vi.waitFor(() => - queryClient.prefetchInfiniteQuery({ - queryKey: ['infinite'], - queryFn: async ({ pageParam }) => - sleep(0).then(() => ({ - items: [`page-${pageParam}`], - nextCursor: pageParam + 1, - })), - initialPageParam: 0, - getNextPageParam: (lastPage: { - items: Array - nextCursor: number - }) => lastPage.nextCursor, - }), - ) + const prefetchPromise = queryClient.prefetchInfiniteQuery({ + queryKey: key, + queryFn: ({ pageParam }) => + sleep(10).then(() => ({ + items: [`page-${pageParam}`], + nextCursor: pageParam + 1, + })), + initialPageParam: 0, + getNextPageParam: (lastPage: { + items: Array + nextCursor: number + }) => lastPage.nextCursor, + }) + await vi.advanceTimersByTimeAsync(10) + await prefetchPromise const dehydrated = dehydrate(queryClient) const infiniteQueryState = dehydrated.queries.find( - (q) => q.queryKey[0] === 'infinite', + (q) => q.queryKey[0] === key[0], ) expect(infiniteQueryState?.queryType).toBe('infinite') @@ -1417,30 +1420,31 @@ describe('dehydration and rehydration', () => { const hydrationClient = new QueryClient({ queryCache: hydrationCache }) hydrate(hydrationClient, dehydrated) - const hydratedQuery = hydrationCache.find({ queryKey: ['infinite'] }) - expect(hydratedQuery?.state.data).toBeDefined() - expect(hydratedQuery?.state.data).toHaveProperty('pages') - expect(hydratedQuery?.state.data).toHaveProperty('pageParams') - expect((hydratedQuery?.state.data as any).pages).toHaveLength(1) + const hydratedQuery = hydrationCache.find({ queryKey: key }) + expect(hydratedQuery?.state.data).toEqual({ + pages: [{ items: ['page-0'], nextCursor: 1 }], + pageParams: [0], + }) }) it('should attach infiniteQueryBehavior during hydration', async () => { + const key = queryKey() const queryCache = new QueryCache() const queryClient = new QueryClient({ queryCache }) - await vi.waitFor(() => - queryClient.prefetchInfiniteQuery({ - queryKey: ['infinite-with-behavior'], - queryFn: async ({ pageParam }) => - sleep(0).then(() => ({ - data: `page-${pageParam}`, - next: pageParam + 1, - })), - initialPageParam: 0, - getNextPageParam: (lastPage: { data: string; next: number }) => - lastPage.next, - }), - ) + const prefetchPromise = queryClient.prefetchInfiniteQuery({ + queryKey: key, + queryFn: ({ pageParam }) => + sleep(10).then(() => ({ + data: `page-${pageParam}`, + next: pageParam + 1, + })), + initialPageParam: 0, + getNextPageParam: (lastPage: { data: string; next: number }) => + lastPage.next, + }) + await vi.advanceTimersByTimeAsync(10) + await prefetchPromise const dehydrated = dehydrate(queryClient) @@ -1448,45 +1452,46 @@ describe('dehydration and rehydration', () => { const hydrationClient = new QueryClient({ queryCache: hydrationCache }) hydrate(hydrationClient, dehydrated) - const result = await vi.waitFor(() => - hydrationClient.fetchInfiniteQuery({ - queryKey: ['infinite-with-behavior'], - queryFn: async ({ pageParam }) => - sleep(0).then(() => ({ - data: `page-${pageParam}`, - next: pageParam + 1, - })), - initialPageParam: 0, - getNextPageParam: (lastPage: { data: string; next: number }) => - lastPage.next, - }), - ) + const resultPromise = hydrationClient.fetchInfiniteQuery({ + queryKey: key, + queryFn: ({ pageParam }) => + sleep(10).then(() => ({ + data: `page-${pageParam}`, + next: pageParam + 1, + })), + initialPageParam: 0, + getNextPageParam: (lastPage: { data: string; next: number }) => + lastPage.next, + }) + await vi.advanceTimersByTimeAsync(10) + const result = await resultPromise expect(result.pages).toHaveLength(1) expect(result.pageParams).toHaveLength(1) }) it('should restore infinite query type through dehydrate and hydrate cycle', async () => { + const key = queryKey() const serverClient = new QueryClient({ queryCache: new QueryCache() }) - await vi.waitFor(() => - serverClient.prefetchInfiniteQuery({ - queryKey: ['infinite-type-restore'], - queryFn: async ({ pageParam }) => - sleep(0).then(() => ({ - items: [`item-${pageParam}`], - next: pageParam + 1, - })), - initialPageParam: 0, - getNextPageParam: (lastPage: { items: Array; next: number }) => - lastPage.next, - }), - ) + const prefetchPromise = serverClient.prefetchInfiniteQuery({ + queryKey: key, + queryFn: ({ pageParam }) => + sleep(10).then(() => ({ + items: [`item-${pageParam}`], + next: pageParam + 1, + })), + initialPageParam: 0, + getNextPageParam: (lastPage: { items: Array; next: number }) => + lastPage.next, + }) + await vi.advanceTimersByTimeAsync(10) + await prefetchPromise const dehydrated = dehydrate(serverClient) const dehydratedQuery = dehydrated.queries.find( - (q) => q.queryKey[0] === 'infinite-type-restore', + (q) => q.queryKey[0] === key[0], ) expect(dehydratedQuery?.queryType).toBe('infinite') @@ -1495,27 +1500,28 @@ describe('dehydration and rehydration', () => { hydrate(clientClient, dehydrated) const hydratedQuery = clientCache.find({ - queryKey: ['infinite-type-restore'], + queryKey: key, }) expect(hydratedQuery?.queryType).toBe('infinite') }) it('should preserve pages structure when refetching infinite query after hydration', async () => { + const key = queryKey() const serverClient = new QueryClient({ queryCache: new QueryCache() }) - await vi.waitFor(() => - serverClient.prefetchInfiniteQuery({ - queryKey: ['refetch'], - queryFn: async ({ pageParam }) => - sleep(0).then(() => ({ - items: [`page-${pageParam}`], - next: pageParam + 1, - })), - initialPageParam: 0, - getNextPageParam: (lastPage: { items: Array; next: number }) => - lastPage.next, - }), - ) + const prefetchPromise = serverClient.prefetchInfiniteQuery({ + queryKey: key, + queryFn: ({ pageParam }) => + sleep(10).then(() => ({ + items: [`page-${pageParam}`], + next: pageParam + 1, + })), + initialPageParam: 0, + getNextPageParam: (lastPage: { items: Array; next: number }) => + lastPage.next, + }) + await vi.advanceTimersByTimeAsync(10) + await prefetchPromise const dehydrated = dehydrate(serverClient) @@ -1526,47 +1532,47 @@ describe('dehydration and rehydration', () => { const beforeRefetch = clientClient.getQueryData<{ pages: Array<{ items: Array; next: number }> pageParams: Array - }>(['refetch']) + }>(key) expect(beforeRefetch?.pages).toHaveLength(1) expect(beforeRefetch?.pageParams).toHaveLength(1) - const result = await vi.waitFor(() => - clientClient.fetchInfiniteQuery({ - queryKey: ['refetch'], - queryFn: async ({ pageParam }) => - sleep(0).then(() => ({ - items: [`page-${pageParam}`], - next: pageParam + 1, - })), - initialPageParam: 0, - getNextPageParam: (lastPage: { items: Array; next: number }) => - lastPage.next, - }), - ) + const resultPromise = clientClient.fetchInfiniteQuery({ + queryKey: key, + queryFn: ({ pageParam }) => + sleep(10).then(() => ({ + items: [`page-${pageParam}`], + next: pageParam + 1, + })), + initialPageParam: 0, + getNextPageParam: (lastPage: { items: Array; next: number }) => + lastPage.next, + }) + await vi.advanceTimersByTimeAsync(10) + const result = await resultPromise - expect(result).toHaveProperty('pages') - expect(result).toHaveProperty('pageParams') - expect(Array.isArray(result.pages)).toBe(true) - expect(result.pages).toHaveLength(1) - expect(result.pages[0]).toHaveProperty('items') + expect(result).toEqual({ + pages: [{ items: ['page-0'], next: 1 }], + pageParams: [0], + }) }) it('should retain infinite query type after subsequent setOptions calls', async () => { + const key = queryKey() const serverClient = new QueryClient({ queryCache: new QueryCache() }) - await vi.waitFor(() => - serverClient.prefetchInfiniteQuery({ - queryKey: ['infinite-setoptions-guard'], - queryFn: async ({ pageParam }) => - sleep(0).then(() => ({ - data: `p${pageParam}`, - next: pageParam + 1, - })), - initialPageParam: 0, - getNextPageParam: (lastPage: { data: string; next: number }) => - lastPage.next, - }), - ) + const prefetchPromise = serverClient.prefetchInfiniteQuery({ + queryKey: key, + queryFn: ({ pageParam }) => + sleep(10).then(() => ({ + data: `p${pageParam}`, + next: pageParam + 1, + })), + initialPageParam: 0, + getNextPageParam: (lastPage: { data: string; next: number }) => + lastPage.next, + }) + await vi.advanceTimersByTimeAsync(10) + await prefetchPromise const dehydrated = dehydrate(serverClient) @@ -1574,30 +1580,31 @@ describe('dehydration and rehydration', () => { const clientClient = new QueryClient({ queryCache: clientCache }) hydrate(clientClient, dehydrated) - const query = clientCache.find({ queryKey: ['infinite-setoptions-guard'] })! + const query = clientCache.find({ queryKey: key })! expect(query.queryType).toBe('infinite') - query.setOptions({ queryKey: ['infinite-setoptions-guard'] }) + query.setOptions({ queryKey: key }) expect(query.queryType).toBe('infinite') }) it('should restore all pages when refetching multi-page infinite query after hydration', async () => { + const key = queryKey() const serverClient = new QueryClient({ queryCache: new QueryCache() }) - await vi.waitFor(() => - serverClient.prefetchInfiniteQuery({ - queryKey: ['infinite-multipage-restore'], - queryFn: async ({ pageParam }) => - sleep(0).then(() => ({ - items: [`item-${pageParam}`], - next: pageParam + 1, - })), - initialPageParam: 0, - pages: 2, - getNextPageParam: (lastPage: { items: Array; next: number }) => - lastPage.next, - }), - ) + const prefetchPromise = serverClient.prefetchInfiniteQuery({ + queryKey: key, + queryFn: ({ pageParam }) => + sleep(10).then(() => ({ + items: [`item-${pageParam}`], + next: pageParam + 1, + })), + initialPageParam: 0, + pages: 2, + getNextPageParam: (lastPage: { items: Array; next: number }) => + lastPage.next, + }) + await vi.advanceTimersByTimeAsync(20) + await prefetchPromise const dehydrated = dehydrate(serverClient) @@ -1608,28 +1615,31 @@ describe('dehydration and rehydration', () => { const beforeRefetch = clientClient.getQueryData<{ pages: Array pageParams: Array - }>(['infinite-multipage-restore']) + }>(key) expect(beforeRefetch?.pages).toHaveLength(2) - const result = await vi.waitFor(() => - clientClient.fetchInfiniteQuery({ - queryKey: ['infinite-multipage-restore'], - queryFn: async ({ pageParam }) => - sleep(0).then(() => ({ - items: [`item-${pageParam}`], - next: pageParam + 1, - })), - initialPageParam: 0, - pages: 2, - getNextPageParam: (lastPage: { items: Array; next: number }) => - lastPage.next, - }), - ) + const resultPromise = clientClient.fetchInfiniteQuery({ + queryKey: key, + queryFn: ({ pageParam }) => + sleep(10).then(() => ({ + items: [`item-${pageParam}`], + next: pageParam + 1, + })), + initialPageParam: 0, + pages: 2, + getNextPageParam: (lastPage: { items: Array; next: number }) => + lastPage.next, + }) + await vi.advanceTimersByTimeAsync(20) + const result = await resultPromise - expect(result.pages).toHaveLength(2) - expect(result.pageParams).toHaveLength(2) - expect(result.pages[0]).toHaveProperty('items') - expect(result.pages[1]).toHaveProperty('items') + expect(result).toEqual({ + pages: [ + { items: ['item-0'], next: 1 }, + { items: ['item-1'], next: 2 }, + ], + pageParams: [0, 1], + }) }) // Companion to the test above: when the query already exists in the cache @@ -1637,7 +1647,7 @@ describe('dehydration and rehydration', () => { // synchronous thenable resolution must also produce status: 'success'. // Previously the if (query) branch would spread status: 'pending' from the // server state without correcting it for the resolved data. - it('should set status to success when rehydrating an existing pending query with a synchronously resolved promise', async () => { + it('should set status to success when rehydrating an existing pending query with a synchronously resolved promise', () => { const key = queryKey() // --- server --- @@ -1804,4 +1814,96 @@ describe('dehydration and rehydration', () => { clientQueryClient.clear() serverQueryClient.clear() }) + + it('should set dataUpdatedAt when hydrating a resolved streamed query into a new cache entry', () => { + const key = queryKey() + + // --- server --- + const serverQueryClient = new QueryClient({ + defaultOptions: { + dehydrate: { shouldDehydrateQuery: () => true }, + }, + }) + + let resolvePrefetch: undefined | ((value?: unknown) => void) + void serverQueryClient.prefetchQuery({ + queryKey: key, + queryFn: () => + new Promise((res) => { + resolvePrefetch = res + }), + }) + + const dehydrated = dehydrate(serverQueryClient) + expect(dehydrated.queries[0]?.state.status).toBe('pending') + + // Resolve before hydration — models a React streaming promise that + // resolved between the dehydrate and hydrate calls + resolvePrefetch?.('streamed data') + // @ts-expect-error + dehydrated.queries[0].promise.then = (cb) => { + cb?.('streamed data') + // @ts-expect-error + return dehydrated.queries[0].promise + } + + // --- client --- + const clientQueryClient = new QueryClient() + hydrate(clientQueryClient, dehydrated) + + const query = clientQueryClient.getQueryCache().find({ queryKey: key })! + expect(query.state.status).toBe('success') + expect(query.state.data).toBe('streamed data') + expect(query.state.dataUpdatedAt).toBeGreaterThan(0) + + clientQueryClient.clear() + serverQueryClient.clear() + }) + + it('should set dataUpdatedAt when hydrating a resolved streamed query into an existing cache entry', () => { + const key = queryKey() + + // --- server --- + const serverQueryClient = new QueryClient({ + defaultOptions: { + dehydrate: { shouldDehydrateQuery: () => true }, + }, + }) + + let resolvePrefetch: undefined | ((value?: unknown) => void) + void serverQueryClient.prefetchQuery({ + queryKey: key, + queryFn: () => + new Promise((res) => { + resolvePrefetch = res + }), + }) + + const dehydrated = dehydrate(serverQueryClient) + + resolvePrefetch?.('streamed data') + // @ts-expect-error + dehydrated.queries[0].promise.then = (cb) => { + cb?.('streamed data') + // @ts-expect-error + return dehydrated.queries[0].promise + } + + // --- client --- + // Pre-existing stale entry — updatedAt: 0 ensures dehydratedAt wins + const clientQueryClient = new QueryClient() + clientQueryClient.setQueryData(key, 'old data', { updatedAt: 0 }) + + const query = clientQueryClient.getQueryCache().find({ queryKey: key })! + expect(query.state.dataUpdatedAt).toBe(0) + + hydrate(clientQueryClient, dehydrated) + + expect(query.state.status).toBe('success') + expect(query.state.data).toBe('streamed data') + expect(query.state.dataUpdatedAt).toBeGreaterThan(0) + + clientQueryClient.clear() + serverQueryClient.clear() + }) }) diff --git a/packages/query-core/src/__tests__/infiniteQueryObserver.test.tsx b/packages/query-core/src/__tests__/infiniteQueryObserver.test.tsx index 99c00e37d22..ad560cba801 100644 --- a/packages/query-core/src/__tests__/infiniteQueryObserver.test.tsx +++ b/packages/query-core/src/__tests__/infiniteQueryObserver.test.tsx @@ -70,7 +70,7 @@ describe('InfiniteQueryObserver', () => { expect(observerResult).toMatchObject({ data: { pages: ['1'], pageParams: [1] }, }) - expect(queryFn).toBeCalledWith(expect.objectContaining({ meta })) + expect(queryFn).toHaveBeenCalledWith(expect.objectContaining({ meta })) }) it('should make getNextPageParam and getPreviousPageParam receive current pageParams', async () => { @@ -162,7 +162,7 @@ describe('InfiniteQueryObserver', () => { await vi.advanceTimersByTimeAsync(10) expect(observer.getCurrentResult().data?.pages).toEqual(['1', '2']) - expect(queryFn).toBeCalledTimes(2) + expect(queryFn).toHaveBeenCalledTimes(2) expect(observer.getCurrentResult().hasNextPage).toBe(true) next = undefined @@ -171,7 +171,7 @@ describe('InfiniteQueryObserver', () => { await vi.advanceTimersByTimeAsync(10) expect(observer.getCurrentResult().data?.pages).toEqual(['1']) - expect(queryFn).toBeCalledTimes(3) + expect(queryFn).toHaveBeenCalledTimes(3) expect(observer.getCurrentResult().hasNextPage).toBe(false) }) @@ -194,7 +194,7 @@ describe('InfiniteQueryObserver', () => { await vi.advanceTimersByTimeAsync(10) expect(observer.getCurrentResult().data?.pages).toEqual(['1', '2']) - expect(queryFn).toBeCalledTimes(2) + expect(queryFn).toHaveBeenCalledTimes(2) expect(observer.getCurrentResult().hasNextPage).toBe(true) next = null @@ -203,7 +203,7 @@ describe('InfiniteQueryObserver', () => { await vi.advanceTimersByTimeAsync(10) expect(observer.getCurrentResult().data?.pages).toEqual(['1']) - expect(queryFn).toBeCalledTimes(3) + expect(queryFn).toHaveBeenCalledTimes(3) expect(observer.getCurrentResult().hasNextPage).toBe(false) }) diff --git a/packages/query-core/src/__tests__/mutationCache.test.tsx b/packages/query-core/src/__tests__/mutationCache.test.tsx index 05d4e581dfa..be46802a8e2 100644 --- a/packages/query-core/src/__tests__/mutationCache.test.tsx +++ b/packages/query-core/src/__tests__/mutationCache.test.tsx @@ -503,9 +503,7 @@ describe('mutationCache', () => { expect(testCache.getAll()).toHaveLength(0) expect(callback).toHaveBeenCalledTimes(1) - expect(callback).toHaveBeenCalledWith( - expect.objectContaining({ type: 'removed', mutation }), - ) + expect(callback).toHaveBeenCalledWith({ type: 'removed', mutation }) unsubscribe() }) diff --git a/packages/query-core/src/__tests__/mutationObserver.test.tsx b/packages/query-core/src/__tests__/mutationObserver.test.tsx index 59cb4ebce81..7c72e21e40c 100644 --- a/packages/query-core/src/__tests__/mutationObserver.test.tsx +++ b/packages/query-core/src/__tests__/mutationObserver.test.tsx @@ -31,12 +31,12 @@ describe('mutationObserver', () => { unsubscribe1() - expect(subscription1Handler).toBeCalledTimes(1) - expect(subscription2Handler).toBeCalledTimes(1) + expect(subscription1Handler).toHaveBeenCalledTimes(1) + expect(subscription2Handler).toHaveBeenCalledTimes(1) await vi.advanceTimersByTimeAsync(20) - expect(subscription1Handler).toBeCalledTimes(1) - expect(subscription2Handler).toBeCalledTimes(2) + expect(subscription1Handler).toHaveBeenCalledTimes(1) + expect(subscription2Handler).toHaveBeenCalledTimes(2) unsubscribe2() }) diff --git a/packages/query-core/src/__tests__/onlineManager.test.tsx b/packages/query-core/src/__tests__/onlineManager.test.tsx index d9219e01970..ad726274311 100644 --- a/packages/query-core/src/__tests__/onlineManager.test.tsx +++ b/packages/query-core/src/__tests__/onlineManager.test.tsx @@ -19,7 +19,7 @@ describe('onlineManager', () => { // Force navigator to be undefined // @ts-expect-error navigatorSpy.mockImplementation(() => undefined) - expect(onlineManager.isOnline()).toBeTruthy() + expect(onlineManager.isOnline()).toBe(true) navigatorSpy.mockRestore() }) @@ -28,7 +28,7 @@ describe('onlineManager', () => { const navigatorSpy = vi.spyOn(navigator, 'onLine', 'get') navigatorSpy.mockImplementation(() => true) - expect(onlineManager.isOnline()).toBeTruthy() + expect(onlineManager.isOnline()).toBe(true) navigatorSpy.mockRestore() }) @@ -48,7 +48,7 @@ describe('onlineManager', () => { vi.advanceTimersByTime(20) expect(count).toEqual(1) - expect(onlineManager.isOnline()).toBeFalsy() + expect(onlineManager.isOnline()).toBe(false) }) it('setEventListener should call previous remove handler when replacing an event listener', () => { diff --git a/packages/query-core/src/__tests__/queriesObserver.test.tsx b/packages/query-core/src/__tests__/queriesObserver.test.tsx index 1adfa6195a6..4e940fca409 100644 --- a/packages/query-core/src/__tests__/queriesObserver.test.tsx +++ b/packages/query-core/src/__tests__/queriesObserver.test.tsx @@ -149,7 +149,9 @@ describe('queriesObserver', () => { const queryCache = queryClient.getQueryCache() expect(queryCache.find({ queryKey: key1, type: 'active' })).toBeUndefined() - expect(queryCache.find({ queryKey: key2, type: 'active' })).toBeDefined() + expect( + queryCache.find({ queryKey: key2, type: 'active' })?.queryKey, + ).toEqual(key2) unsubscribe() expect(queryCache.find({ queryKey: key1, type: 'active' })).toBeUndefined() expect(queryCache.find({ queryKey: key2, type: 'active' })).toBeUndefined() @@ -311,9 +313,9 @@ describe('queriesObserver', () => { await vi.advanceTimersByTimeAsync(20) // 1 call: pending - expect(subscription1Handler).toBeCalledTimes(1) + expect(subscription1Handler).toHaveBeenCalledTimes(1) // 1 call: success - expect(subscription2Handler).toBeCalledTimes(1) + expect(subscription2Handler).toHaveBeenCalledTimes(1) // Clean-up unsubscribe2() @@ -473,7 +475,7 @@ describe('queriesObserver', () => { expect(newCombined.count).toBe(2) }) - it('should skip combine notifications while suspense queries have no data', async () => { + it('should skip combine notifications while suspense queries have no data', () => { const key = queryKey() const combine = vi.fn((results: Array) => results.map((result) => result.data), @@ -506,7 +508,7 @@ describe('queriesObserver', () => { unsubscribe() }) - it('should skip combine notifications after suspense is enabled without structural changes', async () => { + it('should skip combine notifications after suspense is enabled without structural changes', () => { const key = queryKey() const combine = vi.fn((results: Array) => results.map((result) => result.data), diff --git a/packages/query-core/src/__tests__/query.test.tsx b/packages/query-core/src/__tests__/query.test.tsx index 192abac6b8a..fdac03e3987 100644 --- a/packages/query-core/src/__tests__/query.test.tsx +++ b/packages/query-core/src/__tests__/query.test.tsx @@ -279,7 +279,6 @@ describe('query', () => { 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) @@ -601,9 +600,9 @@ describe('query', () => { queryFn: () => 'data', gcTime: 0, }) - expect(queryCache.find({ queryKey: key })).toBeDefined() + expect(queryCache.find({ queryKey: key })?.state.status).toBe('pending') const unsubscribe = observer.subscribe(() => undefined) - expect(queryCache.find({ queryKey: key })).toBeDefined() + expect(queryCache.find({ queryKey: key })?.state.status).toBe('pending') unsubscribe() await vi.advanceTimersByTimeAsync(0) @@ -619,11 +618,11 @@ describe('query', () => { }) const unsubscribe = observer.subscribe(() => undefined) await vi.advanceTimersByTimeAsync(20) - expect(queryCache.find({ queryKey: key })).toBeDefined() + expect(queryCache.find({ queryKey: key })?.state.data).toBe('data') observer.refetch() unsubscribe() // unsubscribe should not remove even though gcTime has elapsed b/c query is still fetching - expect(queryCache.find({ queryKey: key })).toBeDefined() + expect(queryCache.find({ queryKey: key })?.state.data).toBe('data') // should be removed after an additional staleTime wait await vi.advanceTimersByTimeAsync(30) expect(queryCache.find({ queryKey: key })).toBeUndefined() @@ -636,16 +635,16 @@ describe('query', () => { queryFn: () => 'data', gcTime: 0, }) - expect(queryCache.find({ queryKey: key })).toBeDefined() + expect(queryCache.find({ queryKey: key })?.state.status).toBe('pending') const unsubscribe = observer.subscribe(() => undefined) await vi.advanceTimersByTimeAsync(100) - expect(queryCache.find({ queryKey: key })).toBeDefined() + expect(queryCache.find({ queryKey: key })?.state.data).toBe('data') 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() + expect(queryCache.find({ queryKey: key })?.state.data).toBe('data') }) it('should return proper count of observers', () => { @@ -738,7 +737,7 @@ describe('query', () => { await queryClient.prefetchQuery({ queryKey: key, queryFn, meta }) - expect(queryFn).toBeCalledWith( + expect(queryFn).toHaveBeenCalledWith( expect.objectContaining({ meta, }), @@ -807,7 +806,7 @@ describe('query', () => { const query = queryCache.find({ queryKey: key })! query.invalidate() - expect(query.state.isInvalidated).toBeTruthy() + expect(query.state.isInvalidated).toBe(true) const previousState = query.state @@ -966,13 +965,10 @@ describe('query', () => { const unsubscribe = queryClient.getQueryCache().subscribe(fn) queryClient.setQueryData(key, 'data') + const query = queryClient.getQueryCache().find({ queryKey: key }) await vi.advanceTimersByTimeAsync(10) - expect(fn).toHaveBeenCalledWith( - expect.objectContaining({ - type: 'removed', - }), - ) + expect(fn).toHaveBeenLastCalledWith({ type: 'removed', query }) expect(queryClient.getQueryCache().findAll()).toHaveLength(0) @@ -1106,9 +1102,9 @@ describe('query', () => { expect(queryFn).toHaveBeenCalledTimes(1) expect(query.state.status).toBe('error') - expect( - query.state.error?.message.includes('Maximum call stack size exceeded'), - ).toBeTruthy() + expect(query.state.error?.message).toContain( + 'Maximum call stack size exceeded', + ) expect(consoleMock).toHaveBeenCalledWith( expect.stringContaining( diff --git a/packages/query-core/src/__tests__/queryCache.test.tsx b/packages/query-core/src/__tests__/queryCache.test.tsx index c404c1f942b..307cb2d987c 100644 --- a/packages/query-core/src/__tests__/queryCache.test.tsx +++ b/packages/query-core/src/__tests__/queryCache.test.tsx @@ -24,7 +24,7 @@ describe('queryCache', () => { const unsubscribe = queryCache.subscribe(subscriber) queryClient.setQueryData(key, 'foo') const query = queryCache.find({ queryKey: key }) - expect(subscriber).toHaveBeenCalledWith({ query, type: 'added' }) + expect(subscriber).toHaveBeenNthCalledWith(1, { query, type: 'added' }) unsubscribe() }) @@ -37,7 +37,8 @@ describe('queryCache', () => { queryFn: () => sleep(100).then(() => 'data'), }) await vi.advanceTimersByTimeAsync(100) - expect(callback).toHaveBeenCalled() + const query = queryCache.find({ queryKey: key }) + expect(callback).toHaveBeenNthCalledWith(1, { query, type: 'added' }) }) it('should notify query cache when a query becomes stale', async () => { @@ -71,8 +72,9 @@ describe('queryCache', () => { 'observerResultsUpdated', // 8. Observer result updated -> stale ]) + const cachedQuery = queryCache.find({ queryKey: key }) queries.forEach((query) => { - expect(query).toBeDefined() + expect(query).toBe(cachedQuery) }) unsubscribe() @@ -89,7 +91,7 @@ describe('queryCache', () => { }) await vi.advanceTimersByTimeAsync(100) const query = queryCache.find({ queryKey: key }) - expect(callback).toHaveBeenCalledWith({ query, type: 'added' }) + expect(callback).toHaveBeenNthCalledWith(1, { query, type: 'added' }) }) it('should notify subscribers when new query with initialData is added', async () => { @@ -102,7 +104,8 @@ describe('queryCache', () => { initialData: 'initial', }) await vi.advanceTimersByTimeAsync(100) - expect(callback).toHaveBeenCalled() + const query = queryCache.find({ queryKey: key }) + expect(callback).toHaveBeenNthCalledWith(1, { query, type: 'added' }) }) it('should be able to limit cache size', async () => { @@ -159,7 +162,7 @@ describe('queryCache', () => { }) await vi.advanceTimersByTimeAsync(100) const query = queryCache.find({ queryKey: key })! - expect(query).toBeDefined() + expect(query.state.data).toBe('data1') }) it('find should filter correctly with exact set to false', async () => { @@ -170,7 +173,7 @@ describe('queryCache', () => { }) await vi.advanceTimersByTimeAsync(100) const query = queryCache.find({ queryKey: key, exact: false })! - expect(query).toBeDefined() + expect(query.state.data).toBe('data1') }) }) diff --git a/packages/query-core/src/__tests__/queryClient.test-d.tsx b/packages/query-core/src/__tests__/queryClient.test-d.tsx index 4cd092ddd64..866d1d5f775 100644 --- a/packages/query-core/src/__tests__/queryClient.test-d.tsx +++ b/packages/query-core/src/__tests__/queryClient.test-d.tsx @@ -283,7 +283,12 @@ describe('fully typed usage', () => { Array<[ReadonlyArray, unknown]> >() - const queryData3 = queryClient.setQueryData(filterKey, { foo: '' }) + // Type the value before passing it: TypeScript 5.4's `NoInfer` can't match + // an inline object literal against the value branch of the `Updater` union + // here, so it falls back to the function branch and reports the literal as + // excess properties. Annotating sidesteps that (TS >= 5.5 handles it). + const newData: TData = { foo: '' } + const queryData3 = queryClient.setQueryData(filterKey, newData) type SetQueryDataUpdaterArg = Parameters< typeof queryClient.setQueryData >[1] diff --git a/packages/query-core/src/__tests__/queryClient.test.tsx b/packages/query-core/src/__tests__/queryClient.test.tsx index c09db304467..3c2822130a8 100644 --- a/packages/query-core/src/__tests__/queryClient.test.tsx +++ b/packages/query-core/src/__tests__/queryClient.test.tsx @@ -965,9 +965,9 @@ describe('queryClient', () => { queryFn: () => 'data', gcTime: 10, }) - expect(queryCache.find({ queryKey: key })).toBeDefined() + expect(queryCache.find({ queryKey: key })?.state.data).toBe('data') await vi.advanceTimersByTimeAsync(15) - expect(queryCache.find({ queryKey: key })).not.toBeDefined() + expect(queryCache.find({ queryKey: key })).toBeUndefined() }) }) @@ -979,7 +979,7 @@ describe('queryClient', () => { // check the query was added to the cache await queryClient.prefetchQuery({ queryKey: key, queryFn: fetchFn }) - expect(queryCache.find({ queryKey: key })).toBeTruthy() + expect(queryCache.find({ queryKey: key })?.state.data).toBe('data') // check the error doesn't occur expect(() => @@ -987,7 +987,7 @@ describe('queryClient', () => { ).not.toThrow() // check query was successful removed - expect(queryCache.find({ queryKey: key })).toBeFalsy() + expect(queryCache.find({ queryKey: key })).toBeUndefined() }) }) @@ -1614,7 +1614,15 @@ describe('queryClient', () => { queryClient.resetQueries({ queryKey: key }) - expect(callback).toHaveBeenCalled() + const query = queryCache.find({ queryKey: key }) + expect(callback).toHaveBeenNthCalledWith( + 1, + expect.objectContaining({ + type: 'updated', + query, + action: expect.objectContaining({ type: 'setState' }), + }), + ) }) it('should reset query', async () => { @@ -1630,7 +1638,6 @@ describe('queryClient', () => { state = queryClient.getQueryState(key) - expect(state).toBeTruthy() expect(state?.data).toBeUndefined() expect(state?.status).toEqual('pending') expect(state?.fetchStatus).toEqual('idle') @@ -1652,7 +1659,6 @@ describe('queryClient', () => { state = queryClient.getQueryState(key) - expect(state).toBeTruthy() expect(state?.data).toEqual('initial') }) @@ -1781,8 +1787,8 @@ describe('queryClient', () => { void observer1.mutate() void observer2.mutate() - expect(observer1.getCurrentResult().isPaused).toBeTruthy() - expect(observer2.getCurrentResult().isPaused).toBeTruthy() + expect(observer1.getCurrentResult().isPaused).toBe(true) + expect(observer2.getCurrentResult().isPaused).toBe(true) onlineManager.setOnline(true) @@ -1816,8 +1822,8 @@ describe('queryClient', () => { void observer1.mutate() void observer2.mutate() - expect(observer1.getCurrentResult().isPaused).toBeTruthy() - expect(observer2.getCurrentResult().isPaused).toBeTruthy() + expect(observer1.getCurrentResult().isPaused).toBe(true) + expect(observer2.getCurrentResult().isPaused).toBe(true) onlineManager.setOnline(true) @@ -1861,8 +1867,8 @@ describe('queryClient', () => { void observer1.mutate() void observer2.mutate() - expect(observer1.getCurrentResult().isPaused).toBeTruthy() - expect(observer2.getCurrentResult().isPaused).toBeTruthy() + expect(observer1.getCurrentResult().isPaused).toBe(true) + expect(observer2.getCurrentResult().isPaused).toBe(true) onlineManager.setOnline(true) void queryClient.resumePausedMutations() @@ -1885,12 +1891,12 @@ describe('queryClient', () => { void observer.mutate() - expect(observer.getCurrentResult().isPaused).toBeTruthy() + expect(observer.getCurrentResult().isPaused).toBe(true) await queryClient.resumePausedMutations() // still paused because we are still offline - expect(observer.getCurrentResult().isPaused).toBeTruthy() + expect(observer.getCurrentResult().isPaused).toBe(true) onlineManager.setOnline(true) @@ -1909,7 +1915,7 @@ describe('queryClient', () => { void observer.mutate() - expect(observer.getCurrentResult().isPaused).toBeTruthy() + expect(observer.getCurrentResult().isPaused).toBe(true) const state = dehydrate(queryClient) @@ -1928,7 +1934,7 @@ describe('queryClient', () => { // still paused because we are still offline expect( newQueryClient.getMutationCache().getAll()[0]?.state.isPaused, - ).toBeTruthy() + ).toBe(true) await newQueryClient.resumePausedMutations() @@ -2004,9 +2010,9 @@ describe('queryClient', () => { void observer3.mutate() - expect(observer.getCurrentResult().isPaused).toBeTruthy() - expect(observer2.getCurrentResult().isPaused).toBeTruthy() - expect(observer3.getCurrentResult().isPaused).toBeTruthy() + expect(observer.getCurrentResult().isPaused).toBe(true) + expect(observer2.getCurrentResult().isPaused).toBe(true) + expect(observer3.getCurrentResult().isPaused).toBe(true) onlineManager.setOnline(true) await vi.advanceTimersByTimeAsync(110) diff --git a/packages/query-core/src/__tests__/queryObserver.test.tsx b/packages/query-core/src/__tests__/queryObserver.test.tsx index 93946f289a6..557ef79614d 100644 --- a/packages/query-core/src/__tests__/queryObserver.test.tsx +++ b/packages/query-core/src/__tests__/queryObserver.test.tsx @@ -42,6 +42,34 @@ describe('queryObserver', () => { expect(queryFn).toHaveBeenCalledTimes(1) }) + it('should go through a pending state even when the queryFn returns synchronously', async () => { + 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) + + // A synchronous return value is still wrapped in a promise, so the query + // goes through a fetching state before it resolves. + expect(queryFn).toHaveBeenCalledTimes(1) + expect(observer.getCurrentResult()).toMatchObject({ + status: 'pending', + fetchStatus: 'fetching', + data: undefined, + }) + + await vi.advanceTimersByTimeAsync(0) + + expect(observer.getCurrentResult()).toMatchObject({ + status: 'success', + fetchStatus: 'idle', + data: 'data', + }) + + unsubscribe() + }) + it('should be able to read latest data after subscribing', () => { const key = queryKey() queryClient.setQueryData(key, 'data') @@ -75,11 +103,11 @@ describe('queryObserver', () => { queryKey: key, staleTime: Infinity, enabled: () => enabled, - queryFn: async () => { - await sleep(10) - count++ - return 'data' - }, + queryFn: () => + sleep(10).then(() => { + count++ + return 'data' + }), }) }) @@ -239,11 +267,11 @@ describe('queryObserver', () => { const observer = new QueryObserver(queryClient, { queryKey: key, staleTime: Infinity, - queryFn: async () => { - await sleep(10) - count++ - return 'data' - }, + queryFn: () => + sleep(10).then(() => { + count++ + return 'data' + }), }) let unsubscribe = observer.subscribe(vi.fn()) @@ -1212,10 +1240,13 @@ describe('queryObserver', () => { const unsubscribe = queryClient.getQueryCache().subscribe(spy) observer.setOptions({ queryKey: key, enabled: false, refetchInterval: 10 }) + const query = queryClient.getQueryCache().find({ queryKey: key }) expect(spy).toHaveBeenCalledTimes(1) - expect(spy).toHaveBeenCalledWith( - expect.objectContaining({ type: 'observerOptionsUpdated' }), - ) + expect(spy).toHaveBeenCalledWith({ + type: 'observerOptionsUpdated', + query, + observer, + }) unsubscribe() }) diff --git a/packages/query-core/src/__tests__/timeoutManager.test.tsx b/packages/query-core/src/__tests__/timeoutManager.test.tsx index 79ea130db85..d40101555ee 100644 --- a/packages/query-core/src/__tests__/timeoutManager.test.tsx +++ b/packages/query-core/src/__tests__/timeoutManager.test.tsx @@ -93,7 +93,7 @@ describe('timeoutManager', () => { expect.stringMatching( /\[timeoutManager\]: Switching .* might result in unexpected behavior\..*/, ), - expect.anything(), + { previous: customProvider, provider: customProvider2 }, ) // 3. Switching again with no intermediate calls should not warn diff --git a/packages/query-core/src/__tests__/utils.test.tsx b/packages/query-core/src/__tests__/utils.test.tsx index 9abd1bf265c..c9566ddd376 100644 --- a/packages/query-core/src/__tests__/utils.test.tsx +++ b/packages/query-core/src/__tests__/utils.test.tsx @@ -10,6 +10,7 @@ import { hashQueryKeyByOptions, isPlainArray, isPlainObject, + isValidTimeout, keepPreviousData, matchMutation, partialMatchKey, @@ -46,40 +47,40 @@ describe('core/utils', () => { describe('shallowEqualObjects', () => { it('should return `true` for shallow equal objects', () => { - expect(shallowEqualObjects({ a: 1 }, { a: 1 })).toEqual(true) + expect(shallowEqualObjects({ a: 1 }, { a: 1 })).toBe(true) }) it('should return `false` for non shallow equal objects', () => { - expect(shallowEqualObjects({ a: 1 }, { a: 2 })).toEqual(false) + expect(shallowEqualObjects({ a: 1 }, { a: 2 })).toBe(false) }) it('should return `false` if lengths are not equal', () => { - expect(shallowEqualObjects({ a: 1 }, { a: 1, b: 2 })).toEqual(false) + expect(shallowEqualObjects({ a: 1 }, { a: 1, b: 2 })).toBe(false) }) it('should return false if b is undefined', () => { - expect(shallowEqualObjects({ a: 1 }, undefined)).toEqual(false) + expect(shallowEqualObjects({ a: 1 }, undefined)).toBe(false) }) }) describe('isPlainObject', () => { it('should return `true` for a plain object', () => { - expect(isPlainObject({})).toEqual(true) + expect(isPlainObject({})).toBe(true) }) it('should return `false` for an array', () => { - expect(isPlainObject([])).toEqual(false) + expect(isPlainObject([])).toBe(false) }) it('should return `false` for null', () => { - expect(isPlainObject(null)).toEqual(false) + expect(isPlainObject(null)).toBe(false) }) it('should return `false` for undefined', () => { - expect(isPlainObject(undefined)).toEqual(false) + expect(isPlainObject(undefined)).toBe(false) }) it('should return `true` for object with an undefined constructor', () => { - expect(isPlainObject(Object.create(null))).toBeTruthy() + expect(isPlainObject(Object.create(null))).toBe(true) }) it('should return `false` if constructor does not have an Object-specific method', () => { @@ -89,7 +90,7 @@ describe('core/utils', () => { this.abc = {} } } - expect(isPlainObject(new Foo())).toBeFalsy() + expect(isPlainObject(new Foo())).toBe(false) }) it('should return `false` if the object has a modified prototype', () => { @@ -102,7 +103,7 @@ describe('core/utils', () => { this.vertices.push(v) } - expect(isPlainObject(Object.create(Graph))).toBeFalsy() + expect(isPlainObject(Object.create(Graph))).toBe(false) }) it('should return `false` for object with custom prototype', () => { @@ -110,17 +111,17 @@ describe('core/utils', () => { const obj = Object.create(CustomProto) obj.b = 2 - expect(isPlainObject(obj)).toBeFalsy() + expect(isPlainObject(obj)).toBe(false) }) }) describe('isPlainArray', () => { it('should return `true` for plain arrays', () => { - expect(isPlainArray([1, 2])).toEqual(true) + expect(isPlainArray([1, 2])).toBe(true) }) it('should return `false` for non plain arrays', () => { - expect(isPlainArray(Object.assign([1, 2], { a: 'b' }))).toEqual(false) + expect(isPlainArray(Object.assign([1, 2], { a: 'b' }))).toBe(false) }) }) @@ -128,43 +129,55 @@ describe('core/utils', () => { 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) + expect(partialMatchKey(a, b)).toBe(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) + expect(partialMatchKey(a, b)).toBe(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) + expect(partialMatchKey(a, b)).toBe(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) + expect(partialMatchKey(a, b)).toBe(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) + expect(partialMatchKey(a, b)).toBe(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) + expect(partialMatchKey(a, b)).toBe(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) + expect(partialMatchKey(a, b)).toBe(false) + }) + + it('should treat undefined object properties as matching missing properties', () => { + const queryKeyWithUndefined = ['todos', { filters: undefined }] + const queryKeyWithoutProperty = ['todos', {}] + + expect( + partialMatchKey(queryKeyWithoutProperty, queryKeyWithUndefined), + ).toBe(true) + expect( + partialMatchKey(queryKeyWithUndefined, queryKeyWithoutProperty), + ).toBe(true) }) }) @@ -456,14 +469,14 @@ describe('core/utils', () => { mutationCache: queryClient.getMutationCache(), options: {}, }) - expect(matchMutation(filters, mutation)).toBeFalsy() + expect(matchMutation(filters, mutation)).toBe(false) }) }) describe('keepPreviousData', () => { it('should return the parameter as is', () => { const x = { a: 1, b: 2 } - expect(keepPreviousData(x)).toEqual(x) + expect(keepPreviousData(x)).toBe(x) }) }) @@ -559,6 +572,40 @@ describe('core/utils', () => { expect(hashKey(nested1)).toEqual(hashKey(nested2)) }) + + it('should hash undefined object properties the same as missing properties', () => { + const withUndefined = ['todos', { filters: undefined }] + const withoutProperty = ['todos', {}] + + expect(hashKey(withUndefined)).toEqual(hashKey(withoutProperty)) + }) + }) + + describe('isValidTimeout', () => { + it('should accept valid timeout values', () => { + expect(isValidTimeout(0)).toBe(true) + expect(isValidTimeout(1_000)).toBe(true) + }) + + it('should reject a negative timeout value', () => { + expect(isValidTimeout(-1)).toBe(false) + }) + + it('should reject NaN', () => { + expect(isValidTimeout(Number.NaN)).toBe(false) + }) + + it('should reject Infinity', () => { + expect(isValidTimeout(Number.POSITIVE_INFINITY)).toBe(false) + }) + + it('should reject a string timeout value', () => { + expect(isValidTimeout('1000')).toBe(false) + }) + + it('should reject undefined', () => { + expect(isValidTimeout(undefined)).toBe(false) + }) }) describe('ensureQueryFn', () => { diff --git a/packages/query-core/src/hydration.ts b/packages/query-core/src/hydration.ts index 90868dd2623..976b5faafee 100644 --- a/packages/query-core/src/hydration.ts +++ b/packages/query-core/src/hydration.ts @@ -253,6 +253,7 @@ export function hydrate( ...(state.status === 'pending' && data !== undefined && { status: 'success' as const, + dataUpdatedAt: dehydratedAt ?? Date.now(), // Preserve existing fetchStatus if the existing query is actively fetching. ...(!existingQueryIsFetching && { fetchStatus: 'idle' as const, @@ -284,6 +285,10 @@ export function hydrate( state.status === 'pending' && data !== undefined ? 'success' : state.status, + ...(state.status === 'pending' && + data !== undefined && { + dataUpdatedAt: dehydratedAt ?? Date.now(), + }), }, ) } diff --git a/packages/query-core/src/utils.ts b/packages/query-core/src/utils.ts index b97b2cc5a33..f442ab86fdc 100644 --- a/packages/query-core/src/utils.ts +++ b/packages/query-core/src/utils.ts @@ -256,7 +256,22 @@ export function partialMatchKey(a: any, b: any): boolean { } if (a && b && typeof a === 'object' && typeof b === 'object') { - return Object.keys(b).every((key) => partialMatchKey(a[key], b[key])) + if (Array.isArray(a) && Array.isArray(b)) { + for (let i = 0; i < b.length; i++) { + if (!partialMatchKey(a[i], b[i])) { + return false + } + } + return true + } + + const bKeys = Object.keys(b) + for (const key of bKeys) { + if (!partialMatchKey(a[key], b[key])) { + return false + } + } + return true } return false diff --git a/packages/query-devtools/CHANGELOG.md b/packages/query-devtools/CHANGELOG.md index b6672a1f24a..e39a2309c5d 100644 --- a/packages/query-devtools/CHANGELOG.md +++ b/packages/query-devtools/CHANGELOG.md @@ -1,5 +1,27 @@ # @tanstack/query-devtools +## 5.101.4 + +## 5.101.3 + +## 5.101.2 + +### Patch Changes + +- [#10813](https://github.com/TanStack/query/pull/10813) [`f5bf180`](https://github.com/TanStack/query/commit/f5bf180d933d8b8d9d9e7b845e55b26a3a413b07) - fix(query-devtools/PiPContext): reset 'pip_open' in 'localStore' from 'closePipWindow' so the auto-open createEffect does not reopen the window after a programmatic close + +- [#10812](https://github.com/TanStack/query/pull/10812) [`25cdd97`](https://github.com/TanStack/query/commit/25cdd975fed4703d2ca5b600ca5ccd2b600b3dd8) - fix(query-devtools/utils): make 'last updated' sort return 0 for queries with equal 'dataUpdatedAt' to follow the standard comparator contract + +- [#10815](https://github.com/TanStack/query/pull/10815) [`ecd89c8`](https://github.com/TanStack/query/commit/ecd89c8faf7acc226f00633ea3a761d3ab842c1d) - fix(query-devtools/utils): scope the 'setupStyleSheet' dedup check to the target so a 'shadowDOMTarget' still receives its own '#\_goober' style tag when 'document.head' already has one + +- [#10811](https://github.com/TanStack/query/pull/10811) [`01c7634`](https://github.com/TanStack/query/commit/01c763444e3cf3dfa9744f13911aa1533cac3c29) - fix(query-devtools/Devtools): correct the Theme sub-trigger className from 'position' to 'theme' + +- [#10736](https://github.com/TanStack/query/pull/10736) [`49012db`](https://github.com/TanStack/query/commit/49012dbd5192dfe483d3b108b72ffaa7f2849e0f) - `setupStyleSheet` now sets `window.__nonce__` when a `styleNonce` is provided. + + The devtools use [goober](https://goober.js.org/) for CSS-in-JS, which reads `window.__nonce__` every time it creates or accesses its style element. Without this, goober overwrote the nonce with `undefined`, causing CSP violations even when `styleNonce` was correctly passed to ``. + +## 5.101.1 + ## 5.101.0 ### Patch Changes diff --git a/packages/query-devtools/package.json b/packages/query-devtools/package.json index f1e4ae25800..cca18b5a1a8 100644 --- a/packages/query-devtools/package.json +++ b/packages/query-devtools/package.json @@ -1,6 +1,6 @@ { "name": "@tanstack/query-devtools", - "version": "5.101.0", + "version": "5.101.4", "description": "Developer tools to interact with and visualize the TanStack Query cache", "author": "tannerlinsley", "license": "MIT", diff --git a/packages/query-devtools/src/Devtools.tsx b/packages/query-devtools/src/Devtools.tsx index dfb66e496a7..daff934932f 100644 --- a/packages/query-devtools/src/Devtools.tsx +++ b/packages/query-devtools/src/Devtools.tsx @@ -1192,7 +1192,7 @@ export const ContentView: Component = (props) => { class={cx( styles().settingsSubTrigger, 'tsqd-settings-menu-sub-trigger', - 'tsqd-settings-menu-sub-trigger-position', + 'tsqd-settings-menu-sub-trigger-theme', )} > Theme @@ -2745,6 +2745,8 @@ const stylesFactory = ( right: -8px; bottom: -8px; border-radius: 9999px; + -webkit-transform: translateZ(0); + transform: translateZ(0); & svg { position: absolute; diff --git a/packages/query-devtools/src/__tests__/Devtools.test.tsx b/packages/query-devtools/src/__tests__/Devtools.test.tsx index 398bf61e316..f73024502c6 100644 --- a/packages/query-devtools/src/__tests__/Devtools.test.tsx +++ b/packages/query-devtools/src/__tests__/Devtools.test.tsx @@ -1,5 +1,11 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import { QueryClient, QueryObserver, onlineManager } from '@tanstack/query-core' +import { + QueryClient, + QueryObserver, + dehydrate, + hydrate, + onlineManager, +} from '@tanstack/query-core' import { fireEvent, render } from '@solidjs/testing-library' import { createLocalStorage } from '@solid-primitives/storage' import { Devtools } from '../Devtools' @@ -310,6 +316,25 @@ describe('Devtools', () => { window.removeEventListener('@tanstack/query-devtools-event', listener) } }) + + it('should render a query row when a hydrated query uses a custom hash function', async () => { + queryClient.fetchQuery({ + queryKey: ['posts'], + queryFn: () => [{ id: 1 }], + queryKeyHashFn: () => 'custom-posts-hash', + }) + await vi.advanceTimersByTimeAsync(0) + const dehydratedState = dehydrate(queryClient) + + queryClient = new QueryClient() + hydrate(queryClient, dehydratedState) + + const rendered = renderDevtools({ initialIsOpen: true }) + + expect( + rendered.getByLabelText(/Query key custom-posts-hash/), + ).toBeInTheDocument() + }) }) describe('view toggle', () => { @@ -1078,12 +1103,13 @@ describe('Devtools', () => { key: 'Enter', }) - const themeTrigger = Array.from( - document.querySelectorAll( - '.tsqd-settings-menu-sub-trigger', - ), - ).find((el) => String(el.textContent).includes('Theme')) - expect(themeTrigger).not.toBeUndefined() + const themeTrigger = document.querySelector( + '.tsqd-settings-menu-sub-trigger-theme', + ) + expect(themeTrigger).not.toBeNull() + expect(themeTrigger).not.toBe( + document.querySelector('.tsqd-settings-menu-sub-trigger-position'), + ) fireEvent.keyDown(themeTrigger!, { key: 'ArrowRight' }) expect( @@ -1098,12 +1124,10 @@ describe('Devtools', () => { key: 'Enter', }) - const themeTrigger = Array.from( - document.querySelectorAll( - '.tsqd-settings-menu-sub-trigger', - ), - ).find((el) => String(el.textContent).includes('Theme')) - expect(themeTrigger).not.toBeUndefined() + const themeTrigger = document.querySelector( + '.tsqd-settings-menu-sub-trigger-theme', + ) + expect(themeTrigger).not.toBeNull() fireEvent.keyDown(themeTrigger!, { key: 'ArrowRight' }) const themeMenu = document.querySelector( diff --git a/packages/query-devtools/src/__tests__/Explorer.test.tsx b/packages/query-devtools/src/__tests__/Explorer.test.tsx index 2ffc7cd1d93..5f2c1deab49 100644 --- a/packages/query-devtools/src/__tests__/Explorer.test.tsx +++ b/packages/query-devtools/src/__tests__/Explorer.test.tsx @@ -232,7 +232,7 @@ describe('Explorer', () => { ).toBeInTheDocument() expect(consoleError).toHaveBeenCalledWith( 'Failed to copy: ', - expect.any(Error), + new Error('denied'), ) }) diff --git a/packages/query-devtools/src/__tests__/contexts/PiPContext.test.tsx b/packages/query-devtools/src/__tests__/contexts/PiPContext.test.tsx index 2eea2323f39..c277169b0ff 100644 --- a/packages/query-devtools/src/__tests__/contexts/PiPContext.test.tsx +++ b/packages/query-devtools/src/__tests__/contexts/PiPContext.test.tsx @@ -312,6 +312,22 @@ describe('PiPContext', () => { expect(fakeWindow.close).toHaveBeenCalledTimes(1) }) + + it('should reset "pip_open" in "localStore" so the auto-open createEffect does not reopen the window', () => { + stubPipWindow() + + renderAndAct( + (pip) => { + pip().requestPipWindow(640, 480) + pip().closePipWindow() + }, + { disabled: true }, + ) + + expect(localStorage.getItem('TanstackQueryDevtools.pip_open')).toBe( + 'false', + ) + }) }) describe('"pip_open" auto-open createEffect', () => { @@ -351,7 +367,7 @@ describe('PiPContext', () => { }) expect(consoleError).toHaveBeenCalledWith( - expect.stringContaining('Failed to open popup'), + 'Failed to open popup. Please allow popups for this site to view the devtools in picture-in-picture mode.', ) expect(localStorage.getItem('TanstackQueryDevtools.pip_open')).toBe( 'false', @@ -412,10 +428,11 @@ describe('PiPContext', () => { disabled: true, }) - expect(observeSpy).toHaveBeenCalledWith( - gooberStyle, - expect.objectContaining({ childList: true, subtree: true }), - ) + expect(observeSpy).toHaveBeenCalledWith(gooberStyle, { + childList: true, + subtree: true, + characterDataOldValue: true, + }) } finally { gooberStyle.remove() } diff --git a/packages/query-devtools/src/__tests__/utils.test.ts b/packages/query-devtools/src/__tests__/utils.test.ts index 511553e2bb8..496594d63cd 100644 --- a/packages/query-devtools/src/__tests__/utils.test.ts +++ b/packages/query-devtools/src/__tests__/utils.test.ts @@ -982,6 +982,7 @@ describe('Utils tests', () => { describe('setupStyleSheet', () => { afterEach(() => { document.head.querySelector('#_goober')?.remove() + delete (window as any).__nonce__ }) it('should not insert any style tag when "nonce" is missing', () => { @@ -1042,6 +1043,31 @@ describe('Utils tests', () => { expect(styleTags).toHaveLength(1) expect(styleTags[0]?.getAttribute('nonce')).toBe('first-nonce') }) + + it('should install the style tag into the "ShadowRoot" target even when "document.head" already has one', () => { + const host = document.createElement('div') + const shadow = host.attachShadow({ mode: 'open' }) + + setupStyleSheet('host-nonce') + setupStyleSheet('shadow-nonce', shadow) + + expect(shadow.querySelector('#_goober')).not.toBeNull() + expect(shadow.querySelector('#_goober')?.getAttribute('nonce')).toBe( + 'shadow-nonce', + ) + }) + + it('should set window.__nonce__ so goober preserves the nonce on its style element', () => { + setupStyleSheet('test-nonce') + + expect((window as any).__nonce__).toBe('test-nonce') + }) + + it('should not set window.__nonce__ when nonce is missing', () => { + setupStyleSheet() + + expect((window as any).__nonce__).toBeUndefined() + }) }) describe('sortFns', () => { @@ -1076,6 +1102,13 @@ describe('Utils tests', () => { expect(dateSort(older, newer)).toBe(1) expect(dateSort(newer, older)).toBe(-1) }) + + it('should return 0 when both queries share the same "dataUpdatedAt"', () => { + const a = buildQuery(['a'], { dataUpdatedAt: 100 }) + const b = buildQuery(['b'], { dataUpdatedAt: 100 }) + + expect(dateSort(a, b)).toBe(0) + }) }) describe("'query hash'", () => { diff --git a/packages/query-devtools/src/contexts/PiPContext.tsx b/packages/query-devtools/src/contexts/PiPContext.tsx index fb22c8be36a..fe42eef8f2f 100644 --- a/packages/query-devtools/src/contexts/PiPContext.tsx +++ b/packages/query-devtools/src/contexts/PiPContext.tsx @@ -41,6 +41,7 @@ export const PiPProvider = (props: PiPProviderProps) => { const w = pipWindow() if (w != null) { w.close() + props.setLocalStore('pip_open', 'false') setPipWindow(null) } } diff --git a/packages/query-devtools/src/utils.tsx b/packages/query-devtools/src/utils.tsx index 5306f2cf5f2..77247607af8 100644 --- a/packages/query-devtools/src/utils.tsx +++ b/packages/query-devtools/src/utils.tsx @@ -101,8 +101,10 @@ const getStatusRank = (q: Query) => const queryHashSort: SortFn = (a, b) => a.queryHash.localeCompare(b.queryHash) -const dateSort: SortFn = (a, b) => - a.state.dataUpdatedAt < b.state.dataUpdatedAt ? 1 : -1 +const dateSort: SortFn = (a, b) => { + const diff = b.state.dataUpdatedAt - a.state.dataUpdatedAt + return diff < 0 ? -1 : diff > 0 ? 1 : 0 +} const statusAndDateSort: SortFn = (a, b) => { if (getStatusRank(a) === getStatusRank(b)) { @@ -305,19 +307,18 @@ export const deleteNestedDataByPath = ( // 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 + if (!nonce) + return // Goober reads window.__nonce__ every time it creates or accesses its style + // element (el.nonce = window.__nonce__). Without this, goober overwrites the + // nonce we set on the pre-created element with undefined, clearing it. + ;(window as any).__nonce__ = nonce + + const root = target ?? document.head + if (root.querySelector('#_goober')) 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) - } + root.appendChild(styleTag) } diff --git a/packages/query-persist-client-core/CHANGELOG.md b/packages/query-persist-client-core/CHANGELOG.md index 5e875179ee1..d4e5d55f3b2 100644 --- a/packages/query-persist-client-core/CHANGELOG.md +++ b/packages/query-persist-client-core/CHANGELOG.md @@ -1,5 +1,33 @@ # @tanstack/query-persist-client-core +## 5.101.4 + +### Patch Changes + +- Updated dependencies []: + - @tanstack/query-core@5.101.4 + +## 5.101.3 + +### Patch Changes + +- Updated dependencies [[`7e3c822`](https://github.com/TanStack/query/commit/7e3c822a10896f41a8f1031c16b85096277af677)]: + - @tanstack/query-core@5.101.3 + +## 5.101.2 + +### Patch Changes + +- Updated dependencies []: + - @tanstack/query-core@5.101.2 + +## 5.101.1 + +### Patch Changes + +- Updated dependencies [[`9eff92e`](https://github.com/TanStack/query/commit/9eff92ed86e284ec0125b3a3539d028688235bd1)]: + - @tanstack/query-core@5.101.1 + ## 5.101.0 ### Patch Changes diff --git a/packages/query-persist-client-core/package.json b/packages/query-persist-client-core/package.json index 2156cde6941..f4fe2962af3 100644 --- a/packages/query-persist-client-core/package.json +++ b/packages/query-persist-client-core/package.json @@ -1,6 +1,6 @@ { "name": "@tanstack/query-persist-client-core", - "version": "5.101.0", + "version": "5.101.4", "description": "Set of utilities for interacting with persisters, which can save your queryClient for later use", "author": "tannerlinsley", "license": "MIT", diff --git a/packages/query-sync-storage-persister/CHANGELOG.md b/packages/query-sync-storage-persister/CHANGELOG.md index feeb3aac96c..cba244597fd 100644 --- a/packages/query-sync-storage-persister/CHANGELOG.md +++ b/packages/query-sync-storage-persister/CHANGELOG.md @@ -1,5 +1,37 @@ # @tanstack/query-sync-storage-persister +## 5.101.4 + +### Patch Changes + +- Updated dependencies []: + - @tanstack/query-core@5.101.4 + - @tanstack/query-persist-client-core@5.101.4 + +## 5.101.3 + +### Patch Changes + +- Updated dependencies [[`7e3c822`](https://github.com/TanStack/query/commit/7e3c822a10896f41a8f1031c16b85096277af677)]: + - @tanstack/query-core@5.101.3 + - @tanstack/query-persist-client-core@5.101.3 + +## 5.101.2 + +### Patch Changes + +- Updated dependencies []: + - @tanstack/query-core@5.101.2 + - @tanstack/query-persist-client-core@5.101.2 + +## 5.101.1 + +### Patch Changes + +- Updated dependencies [[`9eff92e`](https://github.com/TanStack/query/commit/9eff92ed86e284ec0125b3a3539d028688235bd1)]: + - @tanstack/query-core@5.101.1 + - @tanstack/query-persist-client-core@5.101.1 + ## 5.101.0 ### Patch Changes diff --git a/packages/query-sync-storage-persister/package.json b/packages/query-sync-storage-persister/package.json index da83848041a..858f22c95d0 100644 --- a/packages/query-sync-storage-persister/package.json +++ b/packages/query-sync-storage-persister/package.json @@ -1,6 +1,6 @@ { "name": "@tanstack/query-sync-storage-persister", - "version": "5.101.0", + "version": "5.101.4", "description": "A persister for synchronous storages, to be used with TanStack/Query", "author": "tannerlinsley", "license": "MIT", diff --git a/packages/react-query-devtools/CHANGELOG.md b/packages/react-query-devtools/CHANGELOG.md index 7a94485ff77..cd56e3d2680 100644 --- a/packages/react-query-devtools/CHANGELOG.md +++ b/packages/react-query-devtools/CHANGELOG.md @@ -1,5 +1,37 @@ # @tanstack/react-query-devtools +## 5.101.4 + +### Patch Changes + +- Updated dependencies []: + - @tanstack/query-devtools@5.101.4 + - @tanstack/react-query@5.101.4 + +## 5.101.3 + +### Patch Changes + +- Updated dependencies []: + - @tanstack/query-devtools@5.101.3 + - @tanstack/react-query@5.101.3 + +## 5.101.2 + +### Patch Changes + +- Updated dependencies [[`f5bf180`](https://github.com/TanStack/query/commit/f5bf180d933d8b8d9d9e7b845e55b26a3a413b07), [`25cdd97`](https://github.com/TanStack/query/commit/25cdd975fed4703d2ca5b600ca5ccd2b600b3dd8), [`ecd89c8`](https://github.com/TanStack/query/commit/ecd89c8faf7acc226f00633ea3a761d3ab842c1d), [`01c7634`](https://github.com/TanStack/query/commit/01c763444e3cf3dfa9744f13911aa1533cac3c29), [`49012db`](https://github.com/TanStack/query/commit/49012dbd5192dfe483d3b108b72ffaa7f2849e0f)]: + - @tanstack/query-devtools@5.101.2 + - @tanstack/react-query@5.101.2 + +## 5.101.1 + +### Patch Changes + +- Updated dependencies []: + - @tanstack/query-devtools@5.101.1 + - @tanstack/react-query@5.101.1 + ## 5.101.0 ### Patch Changes diff --git a/packages/react-query-devtools/package.json b/packages/react-query-devtools/package.json index 950f2342d49..38da8701481 100644 --- a/packages/react-query-devtools/package.json +++ b/packages/react-query-devtools/package.json @@ -1,6 +1,6 @@ { "name": "@tanstack/react-query-devtools", - "version": "5.101.0", + "version": "5.101.4", "description": "Developer tools to interact with and visualize the TanStack/react-query cache", "author": "tannerlinsley", "license": "MIT", diff --git a/packages/react-query-devtools/src/__tests__/ReactQueryDevtools.test.tsx b/packages/react-query-devtools/src/__tests__/ReactQueryDevtools.test.tsx index a869ef1fd04..a55b98970b5 100644 --- a/packages/react-query-devtools/src/__tests__/ReactQueryDevtools.test.tsx +++ b/packages/react-query-devtools/src/__tests__/ReactQueryDevtools.test.tsx @@ -2,6 +2,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import { render } from '@testing-library/react' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { TanstackQueryDevtools } from '@tanstack/query-devtools' +import type { ReactQueryDevtools as ReactQueryDevtoolsComponent } from '../ReactQueryDevtools' const mountMock = vi.fn() const unmountMock = vi.fn() @@ -26,22 +27,22 @@ vi.mock('@tanstack/query-devtools', () => ({ })) describe('ReactQueryDevtools', () => { - beforeEach(() => { + let ReactQueryDevtools: typeof ReactQueryDevtoolsComponent + let queryClient: QueryClient + + beforeEach(async () => { vi.clearAllMocks() + ;({ ReactQueryDevtools } = await import('../ReactQueryDevtools')) + queryClient = new QueryClient() }) - it('should throw an error if no query client has been set', async () => { - const { ReactQueryDevtools } = await import('../ReactQueryDevtools') - + it('should throw an error if no query client has been set', () => { expect(() => render()).toThrow( 'No QueryClient set, use QueryClientProvider to set one', ) }) - it('should not throw an error if query client is provided via context', async () => { - const { ReactQueryDevtools } = await import('../ReactQueryDevtools') - const queryClient = new QueryClient() - + it('should not throw an error if query client is provided via context', () => { expect(() => render( @@ -52,20 +53,14 @@ describe('ReactQueryDevtools', () => { expect(mountMock).toHaveBeenCalled() }) - it('should not throw an error if query client is provided via props', async () => { - const { ReactQueryDevtools } = await import('../ReactQueryDevtools') - const queryClient = new QueryClient() - + it('should not throw an error if query client is provided via props', () => { expect(() => render(), ).not.toThrow() expect(mountMock).toHaveBeenCalled() }) - it('should forward "buttonPosition" to the devtools instance', async () => { - const { ReactQueryDevtools } = await import('../ReactQueryDevtools') - const queryClient = new QueryClient() - + it('should forward "buttonPosition" to the devtools instance', () => { render( , ) @@ -73,36 +68,25 @@ describe('ReactQueryDevtools', () => { expect(setButtonPositionMock).toHaveBeenCalledWith('top-left') }) - it('should forward "position" to the devtools instance', async () => { - const { ReactQueryDevtools } = await import('../ReactQueryDevtools') - const queryClient = new QueryClient() - + it('should forward "position" to the devtools instance', () => { render() expect(setPositionMock).toHaveBeenCalledWith('left') }) - it('should forward "initialIsOpen" to the devtools instance', async () => { - const { ReactQueryDevtools } = await import('../ReactQueryDevtools') - const queryClient = new QueryClient() - + it('should forward "initialIsOpen" to the devtools instance', () => { render() expect(setInitialIsOpenMock).toHaveBeenCalledWith(true) }) - it('should default "initialIsOpen" to "false" when the prop is omitted', async () => { - const { ReactQueryDevtools } = await import('../ReactQueryDevtools') - const queryClient = new QueryClient() - + it('should default "initialIsOpen" to "false" when the prop is omitted', () => { render() expect(setInitialIsOpenMock).toHaveBeenCalledWith(false) }) - it('should forward "errorTypes" to the devtools instance', async () => { - const { ReactQueryDevtools } = await import('../ReactQueryDevtools') - const queryClient = new QueryClient() + it('should forward "errorTypes" to the devtools instance', () => { const errorTypes = [ { name: 'Network', initializer: () => new Error('Network') }, ] @@ -112,37 +96,25 @@ describe('ReactQueryDevtools', () => { expect(setErrorTypesMock).toHaveBeenCalledWith(errorTypes) }) - it('should default "errorTypes" to an empty array when the prop is omitted', async () => { - const { ReactQueryDevtools } = await import('../ReactQueryDevtools') - const queryClient = new QueryClient() - + it('should default "errorTypes" to an empty array when the prop is omitted', () => { render() expect(setErrorTypesMock).toHaveBeenCalledWith([]) }) - it('should forward "theme" to the devtools instance', async () => { - const { ReactQueryDevtools } = await import('../ReactQueryDevtools') - const queryClient = new QueryClient() - + it('should forward "theme" to the devtools instance', () => { render() expect(setThemeMock).toHaveBeenCalledWith('dark') }) - it('should forward the resolved "QueryClient" via "setClient"', async () => { - const { ReactQueryDevtools } = await import('../ReactQueryDevtools') - const queryClient = new QueryClient() - + it('should forward the resolved "QueryClient" via "setClient"', () => { render() expect(setClientMock).toHaveBeenCalledWith(queryClient) }) - it('should forward "styleNonce" to the devtools constructor', async () => { - const { ReactQueryDevtools } = await import('../ReactQueryDevtools') - const queryClient = new QueryClient() - + it('should forward "styleNonce" to the devtools constructor', () => { render() expect(TanstackQueryDevtools).toHaveBeenCalledWith( @@ -150,9 +122,7 @@ describe('ReactQueryDevtools', () => { ) }) - it('should forward "shadowDOMTarget" to the devtools constructor', async () => { - const { ReactQueryDevtools } = await import('../ReactQueryDevtools') - const queryClient = new QueryClient() + it('should forward "shadowDOMTarget" to the devtools constructor', () => { const shadowDOMTarget = document .createElement('div') .attachShadow({ mode: 'open' }) @@ -169,10 +139,7 @@ describe('ReactQueryDevtools', () => { ) }) - it('should forward "hideDisabledQueries" to the devtools constructor', async () => { - const { ReactQueryDevtools } = await import('../ReactQueryDevtools') - const queryClient = new QueryClient() - + it('should forward "hideDisabledQueries" to the devtools constructor', () => { render( , ) @@ -182,10 +149,69 @@ describe('ReactQueryDevtools', () => { ) }) - it('should call "unmount" on the devtools instance when the component unmounts', async () => { - const { ReactQueryDevtools } = await import('../ReactQueryDevtools') - const queryClient = new QueryClient() + it('should forward a "buttonPosition" change to the devtools instance after mount', () => { + const { rerender } = render( + , + ) + setButtonPositionMock.mockClear() + + rerender( + , + ) + + expect(setButtonPositionMock).toHaveBeenCalledWith('top-left') + }) + + it('should forward a "position" change to the devtools instance after mount', () => { + const { rerender } = render( + , + ) + setPositionMock.mockClear() + + rerender() + + expect(setPositionMock).toHaveBeenCalledWith('top') + }) + + it('should forward an "initialIsOpen" change to the devtools instance after mount', () => { + const { rerender } = render( + , + ) + setInitialIsOpenMock.mockClear() + + rerender() + + expect(setInitialIsOpenMock).toHaveBeenCalledWith(true) + }) + + it('should forward an "errorTypes" change to the devtools instance after mount', () => { + const { rerender } = render( + , + ) + setErrorTypesMock.mockClear() + + const errorTypes = [ + { name: 'Network', initializer: () => new Error('Network') }, + ] + rerender( + , + ) + + expect(setErrorTypesMock).toHaveBeenCalledWith(errorTypes) + }) + + it('should forward a "theme" change to the devtools instance after mount', () => { + const { rerender } = render( + , + ) + setThemeMock.mockClear() + + rerender() + + expect(setThemeMock).toHaveBeenCalledWith('dark') + }) + it('should call "unmount" on the devtools instance when the component unmounts', () => { const { unmount } = render() unmount() @@ -197,8 +223,8 @@ describe('ReactQueryDevtools', () => { vi.resetModules() try { - const { ReactQueryDevtools } = await import('..') - expect(ReactQueryDevtools({})).toBeNull() + const { ReactQueryDevtools: ProductionDevtools } = await import('..') + expect(ProductionDevtools({})).toBeNull() } finally { vi.unstubAllEnvs() vi.resetModules() diff --git a/packages/react-query-devtools/src/__tests__/ReactQueryDevtoolsPanel.test.tsx b/packages/react-query-devtools/src/__tests__/ReactQueryDevtoolsPanel.test.tsx index 779498826d4..bdee2f39e9a 100644 --- a/packages/react-query-devtools/src/__tests__/ReactQueryDevtoolsPanel.test.tsx +++ b/packages/react-query-devtools/src/__tests__/ReactQueryDevtoolsPanel.test.tsx @@ -2,6 +2,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import { render } from '@testing-library/react' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { TanstackQueryDevtoolsPanel } from '@tanstack/query-devtools' +import type { ReactQueryDevtoolsPanel as ReactQueryDevtoolsPanelComponent } from '../ReactQueryDevtoolsPanel' const mountMock = vi.fn() const unmountMock = vi.fn() @@ -24,24 +25,22 @@ vi.mock('@tanstack/query-devtools', () => ({ })) describe('ReactQueryDevtoolsPanel', () => { - beforeEach(() => { + let ReactQueryDevtoolsPanel: typeof ReactQueryDevtoolsPanelComponent + let queryClient: QueryClient + + beforeEach(async () => { vi.clearAllMocks() + ;({ ReactQueryDevtoolsPanel } = await import('../ReactQueryDevtoolsPanel')) + queryClient = new QueryClient() }) - it('should throw an error if no query client has been set', async () => { - const { ReactQueryDevtoolsPanel } = - await import('../ReactQueryDevtoolsPanel') - + it('should throw an error if no query client has been set', () => { expect(() => render()).toThrow( 'No QueryClient set, use QueryClientProvider to set one', ) }) - it('should not throw an error if query client is provided via context', async () => { - const { ReactQueryDevtoolsPanel } = - await import('../ReactQueryDevtoolsPanel') - const queryClient = new QueryClient() - + it('should not throw an error if query client is provided via context', () => { expect(() => render( @@ -52,42 +51,30 @@ describe('ReactQueryDevtoolsPanel', () => { expect(mountMock).toHaveBeenCalled() }) - it('should not throw an error if query client is provided via props', async () => { - const { ReactQueryDevtoolsPanel } = - await import('../ReactQueryDevtoolsPanel') - const queryClient = new QueryClient() - + it('should not throw an error if query client is provided via props', () => { expect(() => render(), ).not.toThrow() expect(mountMock).toHaveBeenCalled() }) - it('should forward "onClose" to the devtools instance', async () => { - const { ReactQueryDevtoolsPanel } = - await import('../ReactQueryDevtoolsPanel') - const queryClient = new QueryClient() + it('should forward "onClose" to the devtools instance', () => { const onClose = vi.fn() render() - expect(setOnCloseMock).toHaveBeenCalledWith(expect.any(Function)) + expect(setOnCloseMock).toHaveBeenCalledWith(onClose) }) - it('should default "onClose" to a no-op function when the prop is omitted', async () => { - const { ReactQueryDevtoolsPanel } = - await import('../ReactQueryDevtoolsPanel') - const queryClient = new QueryClient() - + it('should default "onClose" to a no-op function when the prop is omitted', () => { render() - expect(setOnCloseMock).toHaveBeenCalledWith(expect.any(Function)) + const forwarded = setOnCloseMock.mock.calls[0]?.[0] + expect(forwarded).toBeInstanceOf(Function) + expect(forwarded()).toBeUndefined() }) - it('should forward "errorTypes" to the devtools instance', async () => { - const { ReactQueryDevtoolsPanel } = - await import('../ReactQueryDevtoolsPanel') - const queryClient = new QueryClient() + it('should forward "errorTypes" to the devtools instance', () => { const errorTypes = [ { name: 'Network', initializer: () => new Error('Network') }, ] @@ -99,41 +86,25 @@ describe('ReactQueryDevtoolsPanel', () => { expect(setErrorTypesMock).toHaveBeenCalledWith(errorTypes) }) - it('should default "errorTypes" to an empty array when the prop is omitted', async () => { - const { ReactQueryDevtoolsPanel } = - await import('../ReactQueryDevtoolsPanel') - const queryClient = new QueryClient() - + it('should default "errorTypes" to an empty array when the prop is omitted', () => { render() expect(setErrorTypesMock).toHaveBeenCalledWith([]) }) - it('should forward "theme" to the devtools instance', async () => { - const { ReactQueryDevtoolsPanel } = - await import('../ReactQueryDevtoolsPanel') - const queryClient = new QueryClient() - + it('should forward "theme" to the devtools instance', () => { render() expect(setThemeMock).toHaveBeenCalledWith('dark') }) - it('should forward the resolved "QueryClient" via "setClient"', async () => { - const { ReactQueryDevtoolsPanel } = - await import('../ReactQueryDevtoolsPanel') - const queryClient = new QueryClient() - + it('should forward the resolved "QueryClient" via "setClient"', () => { render() expect(setClientMock).toHaveBeenCalledWith(queryClient) }) - it('should forward "styleNonce" to the devtools constructor', async () => { - const { ReactQueryDevtoolsPanel } = - await import('../ReactQueryDevtoolsPanel') - const queryClient = new QueryClient() - + it('should forward "styleNonce" to the devtools constructor', () => { render() expect(TanstackQueryDevtoolsPanel).toHaveBeenCalledWith( @@ -141,10 +112,7 @@ describe('ReactQueryDevtoolsPanel', () => { ) }) - it('should forward "shadowDOMTarget" to the devtools constructor', async () => { - const { ReactQueryDevtoolsPanel } = - await import('../ReactQueryDevtoolsPanel') - const queryClient = new QueryClient() + it('should forward "shadowDOMTarget" to the devtools constructor', () => { const shadowDOMTarget = document .createElement('div') .attachShadow({ mode: 'open' }) @@ -161,11 +129,7 @@ describe('ReactQueryDevtoolsPanel', () => { ) }) - it('should forward "hideDisabledQueries" to the devtools constructor', async () => { - const { ReactQueryDevtoolsPanel } = - await import('../ReactQueryDevtoolsPanel') - const queryClient = new QueryClient() - + it('should forward "hideDisabledQueries" to the devtools constructor', () => { render( { ) }) - it('should preserve the default container height when "style" omits "height"', async () => { - const { ReactQueryDevtoolsPanel } = - await import('../ReactQueryDevtoolsPanel') - const queryClient = new QueryClient() - + it('should preserve the default container height when "style" omits "height"', () => { const { container } = render( { }) }) - it('should let "style" override the default container height on the rendered element', async () => { - const { ReactQueryDevtoolsPanel } = - await import('../ReactQueryDevtoolsPanel') - const queryClient = new QueryClient() - + it('should let "style" override the default container height on the rendered element', () => { const { container } = render( { }) }) - it('should call "unmount" on the devtools instance when the component unmounts', async () => { - const { ReactQueryDevtoolsPanel } = - await import('../ReactQueryDevtoolsPanel') - const queryClient = new QueryClient() - + it('should call "unmount" on the devtools instance when the component unmounts', () => { const { unmount } = render() unmount() @@ -230,8 +182,9 @@ describe('ReactQueryDevtoolsPanel', () => { vi.resetModules() try { - const { ReactQueryDevtoolsPanel } = await import('..') - expect(ReactQueryDevtoolsPanel({})).toBeNull() + const { ReactQueryDevtoolsPanel: ProductionDevtoolsPanel } = + await import('..') + expect(ProductionDevtoolsPanel({})).toBeNull() } finally { vi.unstubAllEnvs() vi.resetModules() diff --git a/packages/react-query-next-experimental/CHANGELOG.md b/packages/react-query-next-experimental/CHANGELOG.md index dc28ad19f93..20fb237b96c 100644 --- a/packages/react-query-next-experimental/CHANGELOG.md +++ b/packages/react-query-next-experimental/CHANGELOG.md @@ -1,5 +1,33 @@ # @tanstack/react-query-next-experimental +## 5.101.4 + +### Patch Changes + +- Updated dependencies []: + - @tanstack/react-query@5.101.4 + +## 5.101.3 + +### Patch Changes + +- Updated dependencies []: + - @tanstack/react-query@5.101.3 + +## 5.101.2 + +### Patch Changes + +- Updated dependencies []: + - @tanstack/react-query@5.101.2 + +## 5.101.1 + +### Patch Changes + +- Updated dependencies []: + - @tanstack/react-query@5.101.1 + ## 5.101.0 ### Patch Changes diff --git a/packages/react-query-next-experimental/package.json b/packages/react-query-next-experimental/package.json index 0dfb4629852..7f732090e16 100644 --- a/packages/react-query-next-experimental/package.json +++ b/packages/react-query-next-experimental/package.json @@ -1,6 +1,6 @@ { "name": "@tanstack/react-query-next-experimental", - "version": "5.101.0", + "version": "5.101.4", "description": "Hydration utils for React Query in the NextJs app directory", "author": "tannerlinsley", "license": "MIT", diff --git a/packages/react-query-persist-client/CHANGELOG.md b/packages/react-query-persist-client/CHANGELOG.md index 8bc27b64e94..213a7cae606 100644 --- a/packages/react-query-persist-client/CHANGELOG.md +++ b/packages/react-query-persist-client/CHANGELOG.md @@ -1,5 +1,37 @@ # @tanstack/react-query-persist-client +## 5.101.4 + +### Patch Changes + +- Updated dependencies []: + - @tanstack/query-persist-client-core@5.101.4 + - @tanstack/react-query@5.101.4 + +## 5.101.3 + +### Patch Changes + +- Updated dependencies []: + - @tanstack/query-persist-client-core@5.101.3 + - @tanstack/react-query@5.101.3 + +## 5.101.2 + +### Patch Changes + +- Updated dependencies []: + - @tanstack/query-persist-client-core@5.101.2 + - @tanstack/react-query@5.101.2 + +## 5.101.1 + +### Patch Changes + +- Updated dependencies []: + - @tanstack/query-persist-client-core@5.101.1 + - @tanstack/react-query@5.101.1 + ## 5.101.0 ### Patch Changes diff --git a/packages/react-query-persist-client/package.json b/packages/react-query-persist-client/package.json index fd3c4402de8..e2d61d88cfc 100644 --- a/packages/react-query-persist-client/package.json +++ b/packages/react-query-persist-client/package.json @@ -1,6 +1,6 @@ { "name": "@tanstack/react-query-persist-client", - "version": "5.101.0", + "version": "5.101.4", "description": "React bindings to work with persisters in TanStack/react-query", "author": "tannerlinsley", "license": "MIT", diff --git a/packages/react-query/CHANGELOG.md b/packages/react-query/CHANGELOG.md index de1675fb443..4e1142d140a 100644 --- a/packages/react-query/CHANGELOG.md +++ b/packages/react-query/CHANGELOG.md @@ -1,5 +1,33 @@ # @tanstack/react-query +## 5.101.4 + +### Patch Changes + +- Updated dependencies []: + - @tanstack/query-core@5.101.4 + +## 5.101.3 + +### Patch Changes + +- Updated dependencies [[`7e3c822`](https://github.com/TanStack/query/commit/7e3c822a10896f41a8f1031c16b85096277af677)]: + - @tanstack/query-core@5.101.3 + +## 5.101.2 + +### Patch Changes + +- Updated dependencies []: + - @tanstack/query-core@5.101.2 + +## 5.101.1 + +### Patch Changes + +- Updated dependencies [[`9eff92e`](https://github.com/TanStack/query/commit/9eff92ed86e284ec0125b3a3539d028688235bd1)]: + - @tanstack/query-core@5.101.1 + ## 5.101.0 ### Patch Changes diff --git a/packages/react-query/README.md b/packages/react-query/README.md index 96bffea2f5d..fa10577496c 100644 --- a/packages/react-query/README.md +++ b/packages/react-query/README.md @@ -1,6 +1,20 @@ -![TanStack Query Header](https://github.com/TanStack/query/raw/main/media/repo-header.png) + + + + TanStack React Query + Hooks for fetching, caching and updating asynchronous data in React diff --git a/packages/react-query/package.json b/packages/react-query/package.json index 557737649e3..ae8d8bea192 100644 --- a/packages/react-query/package.json +++ b/packages/react-query/package.json @@ -1,6 +1,6 @@ { "name": "@tanstack/react-query", - "version": "5.101.0", + "version": "5.101.4", "description": "Hooks for managing, caching and syncing asynchronous and remote data in React", "author": "tannerlinsley", "license": "MIT", diff --git a/packages/react-query/src/__tests__/QueryClientProvider.test.tsx b/packages/react-query/src/__tests__/QueryClientProvider.test.tsx index 71f80595680..9488d356a7c 100644 --- a/packages/react-query/src/__tests__/QueryClientProvider.test.tsx +++ b/packages/react-query/src/__tests__/QueryClientProvider.test.tsx @@ -46,7 +46,7 @@ describe('QueryClientProvider', () => { await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('test')).toBeInTheDocument() - expect(queryCache.find({ queryKey: key })).toBeDefined() + expect(queryCache.find({ queryKey: key })?.state.data).toBe('test') }) it('allows multiple caches to be partitioned', async () => { @@ -99,10 +99,10 @@ describe('QueryClientProvider', () => { 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() + expect(queryCache1.find({ queryKey: key1 })?.state.data).toBe('test1') + expect(queryCache1.find({ queryKey: key2 })).toBeUndefined() + expect(queryCache2.find({ queryKey: key1 })).toBeUndefined() + expect(queryCache2.find({ queryKey: key2 })?.state.data).toBe('test2') }) it("uses defaultOptions for queries when they don't provide their own config", async () => { @@ -140,7 +140,6 @@ describe('QueryClientProvider', () => { await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('test')).toBeInTheDocument() - expect(queryCache.find({ queryKey: key })).toBeDefined() expect(queryCache.find({ queryKey: key })?.options.gcTime).toBe(Infinity) }) diff --git a/packages/react-query/src/__tests__/mutationOptions.test.tsx b/packages/react-query/src/__tests__/mutationOptions.test.tsx index 45129a398d0..7d0b034812c 100644 --- a/packages/react-query/src/__tests__/mutationOptions.test.tsx +++ b/packages/react-query/src/__tests__/mutationOptions.test.tsx @@ -522,6 +522,5 @@ describe('mutationOptions', () => { await vi.advanceTimersByTimeAsync(11) expect(mutationStateArray.length).toEqual(1) expect(mutationStateArray[0]?.data).toEqual('data1') - expect(mutationStateArray[1]).toBeFalsy() }) }) diff --git a/packages/react-query/src/__tests__/useInfiniteQuery.test.tsx b/packages/react-query/src/__tests__/useInfiniteQuery.test.tsx index fdcb9ad38ed..fdbd401028c 100644 --- a/packages/react-query/src/__tests__/useInfiniteQuery.test.tsx +++ b/packages/react-query/src/__tests__/useInfiniteQuery.test.tsx @@ -30,21 +30,6 @@ interface Result { 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', () => { let queryCache: QueryCache let queryClient: QueryClient @@ -1617,12 +1602,18 @@ describe('useInfiniteQuery', () => { refetch, } = useInfiniteQuery({ queryKey: key, - queryFn: ({ pageParam }) => - fetchItems( - pageParam, - fetchCountRef.current++, - pageParam === MAX || (pageParam === MAX - 1 && isRemovedLastPage), - ), + queryFn: ({ pageParam }): Promise => { + const noNext = + pageParam === MAX || (pageParam === MAX - 1 && isRemovedLastPage) + return sleep(10).then(() => ({ + items: [...new Array(10)] + .fill(null) + .map((_, d) => pageParam * pageSize + d), + nextId: noNext ? undefined : pageParam + 1, + prevId: pageParam - 1, + ts: fetchCountRef.current++, + })) + }, getNextPageParam: (lastPage) => lastPage.nextId, initialPageParam: 0, }) @@ -1803,8 +1794,15 @@ describe('useInfiniteQuery', () => { useTrackRenders() const fetchCountRef = React.useRef(0) const query = useInfiniteQuery({ - queryFn: ({ pageParam }) => - fetchItems(pageParam, fetchCountRef.current++), + queryFn: ({ pageParam }): Promise => + sleep(10).then(() => ({ + items: [...new Array(10)] + .fill(null) + .map((_, d) => pageParam * pageSize + d), + nextId: pageParam + 1, + prevId: pageParam - 1, + ts: fetchCountRef.current++, + })), getNextPageParam: (lastPage) => lastPage.nextId, initialPageParam: 0, queryKey: key, diff --git a/packages/react-query/src/__tests__/useMutation.test.tsx b/packages/react-query/src/__tests__/useMutation.test.tsx index b5212bc27ab..c23b75bf3a4 100644 --- a/packages/react-query/src/__tests__/useMutation.test.tsx +++ b/packages/react-query/src/__tests__/useMutation.test.tsx @@ -209,7 +209,7 @@ describe('useMutation', () => { function Page() { const { mutateAsync } = useMutation({ - mutationFn: async (_text: string) => + mutationFn: (_text: string) => sleep(10).then(() => { throw new Error('oops') }), @@ -369,7 +369,7 @@ describe('useMutation', () => { function Page() { const { mutateAsync } = useMutation({ - mutationFn: async (_text: string) => + mutationFn: (_text: string) => sleep(10).then(() => { throw new Error('oops') }), @@ -462,15 +462,15 @@ describe('useMutation', () => { expect(getByRole('heading').textContent).toBe('3') expect(onSuccessMock).toHaveBeenCalledTimes(3) - expect(onSuccessMock).toHaveBeenCalledWith(1) - expect(onSuccessMock).toHaveBeenCalledWith(2) - expect(onSuccessMock).toHaveBeenCalledWith(3) + expect(onSuccessMock).toHaveBeenNthCalledWith(1, 1) + expect(onSuccessMock).toHaveBeenNthCalledWith(2, 2) + expect(onSuccessMock).toHaveBeenNthCalledWith(3, 3) expect(onSettledMock).toHaveBeenCalledTimes(3) - expect(onSettledMock).toHaveBeenCalledWith(1) - expect(onSettledMock).toHaveBeenCalledWith(2) - expect(onSettledMock).toHaveBeenCalledWith(3) + expect(onSettledMock).toHaveBeenNthCalledWith(1, 1) + expect(onSettledMock).toHaveBeenNthCalledWith(2, 2) + expect(onSettledMock).toHaveBeenNthCalledWith(3, 3) }) it('should set correct values for `failureReason` and `failureCount` on multiple mutate calls', async () => { @@ -567,24 +567,30 @@ describe('useMutation', () => { await vi.advanceTimersByTimeAsync(0) expect(getByRole('heading').textContent).toBe('3') expect(onErrorMock).toHaveBeenCalledTimes(3) - expect(onErrorMock).toHaveBeenCalledWith( + expect(onErrorMock).toHaveBeenNthCalledWith( + 1, 'Expected mock error. All is well! 1', ) - expect(onErrorMock).toHaveBeenCalledWith( + expect(onErrorMock).toHaveBeenNthCalledWith( + 2, 'Expected mock error. All is well! 2', ) - expect(onErrorMock).toHaveBeenCalledWith( + expect(onErrorMock).toHaveBeenNthCalledWith( + 3, 'Expected mock error. All is well! 3', ) expect(onSettledMock).toHaveBeenCalledTimes(3) - expect(onSettledMock).toHaveBeenCalledWith( + expect(onSettledMock).toHaveBeenNthCalledWith( + 1, 'Expected mock error. All is well! 1', ) - expect(onSettledMock).toHaveBeenCalledWith( + expect(onSettledMock).toHaveBeenNthCalledWith( + 2, 'Expected mock error. All is well! 2', ) - expect(onSettledMock).toHaveBeenCalledWith( + expect(onSettledMock).toHaveBeenNthCalledWith( + 3, 'Expected mock error. All is well! 3', ) }) @@ -733,7 +739,7 @@ describe('useMutation', () => { function Page() { const { mutateAsync } = useMutation({ - mutationFn: async (_text: string) => + mutationFn: (_text: string) => sleep(10).then(() => { throw new Error('oops') }), @@ -853,7 +859,7 @@ describe('useMutation', () => { function Page() { const { mutateAsync } = useMutation({ - mutationFn: async (_text: string) => Promise.reject(new Error('oops')), + mutationFn: (_text: string) => Promise.reject(new Error('oops')), onError: () => { callbacks.push('useMutation.onError') return Promise.resolve() @@ -904,7 +910,7 @@ describe('useMutation', () => { function Page() { const { mutate } = useMutation({ - mutationFn: async (_text: string) => + mutationFn: (_text: string) => sleep(10).then(() => Promise.reject(new Error('oops'))), onError: () => { callbacks.push('useMutation.onError') @@ -1231,13 +1237,13 @@ describe('useMutation', () => { 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')) - }, + mutationFn: (_text: string) => + sleep(10).then(() => { + count++ + return count > 1 + ? Promise.resolve(`data${count}`) + : Promise.reject(new Error('oops')) + }), retry: 1, retryDelay: 5, networkMode: 'offlineFirst', @@ -1801,10 +1807,10 @@ describe('useMutation', () => { function Page() { const mutation = useMutation({ - mutationFn: async (_text: string) => { - await sleep(10) - throw mutateFnError - }, + mutationFn: (_text: string) => + sleep(10).then(() => { + throw mutateFnError + }), onError: () => Promise.reject(error), }) @@ -1847,10 +1853,10 @@ describe('useMutation', () => { function Page() { const mutation = useMutation({ - mutationFn: async (_text: string) => { - await sleep(10) - throw mutateFnError - }, + mutationFn: (_text: string) => + sleep(10).then(() => { + throw mutateFnError + }), onSettled: () => Promise.reject(error), onError, }) @@ -1888,7 +1894,7 @@ describe('useMutation', () => { function Page() { const mutation = useMutation( { - mutationFn: async (text: string) => { + mutationFn: (text: string) => { return Promise.resolve(text) }, }, @@ -2010,13 +2016,13 @@ describe('useMutation', () => { const [message, setMessage] = React.useState('idle') const { mutate } = useMutation({ - mutationFn: async (shouldFail: boolean) => { - await sleep(10) - if (shouldFail) { - throw new Error('submission failed') - } - return 'submitted successfully' - }, + mutationFn: (shouldFail: boolean) => + sleep(10).then(() => { + if (shouldFail) { + throw new Error('submission failed') + } + return 'submitted successfully' + }), retry: false, }) @@ -2052,13 +2058,13 @@ describe('useMutation', () => { const [message, setMessage] = React.useState('idle') const { mutate } = useMutation({ - mutationFn: async (shouldFail: boolean) => { - await sleep(10) - if (shouldFail) { - throw new Error('submission failed') - } - return 'submitted successfully' - }, + mutationFn: (shouldFail: boolean) => + sleep(10).then(() => { + if (shouldFail) { + throw new Error('submission failed') + } + return 'submitted successfully' + }), retry: false, }) @@ -2096,14 +2102,14 @@ describe('useMutation', () => { const [message, setMessage] = React.useState('idle') const { mutate } = useMutation({ - mutationFn: async () => { - await sleep(10) - attempt++ - if (attempt < 2) { - throw new Error('temporary failure') - } - return 'success' - }, + mutationFn: () => + sleep(10).then(() => { + attempt++ + if (attempt < 2) { + throw new Error('temporary failure') + } + return 'success' + }), retry: false, }) @@ -2302,13 +2308,13 @@ describe('useMutation', () => { const [result, setResult] = React.useState('idle') const { mutateAsync } = useMutation({ - mutationFn: async (file: string) => { - await sleep(10) - if (file === 'file2') { - throw new Error('upload failed') - } - return `uploaded: ${file}` - }, + mutationFn: (file: string) => + sleep(10).then(() => { + if (file === 'file2') { + throw new Error('upload failed') + } + return `uploaded: ${file}` + }), retry: false, }) @@ -2350,13 +2356,13 @@ describe('useMutation', () => { const [result, setResult] = React.useState('idle') const { mutateAsync } = useMutation({ - mutationFn: async (file: string) => { - await sleep(10) - if (file === 'file2') { - throw new Error('upload failed') - } - return `uploaded: ${file}` - }, + mutationFn: (file: string) => + sleep(10).then(() => { + if (file === 'file2') { + throw new Error('upload failed') + } + return `uploaded: ${file}` + }), retry: false, }) diff --git a/packages/react-query/src/__tests__/usePrefetchInfiniteQuery.test.tsx b/packages/react-query/src/__tests__/usePrefetchInfiniteQuery.test.tsx index a6ea6a67518..7e3df99f010 100644 --- a/packages/react-query/src/__tests__/usePrefetchInfiniteQuery.test.tsx +++ b/packages/react-query/src/__tests__/usePrefetchInfiniteQuery.test.tsx @@ -9,38 +9,6 @@ import { useSuspenseInfiniteQuery, } from '..' import { renderWithClient } from './utils' -import type { InfiniteData, UseSuspenseInfiniteQueryOptions } from '..' -import type { Mock } from 'vitest' - -const createFallback = () => - vi.fn().mockImplementation(() =>
Loading...
) - -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', () => { let queryCache: QueryCache @@ -57,43 +25,37 @@ describe('usePrefetchInfiniteQuery', () => { vi.useRealTimers() }) - 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 Fallback = createFallback() + const Fallback = vi.fn().mockImplementation(() =>
Loading...
) 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, - }, + 'Do you fetch on render?', + 'Or do you render as you fetch?', + 'Either way, Tanstack Query helps you!', ] const queryOpts = { queryKey: queryKey(), - ...generateInfiniteQueryOptions(data), + queryFn: vi + .fn<(context: { pageParam: number }) => Promise>() + .mockImplementation(({ pageParam }) => + sleep(10).then(() => data[pageParam]!), + ), + initialPageParam: 0, + getNextPageParam: (_lastPage: string, allPages: Array) => + allPages.length < data.length ? allPages.length : undefined, + } + + function Page() { + const state = useSuspenseInfiniteQuery(queryOpts) + + return ( +
+ {state.data.pages.map((page, index) => ( +
data: {page}
+ ))} + +
+ ) } function App() { @@ -101,10 +63,7 @@ describe('usePrefetchInfiniteQuery', () => { return ( }> -
data: {page.data}
} - /> +
) } @@ -126,29 +85,48 @@ describe('usePrefetchInfiniteQuery', () => { }) it('should not display fallback if the query cache is already populated', async () => { - const Fallback = createFallback() + const Fallback = vi.fn().mockImplementation(() =>
Loading...
) + const data = [ + 'Prefetch rocks!', + 'No waterfalls, boy!', + 'Tanstack Query #ftw', + ] + 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 }, - ]), + queryFn: vi + .fn<(context: { pageParam: number }) => Promise>() + .mockImplementation(({ pageParam }) => + sleep(10).then(() => data[pageParam]!), + ), + initialPageParam: 0, + getNextPageParam: (_lastPage: string, allPages: Array) => + allPages.length < data.length ? allPages.length : undefined, } queryClient.prefetchInfiniteQuery({ ...queryOpts, pages: 3 }) await vi.advanceTimersByTimeAsync(30) - ;(queryOpts.queryFn as Mock).mockClear() + queryOpts.queryFn.mockClear() + + function Page() { + const state = useSuspenseInfiniteQuery(queryOpts) + + return ( +
+ {state.data.pages.map((page, index) => ( +
data: {page}
+ ))} + +
+ ) + } function App() { usePrefetchInfiniteQuery(queryOpts) return ( }> -
data: {page.data}
} - /> +
) } @@ -165,13 +143,20 @@ describe('usePrefetchInfiniteQuery', () => { }) it('should not create an endless loop when using inside a suspense boundary', async () => { + const data = ['Infinite Page 1', 'Infinite Page 2', 'Infinite Page 3'] + 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 }, - ]), + queryFn: vi + .fn<(context: { pageParam: number }) => Promise>() + .mockImplementation(({ pageParam }) => + sleep(10).then(() => data[pageParam]!), + ), + initialPageParam: 0, + // always reports another page available, to guard against an endless + // auto-advance loop rather than a bounded pagination sequence + getNextPageParam: (_lastPage: string, allPages: Array) => + allPages.length, } function Prefetch({ children }: { children: React.ReactNode }) { @@ -179,14 +164,24 @@ describe('usePrefetchInfiniteQuery', () => { return <>{children} } + function Page() { + const state = useSuspenseInfiniteQuery(queryOpts) + + return ( +
+ {state.data.pages.map((page, index) => ( +
data: {page}
+ ))} + +
+ ) + } + function App() { return ( -
data: {page.data}
} - /> +
) diff --git a/packages/react-query/src/__tests__/usePrefetchQuery.test.tsx b/packages/react-query/src/__tests__/usePrefetchQuery.test.tsx index 0c6823bb730..9b597d4d283 100644 --- a/packages/react-query/src/__tests__/usePrefetchQuery.test.tsx +++ b/packages/react-query/src/__tests__/usePrefetchQuery.test.tsx @@ -12,13 +12,6 @@ import { } from '..' import { renderWithClient } from './utils' -import type { UseSuspenseQueryOptions } from '..' - -const generateQueryFn = (data: string) => - vi - .fn<(...args: Array) => Promise>() - .mockImplementation(() => sleep(10).then(() => data)) - describe('usePrefetchQuery', () => { let queryCache: QueryCache let queryClient: QueryClient @@ -34,29 +27,20 @@ describe('usePrefetchQuery', () => { 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'), + queryFn: vi.fn(() => sleep(10).then(() => 'prefetchQuery')), } const componentQueryOpts = { ...queryOpts, - queryFn: generateQueryFn('useSuspenseQuery'), + queryFn: () => sleep(10).then(() => 'useSuspenseQuery'), + } + + function Page() { + const state = useSuspenseQuery(componentQueryOpts) + return
data: {String(state.data)}
} function App() { @@ -64,13 +48,15 @@ describe('usePrefetchQuery', () => { return ( - + ) } const rendered = renderWithClient(queryClient, ) + expect(rendered.getByText('Loading...')).toBeInTheDocument() + await act(() => vi.advanceTimersByTimeAsync(10)) expect(rendered.getByText('data: prefetchQuery')).toBeInTheDocument() expect(queryOpts.queryFn).toHaveBeenCalledTimes(1) @@ -79,7 +65,14 @@ describe('usePrefetchQuery', () => { it('should not prefetch query if query state exists', async () => { const queryOpts = { queryKey: queryKey(), - queryFn: generateQueryFn('The usePrefetchQuery hook is smart!'), + queryFn: vi.fn(() => + sleep(10).then(() => 'The usePrefetchQuery hook is smart!'), + ), + } + + function Page() { + const state = useSuspenseQuery(queryOpts) + return
data: {String(state.data)}
} function App() { @@ -87,7 +80,7 @@ describe('usePrefetchQuery', () => { return ( - + ) } @@ -107,18 +100,23 @@ describe('usePrefetchQuery', () => { 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 queryFn = vi.fn(() => sleep(10).then(() => 'Not an error')) const queryOpts = { queryKey: queryKey(), queryFn, } - queryFn.mockImplementationOnce(async () => { - await sleep(10) + queryFn.mockImplementationOnce(() => + sleep(10).then(() => { + throw new Error('Oops! Server error!') + }), + ) - throw new Error('Oops! Server error!') - }) + function Page() { + const state = useSuspenseQuery(queryOpts) + return
data: {String(state.data)}
+ } function App() { usePrefetchQuery(queryOpts) @@ -126,7 +124,7 @@ describe('usePrefetchQuery', () => { return (
Oops!
}> - +
) @@ -145,7 +143,7 @@ describe('usePrefetchQuery', () => { }) it('should not create an endless loop when using inside a suspense boundary', async () => { - const queryFn = generateQueryFn('prefetchedQuery') + const queryFn = vi.fn(() => sleep(10).then(() => 'prefetchedQuery')) const queryOpts = { queryKey: queryKey(), @@ -157,11 +155,16 @@ describe('usePrefetchQuery', () => { return <>{children} } + function Page() { + const state = useSuspenseQuery(queryOpts) + return
data: {String(state.data)}
+ } + function App() { return ( - + ) @@ -176,18 +179,25 @@ describe('usePrefetchQuery', () => { 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 queryFn = vi.fn(() => + sleep(10).then(() => 'This is fine :dog: :fire:'), + ) const queryOpts = { queryKey: queryKey(), queryFn, } - queryFn.mockImplementationOnce(async () => { - await sleep(10) + queryFn.mockImplementationOnce(() => + sleep(10).then(() => { + throw new Error('Oops! Server error!') + }), + ) - throw new Error('Oops! Server error!') - }) + function Page() { + const state = useSuspenseQuery(queryOpts) + return
data: {String(state.data)}
+ } function App() { const { reset } = useQueryErrorResetBoundary() @@ -204,7 +214,7 @@ describe('usePrefetchQuery', () => { )} > - + ) @@ -229,21 +239,48 @@ describe('usePrefetchQuery', () => { it('should not create a suspense waterfall if prefetch is fired', async () => { const firstQueryOpts = { queryKey: queryKey(), - queryFn: generateQueryFn('Prefetch is nice!'), + queryFn: vi.fn(() => sleep(10).then(() => 'Prefetch is nice!')), } const secondQueryOpts = { queryKey: queryKey(), - queryFn: generateQueryFn('Prefetch is really nice!!'), + queryFn: vi.fn(() => sleep(10).then(() => 'Prefetch is really nice!!')), } const thirdQueryOpts = { queryKey: queryKey(), - queryFn: generateQueryFn('Prefetch does not create waterfalls!!'), + queryFn: vi.fn(() => + sleep(10).then(() => 'Prefetch does not create waterfalls!!'), + ), } const Fallback = vi.fn().mockImplementation(() =>
Loading...
) + function FirstQuery({ children }: { children?: React.ReactNode }) { + const state = useSuspenseQuery(firstQueryOpts) + return ( +
+
data: {String(state.data)}
+ {children} +
+ ) + } + + function SecondQuery({ children }: { children?: React.ReactNode }) { + const state = useSuspenseQuery(secondQueryOpts) + return ( +
+
data: {String(state.data)}
+ {children} +
+ ) + } + + function ThirdQuery() { + const state = useSuspenseQuery(thirdQueryOpts) + return
data: {String(state.data)}
+ } + function App() { usePrefetchQuery(firstQueryOpts) usePrefetchQuery(secondQueryOpts) @@ -251,11 +288,11 @@ describe('usePrefetchQuery', () => { return ( }> - - - - - + + + + + ) } diff --git a/packages/react-query/src/__tests__/useQueries.test-d.tsx b/packages/react-query/src/__tests__/useQueries.test-d.tsx index 4346bb76a18..e4bd6497668 100644 --- a/packages/react-query/src/__tests__/useQueries.test-d.tsx +++ b/packages/react-query/src/__tests__/useQueries.test-d.tsx @@ -3,169 +3,1038 @@ import { queryKey } from '@tanstack/query-test-utils' import { skipToken } from '..' import { useQueries } from '../useQueries' import { queryOptions } from '../queryOptions' -import type { OmitKeyof } from '..' +import type { OmitKeyof, QueryFunction, QueryKey } from '..' import type { UseQueryOptions, UseQueryResult } from '../types' +import type { QueryFunctionContext } from '@tanstack/query-core' -describe('UseQueries config object overload', () => { - it('TData should always be defined when initialData is provided as an object', () => { - const query1 = { - queryKey: queryKey(), - queryFn: () => { - return { - wow: true, - } - }, - initialData: { - wow: false, - }, - } - - const query2 = { - queryKey: queryKey(), - queryFn: () => 'Query Data', - initialData: 'initial data', - } - - const query3 = { - queryKey: queryKey(), - 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() - }) +describe('useQueries', () => { + describe('config object overload', () => { + it('TData should always be defined when initialData is provided as an object', () => { + const query1 = { + queryKey: queryKey(), + queryFn: () => { + return { + wow: true, + } + }, + initialData: { + wow: false, + }, + } + + const query2 = { + queryKey: queryKey(), + queryFn: () => 'Query Data', + initialData: 'initial data', + } + + const query3 = { + queryKey: queryKey(), + queryFn: () => 'Query Data', + } - it('TData should be defined when passed through queryOptions', () => { - const options = queryOptions({ - queryKey: queryKey(), - queryFn: () => { - return { + 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: queryKey(), + queryFn: () => { + return { + wow: true, + } + }, + initialData: { wow: true, - } - }, - initialData: { - wow: true, - }, + }, + }) + const queryResults = useQueries({ queries: [options] }) + + const data = queryResults[0].data + + expectTypeOf(data).toEqualTypeOf<{ wow: boolean }>() }) - const queryResults = useQueries({ queries: [options] }) - const data = queryResults[0].data + it('should be possible to define a different TData than TQueryFnData using select with queryOptions spread into useQueries', () => { + const query1 = queryOptions({ + queryKey: queryKey(), + queryFn: () => Promise.resolve(1), + select: (data) => data > 1, + }) - expectTypeOf(data).toEqualTypeOf<{ wow: boolean }>() - }) + const query2 = { + queryKey: queryKey(), + 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 - it('should be possible to define a different TData than TQueryFnData using select with queryOptions spread into useQueries', () => { - const query1 = queryOptions({ - queryKey: queryKey(), - queryFn: () => Promise.resolve(1), - select: (data) => data > 1, + expectTypeOf(query1Data).toEqualTypeOf() + expectTypeOf(query2Data).toEqualTypeOf() }) - const query2 = { - queryKey: queryKey(), - queryFn: () => Promise.resolve(1), - select: (data: number) => data > 1, - } + it('TData should have undefined in the union when initialData is provided as a function which can return undefined', () => { + const queryResults = useQueries({ + queries: [ + { + queryKey: queryKey(), + queryFn: () => { + return { + wow: true, + } + }, + initialData: () => undefined as { wow: boolean } | undefined, + }, + ], + }) + + const data = queryResults[0].data - const queryResults = useQueries({ queries: [query1, query2] }) - const query1Data = queryResults[0].data - const query2Data = queryResults[1].data + expectTypeOf(data).toEqualTypeOf<{ wow: boolean } | undefined>() + }) - expectTypeOf(query1Data).toEqualTypeOf() - expectTypeOf(query2Data).toEqualTypeOf() - }) + describe('custom hook', () => { + it('should allow custom hooks using UseQueryOptions', () => { + type Data = string - it('TData should have undefined in the union when initialData is provided as a function which can return undefined', () => { - const queryResults = useQueries({ - queries: [ - { - queryKey: queryKey(), - queryFn: () => { - return { - wow: true, - } + const useCustomQueries = ( + options?: OmitKeyof, 'queryKey' | 'queryFn'>, + ) => { + return useQueries({ + queries: [ + { + ...options, + queryKey: queryKey(), + 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: queryKey(), + queryFn: Math.random() > 0.5 ? skipToken : () => Promise.resolve(5), }, - initialData: () => undefined as { wow: boolean } | undefined, - }, - ], + ], + }) + + const firstResult = queryResults[0] + + expectTypeOf(firstResult).toEqualTypeOf>() + expectTypeOf(firstResult.data).toEqualTypeOf() }) - const data = queryResults[0].data + it('should return correct data for dynamic queries with mixed result types', () => { + const Queries1 = { + get: () => + queryOptions({ + queryKey: queryKey(), + queryFn: () => Promise.resolve(1), + }), + } + const Queries2 = { + get: () => + queryOptions({ + queryKey: queryKey(), + queryFn: () => Promise.resolve(true), + }), + } - expectTypeOf(data).toEqualTypeOf<{ wow: boolean } | undefined>() + const queries1List = [1, 2, 3].map(() => ({ ...Queries1.get() })) + const result = useQueries({ + queries: [...queries1List, { ...Queries2.get() }], + }) + + expectTypeOf(result).toEqualTypeOf< + [ + ...Array>, + UseQueryResult, + ] + >() + }) }) - describe('custom hook', () => { - it('should allow custom hooks using UseQueryOptions', () => { - type Data = string + describe('type parameters', () => { + it('should handle type parameter - tuple of tuples', () => { + const key1 = queryKey() + const key2 = queryKey() + const key3 = queryKey() - const useCustomQueries = ( - options?: OmitKeyof, 'queryKey' | 'queryFn'>, - ) => { - return useQueries({ + // @ts-expect-error (Page component is not rendered) + function Page() { + const result1 = useQueries< + [[number], [string], [Array, boolean]] + >({ queries: [ { - ...options, - queryKey: queryKey(), - queryFn: () => Promise.resolve('data'), + queryKey: key1, + queryFn: () => 1, + }, + { + queryKey: key2, + queryFn: () => 'string', + }, + { + queryKey: key3, + queryFn: () => ['string[]'], + }, + ], + }) + expectTypeOf(result1[0]).toEqualTypeOf< + UseQueryResult + >() + expectTypeOf(result1[1]).toEqualTypeOf< + UseQueryResult + >() + 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< + UseQueryResult + >() + expectTypeOf(result2[1]).toEqualTypeOf< + UseQueryResult + >() + 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: [], }, ], }) } + }) - const queryResults = useCustomQueries() - const data = queryResults[0].data + it('should handle type parameter - tuple of objects', () => { + const key1 = queryKey() + const key2 = queryKey() + const key3 = queryKey() - expectTypeOf(data).toEqualTypeOf() + // @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< + UseQueryResult + >() + expectTypeOf(result1[1]).toEqualTypeOf< + UseQueryResult + >() + 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< + UseQueryResult + >() + expectTypeOf(result2[1]).toEqualTypeOf< + UseQueryResult + >() + 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< + UseQueryResult + >() + expectTypeOf(result3[1]).toEqualTypeOf< + UseQueryResult + >() + 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('TData should have correct type when conditional skipToken is passed', () => { - const queryResults = useQueries({ - queries: [ - { - queryKey: queryKey(), - queryFn: Math.random() > 0.5 ? skipToken : () => Promise.resolve(5), - }, - ], + it('should return correct 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: queryKey(), + queryFn: () => 'string', + select: (a) => { + expectTypeOf(a).toEqualTypeOf() + return a.toLowerCase() + }, + }), + queryOptions({ + queryKey: queryKey(), + 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() + } }) - const firstResult = queryResults[0] + it('should handle 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> + >() - expectTypeOf(firstResult).toEqualTypeOf>() - expectTypeOf(firstResult.data).toEqualTypeOf() + 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< + UseQueryResult + >() + + // 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< + UseQueryResult + >() + + // 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('should handle 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 return correct data for dynamic queries with mixed result types', () => { - const Queries1 = { - get: () => - queryOptions({ + describe('select', () => { + // Inferring the `select` argument of an *inline* query object from its + // sibling `queryFn` is a known TypeScript limitation, because `useQueries` + // infers its array generic from the argument itself. The two supported + // workarounds are to annotate the `select` parameter, or to define the + // query with the `queryOptions` helper. + // https://github.com/TanStack/query/issues/6556 + + describe('without queryOptions (inline query object)', () => { + it('leaves the select argument as `unknown` without an annotation', () => { + useQueries({ + queries: [ + { + queryKey: queryKey(), + queryFn: () => Promise.resolve(1), + select: (data) => { + expectTypeOf(data).toBeUnknown() + // @ts-expect-error `data` is `unknown`, not the expected `number` + return data.toFixed() + }, + }, + ], + }) + }) + + it('infers the result when the select parameter is annotated', () => { + const queryResults = useQueries({ + queries: [ + { + queryKey: queryKey(), + queryFn: () => Promise.resolve(1), + select: (data: number) => data.toFixed(), + }, + ], + }) + expectTypeOf(queryResults[0].data).toEqualTypeOf() + }) + }) + + describe('with queryOptions passed directly', () => { + it('without select, infers the queryFn data as the result', () => { + const options = queryOptions({ queryKey: queryKey(), queryFn: () => Promise.resolve(1), - }), - } - const Queries2 = { - get: () => - queryOptions({ + }) + const queryResults = useQueries({ queries: [options] }) + expectTypeOf(queryResults[0].data).toEqualTypeOf() + }) + + it('with select, infers the select argument and the result', () => { + const options = queryOptions({ queryKey: queryKey(), - queryFn: () => Promise.resolve(true), - }), - } + queryFn: () => Promise.resolve(1), + select: (data) => { + expectTypeOf(data).toEqualTypeOf() + return data.toFixed() + }, + }) + const queryResults = useQueries({ queries: [options] }) + expectTypeOf(queryResults[0].data).toEqualTypeOf() + }) + + it('infers select when a base queryOptions is re-wrapped with queryOptions', () => { + const baseOptions = queryOptions({ + queryKey: queryKey(), + queryFn: () => Promise.resolve(1), + }) + const queryResults = useQueries({ + queries: [ + queryOptions({ + ...baseOptions, + select: (data) => { + expectTypeOf(data).toEqualTypeOf() + return data.toFixed() + }, + }), + baseOptions, + ], + }) + expectTypeOf(queryResults[0].data).toEqualTypeOf() + expectTypeOf(queryResults[1].data).toEqualTypeOf() + }) - const queries1List = [1, 2, 3].map(() => ({ ...Queries1.get() })) - const result = useQueries({ - queries: [...queries1List, { ...Queries2.get() }], + it('infers an overriding select when a queryOptions with a select is re-wrapped with queryOptions', () => { + const baseOptions = queryOptions({ + queryKey: queryKey(), + queryFn: () => Promise.resolve(1), + select: (data) => data + 1, + }) + const queryResults = useQueries({ + queries: [ + queryOptions({ + ...baseOptions, + select: (data) => { + expectTypeOf(data).toEqualTypeOf() + return data.toFixed() + }, + }), + ], + }) + expectTypeOf(queryResults[0].data).toEqualTypeOf() + }) }) - expectTypeOf(result).toEqualTypeOf< - [...Array>, UseQueryResult] - >() + describe('with queryOptions spread into an inline query object', () => { + it('without select in the factory, leaves an unannotated select as `unknown`', () => { + const options = queryOptions({ + queryKey: queryKey(), + queryFn: () => Promise.resolve(1), + }) + useQueries({ + queries: [ + // @ts-expect-error Without an annotation the inline `select` receives `data: unknown`, which makes the whole spread query object unassignable to the expected options type + { + ...options, + select: (data) => { + expectTypeOf(data).toBeUnknown() + return data + }, + }, + ], + }) + }) + + it('without select in the factory, an annotated select compiles', () => { + const options = queryOptions({ + queryKey: queryKey(), + queryFn: () => Promise.resolve(1), + }) + const queryResults = useQueries({ + queries: [{ ...options, select: (data: number) => data.toFixed() }], + }) + expectTypeOf(queryResults[0].data).toEqualTypeOf() + }) + + it('with select in the factory, leaves an unannotated overriding select as `unknown`', () => { + const options = queryOptions({ + queryKey: queryKey(), + queryFn: () => Promise.resolve(1), + select: (data) => data + 1, + }) + useQueries({ + queries: [ + // @ts-expect-error Without an annotation the inline `select` receives `data: unknown`, which makes the whole spread query object unassignable to the expected options type + { + ...options, + select: (data) => { + expectTypeOf(data).toBeUnknown() + return data + }, + }, + ], + }) + }) + + it('with select in the factory, an annotated overriding select compiles', () => { + const options = queryOptions({ + queryKey: queryKey(), + queryFn: () => Promise.resolve(1), + select: (data) => data + 1, + }) + const queryResults = useQueries({ + queries: [{ ...options, select: (data: number) => data.toFixed() }], + }) + expectTypeOf(queryResults[0].data).toEqualTypeOf() + }) + }) }) }) diff --git a/packages/react-query/src/__tests__/useQueries.test.tsx b/packages/react-query/src/__tests__/useQueries.test.tsx index 29529920f75..c7a9283d07c 100644 --- a/packages/react-query/src/__tests__/useQueries.test.tsx +++ b/packages/react-query/src/__tests__/useQueries.test.tsx @@ -1,33 +1,11 @@ -import { - afterEach, - beforeEach, - describe, - expect, - expectTypeOf, - it, - vi, -} from 'vitest' +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 { - IsRestoringProvider, - QueryCache, - QueryClient, - queryOptions, - skipToken, - useQueries, -} from '..' +import { IsRestoringProvider, QueryCache, QueryClient, useQueries } from '..' import { renderWithClient } from './utils' -import type { - QueryFunction, - QueryKey, - QueryObserverResult, - UseQueryOptions, - UseQueryResult, -} from '..' -import type { QueryFunctionContext } from '@tanstack/query-core' +import type { QueryObserverResult, UseQueryResult } from '..' describe('useQueries', () => { let queryCache: QueryCache @@ -95,11 +73,11 @@ describe('useQueries', () => { queries: [ { queryKey: key1, - queryFn: async () => { - await sleep(10) - count++ - return count - }, + queryFn: () => + sleep(10).then(() => { + count++ + return count + }), }, ], }) @@ -133,672 +111,6 @@ describe('useQueries', () => { 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: queryKey(), - queryFn: () => 'string', - select: (a) => { - expectTypeOf(a).toEqualTypeOf() - return a.toLowerCase() - }, - }), - queryOptions({ - queryKey: queryKey(), - 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') @@ -832,7 +144,7 @@ describe('useQueries', () => { }, { queryKey: key4, - queryFn: async () => + queryFn: () => Promise.reject( new Error('this should not throw because query#2 already did'), ), @@ -900,7 +212,7 @@ describe('useQueries', () => { }, { queryKey: key4, - queryFn: async () => + queryFn: () => Promise.reject( new Error('this should not throw because query#3 already did'), ), @@ -1456,19 +768,19 @@ describe('useQueries', () => { queries: [ { queryKey: [key1], - queryFn: async () => { - await sleep(10) - queryFns.push('first result') - return 'first result' - }, + queryFn: () => + sleep(10).then(() => { + queryFns.push('first result') + return 'first result' + }), }, { queryKey: [key2], - queryFn: async () => { - await sleep(20) - queryFns.push('second result') - return 'second result' - }, + queryFn: () => + sleep(20).then(() => { + queryFns.push('second result') + return 'second result' + }), }, ], combine: () => 'foo', diff --git a/packages/react-query/src/__tests__/useQuery.promise.test.tsx b/packages/react-query/src/__tests__/useQuery.promise.test.tsx index 8707b439d90..e93c7df5646 100644 --- a/packages/react-query/src/__tests__/useQuery.promise.test.tsx +++ b/packages/react-query/src/__tests__/useQuery.promise.test.tsx @@ -499,14 +499,14 @@ describe('useQuery().promise', { timeout: 10_000 }, () => { function Page() { const query = useQuery({ queryKey: key, - queryFn: async () => { - await sleep(10) - if (++queryCount > 1) { - // second time this query mounts, it should not throw - return 'data' - } - throw new Error('Error test') - }, + queryFn: () => + sleep(10).then(() => { + if (++queryCount > 1) { + // second time this query mounts, it should not throw + return 'data' + } + throw new Error('Error test') + }), retry: false, }) @@ -788,14 +788,16 @@ describe('useQuery().promise', { timeout: 10_000 }, () => { expect(queryFn).toHaveBeenCalledOnce() }) - it.skip('should stay pending when canceled with cancelQueries while suspending until refetched', async () => { + 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 sleep(10) - return 'test' + count++ - }) + // `sleep` is longer than usual on purpose: with `shouldAdvanceTime`, the + // real time spent rendering and awaiting `takeRender` (~40ms) is added to + // the fake clock, so a shorter fetch would resolve before `cancel` can take + // effect. A longer fetch keeps the query in-flight when it is cancelled. + const queryFn = vi + .fn() + .mockImplementation(() => sleep(50).then(() => 'test')) const options = { queryKey: key, @@ -1393,13 +1395,13 @@ describe('useQuery().promise', { timeout: 10_000 }, () => { function Page() { const query = useInfiniteQuery({ queryKey: key, - queryFn: async ({ pageParam = 0 }) => { - await sleep(10) - if (pageParam === 0) { - return { nextCursor: 1, data: 'page-1' } - } - throw new Error('page error') - }, + queryFn: ({ pageParam = 0 }) => + sleep(10).then(() => { + if (pageParam === 0) { + return { nextCursor: 1, data: 'page-1' } + } + throw new Error('page error') + }), initialPageParam: 0, getNextPageParam: (lastPage) => lastPage.nextCursor, retry: false, diff --git a/packages/react-query/src/__tests__/useQuery.test.tsx b/packages/react-query/src/__tests__/useQuery.test.tsx index 1698f91af32..05984e66ce5 100644 --- a/packages/react-query/src/__tests__/useQuery.test.tsx +++ b/packages/react-query/src/__tests__/useQuery.test.tsx @@ -750,11 +750,11 @@ describe('useQuery', () => { function Page() { const state = useQuery({ queryKey: key, - queryFn: async () => { - await sleep(10) - count++ - return 'test' + count - }, + queryFn: () => + sleep(10).then(() => { + count++ + return 'test' + count + }), }) states.push(state) @@ -869,10 +869,7 @@ describe('useQuery', () => { function Page() { const state = useQuery({ queryKey: key, - queryFn: async () => { - await sleep(10) - return ++count - }, + queryFn: () => sleep(10).then(() => ++count), notifyOnChangeProps: 'all', }) @@ -931,11 +928,11 @@ describe('useQuery', () => { function Page() { const state = useQuery({ queryKey: key, - queryFn: async () => { - await sleep(10) - count++ - return count === 1 ? result1 : result2 - }, + queryFn: () => + sleep(10).then(() => { + count++ + return count === 1 ? result1 : result2 + }), notifyOnChangeProps: 'all', }) @@ -1019,11 +1016,11 @@ describe('useQuery', () => { function Page() { const state = useQuery({ queryKey: key, - queryFn: async () => { - await sleep(10) - count++ - return count - }, + queryFn: () => + sleep(10).then(() => { + count++ + return count + }), staleTime: Infinity, }) @@ -1065,11 +1062,11 @@ describe('useQuery', () => { function Page() { const state = useQuery({ queryKey: key, - queryFn: async () => { - await sleep(10) - count++ - return count - }, + queryFn: () => + sleep(10).then(() => { + count++ + return count + }), enabled: false, }) @@ -1105,11 +1102,11 @@ describe('useQuery', () => { function Page() { const state = useQuery({ queryKey: key, - queryFn: async () => { - await sleep(10) - count++ - return count - }, + queryFn: () => + sleep(10).then(() => { + count++ + return count + }), enabled: false, }) @@ -1460,13 +1457,13 @@ describe('useQuery', () => { 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) - }, + queryFn: () => + sleep(10).then(() => { + if (count === 2) { + throw new Error('Error test') + } + return Promise.resolve(count) + }), retry: false, placeholderData: keepPreviousData, }) @@ -2178,11 +2175,10 @@ describe('useQuery', () => { const state = useQuery({ queryKey: key, - queryFn: async () => { - await sleep(5) - fetchCounterRef.current++ - return `fetch counter: ${fetchCounterRef.current}` - }, + queryFn: () => + sleep(5).then( + () => `fetch counter: ${++fetchCounterRef.current}`, + ), notifyOnChangeProps, }) @@ -2568,10 +2564,7 @@ describe('useQuery', () => { function Page() { const state = useQuery({ queryKey: key, - queryFn: async () => { - await sleep(10) - return count++ - }, + queryFn: () => sleep(10).then(() => count++), staleTime: Infinity, refetchOnWindowFocus: 'always', @@ -2607,10 +2600,7 @@ describe('useQuery', () => { function Page() { const state = useQuery({ queryKey: key, - queryFn: async () => { - await sleep(10) - return count++ - }, + queryFn: () => sleep(10).then(() => count++), staleTime: 0, retry: 0, @@ -3974,16 +3964,15 @@ describe('useQuery', () => { function Page() { const result = useQuery({ queryKey: key, - queryFn: async () => { - await sleep(10) - return ( - queryFn() || { - data: { - nested: true, + queryFn: () => + sleep(10).then( + () => + queryFn() || { + data: { + nested: true, + }, }, - } - ) - }, + ), }) React.useMemo(() => { @@ -4055,10 +4044,7 @@ describe('useQuery', () => { function Page() { const queryInfo = useQuery({ queryKey: key, - queryFn: async () => { - await sleep(10) - return count++ - }, + queryFn: () => sleep(10).then(() => count++), refetchInterval: ({ state: { data = 0 } }) => (data < 2 ? 10 : false), }) @@ -4694,11 +4680,11 @@ describe('useQuery', () => { function Page() { const state = useQuery({ queryKey: key, - queryFn: async () => { - await sleep(10) - count++ - return count - }, + queryFn: () => + sleep(10).then(() => { + count++ + return count + }), staleTime: Infinity, }) @@ -4765,11 +4751,11 @@ describe('useQuery', () => { function Page() { const state = useQuery({ queryKey: key, - queryFn: async () => { - await sleep(10) - count++ - return count - }, + queryFn: () => + sleep(10).then(() => { + count++ + return count + }), staleTime: Infinity, enabled: false, notifyOnChangeProps: 'all', @@ -4950,14 +4936,14 @@ describe('useQuery', () => { 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' - } - }, + queryFn: () => + sleep(10).then(() => { + if (id % 2 === 1) { + return Promise.reject(new Error('Error')) + } else { + return 'data' + } + }), retry: false, retryOnMount: () => false, refetchOnMount: false, @@ -5072,14 +5058,14 @@ describe('useQuery', () => { function Page() { const state = useQuery({ queryKey: key, - queryFn: async () => { - await sleep(10) - if (count === 0) { - count++ - throw error - } - return 5 - }, + queryFn: () => + sleep(10).then(() => { + if (count === 0) { + count++ + throw error + } + return 5 + }), retry: false, }) diff --git a/packages/react-query/src/__tests__/useSuspenseQueries.test-d.tsx b/packages/react-query/src/__tests__/useSuspenseQueries.test-d.tsx index 98bf336c8df..d9230bdf940 100644 --- a/packages/react-query/src/__tests__/useSuspenseQueries.test-d.tsx +++ b/packages/react-query/src/__tests__/useSuspenseQueries.test-d.tsx @@ -254,4 +254,173 @@ describe('UseSuspenseQueries config object overload', () => { }), ) }) + + describe('select', () => { + // Inferring the `select` argument of an *inline* query object from its + // sibling `queryFn` is a known TypeScript limitation, because + // `useSuspenseQueries` infers its array generic from the argument itself. + // The two supported workarounds are to annotate the `select` parameter, or + // to define the query with the `queryOptions` helper. + // https://github.com/TanStack/query/issues/6556 + + describe('without queryOptions (inline query object)', () => { + it('leaves the select argument as `unknown` without an annotation', () => { + useSuspenseQueries({ + queries: [ + { + queryKey: queryKey(), + queryFn: () => Promise.resolve(1), + select: (data) => { + expectTypeOf(data).toBeUnknown() + // @ts-expect-error `data` is `unknown`, not the expected `number` + return data.toFixed() + }, + }, + ], + }) + }) + + it('infers the result when the select parameter is annotated', () => { + const queryResults = useSuspenseQueries({ + queries: [ + { + queryKey: queryKey(), + queryFn: () => Promise.resolve(1), + select: (data: number) => data.toFixed(), + }, + ], + }) + expectTypeOf(queryResults[0].data).toEqualTypeOf() + }) + }) + + describe('with queryOptions passed directly', () => { + it('without select, infers the queryFn data as the result', () => { + const options = queryOptions({ + queryKey: queryKey(), + queryFn: () => Promise.resolve(1), + }) + const queryResults = useSuspenseQueries({ queries: [options] }) + expectTypeOf(queryResults[0].data).toEqualTypeOf() + }) + + it('with select, infers the select argument and the result', () => { + const options = queryOptions({ + queryKey: queryKey(), + queryFn: () => Promise.resolve(1), + select: (data) => { + expectTypeOf(data).toEqualTypeOf() + return data.toFixed() + }, + }) + const queryResults = useSuspenseQueries({ queries: [options] }) + expectTypeOf(queryResults[0].data).toEqualTypeOf() + }) + + it('infers select when a base queryOptions is re-wrapped with queryOptions', () => { + const baseOptions = queryOptions({ + queryKey: queryKey(), + queryFn: () => Promise.resolve(1), + }) + const queryResults = useSuspenseQueries({ + queries: [ + queryOptions({ + ...baseOptions, + select: (data) => { + expectTypeOf(data).toEqualTypeOf() + return data.toFixed() + }, + }), + baseOptions, + ], + }) + expectTypeOf(queryResults[0].data).toEqualTypeOf() + expectTypeOf(queryResults[1].data).toEqualTypeOf() + }) + + it('infers an overriding select when a queryOptions with a select is re-wrapped with queryOptions', () => { + const baseOptions = queryOptions({ + queryKey: queryKey(), + queryFn: () => Promise.resolve(1), + select: (data) => data + 1, + }) + const queryResults = useSuspenseQueries({ + queries: [ + queryOptions({ + ...baseOptions, + select: (data) => { + expectTypeOf(data).toEqualTypeOf() + return data.toFixed() + }, + }), + ], + }) + expectTypeOf(queryResults[0].data).toEqualTypeOf() + }) + }) + + describe('with queryOptions spread into an inline query object', () => { + it('without select in the factory, leaves an unannotated select untyped', () => { + const options = queryOptions({ + queryKey: queryKey(), + queryFn: () => Promise.resolve(1), + }) + useSuspenseQueries({ + queries: [ + { + ...options, + // @ts-expect-error Without an annotation the inline `select` parameter `data` implicitly has type `any` + select: (data) => { + expectTypeOf(data).toBeAny() + return data + }, + }, + ], + }) + }) + + it('without select in the factory, an annotated select compiles', () => { + const options = queryOptions({ + queryKey: queryKey(), + queryFn: () => Promise.resolve(1), + }) + const queryResults = useSuspenseQueries({ + queries: [{ ...options, select: (data: number) => data.toFixed() }], + }) + expectTypeOf(queryResults[0].data).toEqualTypeOf() + }) + + it('with select in the factory, leaves an unannotated overriding select untyped', () => { + const options = queryOptions({ + queryKey: queryKey(), + queryFn: () => Promise.resolve(1), + select: (data) => data + 1, + }) + useSuspenseQueries({ + queries: [ + { + ...options, + // @ts-expect-error Without an annotation the inline `select` parameter `data` implicitly has type `any` + select: (data) => { + expectTypeOf(data).toBeAny() + return data + }, + }, + ], + }) + }) + + it('with select in the factory, an annotated overriding select compiles', () => { + const options = queryOptions({ + queryKey: queryKey(), + queryFn: () => Promise.resolve(1), + select: (data) => data + 1, + }) + const queryResults = useSuspenseQueries({ + queries: [{ ...options, select: (data: number) => data.toFixed() }], + }) + expectTypeOf(queryResults[0].data).toEqualTypeOf() + }) + }) + }) }) diff --git a/packages/react-query/src/__tests__/useSuspenseQuery.test.tsx b/packages/react-query/src/__tests__/useSuspenseQuery.test.tsx index 10583797f3d..4a7df3a2a62 100644 --- a/packages/react-query/src/__tests__/useSuspenseQuery.test.tsx +++ b/packages/react-query/src/__tests__/useSuspenseQuery.test.tsx @@ -192,7 +192,7 @@ describe('useSuspenseQuery', () => { expect(rendered.queryByText('loading')).not.toBeInTheDocument() expect(rendered.queryByText('rendered')).not.toBeInTheDocument() - expect(queryCache.find({ queryKey: key })).toBeFalsy() + expect(queryCache.find({ queryKey: key })).toBeUndefined() fireEvent.click(rendered.getByLabelText('toggle')) expect(rendered.getByText('loading')).toBeInTheDocument() @@ -748,6 +748,7 @@ describe('useSuspenseQuery', () => { , ) + expect(rendered.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(10)) expect(rendered.getByText('rendered')).toBeInTheDocument() @@ -838,7 +839,7 @@ describe('useSuspenseQuery', () => { const state = useSuspenseQuery({ queryKey: stateKey, - queryFn: async () => sleep(10).then(() => ++count), + queryFn: () => sleep(10).then(() => ++count), }) states.push(state) diff --git a/packages/react-query/src/useQueries.ts b/packages/react-query/src/useQueries.ts index de179837a5f..5f5261a870e 100644 --- a/packages/react-query/src/useQueries.ts +++ b/packages/react-query/src/useQueries.ts @@ -162,7 +162,7 @@ export type QueriesOptions< > : 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! + : // If T is *some* array but we couldn't assign unknown[] to it, then it must hold some known/homogeneous type! // use this to infer the param types in the case of Array.map() argument T extends Array< UseQueryOptionsForUseQueries< diff --git a/packages/react-query/src/useSuspenseQueries.ts b/packages/react-query/src/useSuspenseQueries.ts index f014095d01c..aa10abfb3d1 100644 --- a/packages/react-query/src/useSuspenseQueries.ts +++ b/packages/react-query/src/useSuspenseQueries.ts @@ -125,7 +125,7 @@ export type SuspenseQueriesOptions< > : 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! + : // If T is *some* array but we couldn't assign unknown[] to it, then it must hold some known/homogeneous type! // use this to infer the param types in the case of Array.map() argument T extends Array< UseSuspenseQueryOptions< diff --git a/packages/solid-query-devtools/CHANGELOG.md b/packages/solid-query-devtools/CHANGELOG.md index 4c1103bdb79..35178d589a5 100644 --- a/packages/solid-query-devtools/CHANGELOG.md +++ b/packages/solid-query-devtools/CHANGELOG.md @@ -94,6 +94,38 @@ - Updated dependencies [[`4a27c6c`](https://github.com/TanStack/query/commit/4a27c6c1810956509e0e38c95f4f3fdc0b9b6f55)]: - @tanstack/solid-query@6.0.0-alpha.0 +## 5.101.4 + +### Patch Changes + +- Updated dependencies []: + - @tanstack/query-devtools@5.101.4 + - @tanstack/solid-query@5.101.4 + +## 5.101.3 + +### Patch Changes + +- Updated dependencies []: + - @tanstack/query-devtools@5.101.3 + - @tanstack/solid-query@5.101.3 + +## 5.101.2 + +### Patch Changes + +- Updated dependencies [[`f5bf180`](https://github.com/TanStack/query/commit/f5bf180d933d8b8d9d9e7b845e55b26a3a413b07), [`25cdd97`](https://github.com/TanStack/query/commit/25cdd975fed4703d2ca5b600ca5ccd2b600b3dd8), [`ecd89c8`](https://github.com/TanStack/query/commit/ecd89c8faf7acc226f00633ea3a761d3ab842c1d), [`01c7634`](https://github.com/TanStack/query/commit/01c763444e3cf3dfa9744f13911aa1533cac3c29), [`49012db`](https://github.com/TanStack/query/commit/49012dbd5192dfe483d3b108b72ffaa7f2849e0f)]: + - @tanstack/query-devtools@5.101.2 + - @tanstack/solid-query@5.101.2 + +## 5.101.1 + +### Patch Changes + +- Updated dependencies []: + - @tanstack/query-devtools@5.101.1 + - @tanstack/solid-query@5.101.1 + ## 5.101.0 ### Patch Changes diff --git a/packages/solid-query-devtools/src/__tests__/devtools.test.tsx b/packages/solid-query-devtools/src/__tests__/devtools.test.tsx index 517986860f6..7a80bef538d 100644 --- a/packages/solid-query-devtools/src/__tests__/devtools.test.tsx +++ b/packages/solid-query-devtools/src/__tests__/devtools.test.tsx @@ -1,12 +1,38 @@ -import { afterEach, describe, expect, it, vi } from 'vitest' -import { render } from '@solidjs/testing-library' -import { flush } from 'solid-js' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { createSignal, flush } from 'solid-js' +import { cleanup, render } from '@solidjs/testing-library' import { QueryClient, QueryClientProvider } from '@tanstack/solid-query' import { TanstackQueryDevtools } from '@tanstack/query-devtools' import SolidQueryDevtools from '../devtools' +import type { + DevtoolsButtonPosition, + DevtoolsErrorType, + DevtoolsPosition, + Theme, +} from '@tanstack/query-devtools' describe('SolidQueryDevtools', () => { + let queryClient: QueryClient + + beforeEach(() => { + queryClient = new QueryClient() + // Mounting the real devtools lazily imports the devtools UI, which is Solid + // 1 code that cannot resolve under this Solid 2 test setup. These tests only + // assert what the wrapper forwards to the instance, and the container + // element is rendered by the wrapper itself, so stub the mount out. + vi.spyOn(TanstackQueryDevtools.prototype, 'mount').mockImplementation( + () => {}, + ) + // ...and with nothing really mounted, the real unmount() would throw. + vi.spyOn(TanstackQueryDevtools.prototype, 'unmount').mockImplementation( + () => {}, + ) + }) + afterEach(() => { + // Dispose rendered roots before restoring mocks: disposal calls unmount() + // on the instance, which must still be the stub from beforeEach. + cleanup() vi.restoreAllMocks() }) @@ -17,8 +43,6 @@ describe('SolidQueryDevtools', () => { }) it('should not throw an error if query client is provided via context', () => { - const queryClient = new QueryClient() - expect(() => render(() => ( @@ -29,8 +53,6 @@ describe('SolidQueryDevtools', () => { }) it('should not throw an error if query client is provided via props', () => { - const queryClient = new QueryClient() - expect(() => render(() => ), ).not.toThrow() @@ -41,8 +63,6 @@ describe('SolidQueryDevtools', () => { TanstackQueryDevtools.prototype, 'setButtonPosition', ) - const queryClient = new QueryClient() - render(() => ( )) @@ -53,8 +73,6 @@ describe('SolidQueryDevtools', () => { it('should forward "position" to the devtools instance', () => { const setPosition = vi.spyOn(TanstackQueryDevtools.prototype, 'setPosition') - const queryClient = new QueryClient() - render(() => ) flush() @@ -66,8 +84,6 @@ describe('SolidQueryDevtools', () => { TanstackQueryDevtools.prototype, 'setInitialIsOpen', ) - const queryClient = new QueryClient() - render(() => ( )) @@ -81,8 +97,6 @@ describe('SolidQueryDevtools', () => { TanstackQueryDevtools.prototype, 'setInitialIsOpen', ) - const queryClient = new QueryClient() - render(() => ) flush() @@ -94,7 +108,6 @@ describe('SolidQueryDevtools', () => { TanstackQueryDevtools.prototype, 'setErrorTypes', ) - const queryClient = new QueryClient() const errorTypes = [ { name: 'Network', initializer: () => new Error('Network') }, ] @@ -112,8 +125,6 @@ describe('SolidQueryDevtools', () => { TanstackQueryDevtools.prototype, 'setErrorTypes', ) - const queryClient = new QueryClient() - render(() => ) flush() @@ -122,8 +133,6 @@ describe('SolidQueryDevtools', () => { it('should forward "theme" to the devtools instance', () => { const setTheme = vi.spyOn(TanstackQueryDevtools.prototype, 'setTheme') - const queryClient = new QueryClient() - render(() => ) flush() @@ -132,8 +141,6 @@ describe('SolidQueryDevtools', () => { it('should default "theme" to "system" when the prop is omitted', () => { const setTheme = vi.spyOn(TanstackQueryDevtools.prototype, 'setTheme') - const queryClient = new QueryClient() - render(() => ) flush() @@ -142,18 +149,114 @@ describe('SolidQueryDevtools', () => { it('should forward the resolved "QueryClient" via "setClient"', () => { const setClient = vi.spyOn(TanstackQueryDevtools.prototype, 'setClient') - const queryClient = new QueryClient() - render(() => ) flush() expect(setClient).toHaveBeenCalledWith(queryClient) }) + it('should forward a "buttonPosition" change to the devtools instance after mount', () => { + const setButtonPosition = vi.spyOn( + TanstackQueryDevtools.prototype, + 'setButtonPosition', + ) + const [buttonPosition, setButtonPositionSignal] = + createSignal('bottom-right') + + render(() => ( + + )) + flush() + setButtonPosition.mockClear() + + setButtonPositionSignal('top-left') + flush() + + expect(setButtonPosition).toHaveBeenCalledWith('top-left') + }) + + it('should forward a "position" change to the devtools instance after mount', () => { + const setPosition = vi.spyOn(TanstackQueryDevtools.prototype, 'setPosition') + const [position, setPositionSignal] = + createSignal('bottom') + + render(() => ( + + )) + flush() + setPosition.mockClear() + + setPositionSignal('top') + flush() + + expect(setPosition).toHaveBeenCalledWith('top') + }) + + it('should forward an "initialIsOpen" change to the devtools instance after mount', () => { + const setInitialIsOpen = vi.spyOn( + TanstackQueryDevtools.prototype, + 'setInitialIsOpen', + ) + const [initialIsOpen, setInitialIsOpenSignal] = createSignal(false) + + render(() => ( + + )) + flush() + setInitialIsOpen.mockClear() + + setInitialIsOpenSignal(true) + flush() + + expect(setInitialIsOpen).toHaveBeenCalledWith(true) + }) + + it('should forward an "errorTypes" change to the devtools instance after mount', () => { + const setErrorTypes = vi.spyOn( + TanstackQueryDevtools.prototype, + 'setErrorTypes', + ) + const [errorTypes, setErrorTypesSignal] = createSignal< + Array + >([]) + + render(() => ( + + )) + flush() + setErrorTypes.mockClear() + + const nextErrorTypes = [ + { name: 'Network', initializer: () => new Error('Network') }, + ] + setErrorTypesSignal(nextErrorTypes) + flush() + + expect(setErrorTypes).toHaveBeenCalledWith(nextErrorTypes) + }) + + it('should forward a "theme" change to the devtools instance after mount', () => { + const setTheme = vi.spyOn(TanstackQueryDevtools.prototype, 'setTheme') + const [theme, setThemeSignal] = createSignal('light') + + render(() => ) + flush() + setTheme.mockClear() + + setThemeSignal('dark') + flush() + + expect(setTheme).toHaveBeenCalledWith('dark') + }) + it('should call "unmount" on the devtools instance when the component unmounts', async () => { const unmount = vi.spyOn(TanstackQueryDevtools.prototype, 'unmount') - const queryClient = new QueryClient() - const { unmount: unmountComponent } = render(() => ( )) diff --git a/packages/solid-query-devtools/src/__tests__/devtoolsPanel.test.tsx b/packages/solid-query-devtools/src/__tests__/devtoolsPanel.test.tsx index 5732a8b6ab3..9e774a309fd 100644 --- a/packages/solid-query-devtools/src/__tests__/devtoolsPanel.test.tsx +++ b/packages/solid-query-devtools/src/__tests__/devtoolsPanel.test.tsx @@ -1,12 +1,33 @@ -import { afterEach, describe, expect, it, vi } from 'vitest' -import { render } from '@solidjs/testing-library' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { cleanup, render } from '@solidjs/testing-library' import { flush } from 'solid-js' import { QueryClient, QueryClientProvider } from '@tanstack/solid-query' import { TanstackQueryDevtoolsPanel } from '@tanstack/query-devtools' import SolidQueryDevtoolsPanel from '../devtoolsPanel' describe('SolidQueryDevtoolsPanel', () => { + let queryClient: QueryClient + + beforeEach(() => { + queryClient = new QueryClient() + // Mounting the real panel lazily imports the devtools UI, which is Solid 1 + // code that cannot resolve under this Solid 2 test setup. These tests only + // assert what the wrapper forwards to the instance, and the container + // element is rendered by the wrapper itself, so stub the mount out. + vi.spyOn(TanstackQueryDevtoolsPanel.prototype, 'mount').mockImplementation( + () => {}, + ) + // ...and with nothing really mounted, the real unmount() would throw. + vi.spyOn( + TanstackQueryDevtoolsPanel.prototype, + 'unmount', + ).mockImplementation(() => {}) + }) + afterEach(() => { + // Dispose rendered roots before restoring mocks: disposal calls unmount() + // on the instance, which must still be the stub from beforeEach. + cleanup() vi.restoreAllMocks() }) @@ -17,8 +38,6 @@ describe('SolidQueryDevtoolsPanel', () => { }) it('should not throw an error if query client is provided via context', () => { - const queryClient = new QueryClient() - expect(() => render(() => ( @@ -29,8 +48,6 @@ describe('SolidQueryDevtoolsPanel', () => { }) it('should not throw an error if query client is provided via props', () => { - const queryClient = new QueryClient() - expect(() => render(() => ), ).not.toThrow() @@ -41,7 +58,6 @@ describe('SolidQueryDevtoolsPanel', () => { TanstackQueryDevtoolsPanel.prototype, 'setOnClose', ) - const queryClient = new QueryClient() const onClose = vi.fn() render(() => ( @@ -57,12 +73,12 @@ describe('SolidQueryDevtoolsPanel', () => { TanstackQueryDevtoolsPanel.prototype, 'setOnClose', ) - const queryClient = new QueryClient() - render(() => ) flush() - expect(setOnClose).toHaveBeenCalledWith(expect.any(Function)) + const forwarded = setOnClose.mock.calls[0]![0] + expect(forwarded).toBeInstanceOf(Function) + expect(forwarded()).toBeUndefined() }) it('should forward "errorTypes" to the devtools instance', () => { @@ -70,7 +86,6 @@ describe('SolidQueryDevtoolsPanel', () => { TanstackQueryDevtoolsPanel.prototype, 'setErrorTypes', ) - const queryClient = new QueryClient() const errorTypes = [ { name: 'Network', initializer: () => new Error('Network') }, ] @@ -88,8 +103,6 @@ describe('SolidQueryDevtoolsPanel', () => { TanstackQueryDevtoolsPanel.prototype, 'setErrorTypes', ) - const queryClient = new QueryClient() - render(() => ) flush() @@ -98,8 +111,6 @@ describe('SolidQueryDevtoolsPanel', () => { it('should forward "theme" to the devtools instance', () => { const setTheme = vi.spyOn(TanstackQueryDevtoolsPanel.prototype, 'setTheme') - const queryClient = new QueryClient() - render(() => ) flush() @@ -108,8 +119,6 @@ describe('SolidQueryDevtoolsPanel', () => { it('should default "theme" to "system" when the prop is omitted', () => { const setTheme = vi.spyOn(TanstackQueryDevtoolsPanel.prototype, 'setTheme') - const queryClient = new QueryClient() - render(() => ) flush() @@ -121,8 +130,6 @@ describe('SolidQueryDevtoolsPanel', () => { TanstackQueryDevtoolsPanel.prototype, 'setClient', ) - const queryClient = new QueryClient() - render(() => ) flush() @@ -130,8 +137,6 @@ describe('SolidQueryDevtoolsPanel', () => { }) it('should preserve the default container height when "style" omits "height"', () => { - const queryClient = new QueryClient() - const { container } = render(() => ( { }) it('should let "style" override the default container height on the rendered element', () => { - const queryClient = new QueryClient() - const { container } = render(() => ( { it('should call "unmount" on the devtools instance when the component unmounts', async () => { const unmount = vi.spyOn(TanstackQueryDevtoolsPanel.prototype, 'unmount') - const queryClient = new QueryClient() - const { unmount: unmountComponent } = render(() => ( )) diff --git a/packages/solid-query-persist-client/CHANGELOG.md b/packages/solid-query-persist-client/CHANGELOG.md index 04ee2113a4c..503d6b04311 100644 --- a/packages/solid-query-persist-client/CHANGELOG.md +++ b/packages/solid-query-persist-client/CHANGELOG.md @@ -94,6 +94,38 @@ - Updated dependencies [[`4a27c6c`](https://github.com/TanStack/query/commit/4a27c6c1810956509e0e38c95f4f3fdc0b9b6f55)]: - @tanstack/solid-query@6.0.0-alpha.0 +## 5.101.4 + +### Patch Changes + +- Updated dependencies []: + - @tanstack/query-persist-client-core@5.101.4 + - @tanstack/solid-query@5.101.4 + +## 5.101.3 + +### Patch Changes + +- Updated dependencies []: + - @tanstack/query-persist-client-core@5.101.3 + - @tanstack/solid-query@5.101.3 + +## 5.101.2 + +### Patch Changes + +- Updated dependencies []: + - @tanstack/query-persist-client-core@5.101.2 + - @tanstack/solid-query@5.101.2 + +## 5.101.1 + +### Patch Changes + +- Updated dependencies []: + - @tanstack/query-persist-client-core@5.101.1 + - @tanstack/solid-query@5.101.1 + ## 5.101.0 ### Patch Changes diff --git a/packages/solid-query-persist-client/src/__tests__/PersistQueryClientProvider.test.tsx b/packages/solid-query-persist-client/src/__tests__/PersistQueryClientProvider.test.tsx index a8d216c54ca..52e5d745cb7 100644 --- a/packages/solid-query-persist-client/src/__tests__/PersistQueryClientProvider.test.tsx +++ b/packages/solid-query-persist-client/src/__tests__/PersistQueryClientProvider.test.tsx @@ -119,19 +119,19 @@ describe('PersistQueryClientProvider', () => { expect(states).toHaveLength(3) - expect(states[0]).toMatchObject({ + expect(states[0]).toStrictEqual({ status: 'pending', fetchStatus: 'idle', data: undefined, }) - expect(states[1]).toMatchObject({ + expect(states[1]).toStrictEqual({ status: 'success', fetchStatus: 'fetching', data: 'hydrated', }) - expect(states[2]).toMatchObject({ + expect(states[2]).toStrictEqual({ status: 'success', fetchStatus: 'idle', data: 'fetched', @@ -208,19 +208,19 @@ describe('PersistQueryClientProvider', () => { expect(states).toHaveLength(3) - expect(states[0]).toMatchObject({ + expect(states[0]).toStrictEqual({ status: 'pending', fetchStatus: 'idle', data: undefined, }) - expect(states[1]).toMatchObject({ + expect(states[1]).toStrictEqual({ status: 'success', fetchStatus: 'fetching', data: 'hydrated', }) - expect(states[2]).toMatchObject({ + expect(states[2]).toStrictEqual({ status: 'success', fetchStatus: 'idle', data: 'fetched', @@ -297,19 +297,19 @@ describe('PersistQueryClientProvider', () => { expect(states).toHaveLength(3) - expect(states[0]).toMatchObject({ + expect(states[0]).toStrictEqual({ status: 'success', fetchStatus: 'idle', data: 'initial', }) - expect(states[1]).toMatchObject({ + expect(states[1]).toStrictEqual({ status: 'success', fetchStatus: 'fetching', data: 'hydrated', }) - expect(states[2]).toMatchObject({ + expect(states[2]).toStrictEqual({ status: 'success', fetchStatus: 'idle', data: 'fetched', @@ -343,11 +343,11 @@ describe('PersistQueryClientProvider', () => { function Page() { const state = useQuery(() => ({ queryKey: key, - queryFn: async () => { - await sleep(10) - fetched = true - return 'fetched' - }, + queryFn: () => + sleep(10).then(() => { + fetched = true + return 'fetched' + }), staleTime: Infinity, })) @@ -391,13 +391,13 @@ describe('PersistQueryClientProvider', () => { expect(states).toHaveLength(2) - expect(states[0]).toMatchObject({ + expect(states[0]).toStrictEqual({ status: 'pending', fetchStatus: 'idle', data: undefined, }) - expect(states[1]).toMatchObject({ + expect(states[1]).toStrictEqual({ status: 'success', fetchStatus: 'idle', data: 'hydrated', @@ -613,19 +613,19 @@ describe('PersistQueryClientProvider', () => { expect(states).toHaveLength(3) - expect(states[0]).toMatchObject({ + expect(states[0]).toStrictEqual({ status: 'pending', fetchStatus: 'idle', data: undefined, }) - expect(states[1]).toMatchObject({ + expect(states[1]).toStrictEqual({ status: 'success', fetchStatus: 'fetching', data: 'hydrated', }) - expect(states[2]).toMatchObject({ + expect(states[2]).toStrictEqual({ status: 'success', fetchStatus: 'idle', data: 'queryFn2', diff --git a/packages/solid-query/CHANGELOG.md b/packages/solid-query/CHANGELOG.md index 07975d2597a..f55f3d8c7d2 100644 --- a/packages/solid-query/CHANGELOG.md +++ b/packages/solid-query/CHANGELOG.md @@ -64,6 +64,34 @@ - Support Solid 2.0 ([#10272](https://github.com/TanStack/query/pull/10272)) +## 5.101.4 + +### Patch Changes + +- Updated dependencies []: + - @tanstack/query-core@5.101.4 + +## 5.101.3 + +### Patch Changes + +- Updated dependencies [[`7e3c822`](https://github.com/TanStack/query/commit/7e3c822a10896f41a8f1031c16b85096277af677)]: + - @tanstack/query-core@5.101.3 + +## 5.101.2 + +### Patch Changes + +- Updated dependencies []: + - @tanstack/query-core@5.101.2 + +## 5.101.1 + +### Patch Changes + +- Updated dependencies [[`9eff92e`](https://github.com/TanStack/query/commit/9eff92ed86e284ec0125b3a3539d028688235bd1)]: + - @tanstack/query-core@5.101.1 + ## 5.101.0 ### Patch Changes diff --git a/packages/solid-query/README.md b/packages/solid-query/README.md index 085229258f2..1f4e5d4f3fe 100644 --- a/packages/solid-query/README.md +++ b/packages/solid-query/README.md @@ -1,6 +1,20 @@ -![TanStack Query Header](https://github.com/TanStack/query/raw/main/media/repo-header.png) + + + + TanStack Solid Query + Hooks for fetching, caching and updating asynchronous data in Solid diff --git a/packages/solid-query/src/__tests__/QueryClientProvider.test.tsx b/packages/solid-query/src/__tests__/QueryClientProvider.test.tsx index e9f06bb31ab..5666d027064 100644 --- a/packages/solid-query/src/__tests__/QueryClientProvider.test.tsx +++ b/packages/solid-query/src/__tests__/QueryClientProvider.test.tsx @@ -44,7 +44,7 @@ describe('QueryClientProvider', () => { await vi.advanceTimersByTimeAsync(10) expect(rendered.getByText('test')).toBeInTheDocument() - expect(queryCache.find({ queryKey: key })).toBeDefined() + expect(queryCache.find({ queryKey: key })?.state.data).toBe('test') }) it('allows multiple caches to be partitioned', async () => { @@ -101,10 +101,10 @@ describe('QueryClientProvider', () => { 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() + expect(queryCache1.find({ queryKey: key1 })?.state.data).toBe('test1') + expect(queryCache1.find({ queryKey: key2 })).toBeUndefined() + expect(queryCache2.find({ queryKey: key1 })).toBeUndefined() + expect(queryCache2.find({ queryKey: key2 })?.state.data).toBe('test2') }) it("uses defaultOptions for queries when they don't provide their own config", async () => { @@ -144,7 +144,6 @@ describe('QueryClientProvider', () => { await vi.advanceTimersByTimeAsync(10) expect(rendered.getByText('test')).toBeInTheDocument() - expect(queryCache.find({ queryKey: key })).toBeDefined() expect(queryCache.find({ queryKey: key })?.options.gcTime).toBe(Infinity) }) diff --git a/packages/solid-query/src/__tests__/mutationOptions.test.tsx b/packages/solid-query/src/__tests__/mutationOptions.test.tsx index 20f11cf5462..ad01361f496 100644 --- a/packages/solid-query/src/__tests__/mutationOptions.test.tsx +++ b/packages/solid-query/src/__tests__/mutationOptions.test.tsx @@ -523,6 +523,5 @@ describe('mutationOptions', () => { const lastSnapshot = mutationStateArray[mutationStateArray.length - 1]! expect(lastSnapshot.length).toEqual(1) expect(lastSnapshot[0]?.data).toEqual('data1') - expect(lastSnapshot[1]).toBeFalsy() }) }) diff --git a/packages/solid-query/src/__tests__/suspense.test.tsx b/packages/solid-query/src/__tests__/suspense.test.tsx index a85fb56fd87..bc30409acf9 100644 --- a/packages/solid-query/src/__tests__/suspense.test.tsx +++ b/packages/solid-query/src/__tests__/suspense.test.tsx @@ -180,7 +180,7 @@ describe("useQuery's in Loading mode", () => { const rendered = renderWithClient(queryClient, () => ) expect(rendered.queryByText('rendered')).not.toBeInTheDocument() - expect(queryCache.find({ queryKey: key })).toBeFalsy() + expect(queryCache.find({ queryKey: key })).toBeUndefined() fireEvent.click(rendered.getByLabelText('toggle')) await vi.advanceTimersByTimeAsync(10) diff --git a/packages/solid-query/src/__tests__/useInfiniteQuery.test.tsx b/packages/solid-query/src/__tests__/useInfiniteQuery.test.tsx index ece0ee516c6..939b30ef8fa 100644 --- a/packages/solid-query/src/__tests__/useInfiniteQuery.test.tsx +++ b/packages/solid-query/src/__tests__/useInfiniteQuery.test.tsx @@ -35,19 +35,6 @@ interface Result { const pageSize = 10 -const fetchItems = ( - page: number, - ts: number, - noNext?: boolean, - noPrev?: boolean, -): Promise => - sleep(10).then(() => ({ - items: [...new Array(10)].fill(null).map((_, d) => page * pageSize + d), - nextId: noNext ? undefined : page + 1, - prevId: noPrev ? undefined : page - 1, - ts, - })) - describe('useInfiniteQuery', () => { let queryCache: QueryCache let queryClient: QueryClient @@ -1931,12 +1918,18 @@ describe('useInfiniteQuery', () => { const state = useInfiniteQuery(() => ({ queryKey: key, - queryFn: ({ pageParam }) => - fetchItems( - pageParam, - fetchCountRef++, - pageParam === MAX || (pageParam === MAX - 1 && isRemovedLastPage()), - ), + queryFn: ({ pageParam }): Promise => { + const noNext = + pageParam === MAX || (pageParam === MAX - 1 && isRemovedLastPage()) + return sleep(10).then(() => ({ + items: [...new Array(10)] + .fill(null) + .map((_, d) => pageParam * pageSize + d), + nextId: noNext ? undefined : pageParam + 1, + prevId: pageParam - 1, + ts: fetchCountRef++, + })) + }, initialPageParam: 0, getNextPageParam: (lastPage) => lastPage.nextId, })) diff --git a/packages/solid-query/src/__tests__/useMutation.test.tsx b/packages/solid-query/src/__tests__/useMutation.test.tsx index 90b580d9a73..7694c844209 100644 --- a/packages/solid-query/src/__tests__/useMutation.test.tsx +++ b/packages/solid-query/src/__tests__/useMutation.test.tsx @@ -477,15 +477,15 @@ describe('useMutation', () => { expect(onSuccessMock).toHaveBeenCalledTimes(3) - expect(onSuccessMock).toHaveBeenCalledWith(1) - expect(onSuccessMock).toHaveBeenCalledWith(2) - expect(onSuccessMock).toHaveBeenCalledWith(3) + expect(onSuccessMock).toHaveBeenNthCalledWith(1, 1) + expect(onSuccessMock).toHaveBeenNthCalledWith(2, 2) + expect(onSuccessMock).toHaveBeenNthCalledWith(3, 3) expect(onSettledMock).toHaveBeenCalledTimes(3) - expect(onSettledMock).toHaveBeenCalledWith(1) - expect(onSettledMock).toHaveBeenCalledWith(2) - expect(onSettledMock).toHaveBeenCalledWith(3) + expect(onSettledMock).toHaveBeenNthCalledWith(1, 1) + expect(onSettledMock).toHaveBeenNthCalledWith(2, 2) + expect(onSettledMock).toHaveBeenNthCalledWith(3, 3) }) it('should set correct values for `failureReason` and `failureCount` on multiple mutate calls', async () => { @@ -597,24 +597,30 @@ describe('useMutation', () => { expect(rendered.getByRole('heading').textContent).toBe('3') expect(onErrorMock).toHaveBeenCalledTimes(3) - expect(onErrorMock).toHaveBeenCalledWith( + expect(onErrorMock).toHaveBeenNthCalledWith( + 1, 'Expected mock error. All is well! 1', ) - expect(onErrorMock).toHaveBeenCalledWith( + expect(onErrorMock).toHaveBeenNthCalledWith( + 2, 'Expected mock error. All is well! 2', ) - expect(onErrorMock).toHaveBeenCalledWith( + expect(onErrorMock).toHaveBeenNthCalledWith( + 3, 'Expected mock error. All is well! 3', ) expect(onSettledMock).toHaveBeenCalledTimes(3) - expect(onSettledMock).toHaveBeenCalledWith( + expect(onSettledMock).toHaveBeenNthCalledWith( + 1, 'Expected mock error. All is well! 1', ) - expect(onSettledMock).toHaveBeenCalledWith( + expect(onSettledMock).toHaveBeenNthCalledWith( + 2, 'Expected mock error. All is well! 2', ) - expect(onSettledMock).toHaveBeenCalledWith( + expect(onSettledMock).toHaveBeenNthCalledWith( + 3, 'Expected mock error. All is well! 3', ) }) @@ -1405,10 +1411,10 @@ describe('useMutation', () => { function Page() { const mutation = useMutation(() => ({ - mutationFn: async (_text: string) => { - await sleep(10) - throw mutateFnError - }, + mutationFn: (_text: string) => + sleep(10).then(() => { + throw mutateFnError + }), onError: () => Promise.reject(error), })) @@ -1451,10 +1457,10 @@ describe('useMutation', () => { function Page() { const mutation = useMutation(() => ({ - mutationFn: async (_text: string) => { - await sleep(10) - throw mutateFnError - }, + mutationFn: (_text: string) => + sleep(10).then(() => { + throw mutateFnError + }), onSettled: () => Promise.reject(error), onError, })) @@ -1492,7 +1498,7 @@ describe('useMutation', () => { function Page() { const mutation = useMutation( () => ({ - mutationFn: async (text: string) => { + mutationFn: (text: string) => { return Promise.resolve(text) }, }), diff --git a/packages/solid-query/src/__tests__/useQueries.test-d.tsx b/packages/solid-query/src/__tests__/useQueries.test-d.tsx index 9526923872e..eaa0a42d45f 100644 --- a/packages/solid-query/src/__tests__/useQueries.test-d.tsx +++ b/packages/solid-query/src/__tests__/useQueries.test-d.tsx @@ -3,8 +3,14 @@ import { skipToken } from '@tanstack/query-core' import { queryKey } from '@tanstack/query-test-utils' import { queryOptions, useQueries } from '..' import { QueryClient } from '../QueryClient' +import type * as QueryCore from '@tanstack/query-core' import type { OmitKeyof } from '@tanstack/query-core' -import type { UseQueryResult } from '..' +import type { + QueryFunction, + QueryFunctionContext, + QueryKey, + UseQueryResult, +} from '..' import type { QueryOptions } from '../types' describe('useQueries', () => { @@ -289,4 +295,600 @@ describe('useQueries', () => { }, })) }) + + describe('type parameters', () => { + it('should handle 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< + UseQueryResult + >() + expectTypeOf(result2[1]).toEqualTypeOf< + UseQueryResult + >() + 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', + }, + ], + })) + } + }) + + it('should handle 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< + UseQueryResult + >() + expectTypeOf(result1[1]).toEqualTypeOf< + UseQueryResult + >() + 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< + UseQueryResult + >() + expectTypeOf(result2[1]).toEqualTypeOf< + UseQueryResult + >() + 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< + UseQueryResult + >() + expectTypeOf(result3[1]).toEqualTypeOf< + UseQueryResult + >() + 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', + }, + ], + })) + } + }) + + it('should handle array literal without type parameter to infer result type', () => { + const key1 = queryKey() + const key2 = queryKey() + const key3 = queryKey() + const key4 = queryKey() + + // @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 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 result3 = useQueries(() => ({ + queries: [ + { + queryKey: key1, + queryFn: () => 1, + }, + { + queryKey: key2, + queryFn: () => 'string', + }, + { + queryKey: key3, + queryFn: () => ['string[]'], + select: () => 123, + }, + ], + })) + expectTypeOf(result3[0]).toEqualTypeOf>() + expectTypeOf(result3[1]).toEqualTypeOf>() + expectTypeOf(result3[2]).toEqualTypeOf>() + expectTypeOf(result3[0].data).toEqualTypeOf() + expectTypeOf(result3[1].data).toEqualTypeOf() + // select takes precedence over queryFn + expectTypeOf(result3[2].data).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 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), + }, + ], + })) + + // 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), + }, + ], + })) + expectTypeOf(result4[0]).toEqualTypeOf>() + expectTypeOf(result4[1]).toEqualTypeOf>() + expectTypeOf(result4[2]).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, + }, + ], + }) as const, + ) + expectTypeOf(result6[0]).toEqualTypeOf>() + expectTypeOf(result6[1]).toEqualTypeOf>() + + // field names should be enforced - array literal + useQueries(() => ({ + queries: [ + { + queryKey: key1, + queryFn: () => 'string', + }, + ], + })) + + // 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', + }, + ], + })) + + // 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('should handle strongly typed queryFn factories and useQueries wrappers', () => { + // QueryKey + queryFn factory + type QueryKeyA = ['queryA'] + const getQueryKeyA = (): QueryKeyA => ['queryA'] + type GetQueryFunctionA = () => QueryFunction + const getQueryFunctionA: GetQueryFunctionA = () => () => { + return 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 '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 QueryCore.skipToken + | QueryCore.QueryFunction + | undefined + >() + return { + queryKey: key, + queryFn: fn + ? (ctx: QueryFunctionContext) => { + // eslint-disable-next-line vitest/valid-expect + expectTypeOf(ctx.queryKey) + return ( + fn as QueryFunction + ).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> + >() + } + }) + }) }) diff --git a/packages/solid-query/src/__tests__/useQueries.test.tsx b/packages/solid-query/src/__tests__/useQueries.test.tsx index 4d96b4fa5d5..47abb6d0b3f 100644 --- a/packages/solid-query/src/__tests__/useQueries.test.tsx +++ b/packages/solid-query/src/__tests__/useQueries.test.tsx @@ -1,12 +1,4 @@ -import { - afterEach, - beforeEach, - describe, - expect, - expectTypeOf, - it, - vi, -} from 'vitest' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { fireEvent, render } from '@solidjs/testing-library' import * as QueryCore from '@tanstack/query-core' import { createSignal, createTrackedEffect, deep } from 'solid-js' @@ -19,13 +11,7 @@ import { useQueries, } from '..' import { renderWithClient } from './utils' -import type { - QueryFunction, - QueryFunctionContext, - QueryKey, - QueryOptions, - UseQueryResult, -} from '..' +import type { UseQueryResult } from '..' describe('useQueries', () => { let queryCache: QueryCache @@ -88,583 +74,6 @@ describe('useQueries', () => { expect(results[2]).toMatchObject([{ data: 1 }, { 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', - }, - ], - })) - } - }) - - 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', - }, - ], - })) - } - }) - - it('handles array literal without type parameter to infer result type', () => { - const key1 = queryKey() - const key2 = queryKey() - const key3 = queryKey() - const key4 = queryKey() - - // @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 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 result3 = useQueries(() => ({ - queries: [ - { - queryKey: key1, - queryFn: () => 1, - }, - { - queryKey: key2, - queryFn: () => 'string', - }, - { - queryKey: key3, - queryFn: () => ['string[]'], - select: () => 123, - }, - ], - })) - expectTypeOf(result3[0]).toEqualTypeOf>() - expectTypeOf(result3[1]).toEqualTypeOf>() - expectTypeOf(result3[2]).toEqualTypeOf>() - expectTypeOf(result3[0].data).toEqualTypeOf() - expectTypeOf(result3[1].data).toEqualTypeOf() - // select takes precedence over queryFn - expectTypeOf(result3[2].data).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 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), - }, - ], - })) - - // 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), - }, - ], - })) - expectTypeOf(result4[0]).toEqualTypeOf>() - expectTypeOf(result4[1]).toEqualTypeOf>() - expectTypeOf(result4[2]).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, - }, - ], - }) as const, - ) - expectTypeOf(result6[0]).toEqualTypeOf>() - expectTypeOf(result6[1]).toEqualTypeOf>() - - // field names should be enforced - array literal - useQueries(() => ({ - queries: [ - { - queryKey: key1, - queryFn: () => 'string', - }, - ], - })) - - // 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', - }, - ], - })) - - // 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 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 '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 QueryCore.skipToken - | QueryCore.QueryFunction - | undefined - >() - return { - queryKey: key, - queryFn: fn - ? (ctx: QueryFunctionContext) => { - // eslint-disable-next-line vitest/valid-expect - expectTypeOf(ctx.queryKey) - return (fn as QueryFunction).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> - >() - } - }) - // eslint-disable-next-line vitest/expect-expect it('should not change state if unmounted', async () => { const key1 = queryKey() diff --git a/packages/solid-query/src/__tests__/useQuery.test-d.tsx b/packages/solid-query/src/__tests__/useQuery.test-d.tsx index 64eaa5eb694..278f4710b87 100644 --- a/packages/solid-query/src/__tests__/useQuery.test-d.tsx +++ b/packages/solid-query/src/__tests__/useQuery.test-d.tsx @@ -1,8 +1,160 @@ import { describe, expectTypeOf, it } from 'vitest' import { queryKey } from '@tanstack/query-test-utils' import { queryOptions, useQuery } from '../index' +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() + + // 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: () => 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 n + 42 + } + + useQuery(() => ({ + queryKey: ['my-data', 100] as const, + queryFn: getMyDataArrayKey, + })) + + const getMyDataStringKey: QueryFunction = (context) => { + expectTypeOf(context.queryKey).toEqualTypeOf<['1']>() + return Number(context.queryKey[0]) + 42 + } + + useQuery(() => ({ + queryKey: ['1'] as ['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', + 'safely' + >, + ) => + useQuery(() => ({ + queryKey: qk, + queryFn: () => fetcher(qk[1], 'token'), + ...options, + })) + const test = useWrappedQuery([''], () => Promise.resolve('1')) + expectTypeOf(test.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', + 'safely' + >, + ) => useQuery(() => ({ queryKey: qk, queryFn: fetcher, ...options })) + const testFuncStyle = useWrappedFuncStyleQuery([''], () => + Promise.resolve(true), + ) + expectTypeOf(testFuncStyle.data).toEqualTypeOf() + describe('initialData', () => { describe('Config object overload', () => { it('TData should always be defined when initialData is provided as an object', () => { @@ -98,4 +250,52 @@ describe('useQuery', () => { }) }) }) + + describe('generic indexed access TData', () => { + // https://github.com/TanStack/query/issues/9937 + it('should be assignable back to its source indexed type when passed to a generic function parameter', () => { + enum DataType { + Account = 'account', + Product = 'product', + } + + interface Account { + name: string + } + interface Product { + code: string + } + + type DataTypeToEntity = { + [DataType.Account]: Account + [DataType.Product]: Product + } + + const getData = ( + _dataType: TDataType, + ): Promise => + Promise.resolve({} as DataTypeToEntity[TDataType]) + + const getLabel = ( + _dataType: TDataType, + _data: DataTypeToEntity[TDataType], + ) => 'test' + + function Test(props: { + dataType: TDataType + }) { + const { data } = useQuery(() => ({ + queryKey: ['test'], + queryFn: () => getData(props.dataType), + })) + + // Regression guard: this call must compile. With the previous + // hand-rolled NoInfer, `data` failed to flow back into the generic + // indexed-access parameter `DataTypeToEntity[TDataType]`. + return data ? getLabel(props.dataType, data) : null + } + + expectTypeOf(Test).toBeFunction() + }) + }) }) diff --git a/packages/solid-query/src/__tests__/useQuery.test.tsx b/packages/solid-query/src/__tests__/useQuery.test.tsx index 88c02e8d619..1b43f61a4a8 100644 --- a/packages/solid-query/src/__tests__/useQuery.test.tsx +++ b/packages/solid-query/src/__tests__/useQuery.test.tsx @@ -35,13 +35,7 @@ import { renderWithClient, setActTimeout, } from './utils' -import type { - DefinedUseQueryResult, - OmitKeyof, - QueryFunction, - UseQueryOptions, - UseQueryResult, -} from '..' +import type { DefinedUseQueryResult, QueryFunction, UseQueryResult } from '..' import type { Mock } from 'vitest' import type { JSX } from '@solidjs/web' @@ -60,164 +54,6 @@ describe('useQuery', () => { vi.useRealTimers() }) - it('should return the correct types', () => { - const key = queryKey() - - // @ts-expect-error - function Page() { - // 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() - - // 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: () => 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 n + 42 - } - - useQuery(() => ({ - queryKey: ['my-data', 100] as const, - queryFn: getMyDataArrayKey, - })) - - const getMyDataStringKey: QueryFunction = (context) => { - expectTypeOf(context.queryKey).toEqualTypeOf<['1']>() - return Number(context.queryKey[0]) + 42 - } - - useQuery(() => ({ - queryKey: ['1'] as ['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', - 'safely' - >, - ) => - useQuery(() => ({ - queryKey: qk, - queryFn: () => fetcher(qk[1], 'token'), - ...options, - })) - const test = useWrappedQuery([''], () => Promise.resolve('1')) - expectTypeOf(test.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', - 'safely' - >, - ) => useQuery(() => ({ queryKey: qk, queryFn: fetcher, ...options })) - const testFuncStyle = useWrappedFuncStyleQuery([''], () => - Promise.resolve(true), - ) - expectTypeOf(testFuncStyle.data).toEqualTypeOf() - } - }) - // See https://github.com/tannerlinsley/react-query/issues/105 it('should allow to set default data value', async () => { const key = queryKey() @@ -543,11 +379,11 @@ describe('useQuery', () => { function Page() { const state = useQuery(() => ({ queryKey: key, - queryFn: async () => { - await sleep(10) - fetchCount++ - return 'data' - }, + queryFn: () => + sleep(10).then(() => { + fetchCount++ + return 'data' + }), enabled: false, initialData: 'initialData', })) @@ -582,11 +418,11 @@ describe('useQuery', () => { function Page() { const state = useQuery(() => ({ queryKey: key, - queryFn: async () => { - await sleep(10) - fetchCount++ - return 'data' - }, + queryFn: () => + sleep(10).then(() => { + fetchCount++ + return 'data' + }), enabled: false, initialData: 'initialData', })) @@ -621,11 +457,11 @@ describe('useQuery', () => { function Page() { const state = useQuery(() => ({ queryKey: key, - queryFn: async () => { - await sleep(10) - fetchCount++ - return 'data' - }, + queryFn: () => + sleep(10).then(() => { + fetchCount++ + return 'data' + }), enabled: false, })) @@ -1103,14 +939,14 @@ describe('useQuery', () => { function Page() { const state = useQuery(() => ({ queryKey: key, - queryFn: async () => { - await sleep(10) - count++ - return [ - { id: '1', done: false }, - { id: '2', done: count > 1 }, - ] - }, + queryFn: () => + sleep(10).then(() => { + count++ + return [ + { id: '1', done: false }, + { id: '2', done: count > 1 }, + ] + }), reconcile: 'id', })) @@ -1179,11 +1015,11 @@ describe('useQuery', () => { function Page() { const state = useQuery(() => ({ queryKey: key, - queryFn: async () => { - await sleep(10) - count++ - return count === 1 ? result1 : result2 - }, + queryFn: () => + sleep(10).then(() => { + count++ + return count === 1 ? result1 : result2 + }), reconcile: (oldData, newData) => { if (oldData === undefined) return newData reconcile(newData, 'id')(oldData) @@ -1303,11 +1139,11 @@ describe('useQuery', () => { function Page() { const state = useQuery(() => ({ queryKey: key, - queryFn: async () => { - await sleep(10) - count++ - return count - }, + queryFn: () => + sleep(10).then(() => { + count++ + return count + }), staleTime: Infinity, })) @@ -1390,11 +1226,11 @@ describe('useQuery', () => { function Page() { const state = useQuery(() => ({ queryKey: key, - queryFn: async () => { - await sleep(10) - count++ - return count - }, + queryFn: () => + sleep(10).then(() => { + count++ + return count + }), enabled: false, })) @@ -1439,11 +1275,11 @@ describe('useQuery', () => { function Page() { const state = useQuery(() => ({ queryKey: key, - queryFn: async () => { - await sleep(10) - count++ - return count - }, + queryFn: () => + sleep(10).then(() => { + count++ + return count + }), enabled: false, })) @@ -2850,11 +2686,11 @@ describe('useQuery', () => { function Page() { const result = useQuery(() => ({ queryKey: key, - queryFn: async () => { - await sleep(10) - count++ - throw new Error('some error') - }, + queryFn: () => + sleep(10).then(() => { + count++ + throw new Error('some error') + }), retry: 2, retryDelay: 100, @@ -2916,11 +2752,11 @@ describe('useQuery', () => { function Page() { const result = useQuery(() => ({ queryKey: key, - queryFn: async () => { - await sleep(10) - count++ - throw new Error('some error') - }, + queryFn: () => + sleep(10).then(() => { + count++ + throw new Error('some error') + }), retry: 2, retryDelay: 100, })) @@ -3418,11 +3254,11 @@ describe('useQuery', () => { function Page() { const query = useQuery(() => ({ queryKey: key, - queryFn: async () => { - await sleep(10) - count++ - throw `fetching error ${count}` - }, + queryFn: () => + sleep(10).then(() => { + count++ + throw `fetching error ${count}` + }), retry: 3, retryDelay: 1, })) @@ -3691,15 +3527,15 @@ describe('useQuery', () => { const query = useQuery(() => ({ queryKey: key, - queryFn: async () => { - await sleep(10) - if (counter < 2) { - counter++ - throw new Error('error') - } else { - return 'data' - } - }, + queryFn: () => + sleep(10).then(() => { + if (counter < 2) { + counter++ + throw new Error('error') + } else { + return 'data' + } + }), retryDelay: 10, })) @@ -3745,11 +3581,11 @@ describe('useQuery', () => { const query = useQuery(() => ({ queryKey: key, - queryFn: async () => { - await sleep(10) - count++ - return count - }, + queryFn: () => + sleep(10).then(() => { + count++ + return count + }), enabled: enabled(), })) @@ -5041,11 +4877,11 @@ describe('useQuery', () => { function Page() { const state = useQuery(() => ({ queryKey: key, - queryFn: async () => { - await sleep(10) - count++ - return count - }, + queryFn: () => + sleep(10).then(() => { + count++ + return count + }), staleTime: Infinity, })) @@ -5119,11 +4955,11 @@ describe('useQuery', () => { function Page() { const state = useQuery(() => ({ queryKey: key, - queryFn: async () => { - await sleep(10) - count++ - return count - }, + queryFn: () => + sleep(10).then(() => { + count++ + return count + }), staleTime: Infinity, enabled: false, notifyOnChangeProps: 'all', @@ -5421,14 +5257,14 @@ describe('useQuery', () => { function Page() { const state = useQuery(() => ({ queryKey: key, - queryFn: async () => { - await sleep(10) - if (count === 0) { - count++ - throw error - } - return 5 - }, + queryFn: () => + sleep(10).then(() => { + if (count === 0) { + count++ + throw error + } + return 5 + }), retry: false, })) @@ -5549,11 +5385,11 @@ describe('useQuery', () => { function Page() { const state = useQuery(() => ({ queryKey: key, - queryFn: async () => { - await sleep(10) - count++ - return 'data' + count - }, + queryFn: () => + sleep(10).then(() => { + count++ + return 'data' + count + }), })) return ( @@ -5621,11 +5457,11 @@ describe('useQuery', () => { function Page() { const state = useQuery(() => ({ queryKey: key, - queryFn: async () => { - await sleep(10) - count++ - return 'data' + count - }, + queryFn: () => + sleep(10).then(() => { + count++ + return 'data' + count + }), })) return ( @@ -5672,11 +5508,11 @@ describe('useQuery', () => { function Page() { const state = useQuery(() => ({ queryKey: key, - queryFn: async () => { - await sleep(10) - count++ - return 'data' + count - }, + queryFn: () => + sleep(10).then(() => { + count++ + return 'data' + count + }), })) return ( @@ -5718,11 +5554,11 @@ describe('useQuery', () => { function Page() { const state = useQuery(() => ({ queryKey: key, - queryFn: async () => { - await sleep(10) - count++ - return 'data' + count - }, + queryFn: () => + sleep(10).then(() => { + count++ + return 'data' + count + }), initialData: 'initial', })) @@ -5767,11 +5603,11 @@ describe('useQuery', () => { function Page() { const state = useQuery(() => ({ queryKey: key, - queryFn: async () => { - await sleep(10) - count++ - return 'data' + count - }, + queryFn: () => + sleep(10).then(() => { + count++ + return 'data' + count + }), initialData: 'initial', })) @@ -5832,11 +5668,11 @@ describe('useQuery', () => { function Page() { const state = useQuery(() => ({ queryKey: key, - queryFn: async () => { - await sleep(10) - count++ - throw new Error('failed' + count) - }, + queryFn: () => + sleep(10).then(() => { + count++ + throw new Error('failed' + count) + }), retry: 2, retryDelay: 10, })) @@ -6020,11 +5856,11 @@ describe('useQuery', () => { function Page() { const state = useQuery(() => ({ queryKey: key, - queryFn: async () => { - await sleep(10) - count++ - return 'data' + count - }, + queryFn: () => + sleep(10).then(() => { + count++ + return 'data' + count + }), refetchOnReconnect: false, })) @@ -6148,11 +5984,11 @@ describe('useQuery', () => { function Page() { const state = useQuery(() => ({ queryKey: key, - queryFn: async () => { - await sleep(10) - count++ - return 'data ' + count - }, + queryFn: () => + sleep(10).then(() => { + count++ + return 'data ' + count + }), networkMode: 'always', })) @@ -6186,11 +6022,11 @@ describe('useQuery', () => { function Page() { const state = useQuery(() => ({ queryKey: key, - queryFn: async () => { - await sleep(10) - count++ - throw new Error('error ' + count) - }, + queryFn: () => + sleep(10).then(() => { + count++ + throw new Error('error ' + count) + }), networkMode: 'always', retry: 1, retryDelay: 5, @@ -6236,11 +6072,11 @@ describe('useQuery', () => { function Page() { const state = useQuery(() => ({ queryKey: key, - queryFn: async () => { - await sleep(10) - count++ - throw new Error('failed' + count) - }, + queryFn: () => + sleep(10).then(() => { + count++ + throw new Error('failed' + count) + }), retry: 2, retryDelay: 1, networkMode: 'offlineFirst', @@ -6509,7 +6345,9 @@ describe('useQuery', () => { await vi.advanceTimersByTimeAsync(10) expect(rendered.getByText('status: success')).toBeInTheDocument() - expect(queryClient1.getQueryCache().find({ queryKey: key })).toBeDefined() + expect( + queryClient1.getQueryCache().find({ queryKey: key })?.state.data, + ).toBe('data') expect(queryFn).toHaveBeenCalledTimes(1) setClient(queryClient2) diff --git a/packages/solid-query/src/useQueries.ts b/packages/solid-query/src/useQueries.ts index eaf02cc2da6..a17455cbf8e 100644 --- a/packages/solid-query/src/useQueries.ts +++ b/packages/solid-query/src/useQueries.ts @@ -140,7 +140,7 @@ type QueriesOptions< > : 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! + : // If T is *some* array but we couldn't assign unknown[] to it, then it must hold some known/homogeneous type! // use this to infer the param types in the case of Array.map() argument T extends Array< UseQueryOptionsForUseQueries< diff --git a/packages/svelte-query-devtools/CHANGELOG.md b/packages/svelte-query-devtools/CHANGELOG.md index 471b72e174e..8c339af2c8c 100644 --- a/packages/svelte-query-devtools/CHANGELOG.md +++ b/packages/svelte-query-devtools/CHANGELOG.md @@ -1,5 +1,37 @@ # @tanstack/svelte-query-devtools +## 6.1.38 + +### Patch Changes + +- Updated dependencies []: + - @tanstack/query-devtools@5.101.4 + - @tanstack/svelte-query@6.1.38 + +## 6.1.37 + +### Patch Changes + +- Updated dependencies []: + - @tanstack/query-devtools@5.101.3 + - @tanstack/svelte-query@6.1.37 + +## 6.1.36 + +### Patch Changes + +- Updated dependencies [[`f5bf180`](https://github.com/TanStack/query/commit/f5bf180d933d8b8d9d9e7b845e55b26a3a413b07), [`25cdd97`](https://github.com/TanStack/query/commit/25cdd975fed4703d2ca5b600ca5ccd2b600b3dd8), [`ecd89c8`](https://github.com/TanStack/query/commit/ecd89c8faf7acc226f00633ea3a761d3ab842c1d), [`01c7634`](https://github.com/TanStack/query/commit/01c763444e3cf3dfa9744f13911aa1533cac3c29), [`49012db`](https://github.com/TanStack/query/commit/49012dbd5192dfe483d3b108b72ffaa7f2849e0f)]: + - @tanstack/query-devtools@5.101.2 + - @tanstack/svelte-query@6.1.36 + +## 6.1.35 + +### Patch Changes + +- Updated dependencies []: + - @tanstack/query-devtools@5.101.1 + - @tanstack/svelte-query@6.1.35 + ## 6.1.34 ### Patch Changes diff --git a/packages/svelte-query-devtools/package.json b/packages/svelte-query-devtools/package.json index 8ce36174d37..9c08e4acb9f 100644 --- a/packages/svelte-query-devtools/package.json +++ b/packages/svelte-query-devtools/package.json @@ -1,6 +1,6 @@ { "name": "@tanstack/svelte-query-devtools", - "version": "6.1.34", + "version": "6.1.38", "description": "Developer tools to interact with and visualize the TanStack/svelte-query cache", "author": "Lachlan Collins", "license": "MIT", diff --git a/packages/svelte-query-persist-client/CHANGELOG.md b/packages/svelte-query-persist-client/CHANGELOG.md index ea17318c566..049ecf6dd4e 100644 --- a/packages/svelte-query-persist-client/CHANGELOG.md +++ b/packages/svelte-query-persist-client/CHANGELOG.md @@ -1,5 +1,37 @@ # @tanstack/svelte-query-persist-client +## 6.1.38 + +### Patch Changes + +- Updated dependencies []: + - @tanstack/query-persist-client-core@5.101.4 + - @tanstack/svelte-query@6.1.38 + +## 6.1.37 + +### Patch Changes + +- Updated dependencies []: + - @tanstack/query-persist-client-core@5.101.3 + - @tanstack/svelte-query@6.1.37 + +## 6.1.36 + +### Patch Changes + +- Updated dependencies []: + - @tanstack/query-persist-client-core@5.101.2 + - @tanstack/svelte-query@6.1.36 + +## 6.1.35 + +### Patch Changes + +- Updated dependencies []: + - @tanstack/query-persist-client-core@5.101.1 + - @tanstack/svelte-query@6.1.35 + ## 6.1.34 ### Patch Changes diff --git a/packages/svelte-query-persist-client/package.json b/packages/svelte-query-persist-client/package.json index d776b95f27d..42bcc352bf3 100644 --- a/packages/svelte-query-persist-client/package.json +++ b/packages/svelte-query-persist-client/package.json @@ -1,6 +1,6 @@ { "name": "@tanstack/svelte-query-persist-client", - "version": "6.1.34", + "version": "6.1.38", "description": "Svelte bindings to work with persisters in TanStack/svelte-query", "author": "Lachlan Collins", "license": "MIT", diff --git a/packages/svelte-query/CHANGELOG.md b/packages/svelte-query/CHANGELOG.md index 33495919b1d..1d97eee2cb7 100644 --- a/packages/svelte-query/CHANGELOG.md +++ b/packages/svelte-query/CHANGELOG.md @@ -1,5 +1,33 @@ # @tanstack/svelte-query +## 6.1.38 + +### Patch Changes + +- Updated dependencies []: + - @tanstack/query-core@5.101.4 + +## 6.1.37 + +### Patch Changes + +- Updated dependencies [[`7e3c822`](https://github.com/TanStack/query/commit/7e3c822a10896f41a8f1031c16b85096277af677)]: + - @tanstack/query-core@5.101.3 + +## 6.1.36 + +### Patch Changes + +- Updated dependencies []: + - @tanstack/query-core@5.101.2 + +## 6.1.35 + +### Patch Changes + +- Updated dependencies [[`9eff92e`](https://github.com/TanStack/query/commit/9eff92ed86e284ec0125b3a3539d028688235bd1)]: + - @tanstack/query-core@5.101.1 + ## 6.1.34 ### Patch Changes diff --git a/packages/svelte-query/package.json b/packages/svelte-query/package.json index faaff83c4a4..9bc1718ef44 100644 --- a/packages/svelte-query/package.json +++ b/packages/svelte-query/package.json @@ -1,6 +1,6 @@ { "name": "@tanstack/svelte-query", - "version": "6.1.34", + "version": "6.1.38", "description": "Primitives for managing, caching and syncing asynchronous and remote data in Svelte", "author": "Lachlan Collins", "license": "MIT", diff --git a/packages/svelte-query/src/containers.svelte.ts b/packages/svelte-query/src/containers.svelte.ts index 60d27c68431..df602bcb289 100644 --- a/packages/svelte-query/src/containers.svelte.ts +++ b/packages/svelte-query/src/containers.svelte.ts @@ -94,7 +94,7 @@ export function createRawRef>( } for (const key of newKeys) { // @ts-expect-error - // This craziness is required because Tanstack Query defines getters for all of the keys on the object. + // This craziness is required because TanStack Query defines getters for all of the keys on the object. // These getters track property access, so if we access all of them here, we'll end up tracking everything. // So we wrap the property access in a special function that we can identify later to lazily access the value. // (See above) diff --git a/packages/svelte-query/src/createQueries.svelte.ts b/packages/svelte-query/src/createQueries.svelte.ts index dec57561299..7f46ddf33ab 100644 --- a/packages/svelte-query/src/createQueries.svelte.ts +++ b/packages/svelte-query/src/createQueries.svelte.ts @@ -144,7 +144,7 @@ export type QueriesOptions< > : 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! + : // If T is *some* array but we couldn't assign unknown[] to it, then it must hold some known/homogeneous type! // use this to infer the param types in the case of Array.map() argument T extends Array< CreateQueryOptionsForCreateQueries< diff --git a/packages/svelte-query/tests/QueryClientProvider/QueryClientProvider.svelte.test.ts b/packages/svelte-query/tests/QueryClientProvider/QueryClientProvider.svelte.test.ts index 8db232d9a3d..a8b338bbfad 100644 --- a/packages/svelte-query/tests/QueryClientProvider/QueryClientProvider.svelte.test.ts +++ b/packages/svelte-query/tests/QueryClientProvider/QueryClientProvider.svelte.test.ts @@ -28,6 +28,6 @@ describe('QueryClientProvider', () => { await vi.advanceTimersByTimeAsync(10) expect(rendered.getByText('Data: test')).toBeInTheDocument() - expect(queryCache.find({ queryKey: ['hello'] })).toBeDefined() + expect(queryCache.find({ queryKey: ['hello'] })?.state.data).toBe('test') }) }) diff --git a/packages/svelte-query/tests/containers.svelte.test.ts b/packages/svelte-query/tests/containers.svelte.test.ts index 3511dbb5b5d..c2ed0e3f2e4 100644 --- a/packages/svelte-query/tests/containers.svelte.test.ts +++ b/packages/svelte-query/tests/containers.svelte.test.ts @@ -198,6 +198,13 @@ describe('createRawRef', () => { expect(ref).toEqual([7, 8, 9]) }) + it('should return `false` when deleting a property that does not exist', () => { + const [ref] = createRawRef>({ a: 1, b: 2 }) + + expect(Reflect.deleteProperty(ref, 'c')).toBe(false) + expect(ref).toEqual({ a: 1, b: 2 }) + }) + it('should behave like a regular object when not using `update`', () => { const [ref] = createRawRef>({ a: 1, b: 2 }) diff --git a/packages/svelte-query/tests/createQuery/createQuery.svelte.test.ts b/packages/svelte-query/tests/createQuery/createQuery.svelte.test.ts index 965eeb37c7c..3168f732962 100644 --- a/packages/svelte-query/tests/createQuery/createQuery.svelte.test.ts +++ b/packages/svelte-query/tests/createQuery/createQuery.svelte.test.ts @@ -1607,12 +1607,16 @@ describe('createQuery', () => { () => currentClient, ) - expect(queryClient1.getQueryCache().find({ queryKey: key })).toBeDefined() + expect( + queryClient1.getQueryCache().find({ queryKey: key })?.queryKey, + ).toEqual(key) currentClient = queryClient2 flushSync() - expect(queryClient2.getQueryCache().find({ queryKey: key })).toBeDefined() + expect( + queryClient2.getQueryCache().find({ queryKey: key })?.queryKey, + ).toEqual(key) }), ) diff --git a/packages/svelte-query/tests/utils.svelte.test.ts b/packages/svelte-query/tests/utils.svelte.test.ts new file mode 100644 index 00000000000..425586cfac4 --- /dev/null +++ b/packages/svelte-query/tests/utils.svelte.test.ts @@ -0,0 +1,79 @@ +import { flushSync } from 'svelte' +import { describe, expect, it, vi } from 'vitest' +import { watchChanges } from '../src/utils.svelte.js' +import { ref, withEffectRoot } from './utils.svelte.js' + +describe('watchChanges', () => { + it( + 'should skip the first run and only call the effect on subsequent changes', + withEffectRoot(() => { + const source = ref(0) + const effect = vi.fn() + + watchChanges(() => source.value, 'pre', effect) + + // first run only records previousValues, effect is not called + flushSync() + expect(effect).not.toHaveBeenCalled() + + source.value = 1 + flushSync() + expect(effect).toHaveBeenCalledExactlyOnceWith(1, 0) + }), + ) + + it( + 'should run with the "post" flush timing', + withEffectRoot(() => { + const source = ref(0) + const effect = vi.fn() + + watchChanges(() => source.value, 'post', effect) + + flushSync() + expect(effect).not.toHaveBeenCalled() + + source.value = 1 + flushSync() + expect(effect).toHaveBeenCalledExactlyOnceWith(1, 0) + }), + ) + + it( + 'should track an array of sources and pass arrays of values', + withEffectRoot(() => { + const a = ref(1) + const b = ref(2) + const effect = vi.fn() + + watchChanges([() => a.value, () => b.value], 'pre', effect) + + flushSync() + expect(effect).not.toHaveBeenCalled() + + a.value = 10 + flushSync() + expect(effect).toHaveBeenCalledExactlyOnceWith([10, 2], [1, 2]) + }), + ) + + it( + 'should run the returned cleanup before the next effect run', + withEffectRoot(() => { + const source = ref(0) + const cleanup = vi.fn() + const effect = vi.fn(() => cleanup) + + watchChanges(() => source.value, 'pre', effect) + + flushSync() + source.value = 1 + flushSync() + expect(cleanup).not.toHaveBeenCalled() + + source.value = 2 + flushSync() + expect(cleanup).toHaveBeenCalledOnce() + }), + ) +}) diff --git a/packages/vue-query-devtools/CHANGELOG.md b/packages/vue-query-devtools/CHANGELOG.md index 30430e3f3cf..fd565c00bda 100644 --- a/packages/vue-query-devtools/CHANGELOG.md +++ b/packages/vue-query-devtools/CHANGELOG.md @@ -1,5 +1,37 @@ # @tanstack/vue-query-devtools +## 6.1.38 + +### Patch Changes + +- Updated dependencies []: + - @tanstack/query-devtools@5.101.4 + - @tanstack/vue-query@5.101.4 + +## 6.1.37 + +### Patch Changes + +- Updated dependencies []: + - @tanstack/query-devtools@5.101.3 + - @tanstack/vue-query@5.101.3 + +## 6.1.36 + +### Patch Changes + +- Updated dependencies [[`f5bf180`](https://github.com/TanStack/query/commit/f5bf180d933d8b8d9d9e7b845e55b26a3a413b07), [`25cdd97`](https://github.com/TanStack/query/commit/25cdd975fed4703d2ca5b600ca5ccd2b600b3dd8), [`ecd89c8`](https://github.com/TanStack/query/commit/ecd89c8faf7acc226f00633ea3a761d3ab842c1d), [`01c7634`](https://github.com/TanStack/query/commit/01c763444e3cf3dfa9744f13911aa1533cac3c29), [`49012db`](https://github.com/TanStack/query/commit/49012dbd5192dfe483d3b108b72ffaa7f2849e0f)]: + - @tanstack/query-devtools@5.101.2 + - @tanstack/vue-query@5.101.2 + +## 6.1.35 + +### Patch Changes + +- Updated dependencies []: + - @tanstack/query-devtools@5.101.1 + - @tanstack/vue-query@5.101.1 + ## 6.1.34 ### Patch Changes diff --git a/packages/vue-query-devtools/package.json b/packages/vue-query-devtools/package.json index b14cb296e38..7058f6bff41 100644 --- a/packages/vue-query-devtools/package.json +++ b/packages/vue-query-devtools/package.json @@ -1,6 +1,6 @@ { "name": "@tanstack/vue-query-devtools", - "version": "6.1.34", + "version": "6.1.38", "description": "Developer tools to interact with and visualize the TanStack/vue-query cache", "author": "tannerlinsley", "license": "MIT", diff --git a/packages/vue-query/CHANGELOG.md b/packages/vue-query/CHANGELOG.md index e991c7b7a72..bd906d9691b 100644 --- a/packages/vue-query/CHANGELOG.md +++ b/packages/vue-query/CHANGELOG.md @@ -1,5 +1,33 @@ # @tanstack/vue-query +## 5.101.4 + +### Patch Changes + +- Updated dependencies []: + - @tanstack/query-core@5.101.4 + +## 5.101.3 + +### Patch Changes + +- Updated dependencies [[`7e3c822`](https://github.com/TanStack/query/commit/7e3c822a10896f41a8f1031c16b85096277af677)]: + - @tanstack/query-core@5.101.3 + +## 5.101.2 + +### Patch Changes + +- Updated dependencies []: + - @tanstack/query-core@5.101.2 + +## 5.101.1 + +### Patch Changes + +- Updated dependencies [[`9eff92e`](https://github.com/TanStack/query/commit/9eff92ed86e284ec0125b3a3539d028688235bd1)]: + - @tanstack/query-core@5.101.1 + ## 5.101.0 ### Patch Changes diff --git a/packages/vue-query/README.md b/packages/vue-query/README.md index e2d06dbdb85..bc30409f820 100644 --- a/packages/vue-query/README.md +++ b/packages/vue-query/README.md @@ -1,3 +1,20 @@ +
+ + + + TanStack Vue Query + +
[![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) diff --git a/packages/vue-query/package.json b/packages/vue-query/package.json index 1b4ed4a2bbe..b29ef7ce4e4 100644 --- a/packages/vue-query/package.json +++ b/packages/vue-query/package.json @@ -1,6 +1,6 @@ { "name": "@tanstack/vue-query", - "version": "5.101.0", + "version": "5.101.4", "description": "Hooks for managing, caching and syncing asynchronous and remote data in Vue", "author": "Damian Osipiuk", "license": "MIT", diff --git a/packages/vue-query/src/__tests__/mutationOptions.test.ts b/packages/vue-query/src/__tests__/mutationOptions.test.ts index c58bc8e715a..8c0d424caff 100644 --- a/packages/vue-query/src/__tests__/mutationOptions.test.ts +++ b/packages/vue-query/src/__tests__/mutationOptions.test.ts @@ -24,7 +24,7 @@ describe('mutationOptions', () => { mutationFn: () => sleep(10).then(() => 5), } as const - expect(mutationOptions(object)).toStrictEqual(object) + expect(mutationOptions(object)).toBe(object) }) it('should return the object received as a parameter without any modification (without mutationKey in mutationOptions)', () => { @@ -32,7 +32,7 @@ describe('mutationOptions', () => { mutationFn: () => sleep(10).then(() => 5), } as const - expect(mutationOptions(object)).toStrictEqual(object) + expect(mutationOptions(object)).toBe(object) }) it('should return the getter received as a parameter without any modification (with mutationKey in mutationOptions)', () => { diff --git a/packages/vue-query/src/__tests__/useMutation.test.ts b/packages/vue-query/src/__tests__/useMutation.test.ts index e8102608f69..7a37a7cff52 100644 --- a/packages/vue-query/src/__tests__/useMutation.test.ts +++ b/packages/vue-query/src/__tests__/useMutation.test.ts @@ -356,9 +356,11 @@ describe('useMutation', () => { mutationFn: (params: string) => sleep(10).then(() => params), }) - await vi.waitFor(() => - expect(mutation.mutateAsync(result)).resolves.toBe(result), - ) + const promise = mutation.mutateAsync(result) + + await vi.advanceTimersByTimeAsync(10) + + await expect(promise).resolves.toBe(result) expect(mutation).toMatchObject({ isIdle: { value: false }, @@ -376,9 +378,10 @@ describe('useMutation', () => { sleep(10).then(() => Promise.reject(new Error('Some error'))), }) - await vi.waitFor(() => + await Promise.all([ expect(mutation.mutateAsync()).rejects.toThrow('Some error'), - ) + vi.advanceTimersByTimeAsync(10), + ]) expect(mutation).toMatchObject({ isIdle: { value: false }, diff --git a/packages/vue-query/src/__tests__/usePrefetchInfiniteQuery.test.ts b/packages/vue-query/src/__tests__/usePrefetchInfiniteQuery.test.ts index 477abc148be..2456fa9e62e 100644 --- a/packages/vue-query/src/__tests__/usePrefetchInfiniteQuery.test.ts +++ b/packages/vue-query/src/__tests__/usePrefetchInfiniteQuery.test.ts @@ -19,9 +19,9 @@ describe('usePrefetchInfiniteQuery', () => { queryClient, 'prefetchInfiniteQuery', ) - const queryFn = vi.fn(() => - Promise.resolve({ data: 'prefetched', currentPage: 1 }), - ) + const queryFn = () => + Promise.resolve({ data: 'prefetched', currentPage: 1 }) + const getNextPageParam = () => undefined const key = queryKey() @@ -30,7 +30,7 @@ describe('usePrefetchInfiniteQuery', () => { queryKey: key, queryFn, initialPageParam: 1, - getNextPageParam: () => undefined, + getNextPageParam, }, queryClient, ) @@ -40,7 +40,7 @@ describe('usePrefetchInfiniteQuery', () => { queryKey: key, queryFn, initialPageParam: 1, - getNextPageParam: expect.any(Function), + getNextPageParam, }) }) @@ -50,9 +50,8 @@ describe('usePrefetchInfiniteQuery', () => { queryClient, 'prefetchInfiniteQuery', ) - const queryFn = vi.fn(() => - Promise.resolve({ data: 'prefetched', currentPage: 1 }), - ) + const queryFn = () => + Promise.resolve({ data: 'prefetched', currentPage: 1 }) const key = queryKey() queryClient.setQueryData(key, { @@ -81,22 +80,26 @@ describe('usePrefetchInfiniteQuery', () => { ) const nestedRef = ref('value') const key = queryKey() + const queryFn = () => + Promise.resolve({ data: 'prefetched', currentPage: 1 }) + const getNextPageParam = () => undefined usePrefetchInfiniteQuery( { queryKey: [...key, nestedRef], - queryFn: () => Promise.resolve({ data: 'prefetched', currentPage: 1 }), + queryFn, initialPageParam: 1, - getNextPageParam: () => undefined, + getNextPageParam, }, queryClient, ) - expect(prefetchInfiniteQuerySpy).toHaveBeenCalledWith( - expect.objectContaining({ - queryKey: [...key, 'value'], - }), - ) + expect(prefetchInfiniteQuerySpy).toHaveBeenCalledWith({ + queryKey: [...key, 'value'], + queryFn, + initialPageParam: 1, + getNextPageParam, + }) }) it('should prefetch infinite query again when query key changes reactively', async () => { @@ -107,33 +110,37 @@ describe('usePrefetchInfiniteQuery', () => { ) const keyRef = ref('first') const key = queryKey() + const queryFn = () => + Promise.resolve({ data: keyRef.value, currentPage: 1 }) + const getNextPageParam = () => undefined usePrefetchInfiniteQuery( () => ({ queryKey: [...key, keyRef.value], - queryFn: () => Promise.resolve({ data: keyRef.value, currentPage: 1 }), + queryFn, initialPageParam: 1, - getNextPageParam: () => undefined, + getNextPageParam, }), queryClient, ) - expect(prefetchInfiniteQuerySpy).toHaveBeenCalledTimes(1) - expect(prefetchInfiniteQuerySpy).toHaveBeenLastCalledWith( - expect.objectContaining({ - queryKey: [...key, 'first'], - }), - ) + expect(prefetchInfiniteQuerySpy).toHaveBeenNthCalledWith(1, { + queryKey: [...key, 'first'], + queryFn, + initialPageParam: 1, + getNextPageParam, + }) keyRef.value = 'second' await nextTick() expect(prefetchInfiniteQuerySpy).toHaveBeenCalledTimes(2) - expect(prefetchInfiniteQuerySpy).toHaveBeenLastCalledWith( - expect.objectContaining({ - queryKey: [...key, 'second'], - }), - ) + expect(prefetchInfiniteQuerySpy).toHaveBeenNthCalledWith(2, { + queryKey: [...key, 'second'], + queryFn, + initialPageParam: 1, + getNextPageParam, + }) }) it('should warn when used outside of setup function in development mode', () => { diff --git a/packages/vue-query/src/__tests__/usePrefetchQuery.test.ts b/packages/vue-query/src/__tests__/usePrefetchQuery.test.ts index 88b31026289..4cfcf832c4f 100644 --- a/packages/vue-query/src/__tests__/usePrefetchQuery.test.ts +++ b/packages/vue-query/src/__tests__/usePrefetchQuery.test.ts @@ -16,7 +16,7 @@ describe('usePrefetchQuery', () => { it('should prefetch query if query state does not exist', () => { const queryClient = new QueryClient() const prefetchQuerySpy = vi.spyOn(queryClient, 'prefetchQuery') - const queryFn = vi.fn(() => Promise.resolve('prefetched')) + const queryFn = () => Promise.resolve('prefetched') const key = queryKey() usePrefetchQuery( @@ -37,7 +37,7 @@ describe('usePrefetchQuery', () => { it('should not prefetch query if query state exists', () => { const queryClient = new QueryClient() const prefetchQuerySpy = vi.spyOn(queryClient, 'prefetchQuery') - const queryFn = vi.fn(() => Promise.resolve('prefetched')) + const queryFn = () => Promise.resolve('prefetched') const key = queryKey() queryClient.setQueryData(key, 'existing') @@ -57,20 +57,20 @@ describe('usePrefetchQuery', () => { const prefetchQuerySpy = vi.spyOn(queryClient, 'prefetchQuery') const nestedRef = ref('value') const key = queryKey() + const queryFn = () => Promise.resolve('prefetched') usePrefetchQuery( { queryKey: [...key, nestedRef], - queryFn: () => Promise.resolve('prefetched'), + queryFn, }, queryClient, ) - expect(prefetchQuerySpy).toHaveBeenCalledWith( - expect.objectContaining({ - queryKey: [...key, 'value'], - }), - ) + expect(prefetchQuerySpy).toHaveBeenCalledWith({ + queryKey: [...key, 'value'], + queryFn, + }) }) it('should prefetch again when query key changes reactively', async () => { @@ -78,28 +78,28 @@ describe('usePrefetchQuery', () => { const prefetchQuerySpy = vi.spyOn(queryClient, 'prefetchQuery') const keyRef = ref('first') const key = queryKey() + const queryFn = () => Promise.resolve(keyRef.value) usePrefetchQuery( () => ({ queryKey: [...key, keyRef.value], - queryFn: () => Promise.resolve(keyRef.value), + queryFn, }), queryClient, ) - expect(prefetchQuerySpy).toHaveBeenCalledTimes(1) - expect(prefetchQuerySpy).toHaveBeenLastCalledWith({ + expect(prefetchQuerySpy).toHaveBeenNthCalledWith(1, { queryKey: [...key, 'first'], - queryFn: expect.any(Function), + queryFn, }) keyRef.value = 'second' await nextTick() expect(prefetchQuerySpy).toHaveBeenCalledTimes(2) - expect(prefetchQuerySpy).toHaveBeenLastCalledWith({ + expect(prefetchQuerySpy).toHaveBeenNthCalledWith(2, { queryKey: [...key, 'second'], - queryFn: expect.any(Function), + queryFn, }) }) diff --git a/packages/vue-query/src/__tests__/useQueries.test.ts b/packages/vue-query/src/__tests__/useQueries.test.ts index d9dd4b37e37..e0ba22953f1 100644 --- a/packages/vue-query/src/__tests__/useQueries.test.ts +++ b/packages/vue-query/src/__tests__/useQueries.test.ts @@ -264,7 +264,7 @@ describe('useQueries', () => { ) await vi.advanceTimersByTimeAsync(0) - expect(queriesResult.value).toMatchObject({ + expect(queriesResult.value).toEqual({ combined: true, res: [firstResult, secondResult], }) diff --git a/packages/vue-query/src/__tests__/useQuery.test.ts b/packages/vue-query/src/__tests__/useQuery.test.ts index 2f05080f917..698605e872d 100644 --- a/packages/vue-query/src/__tests__/useQuery.test.ts +++ b/packages/vue-query/src/__tests__/useQuery.test.ts @@ -34,7 +34,7 @@ describe('useQuery', () => { staleTime: 1000, }) - expect(useBaseQuery).toBeCalledWith( + expect(useBaseQuery).toHaveBeenCalledWith( QueryObserver, { queryKey: key, @@ -332,7 +332,8 @@ describe('useQuery', () => { expect(fetchFn).not.toHaveBeenCalled() await query.refetch() expect(fetchFn).toHaveBeenCalledTimes(1) - expect(fetchFn).toHaveBeenCalledWith( + expect(fetchFn).toHaveBeenNthCalledWith( + 1, expect.objectContaining({ queryKey: [...key, 'key11'], }), @@ -341,7 +342,8 @@ describe('useQuery', () => { keyRef.value = 'key12' await query.refetch() expect(fetchFn).toHaveBeenCalledTimes(2) - expect(fetchFn).toHaveBeenCalledWith( + expect(fetchFn).toHaveBeenNthCalledWith( + 2, expect.objectContaining({ queryKey: [...key, 'key12'], }), diff --git a/packages/vue-query/src/__tests__/useQueryClient.test.ts b/packages/vue-query/src/__tests__/useQueryClient.test.ts index 8811089184d..a3d2c1a9d23 100644 --- a/packages/vue-query/src/__tests__/useQueryClient.test.ts +++ b/packages/vue-query/src/__tests__/useQueryClient.test.ts @@ -34,7 +34,9 @@ describe('useQueryClient', () => { it('should throw an error when queryClient does not exist in the context', () => { injectSpy.mockReturnValueOnce(undefined) - expect(useQueryClient).toThrow() + expect(useQueryClient).toThrow( + "No 'queryClient' found in Vue context, use 'VueQueryPlugin' to properly initialize the library.", + ) expect(injectSpy).toHaveBeenCalledTimes(1) expect(injectSpy).toHaveBeenCalledWith(VUE_QUERY_CLIENT) }) @@ -42,7 +44,9 @@ describe('useQueryClient', () => { it('should throw an error when used outside of setup function', () => { hasInjectionContextSpy.mockReturnValueOnce(false) - expect(useQueryClient).toThrow() + expect(useQueryClient).toThrow( + 'vue-query hooks can only be used inside setup() function or functions that support injection context.', + ) expect(hasInjectionContextSpy).toHaveBeenCalledTimes(1) }) diff --git a/packages/vue-query/src/__tests__/vueQueryPlugin.test.ts b/packages/vue-query/src/__tests__/vueQueryPlugin.test.ts index cfcb18ce956..11849efedb9 100644 --- a/packages/vue-query/src/__tests__/vueQueryPlugin.test.ts +++ b/packages/vue-query/src/__tests__/vueQueryPlugin.test.ts @@ -271,11 +271,11 @@ describe('VueQueryPlugin', () => { ], }) - expect(customClient.isRestoring?.value).toBeTruthy() + expect(customClient.isRestoring?.value).toBe(true) await vi.advanceTimersByTimeAsync(0) - expect(customClient.isRestoring?.value).toBeFalsy() + expect(customClient.isRestoring?.value).toBe(false) }) it('should delay useQuery subscription and not call fetcher if data is not stale', async () => { @@ -314,14 +314,14 @@ describe('VueQueryPlugin', () => { customClient, ) - expect(customClient.isRestoring?.value).toBeTruthy() - expect(query.isFetching.value).toBeFalsy() + expect(customClient.isRestoring?.value).toBe(true) + expect(query.isFetching.value).toBe(false) expect(query.data.value).toStrictEqual(undefined) expect(fnSpy).toHaveBeenCalledTimes(0) await vi.advanceTimersByTimeAsync(0) - expect(customClient.isRestoring?.value).toBeFalsy() + expect(customClient.isRestoring?.value).toBe(false) expect(query.data.value).toStrictEqual({ foo: 'bar' }) expect(fnSpy).toHaveBeenCalledTimes(0) }) @@ -378,18 +378,18 @@ describe('VueQueryPlugin', () => { customClient, ) - expect(customClient.isRestoring?.value).toBeTruthy() + expect(customClient.isRestoring?.value).toBe(true) - expect(query.isFetching.value).toBeFalsy() + expect(query.isFetching.value).toBe(false) expect(query.data.value).toStrictEqual(undefined) - expect(queries.value[0].isFetching).toBeFalsy() + expect(queries.value[0].isFetching).toBe(false) expect(queries.value[0].data).toStrictEqual(undefined) expect(fnSpy).toHaveBeenCalledTimes(0) await vi.advanceTimersByTimeAsync(0) - expect(customClient.isRestoring?.value).toBeFalsy() + expect(customClient.isRestoring?.value).toBe(false) expect(query.data.value).toStrictEqual({ foo1: 'bar1' }) expect(queries.value[0].data).toStrictEqual({ foo2: 'bar2' }) expect(fnSpy).toHaveBeenCalledTimes(0) diff --git a/packages/vue-query/src/useQueries.ts b/packages/vue-query/src/useQueries.ts index f290976a145..43066a54846 100644 --- a/packages/vue-query/src/useQueries.ts +++ b/packages/vue-query/src/useQueries.ts @@ -185,7 +185,7 @@ export type UseQueriesOptions< > : 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! + : // If T is *some* array but we couldn't assign unknown[] to it, then it must hold some known/homogeneous type! // use this to infer the param types in the case of Array.map() argument T extends Array< UseQueryOptionsForUseQueries< diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 19fc86ad394..14e68967d85 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -170,7 +170,7 @@ importers: specifier: ^20.0.0 version: 20.3.18(@angular/animations@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)) '@tanstack/angular-query-experimental': - specifier: ^5.101.0 + specifier: ^5.101.4 version: link:../../../packages/angular-query-experimental rxjs: specifier: ^7.8.2 @@ -210,7 +210,7 @@ importers: specifier: ^20.0.0 version: 20.3.18(@angular/animations@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)) '@tanstack/angular-query-experimental': - specifier: ^5.101.0 + specifier: ^5.101.4 version: link:../../../packages/angular-query-experimental rxjs: specifier: ^7.8.2 @@ -250,13 +250,13 @@ importers: specifier: ^20.0.0 version: 20.3.18(@angular/animations@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)) '@tanstack/angular-query-experimental': - specifier: ^5.101.0 + specifier: ^5.101.4 version: link:../../../packages/angular-query-experimental '@tanstack/angular-query-persist-client': - specifier: ^5.101.0 + specifier: ^5.101.4 version: link:../../../packages/angular-query-persist-client '@tanstack/query-async-storage-persister': - specifier: ^5.101.0 + specifier: ^5.101.4 version: link:../../../packages/query-async-storage-persister rxjs: specifier: ^7.8.2 @@ -299,7 +299,7 @@ importers: specifier: ^20.0.0 version: 20.3.18(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@20.3.18(@angular/animations@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(rxjs@7.8.2) '@tanstack/angular-query-experimental': - specifier: ^5.101.0 + specifier: ^5.101.4 version: link:../../../packages/angular-query-experimental rxjs: specifier: ^7.8.2 @@ -339,7 +339,7 @@ importers: specifier: ^20.0.0 version: 20.3.18(@angular/animations@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)) '@tanstack/angular-query-experimental': - specifier: ^5.101.0 + specifier: ^5.101.4 version: link:../../../packages/angular-query-experimental rxjs: specifier: ^7.8.2 @@ -382,7 +382,7 @@ importers: specifier: ^20.0.0 version: 20.3.18(@angular/animations@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)) '@tanstack/angular-query-experimental': - specifier: ^5.101.0 + specifier: ^5.101.4 version: link:../../../packages/angular-query-experimental rxjs: specifier: ^7.8.2 @@ -422,7 +422,7 @@ importers: specifier: ^20.0.0 version: 20.3.18(@angular/animations@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)) '@tanstack/angular-query-experimental': - specifier: ^5.101.0 + specifier: ^5.101.4 version: link:../../../packages/angular-query-experimental rxjs: specifier: ^7.8.2 @@ -465,7 +465,7 @@ importers: specifier: ^20.0.0 version: 20.3.18(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@20.3.18(@angular/animations@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(rxjs@7.8.2) '@tanstack/angular-query-experimental': - specifier: ^5.101.0 + specifier: ^5.101.4 version: link:../../../packages/angular-query-experimental rxjs: specifier: ^7.8.2 @@ -508,7 +508,7 @@ importers: specifier: ^20.0.0 version: 20.3.18(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@20.3.18(@angular/animations@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(rxjs@7.8.2) '@tanstack/angular-query-experimental': - specifier: ^5.101.0 + specifier: ^5.101.4 version: link:../../../packages/angular-query-experimental rxjs: specifier: ^7.8.2 @@ -551,7 +551,7 @@ importers: specifier: ^20.0.0 version: 20.3.18(@angular/animations@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)) '@tanstack/angular-query-experimental': - specifier: ^5.101.0 + specifier: ^5.101.4 version: link:../../../packages/angular-query-experimental rxjs: specifier: ^7.8.2 @@ -591,7 +591,7 @@ importers: specifier: ^20.0.0 version: 20.3.18(@angular/animations@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)) '@tanstack/angular-query-experimental': - specifier: ^5.101.0 + specifier: ^5.101.4 version: link:../../../packages/angular-query-experimental rxjs: specifier: ^7.8.2 @@ -619,10 +619,10 @@ importers: examples/lit/basic: dependencies: '@tanstack/lit-query': - specifier: ^0.2.7 + specifier: ^0.2.11 version: link:../../../packages/lit-query '@tanstack/query-core': - specifier: ^5.101.0 + specifier: ^5.101.4 version: link:../../../packages/query-core lit: specifier: ^3.3.1 @@ -638,10 +638,10 @@ importers: examples/lit/pagination: dependencies: '@tanstack/lit-query': - specifier: ^0.2.7 + specifier: ^0.2.11 version: link:../../../packages/lit-query '@tanstack/query-core': - specifier: ^5.101.0 + specifier: ^5.101.4 version: link:../../../packages/query-core lit: specifier: ^3.3.1 @@ -660,10 +660,10 @@ importers: specifier: ^3.3.0 version: 3.3.1 '@tanstack/lit-query': - specifier: ^0.2.7 + specifier: ^0.2.11 version: link:../../../packages/lit-query '@tanstack/query-core': - specifier: ^5.101.0 + specifier: ^5.101.4 version: link:../../../packages/query-core lit: specifier: ^3.3.1 @@ -685,7 +685,7 @@ importers: examples/preact/simple: dependencies: '@tanstack/preact-query': - specifier: ^5.101.0 + specifier: ^5.101.4 version: link:../../../packages/preact-query preact: specifier: ^10.28.0 @@ -713,10 +713,10 @@ importers: specifier: 5.2.1 version: 5.2.1 '@tanstack/react-query': - specifier: ^5.101.0 + specifier: ^5.101.4 version: link:../../../packages/react-query '@tanstack/react-query-devtools': - specifier: ^5.101.0 + specifier: ^5.101.4 version: link:../../../packages/react-query-devtools react: specifier: ^19.0.0 @@ -726,7 +726,7 @@ importers: version: 19.2.4(react@19.2.4) devDependencies: '@tanstack/eslint-plugin-query': - specifier: ^5.101.0 + specifier: ^5.101.4 version: link:../../../packages/eslint-plugin-query '@types/react': specifier: ^19.2.7 @@ -747,10 +747,10 @@ importers: examples/react/auto-refetching: dependencies: '@tanstack/react-query': - specifier: ^5.101.0 + specifier: ^5.101.4 version: link:../../../packages/react-query '@tanstack/react-query-devtools': - specifier: ^5.101.0 + specifier: ^5.101.4 version: link:../../../packages/react-query-devtools next: specifier: ^16.0.7 @@ -775,16 +775,16 @@ importers: examples/react/basic: dependencies: '@tanstack/query-async-storage-persister': - specifier: ^5.101.0 + specifier: ^5.101.4 version: link:../../../packages/query-async-storage-persister '@tanstack/react-query': - specifier: ^5.101.0 + specifier: ^5.101.4 version: link:../../../packages/react-query '@tanstack/react-query-devtools': - specifier: ^5.101.0 + specifier: ^5.101.4 version: link:../../../packages/react-query-devtools '@tanstack/react-query-persist-client': - specifier: ^5.101.0 + specifier: ^5.101.4 version: link:../../../packages/react-query-persist-client react: specifier: ^19.0.0 @@ -794,7 +794,7 @@ importers: version: 19.2.4(react@19.2.4) devDependencies: '@tanstack/eslint-plugin-query': - specifier: ^5.101.0 + specifier: ^5.101.4 version: link:../../../packages/eslint-plugin-query '@types/react': specifier: ^19.2.7 @@ -815,10 +815,10 @@ importers: examples/react/basic-graphql-request: dependencies: '@tanstack/react-query': - specifier: ^5.101.0 + specifier: ^5.101.4 version: link:../../../packages/react-query '@tanstack/react-query-devtools': - specifier: ^5.101.0 + specifier: ^5.101.4 version: link:../../../packages/react-query-devtools graphql: specifier: ^16.9.0 @@ -843,10 +843,10 @@ importers: examples/react/chat: dependencies: '@tanstack/react-query': - specifier: ^5.101.0 + specifier: ^5.101.4 version: link:../../../packages/react-query '@tanstack/react-query-devtools': - specifier: ^5.101.0 + specifier: ^5.101.4 version: link:../../../packages/react-query-devtools react: specifier: ^19.0.0 @@ -874,10 +874,10 @@ importers: examples/react/default-query-function: dependencies: '@tanstack/react-query': - specifier: ^5.101.0 + specifier: ^5.101.4 version: link:../../../packages/react-query '@tanstack/react-query-devtools': - specifier: ^5.101.0 + specifier: ^5.101.4 version: link:../../../packages/react-query-devtools react: specifier: ^19.0.0 @@ -899,10 +899,10 @@ importers: examples/react/devtools-panel: dependencies: '@tanstack/react-query': - specifier: ^5.101.0 + specifier: ^5.101.4 version: link:../../../packages/react-query '@tanstack/react-query-devtools': - specifier: ^5.101.0 + specifier: ^5.101.4 version: link:../../../packages/react-query-devtools react: specifier: ^19.0.0 @@ -924,16 +924,16 @@ importers: examples/react/eslint-legacy: dependencies: '@tanstack/query-async-storage-persister': - specifier: ^5.101.0 + specifier: ^5.101.4 version: link:../../../packages/query-async-storage-persister '@tanstack/react-query': - specifier: ^5.101.0 + specifier: ^5.101.4 version: link:../../../packages/react-query '@tanstack/react-query-devtools': - specifier: ^5.101.0 + specifier: ^5.101.4 version: link:../../../packages/react-query-devtools '@tanstack/react-query-persist-client': - specifier: ^5.101.0 + specifier: ^5.101.4 version: link:../../../packages/react-query-persist-client react: specifier: ^19.0.0 @@ -943,7 +943,7 @@ importers: version: 19.2.4(react@19.2.4) devDependencies: '@tanstack/eslint-plugin-query': - specifier: ^5.101.0 + specifier: ^5.101.4 version: link:../../../packages/eslint-plugin-query '@types/react': specifier: ^19.2.7 @@ -964,14 +964,14 @@ importers: examples/react/eslint-plugin-demo: dependencies: '@tanstack/react-query': - specifier: ^5.101.0 + specifier: ^5.101.4 version: link:../../../packages/react-query react: specifier: ^19.0.0 version: 19.2.4 devDependencies: '@tanstack/eslint-plugin-query': - specifier: ^5.101.0 + specifier: ^5.101.4 version: link:../../../packages/eslint-plugin-query eslint: specifier: ^9.39.0 @@ -986,10 +986,10 @@ importers: examples/react/infinite-query-with-max-pages: dependencies: '@tanstack/react-query': - specifier: ^5.101.0 + specifier: ^5.101.4 version: link:../../../packages/react-query '@tanstack/react-query-devtools': - specifier: ^5.101.0 + specifier: ^5.101.4 version: link:../../../packages/react-query-devtools next: specifier: ^16.0.7 @@ -1014,10 +1014,10 @@ importers: examples/react/load-more-infinite-scroll: dependencies: '@tanstack/react-query': - specifier: ^5.101.0 + specifier: ^5.101.4 version: link:../../../packages/react-query '@tanstack/react-query-devtools': - specifier: ^5.101.0 + specifier: ^5.101.4 version: link:../../../packages/react-query-devtools next: specifier: ^16.0.7 @@ -1045,10 +1045,10 @@ importers: examples/react/nextjs: dependencies: '@tanstack/react-query': - specifier: ^5.101.0 + specifier: ^5.101.4 version: link:../../../packages/react-query '@tanstack/react-query-devtools': - specifier: ^5.101.0 + specifier: ^5.101.4 version: link:../../../packages/react-query-devtools next: specifier: ^16.0.7 @@ -1073,10 +1073,10 @@ importers: examples/react/nextjs-app-prefetching: dependencies: '@tanstack/react-query': - specifier: ^5.101.0 + specifier: ^5.101.4 version: link:../../../packages/react-query '@tanstack/react-query-devtools': - specifier: ^5.101.0 + specifier: ^5.101.4 version: link:../../../packages/react-query-devtools next: specifier: ^16.0.7 @@ -1101,13 +1101,13 @@ importers: examples/react/nextjs-suspense-streaming: dependencies: '@tanstack/react-query': - specifier: ^5.101.0 + specifier: ^5.101.4 version: link:../../../packages/react-query '@tanstack/react-query-devtools': - specifier: ^5.101.0 + specifier: ^5.101.4 version: link:../../../packages/react-query-devtools '@tanstack/react-query-next-experimental': - specifier: ^5.101.0 + specifier: ^5.101.4 version: link:../../../packages/react-query-next-experimental next: specifier: ^16.0.7 @@ -1132,19 +1132,19 @@ importers: examples/react/offline: dependencies: '@tanstack/query-async-storage-persister': - specifier: ^5.101.0 + specifier: ^5.101.4 version: link:../../../packages/query-async-storage-persister '@tanstack/react-location': specifier: ^3.7.4 version: 3.7.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@tanstack/react-query': - specifier: ^5.101.0 + specifier: ^5.101.4 version: link:../../../packages/react-query '@tanstack/react-query-devtools': - specifier: ^5.101.0 + specifier: ^5.101.4 version: link:../../../packages/react-query-devtools '@tanstack/react-query-persist-client': - specifier: ^5.101.0 + specifier: ^5.101.4 version: link:../../../packages/react-query-persist-client msw: specifier: ^2.6.6 @@ -1172,10 +1172,10 @@ importers: examples/react/optimistic-updates-cache: dependencies: '@tanstack/react-query': - specifier: ^5.101.0 + specifier: ^5.101.4 version: link:../../../packages/react-query '@tanstack/react-query-devtools': - specifier: ^5.101.0 + specifier: ^5.101.4 version: link:../../../packages/react-query-devtools next: specifier: ^16.0.7 @@ -1200,10 +1200,10 @@ importers: examples/react/optimistic-updates-ui: dependencies: '@tanstack/react-query': - specifier: ^5.101.0 + specifier: ^5.101.4 version: link:../../../packages/react-query '@tanstack/react-query-devtools': - specifier: ^5.101.0 + specifier: ^5.101.4 version: link:../../../packages/react-query-devtools next: specifier: ^16.0.7 @@ -1228,10 +1228,10 @@ importers: examples/react/pagination: dependencies: '@tanstack/react-query': - specifier: ^5.101.0 + specifier: ^5.101.4 version: link:../../../packages/react-query '@tanstack/react-query-devtools': - specifier: ^5.101.0 + specifier: ^5.101.4 version: link:../../../packages/react-query-devtools next: specifier: ^16.0.7 @@ -1256,10 +1256,10 @@ importers: examples/react/playground: dependencies: '@tanstack/react-query': - specifier: ^5.101.0 + specifier: ^5.101.4 version: link:../../../packages/react-query '@tanstack/react-query-devtools': - specifier: ^5.101.0 + specifier: ^5.101.4 version: link:../../../packages/react-query-devtools react: specifier: ^19.0.0 @@ -1281,10 +1281,10 @@ importers: examples/react/prefetching: dependencies: '@tanstack/react-query': - specifier: ^5.101.0 + specifier: ^5.101.4 version: link:../../../packages/react-query '@tanstack/react-query-devtools': - specifier: ^5.101.0 + specifier: ^5.101.4 version: link:../../../packages/react-query-devtools next: specifier: ^16.0.7 @@ -1318,10 +1318,10 @@ importers: specifier: ^6.4.1 version: 6.4.1(@react-navigation/native@6.1.18(react-native@0.76.9(@babel/core@7.29.0)(@babel/preset-env@7.29.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.4))(react@19.2.4))(react-native-gesture-handler@2.30.1(react-native@0.76.9(@babel/core@7.29.0)(@babel/preset-env@7.29.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.4))(react@19.2.4))(react-native-safe-area-context@4.14.1(react-native@0.76.9(@babel/core@7.29.0)(@babel/preset-env@7.29.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.4))(react@19.2.4))(react-native-screens@4.24.0(react-native@0.76.9(@babel/core@7.29.0)(@babel/preset-env@7.29.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.4))(react@19.2.4))(react-native@0.76.9(@babel/core@7.29.0)(@babel/preset-env@7.29.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.4))(react@19.2.4) '@tanstack/react-query': - specifier: ^5.101.0 + specifier: ^5.101.4 version: link:../../../packages/react-query '@tanstack/react-query-devtools': - specifier: ^5.101.0 + specifier: ^5.101.4 version: link:../../../packages/react-query-devtools expo: specifier: ^52.0.11 @@ -1370,10 +1370,10 @@ importers: examples/react/react-router: dependencies: '@tanstack/react-query': - specifier: ^5.101.0 + specifier: ^5.101.4 version: link:../../../packages/react-query '@tanstack/react-query-devtools': - specifier: ^5.101.0 + specifier: ^5.101.4 version: link:../../../packages/react-query-devtools localforage: specifier: ^1.10.0 @@ -1422,10 +1422,10 @@ importers: examples/react/rick-morty: dependencies: '@tanstack/react-query': - specifier: ^5.101.0 + specifier: ^5.101.4 version: link:../../../packages/react-query '@tanstack/react-query-devtools': - specifier: ^5.101.0 + specifier: ^5.101.4 version: link:../../../packages/react-query-devtools react: specifier: ^19.0.0 @@ -1459,10 +1459,10 @@ importers: examples/react/shadow-dom: dependencies: '@tanstack/react-query': - specifier: ^5.101.0 + specifier: ^5.101.4 version: link:../../../packages/react-query '@tanstack/react-query-devtools': - specifier: ^5.101.0 + specifier: ^5.101.4 version: link:../../../packages/react-query-devtools react: specifier: ^19.0.0 @@ -1490,10 +1490,10 @@ importers: examples/react/simple: dependencies: '@tanstack/react-query': - specifier: ^5.101.0 + specifier: ^5.101.4 version: link:../../../packages/react-query '@tanstack/react-query-devtools': - specifier: ^5.101.0 + specifier: ^5.101.4 version: link:../../../packages/react-query-devtools react: specifier: ^19.0.0 @@ -1515,10 +1515,10 @@ importers: examples/react/star-wars: dependencies: '@tanstack/react-query': - specifier: ^5.101.0 + specifier: ^5.101.4 version: link:../../../packages/react-query '@tanstack/react-query-devtools': - specifier: ^5.101.0 + specifier: ^5.101.4 version: link:../../../packages/react-query-devtools react: specifier: ^19.0.0 @@ -1552,10 +1552,10 @@ importers: examples/react/suspense: dependencies: '@tanstack/react-query': - specifier: ^5.101.0 + specifier: ^5.101.4 version: link:../../../packages/react-query '@tanstack/react-query-devtools': - specifier: ^5.101.0 + specifier: ^5.101.4 version: link:../../../packages/react-query-devtools font-awesome: specifier: ^4.7.0 @@ -1706,7 +1706,7 @@ importers: specifier: ^2.0.0-rc.0 version: 2.0.0-rc.0(solid-js@2.0.0-rc.0) '@tanstack/query-async-storage-persister': - specifier: ^5.101.0 + specifier: ^5.101.4 version: link:../../../packages/query-async-storage-persister '@tanstack/solid-query': specifier: ^6.0.0-rc.0 @@ -1753,7 +1753,7 @@ importers: specifier: ^3.0.0-next.27 version: 3.0.0-next.27(@solidjs/web@2.0.0-rc.0(solid-js@2.0.0-rc.0))(@testing-library/jest-dom@6.9.1)(solid-js@2.0.0-rc.0)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) '@tanstack/eslint-plugin-query': - specifier: ^5.101.0 + specifier: ^5.101.4 version: link:../../../packages/eslint-plugin-query typescript: specifier: 5.8.3 @@ -1792,10 +1792,10 @@ importers: examples/svelte/auto-refetching: dependencies: '@tanstack/svelte-query': - specifier: ^6.1.34 + specifier: ^6.1.38 version: link:../../../packages/svelte-query '@tanstack/svelte-query-devtools': - specifier: ^6.1.34 + specifier: ^6.1.38 version: link:../../../packages/svelte-query-devtools devDependencies: '@sveltejs/adapter-auto': @@ -1823,16 +1823,16 @@ importers: examples/svelte/basic: dependencies: '@tanstack/query-async-storage-persister': - specifier: ^5.101.0 + specifier: ^5.101.4 version: link:../../../packages/query-async-storage-persister '@tanstack/svelte-query': - specifier: ^6.1.34 + specifier: ^6.1.38 version: link:../../../packages/svelte-query '@tanstack/svelte-query-devtools': - specifier: ^6.1.34 + specifier: ^6.1.38 version: link:../../../packages/svelte-query-devtools '@tanstack/svelte-query-persist-client': - specifier: ^6.1.34 + specifier: ^6.1.38 version: link:../../../packages/svelte-query-persist-client devDependencies: '@sveltejs/adapter-auto': @@ -1860,10 +1860,10 @@ importers: examples/svelte/load-more-infinite-scroll: dependencies: '@tanstack/svelte-query': - specifier: ^6.1.34 + specifier: ^6.1.38 version: link:../../../packages/svelte-query '@tanstack/svelte-query-devtools': - specifier: ^6.1.34 + specifier: ^6.1.38 version: link:../../../packages/svelte-query-devtools devDependencies: '@sveltejs/adapter-auto': @@ -1891,10 +1891,10 @@ importers: examples/svelte/optimistic-updates: dependencies: '@tanstack/svelte-query': - specifier: ^6.1.34 + specifier: ^6.1.38 version: link:../../../packages/svelte-query '@tanstack/svelte-query-devtools': - specifier: ^6.1.34 + specifier: ^6.1.38 version: link:../../../packages/svelte-query-devtools devDependencies: '@sveltejs/adapter-auto': @@ -1922,10 +1922,10 @@ importers: examples/svelte/playground: dependencies: '@tanstack/svelte-query': - specifier: ^6.1.34 + specifier: ^6.1.38 version: link:../../../packages/svelte-query '@tanstack/svelte-query-devtools': - specifier: ^6.1.34 + specifier: ^6.1.38 version: link:../../../packages/svelte-query-devtools devDependencies: '@sveltejs/adapter-auto': @@ -1953,10 +1953,10 @@ importers: examples/svelte/simple: dependencies: '@tanstack/svelte-query': - specifier: ^6.1.34 + specifier: ^6.1.38 version: link:../../../packages/svelte-query '@tanstack/svelte-query-devtools': - specifier: ^6.1.34 + specifier: ^6.1.38 version: link:../../../packages/svelte-query-devtools devDependencies: '@sveltejs/vite-plugin-svelte': @@ -1981,10 +1981,10 @@ importers: examples/svelte/ssr: dependencies: '@tanstack/svelte-query': - specifier: ^6.1.34 + specifier: ^6.1.38 version: link:../../../packages/svelte-query '@tanstack/svelte-query-devtools': - specifier: ^6.1.34 + specifier: ^6.1.38 version: link:../../../packages/svelte-query-devtools devDependencies: '@sveltejs/adapter-auto': @@ -2012,10 +2012,10 @@ importers: examples/svelte/star-wars: dependencies: '@tanstack/svelte-query': - specifier: ^6.1.34 + specifier: ^6.1.38 version: link:../../../packages/svelte-query '@tanstack/svelte-query-devtools': - specifier: ^6.1.34 + specifier: ^6.1.38 version: link:../../../packages/svelte-query-devtools devDependencies: '@sveltejs/adapter-auto': @@ -2049,10 +2049,10 @@ importers: examples/vue/basic: dependencies: '@tanstack/vue-query': - specifier: ^5.101.0 + specifier: ^5.101.4 version: link:../../../packages/vue-query '@tanstack/vue-query-devtools': - specifier: ^6.1.34 + specifier: ^6.1.38 version: link:../../../packages/vue-query-devtools vue: specifier: ^3.4.27 @@ -2071,7 +2071,7 @@ importers: examples/vue/dependent-queries: dependencies: '@tanstack/vue-query': - specifier: ^5.101.0 + specifier: ^5.101.4 version: link:../../../packages/vue-query vue: specifier: ^3.4.27 @@ -2090,16 +2090,16 @@ importers: examples/vue/persister: dependencies: '@tanstack/query-core': - specifier: ^5.101.0 + specifier: ^5.101.4 version: link:../../../packages/query-core '@tanstack/query-persist-client-core': - specifier: ^5.101.0 + specifier: ^5.101.4 version: link:../../../packages/query-persist-client-core '@tanstack/query-sync-storage-persister': - specifier: ^5.101.0 + specifier: ^5.101.4 version: link:../../../packages/query-sync-storage-persister '@tanstack/vue-query': - specifier: ^5.101.0 + specifier: ^5.101.4 version: link:../../../packages/vue-query idb-keyval: specifier: ^6.2.1 @@ -2121,10 +2121,10 @@ importers: examples/vue/simple: dependencies: '@tanstack/vue-query': - specifier: ^5.101.0 + specifier: ^5.101.4 version: link:../../../packages/vue-query '@tanstack/vue-query-devtools': - specifier: ^6.1.34 + specifier: ^6.1.38 version: link:../../../packages/vue-query-devtools vue: specifier: ^3.4.27 @@ -2161,7 +2161,7 @@ importers: specifier: ^20.0.0 version: 20.3.18(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@20.3.18(@angular/animations@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(rxjs@7.8.2) '@tanstack/angular-query-experimental': - specifier: ^5.101.0 + specifier: ^5.101.4 version: link:../../packages/angular-query-experimental rxjs: specifier: ~7.8.0 @@ -2483,13 +2483,13 @@ importers: version: 7.8.2 vite-plugin-dts: specifier: 4.2.3 - version: 4.2.3(@types/node@22.19.15)(rollup@4.60.1)(typescript@6.0.1-rc)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.2.3(@types/node@22.19.15)(rollup@4.60.1)(typescript@5.9.3)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) vite-plugin-externalize-deps: specifier: ^0.9.0 version: 0.9.0(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) vite-tsconfig-paths: specifier: ^5.1.4 - version: 5.1.4(typescript@6.0.1-rc)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 5.1.4(typescript@5.9.3)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) optionalDependencies: '@tanstack/query-devtools': specifier: workspace:* @@ -2581,7 +2581,7 @@ importers: version: 17.5.0 typescript-eslint: specifier: 8.58.1 - version: 8.58.1(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.1-rc) + version: 8.58.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) packages/preact-query: dependencies: @@ -5029,7 +5029,7 @@ packages: '@expo/bunyan@4.0.1': resolution: {integrity: sha512-+Lla7nYSiHZirgK+U/uYzsLv/X+HaJienbD5AKX1UQZHYfWaP+9uuQluRB4GrEVWF0GZ7vEVp/jzaOT9k/SQlg==} - engines: {'0': node >=0.10.0} + engines: {node: '>=0.10.0'} '@expo/cli@0.22.28': resolution: {integrity: sha512-lvt72KNitGuixYD2l3SZmRKVu2G4zJpmg5V7WfUBNpmUU5oODBw/6qmiJ6kSLAlfDozscUk+BBGknBBzxUrwrA==} @@ -15458,6 +15458,7 @@ packages: tsconfck@3.1.6: resolution: {integrity: sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w==} engines: {node: ^18 || >=20} + deprecated: unmaintained hasBin: true peerDependencies: typescript: ^5.0.0 @@ -21817,22 +21818,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/eslint-plugin@8.58.1(@typescript-eslint/parser@8.58.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.1-rc)': - dependencies: - '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.58.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/scope-manager': 8.58.1 - '@typescript-eslint/type-utils': 8.58.1(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.1-rc) - '@typescript-eslint/utils': 8.58.1(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.1-rc) - '@typescript-eslint/visitor-keys': 8.58.1 - eslint: 9.39.4(jiti@2.6.1) - ignore: 7.0.5 - natural-compare: 1.4.0 - ts-api-utils: 2.5.0(typescript@6.0.1-rc) - typescript: 6.0.1-rc - transitivePeerDependencies: - - supports-color - '@typescript-eslint/parser@8.58.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.8.3)': dependencies: '@typescript-eslint/scope-manager': 8.58.1 @@ -21857,18 +21842,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.58.1(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.1-rc)': - dependencies: - '@typescript-eslint/scope-manager': 8.58.1 - '@typescript-eslint/types': 8.58.1 - '@typescript-eslint/typescript-estree': 8.58.1(typescript@6.0.1-rc) - '@typescript-eslint/visitor-keys': 8.58.1 - debug: 4.4.3 - eslint: 9.39.4(jiti@2.6.1) - typescript: 6.0.1-rc - transitivePeerDependencies: - - supports-color - '@typescript-eslint/project-service@8.58.1(typescript@5.8.3)': dependencies: '@typescript-eslint/tsconfig-utils': 8.58.1(typescript@5.8.3) @@ -21887,15 +21860,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.58.1(typescript@6.0.1-rc)': - dependencies: - '@typescript-eslint/tsconfig-utils': 8.58.1(typescript@6.0.1-rc) - '@typescript-eslint/types': 8.58.1 - debug: 4.4.3 - typescript: 6.0.1-rc - transitivePeerDependencies: - - supports-color - '@typescript-eslint/rule-tester@8.58.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@typescript-eslint/parser': 8.58.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) @@ -21923,10 +21887,6 @@ snapshots: dependencies: typescript: 5.9.3 - '@typescript-eslint/tsconfig-utils@8.58.1(typescript@6.0.1-rc)': - dependencies: - typescript: 6.0.1-rc - '@typescript-eslint/type-utils@8.58.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.8.3)': dependencies: '@typescript-eslint/types': 8.58.1 @@ -21951,18 +21911,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/type-utils@8.58.1(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.1-rc)': - dependencies: - '@typescript-eslint/types': 8.58.1 - '@typescript-eslint/typescript-estree': 8.58.1(typescript@6.0.1-rc) - '@typescript-eslint/utils': 8.58.1(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.1-rc) - debug: 4.4.3 - eslint: 9.39.4(jiti@2.6.1) - ts-api-utils: 2.5.0(typescript@6.0.1-rc) - typescript: 6.0.1-rc - transitivePeerDependencies: - - supports-color - '@typescript-eslint/types@8.58.1': {} '@typescript-eslint/typescript-estree@8.58.1(typescript@5.8.3)': @@ -21995,21 +21943,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/typescript-estree@8.58.1(typescript@6.0.1-rc)': - dependencies: - '@typescript-eslint/project-service': 8.58.1(typescript@6.0.1-rc) - '@typescript-eslint/tsconfig-utils': 8.58.1(typescript@6.0.1-rc) - '@typescript-eslint/types': 8.58.1 - '@typescript-eslint/visitor-keys': 8.58.1 - debug: 4.4.3 - minimatch: 10.2.5 - semver: 7.7.4 - tinyglobby: 0.2.15 - ts-api-utils: 2.5.0(typescript@6.0.1-rc) - typescript: 6.0.1-rc - transitivePeerDependencies: - - supports-color - '@typescript-eslint/utils@8.58.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.8.3)': dependencies: '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@2.6.1)) @@ -22032,17 +21965,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.58.1(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.1-rc)': - dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@2.6.1)) - '@typescript-eslint/scope-manager': 8.58.1 - '@typescript-eslint/types': 8.58.1 - '@typescript-eslint/typescript-estree': 8.58.1(typescript@6.0.1-rc) - eslint: 9.39.4(jiti@2.6.1) - typescript: 6.0.1-rc - transitivePeerDependencies: - - supports-color - '@typescript-eslint/visitor-keys@8.58.1': dependencies: '@typescript-eslint/types': 8.58.1 @@ -22446,19 +22368,6 @@ snapshots: optionalDependencies: typescript: 5.9.3 - '@vue/language-core@2.1.6(typescript@6.0.1-rc)': - dependencies: - '@volar/language-core': 2.4.28 - '@vue/compiler-dom': 3.5.31 - '@vue/compiler-vue2': 2.7.16 - '@vue/shared': 3.5.31 - computeds: 0.0.1 - minimatch: 9.0.9 - muggle-string: 0.4.1 - path-browserify: 1.0.1 - optionalDependencies: - typescript: 6.0.1-rc - '@vue/language-core@2.2.12(typescript@5.8.3)': dependencies: '@volar/language-core': 2.4.15 @@ -31766,10 +31675,6 @@ snapshots: dependencies: typescript: 5.9.3 - ts-api-utils@2.5.0(typescript@6.0.1-rc): - dependencies: - typescript: 6.0.1-rc - ts-declaration-location@1.0.7(typescript@5.9.3): dependencies: picomatch: 4.0.4 @@ -31789,10 +31694,6 @@ snapshots: optionalDependencies: typescript: 5.9.3 - tsconfck@3.1.6(typescript@6.0.1-rc): - optionalDependencies: - typescript: 6.0.1-rc - tsconfig-paths@4.2.0: dependencies: json5: 2.2.3 @@ -31974,17 +31875,6 @@ snapshots: transitivePeerDependencies: - supports-color - typescript-eslint@8.58.1(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.1-rc): - dependencies: - '@typescript-eslint/eslint-plugin': 8.58.1(@typescript-eslint/parser@8.58.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.1-rc) - '@typescript-eslint/parser': 8.58.1(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.1-rc) - '@typescript-eslint/typescript-estree': 8.58.1(typescript@6.0.1-rc) - '@typescript-eslint/utils': 8.58.1(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.1-rc) - eslint: 9.39.4(jiti@2.6.1) - typescript: 6.0.1-rc - transitivePeerDependencies: - - supports-color - typescript@5.3.3: {} typescript@5.4.2: {} @@ -32445,25 +32335,6 @@ snapshots: - rollup - supports-color - vite-plugin-dts@4.2.3(@types/node@22.19.15)(rollup@4.60.1)(typescript@6.0.1-rc)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)): - dependencies: - '@microsoft/api-extractor': 7.47.7(@types/node@22.19.15) - '@rollup/pluginutils': 5.3.0(rollup@4.60.1) - '@volar/typescript': 2.4.28 - '@vue/language-core': 2.1.6(typescript@6.0.1-rc) - compare-versions: 6.1.1 - debug: 4.4.3 - kolorist: 1.8.0 - local-pkg: 0.5.1 - magic-string: 0.30.21 - typescript: 6.0.1-rc - optionalDependencies: - vite: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) - transitivePeerDependencies: - - '@types/node' - - rollup - - supports-color - vite-plugin-externalize-deps@0.10.0(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)): dependencies: vite: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) @@ -32538,17 +32409,6 @@ snapshots: - supports-color - typescript - vite-tsconfig-paths@5.1.4(typescript@6.0.1-rc)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)): - dependencies: - debug: 4.4.3 - globrex: 0.1.2 - tsconfck: 3.1.6(typescript@6.0.1-rc) - optionalDependencies: - vite: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) - transitivePeerDependencies: - - supports-color - - typescript - vite@6.4.1(@types/node@22.19.15)(jiti@1.21.7)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3): dependencies: esbuild: 0.27.4 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 0cdac97e210..0b1157bc3c6 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,10 +1,15 @@ cleanupUnusedCatalogs: true +minimumReleaseAge: 1440 linkWorkspacePackages: true preferWorkspacePackages: true blockExoticSubdeps: true trustPolicy: 'no-downgrade' trustPolicyExclude: - - 'vite@6.4.1' + - 'vite@6.4.1' # Socket score 94/100; popular and healthy. + - 'semver@5.7.2' # Socket score 100/100; popular and healthy. + - 'semver@6.3.1' # Socket score 100/100; popular and healthy. + - 'ua-parser-js@1.0.41' # Socket score 100/100; popular and healthy. + - 'undici-types@6.21.0' # Socket score 100/100; popular and healthy. minimumReleaseAgeExclude: - 'solid-js' - '@solidjs/*' @@ -73,4 +78,3 @@ allowBuilds: # nextJs sharp: false # not directly required for build -