From 48bebc4fbce8128db8e607cf983ec9d3440e9811 Mon Sep 17 00:00:00 2001 From: Wonsuk Choi Date: Fri, 25 Sep 2026 16:30:40 +0900 Subject: [PATCH 1/7] docs(vue-query): document how 'suspense()' resolves, including the disabled query caveat on the server --- docs/framework/vue/guides/ssr.md | 31 ++++++++++++++++++++++++++ docs/framework/vue/guides/suspense.md | 7 ++++++ packages/vue-query/src/useBaseQuery.ts | 6 +++++ 3 files changed, 44 insertions(+) diff --git a/docs/framework/vue/guides/ssr.md b/docs/framework/vue/guides/ssr.md index 31c3ba83489..c5c84760048 100644 --- a/docs/framework/vue/guides/ssr.md +++ b/docs/framework/vue/guides/ssr.md @@ -250,6 +250,37 @@ Because `staleTime` defaults to `0`, queries will be refetched in the background This refetching of stale queries is a perfect match when caching markup in a CDN! You can set the cache time of the page itself decently high to avoid having to re-render pages on the server, but configure the `staleTime` of the queries lower to make sure data is refetched in the background as soon as a user visits the page. Maybe you want to cache the pages for a week, but refetch the data automatically on page load if it's older than a day? +### `suspense()` of a disabled query never resolves on the server + +`suspense()` waits until the query is enabled. On the client, it continues once `enabled` becomes `true`, but watchers don't run during server-side rendering, so awaiting `suspense()` of a query that stays disabled on the server (for example, a dependent query whose dependency failed) never resolves and blocks the render. Skip it when the query is disabled: + +```html + +``` + ### High memory consumption on server In case you are creating the `QueryClient` for every request, Vue Query creates the isolated cache for this client, which is preserved in memory for the `gcTime` period. That may lead to high memory consumption on server in case of high number of requests during that period. diff --git a/docs/framework/vue/guides/suspense.md b/docs/framework/vue/guides/suspense.md index d05ea24d445..de1084ae70c 100644 --- a/docs/framework/vue/guides/suspense.md +++ b/docs/framework/vue/guides/suspense.md @@ -52,6 +52,13 @@ export default defineComponent({ ``` +## How `suspense()` resolves + +- If the query has no data or its data is stale, it fetches the query and resolves with the result once the fetch finishes. +- If the data is fresh, it resolves immediately without refetching. +- While the query is disabled (`enabled: false`), it waits until the query is enabled. On the server, this means it never resolves for a query that stays disabled, see [SSR](./ssr.md#suspense-of-a-disabled-query-never-resolves-on-the-server). +- If the fetch fails, it resolves with the query result in the error state. It rejects with the error only when `throwOnError` is (or returns) `true`. + ## Fetch-on-render vs Render-as-you-fetch Out of the box, Vue Query in `suspense` mode works really well as a **Fetch-on-render** solution with no additional configuration. This means that when your components attempt to mount, they will trigger query fetching and suspend, but only once you have imported them and mounted them. If you want to take it to the next level and implement a **Render-as-you-fetch** model, we recommend implementing [Prefetching](./prefetching) on routing callbacks and/or user interactions events to start loading queries before they are mounted and hopefully even before you start importing or mounting their parent components. diff --git a/packages/vue-query/src/useBaseQuery.ts b/packages/vue-query/src/useBaseQuery.ts index 57aa3f8077e..44471ddedb2 100644 --- a/packages/vue-query/src/useBaseQuery.ts +++ b/packages/vue-query/src/useBaseQuery.ts @@ -34,6 +34,12 @@ export type UseBaseQueryReturnType< ? TResult[K] : Ref[K]> } & { + /** + * Returns a promise for use with Vue's `Suspense` or `onServerPrefetch`. It fetches the query if it has no + * data or its data is stale and resolves with the result once the fetch finishes, or resolves immediately if + * the data is fresh. While the query is disabled, it waits until the query is enabled. If the fetch fails, it + * resolves with the error result, unless `throwOnError` is (or returns) `true`, in which case it rejects. + */ suspense: () => Promise } From fbfe30bad9b76f9ccf1a304f9db4ab4d9086369c Mon Sep 17 00:00:00 2001 From: Wonsuk Choi Date: Fri, 25 Sep 2026 16:50:38 +0900 Subject: [PATCH 2/7] docs(vue-query/suspense): add error handling with 'throwOnError' and 'onErrorCaptured' --- docs/framework/vue/guides/suspense.md | 32 ++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/docs/framework/vue/guides/suspense.md b/docs/framework/vue/guides/suspense.md index de1084ae70c..137f2695894 100644 --- a/docs/framework/vue/guides/suspense.md +++ b/docs/framework/vue/guides/suspense.md @@ -26,7 +26,7 @@ import SuspendableComponent from './SuspendableComponent.vue' ``` -And change your `setup` function in suspendable component to be `async`. Then you can use async `suspense` function that is provided by `vue-query`. +And change your `setup` function in suspendable component to be `async`. Then you can use async `suspense` function that is provided by `vue-query` (both `useQuery` and `useInfiniteQuery` return it). ```vue + + +``` + ## Fetch-on-render vs Render-as-you-fetch Out of the box, Vue Query in `suspense` mode works really well as a **Fetch-on-render** solution with no additional configuration. This means that when your components attempt to mount, they will trigger query fetching and suspend, but only once you have imported them and mounted them. If you want to take it to the next level and implement a **Render-as-you-fetch** model, we recommend implementing [Prefetching](./prefetching) on routing callbacks and/or user interactions events to start loading queries before they are mounted and hopefully even before you start importing or mounting their parent components. From 38e7b8c45dc9822bbf8b69e4f07c25eb9d5b00a9 Mon Sep 17 00:00:00 2001 From: Wonsuk Choi Date: Fri, 25 Sep 2026 16:50:38 +0900 Subject: [PATCH 3/7] docs(vue-query/ssr): fix the Nuxt examples that call 'suspense' without destructuring it and destructure a nonexistent 'data2' --- docs/framework/vue/guides/ssr.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/framework/vue/guides/ssr.md b/docs/framework/vue/guides/ssr.md index c5c84760048..45326c2f49d 100644 --- a/docs/framework/vue/guides/ssr.md +++ b/docs/framework/vue/guides/ssr.md @@ -57,8 +57,7 @@ Now you are ready to prefetch some data in your pages with `onServerPrefetch`. ```ts export default defineComponent({ setup() { - const queryClient = useQueryClient() - const { data } = useQuery({ + const { data, suspense } = useQuery({ queryKey: ['test'], queryFn: fetcher, }) @@ -150,7 +149,7 @@ export default defineComponent({ queryClient, ) // This won't be prefetched, it will start fetching on client side - const { data2 } = useQuery( + const { data: data2 } = useQuery( { queryKey: ['todos2'], queryFn: getTodos, From e0f62a620757b5de2df71206821e01592b79857f Mon Sep 17 00:00:00 2001 From: Wonsuk Choi Date: Fri, 25 Sep 2026 16:51:48 +0900 Subject: [PATCH 4/7] docs(vue-query/suspense): note that 'suspense()' can resolve before the query function finishes --- docs/framework/vue/guides/suspense.md | 2 +- packages/vue-query/src/useBaseQuery.ts | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/framework/vue/guides/suspense.md b/docs/framework/vue/guides/suspense.md index 137f2695894..51900ea333b 100644 --- a/docs/framework/vue/guides/suspense.md +++ b/docs/framework/vue/guides/suspense.md @@ -54,7 +54,7 @@ export default defineComponent({ ## How `suspense()` resolves -- If the query has no data or its data is stale, it fetches the query and resolves with the result once the fetch finishes. +- If the query has no data or its data is stale, it fetches the query and resolves with the result once that fetch resolves. This is usually when the query function finishes, but can be earlier, such as after the first chunk of an `experimental_streamedQuery` or when `setQueryData` sets data while the fetch is in flight. - If the data is fresh, it resolves immediately without refetching. - While the query is disabled (`enabled: false`), it waits until the query is enabled. On the server, this means it never resolves for a query that stays disabled, see [SSR](./ssr.md#suspense-of-a-disabled-query-never-resolves-on-the-server). - If the fetch fails, it resolves with the query result in the error state. It rejects with the error only when `throwOnError` is (or returns) `true`. diff --git a/packages/vue-query/src/useBaseQuery.ts b/packages/vue-query/src/useBaseQuery.ts index 44471ddedb2..82f6ce54eab 100644 --- a/packages/vue-query/src/useBaseQuery.ts +++ b/packages/vue-query/src/useBaseQuery.ts @@ -36,9 +36,11 @@ export type UseBaseQueryReturnType< } & { /** * Returns a promise for use with Vue's `Suspense` or `onServerPrefetch`. It fetches the query if it has no - * data or its data is stale and resolves with the result once the fetch finishes, or resolves immediately if - * the data is fresh. While the query is disabled, it waits until the query is enabled. If the fetch fails, it - * resolves with the error result, unless `throwOnError` is (or returns) `true`, in which case it rejects. + * data or its data is stale and resolves with the result once that fetch resolves (usually when the query + * function finishes, but earlier after the first chunk of a streamed query or when data is set during the + * fetch), or resolves immediately if the data is fresh. While the query is disabled, it waits until the query + * is enabled. If the fetch fails, it resolves with the error result, unless `throwOnError` is (or returns) + * `true`, in which case it rejects. */ suspense: () => Promise } From 8138e37c470f153e92b107ef617668e732a58750 Mon Sep 17 00:00:00 2001 From: Wonsuk Choi Date: Fri, 25 Sep 2026 16:53:56 +0900 Subject: [PATCH 5/7] docs(vue-query/{ssr,suspense}): scope the disabled 'suspense()' caveat to queries that stay disabled --- docs/framework/vue/guides/ssr.md | 2 +- docs/framework/vue/guides/suspense.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/framework/vue/guides/ssr.md b/docs/framework/vue/guides/ssr.md index 45326c2f49d..117ec6cc2ae 100644 --- a/docs/framework/vue/guides/ssr.md +++ b/docs/framework/vue/guides/ssr.md @@ -251,7 +251,7 @@ This refetching of stale queries is a perfect match when caching markup in a CDN ### `suspense()` of a disabled query never resolves on the server -`suspense()` waits until the query is enabled. On the client, it continues once `enabled` becomes `true`, but watchers don't run during server-side rendering, so awaiting `suspense()` of a query that stays disabled on the server (for example, a dependent query whose dependency failed) never resolves and blocks the render. Skip it when the query is disabled: +`suspense()` waits until the query is enabled, so it never resolves for a query that stays disabled. On the server, awaiting it in `onServerPrefetch` for such a query (for example, a dependent query whose dependency failed) blocks the render. Skip it when the query is disabled: ```html ``` @@ -253,30 +253,30 @@ This refetching of stale queries is a perfect match when caching markup in a CDN `suspense()` waits until the query is enabled, so it never resolves for a query that stays disabled. On the server, awaiting it in `onServerPrefetch` for such a query (for example, a dependent query whose dependency failed) blocks the render. Skip it when the query is disabled: -```html +```vue ```