Skip to content
54 changes: 42 additions & 12 deletions docs/framework/vue/guides/ssr.md
Original file line number Diff line number Diff line change
Expand Up @@ -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,
})
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -211,7 +210,7 @@ export default viteSSR(App, { routes: [] }, ({ app, initialState }) => {

Then, call VueQuery from any component using Vue's `onServerPrefetch`:

```html
```vue
<!-- MyComponent.vue -->
<template>
<div>
Expand All @@ -221,16 +220,16 @@ Then, call VueQuery from any component using Vue's `onServerPrefetch`:
</template>

<script setup>
import { useQuery } from '@tanstack/vue-query'
import { onServerPrefetch } from 'vue'
import { useQuery } from '@tanstack/vue-query'
import { onServerPrefetch } from 'vue'

// This will be prefetched and sent from the server
const { refetch, data, suspense } = useQuery({
queryKey: ['todos'],
queryFn: getTodos,
})
// This will be prefetched and sent from the server
const { refetch, data, suspense } = useQuery({
queryKey: ['todos'],
queryFn: getTodos,
})

onServerPrefetch(suspense)
onServerPrefetch(suspense)
</script>
```

Expand All @@ -250,6 +249,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 query that stays disabled blocks the render on the server

`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:

```vue
<script setup>
import { computed, onServerPrefetch } from 'vue'
import { useQuery } from '@tanstack/vue-query'

const { data: user, suspense: userSuspense } = useQuery({
queryKey: ['user'],
queryFn: getUser,
})

const userId = computed(() => user.value?.id)
const enabled = computed(() => !!user.value?.id)
const { data: projects, suspense: projectsSuspense } = useQuery({
queryKey: ['projects', userId],
queryFn: () => getProjectsByUser(userId.value),
enabled,
})

onServerPrefetch(async () => {
await userSuspense()
if (enabled.value) {
await projectsSuspense()
}
})
</script>
```

### 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.
Expand Down
39 changes: 38 additions & 1 deletion docs/framework/vue/guides/suspense.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import SuspendableComponent from './SuspendableComponent.vue'
</template>
```

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
<script>
Expand All @@ -52,6 +52,43 @@ export default defineComponent({
</script>
```

## How `suspense()` resolves

- 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, so it never resolves for a query that stays disabled. On the server, this blocks the render, see [SSR](./ssr.md#suspense-of-a-query-that-stays-disabled-blocks-the-render-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`.

## Error handling

Since `suspense()` resolves with the error result by default, the component still renders after a failed fetch and can read `error` from the query. To let a parent component handle the error instead, set `throwOnError: true`. `await suspense()` then rejects, the error propagates out of the `async` `setup`, and you can catch it with [`onErrorCaptured`](https://vuejs.org/api/composition-api-lifecycle.html#onerrorcaptured) in a parent of `Suspense`:

```vue
<script setup>
import { onErrorCaptured, ref } from 'vue'
import SuspendableComponent from './SuspendableComponent.vue'

const error = ref(null)

onErrorCaptured((err) => {
error.value = err
return false
})
</script>

<template>
<div v-if="error">Something went wrong: {{ error.message }}</div>
<Suspense v-else>
<template #default>
<SuspendableComponent />
</template>
<template #fallback>
<div>Loading...</div>
</template>
</Suspense>
</template>
```

## 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.
8 changes: 8 additions & 0 deletions packages/vue-query/src/useBaseQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,14 @@ export type UseBaseQueryReturnType<
? TResult[K]
: Ref<Readonly<TResult>[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 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<TResult>
}

Expand Down
Loading