Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/cold-islands-move.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
'@tanstack/vue-query': minor
---

add new imperitive methods to QueryClient proxy
Comment thread
DogPawHat marked this conversation as resolved.
36 changes: 23 additions & 13 deletions docs/framework/vue/guides/prefetching.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,24 +3,30 @@ id: prefetching
title: Prefetching
---

If you're lucky enough, you may know enough about what your users will do to be able to prefetch the data they need before it's needed! If this is the case, you can use the `prefetchQuery` method to prefetch the results of a query to be placed into the cache:
If you're lucky enough, you may know enough about what your users will do to be able to prefetch the data they need before it's needed. If this is the case, use `queryClient.query` or `queryClient.infiniteQuery`to warm the cache ahead of time:

[//]: #'ExamplePrefetching'

```tsx
import { noop } from'@tanstack/vue-query'

const prefetchTodos =async () => {
// The results of this query will be cached like a normal query
awaitqueryClient.prefetchQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
})
awaitqueryClient
.query({
queryKey: ['todos'],
queryFn: fetchTodos,
})
.catch(noop)
}
```

[//]: #'ExamplePrefetching'

- If **fresh** data for this query is already in the cache, the data will not be fetched
- If a `staleTime` is passed eg. `prefetchQuery({ queryKey: ['todos'], queryFn: fn, staleTime: 5000 })` and the data is older than the specified `staleTime`, the query will be fetched
- If a `staleTime` is passed e.g. `queryClient.query({ queryKey: ['todos'], queryFn: fn, staleTime: 5000 })` and the data is older than the specified `staleTime`, the query will be fetched
- As `useQuery` will retry fetches and handle errors, you can use `void` to ignore the promise from `query` and `.catch(noop)` to ignore errors.
- If you want to always return cached data when it exists, use `staleTime: 'static'`
- If no instances of `useQuery` appear for a prefetched query, it will be deleted and garbage collected after the time specified in `gcTime`.

## Prefetching Infinite Queries
Expand All@@ -30,15 +36,19 @@ Infinite Queries can be prefetched like regular Queries. Per default, only the f
[//]: #'ExampleInfiniteQuery'

```tsx
import { noop } from'@tanstack/vue-query'

const prefetchProjects =async () => {
// The results of this query will be cached like a normal query
awaitqueryClient.prefetchInfiniteQuery({
queryKey: ['projects'],
queryFn: fetchProjects,
initialPageParam: 0,
getNextPageParam: (lastPage, pages) =>lastPage.nextCursor,
pages: 3, // prefetch the first 3 pages
})
awaitqueryClient
.infiniteQuery({
queryKey: ['projects'],
queryFn: fetchProjects,
initialPageParam: 0,
getNextPageParam: (lastPage, pages) =>lastPage.nextCursor,
pages: 3, // prefetch the first 3 pages
})
.catch(noop)
}
```

Expand Down
11 changes: 6 additions & 5 deletions docs/framework/vue/guides/ssr.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -50,12 +50,13 @@ export default defineNuxtPlugin((nuxt) => {

Now you are ready to prefetch some data in your pages with `onServerPrefetch`.

- Prefetch all the queries that you need with `queryClient.prefetchQuery` or `suspense`
- Prefetch all the queries that you need with `queryClient.query`, `queryClient.infiniteQuery`, or `suspense`

```ts
export default defineComponent({
setup() {
const { data, suspense } = useQuery({
const queryClient = useQueryClient()
const { data } = useQuery({
Comment thread
DogPawHat marked this conversation as resolved.
queryKey: ['test'],
queryFn: fetcher,
})
Expand DownExpand Up@@ -110,7 +111,7 @@ Now you are ready to prefetch some data in your pages with `onServerPrefetch`.

- Use `useContext` to get nuxt context
- Use `useQueryClient` to get server-side instance of `queryClient`
- Prefetch all the queries that you need with `queryClient.prefetchQuery` or `suspense`
- Prefetch all the queries that you need with `queryClient.query`, `queryClient.infiniteQuery`, or `suspense`
- Dehydrate `queryClient` to the `nuxtContext`

```vue
Expand DownExpand Up@@ -169,7 +170,7 @@ export default defineComponent({
</script>
```

As demonstrated, it's fine to prefetch some queries and let others fetch on the queryClient. This means you can control what content server renders or not by adding or removing `prefetchQuery` or `suspense` for a specific query.
As demonstrated, it's fine to prefetch some queries and let others fetch on the client. This means you can control what content server renders or not by adding or removing `queryClient.query` or `suspense` for a specific query.

## Using Vite SSR

Expand DownExpand Up@@ -237,7 +238,7 @@ Then, call VueQuery from any component using Vue's `onServerPrefetch`:

Any query with an error is automatically excluded from dehydration. This means that the default behavior is to pretend these queries were never loaded on the server, usually showing a loading state instead, and retrying the queries on the queryClient. This happens regardless of error.

Sometimes this behavior is not desirable, maybe you want to render an error page with a correct status code instead on certain errors or queries. In those cases, use `fetchQuery` and catch any errors to handle those manually.
Sometimes this behavior is not desirable, maybe you want to render an error page with a correct status code instead on certain errors or queries. In those cases, use `queryClient.query` and catch any errors to handle those manually.

### Staleness is measured from when the query was fetched on the server

Expand Down
33 changes: 33 additions & 0 deletions packages/vue-query/src/__tests__/infiniteQueryOptions.test-d.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -58,6 +58,39 @@ describe('infiniteQueryOptions', () => {
InfiniteData<string, unknown> | undefined
>()
})
it('should work when passed to infiniteQuery', async () => {
const options = infiniteQueryOptions({
queryKey: ['key'],
queryFn: () => Promise.resolve('string'),
getNextPageParam: () => 1,
initialPageParam: 1,
})

const data = await new QueryClient().infiniteQuery({
...options,
staleTime: 0,
pages: 1,
})

expectTypeOf(data).toEqualTypeOf<InfiniteData<string, number>>()
})
it('should work when passed to infiniteQuery with select', async () => {
const options = infiniteQueryOptions({
queryKey: ['key'],
queryFn: () => Promise.resolve('string'),
getNextPageParam: () => 1,
initialPageParam: 1,
select: (data) => data.pages,
})

const data = await new QueryClient().infiniteQuery({
...options,
staleTime: 0,
pages: 1,
})

expectTypeOf(data).toEqualTypeOf<Array<string>>()
})
it('should tag the queryKey with the result type of the QueryFn', () => {
const key = queryKey()
const { queryKey: tagged } = infiniteQueryOptions({
Expand Down
104 changes: 104 additions & 0 deletions packages/vue-query/src/__tests__/queryClient.test-d.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -151,3 +151,107 @@ describe('fetchInfiniteQuery', () => {
])
})
})

describe('query', () => {
it('should return the type of the query fn', () => {
const result = new QueryClient().query({
queryKey: ['key'],
queryFn: () => Promise.resolve('string'),
})

expectTypeOf(result).toEqualTypeOf<Promise<string>>()
})

it('should return the selected type', () => {
const result = new QueryClient().query({
queryKey: ['key'],
queryFn: () => Promise.resolve('string'),
select: (data) => data.length,
})

expectTypeOf(result).toEqualTypeOf<Promise<number>>()
})

it('should not accept top-level options getters', () => {
assertType<Parameters<QueryClient['query']>>([
// @ts-expect-error One-shot imperative methods do not resolve top-level getters
() => ({
queryKey: ['key'],
queryFn: () => Promise.resolve('string'),
}),
])
})
})

describe('infiniteQuery', () => {
it('should return infinite data', async () => {
const data = await new QueryClient().infiniteQuery({
queryKey: ['key'],
queryFn: () => Promise.resolve('string'),
getNextPageParam: () => 1,
initialPageParam: 1,
})

expectTypeOf(data).toEqualTypeOf<InfiniteData<string, number>>()
})

it('should return the selected type', () => {
const result = new QueryClient().infiniteQuery({
queryKey: ['key'],
queryFn: () => Promise.resolve({ count: 1 }),
getNextPageParam: () => 2,
initialPageParam: 1,
select: (data) => data.pages.map((page) => page.count),
})

expectTypeOf(result).toEqualTypeOf<Promise<Array<number>>>()
})

it('should not accept top-level options getters', () => {
assertType<Parameters<QueryClient['infiniteQuery']>>([
// @ts-expect-error One-shot imperative methods do not resolve top-level getters
() => ({
queryKey: ['key'],
queryFn: () => Promise.resolve('string'),
getNextPageParam: () => 1,
initialPageParam: 1,
}),
])
})

it('should allow passing pages with getNextPageParam', () => {
assertType<Parameters<QueryClient['infiniteQuery']>>([
{
queryKey: ['key'],
queryFn: () => Promise.resolve('string'),
initialPageParam: 1,
getNextPageParam: () => 1,
pages: 5,
},
])
})

it('should not allow passing pages without getNextPageParam', () => {
assertType<Parameters<QueryClient['infiniteQuery']>>([
// @ts-expect-error Property 'getNextPageParam' is missing
{
queryKey: ['key'],
queryFn: () => Promise.resolve('string'),
initialPageParam: 1,
pages: 5,
},
])
})

it('should preserve page param inference', () => {
new QueryClient().infiniteQuery({
queryKey: ['key'],
queryFn: ({ pageParam }) => {
expectTypeOf(pageParam).toEqualTypeOf<number>()
return Promise.resolve(pageParam.toString())
},
initialPageParam: 1,
getNextPageParam: () => undefined,
})
})
})
100 changes: 99 additions & 1 deletion packages/vue-query/src/__tests__/queryClient.test.ts
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { ref } from 'vue-demi'
import { ref, unref } from 'vue-demi'
import { QueryClient as QueryClientOrigin } from '@tanstack/query-core'
import { QueryClient } from '../queryClient'
import { infiniteQueryOptions } from '../infiniteQueryOptions'
import { queryOptions } from '../queryOptions'

vi.mock('@tanstack/query-core', async () => {
const actual = await vi.importActual<{
Expand DownExpand Up@@ -340,6 +341,51 @@ describe('QueryCache', () => {
})
})

describe('query', () => {
it('should properly unwrap queryKey', () => {
const queryClient = new QueryClient()

queryClient.query({
queryKey: queryKeyRef,
})

expect(QueryClientOrigin.prototype.query).toHaveBeenCalledWith({
queryKey: queryKeyUnref,
})
})

it('should properly unwrap staleTime, and select', () => {
const queryClient = new QueryClient()
const staleTime = () => 1000
const select = (data: string) => data.length

queryClient.query({
queryKey: queryKeyRef,
staleTime: ref(staleTime),
select: ref(select),
})

expect(QueryClientOrigin.prototype.query).toHaveBeenCalledWith({
queryKey: queryKeyUnref,
staleTime,
select,
})
})

it('should accept explicitly resolved getter options and unwrap queryKey', () => {
const queryClient = new QueryClient()
const options = queryOptions(() => ({
queryKey: queryKeyRef,
}))

queryClient.query(options())

expect(QueryClientOrigin.prototype.query).toHaveBeenCalledWith({
queryKey: queryKeyUnref,
})
})
})

describe('prefetchQuery', () => {
it('should properly unwrap parameters', () => {
const queryClient = new QueryClient()
Expand DownExpand Up@@ -393,6 +439,58 @@ describe('QueryCache', () => {
})
})

describe('infiniteQuery', () => {
it('should properly unwrap queryKey, initialPageParam, pages, and select', () => {
const queryClient = new QueryClient()
const getNextPageParam = () => 1
const select = (data: { pages: Array<string> }) => data.pages.length

queryClient.infiniteQuery({
queryKey: queryKeyRef,
initialPageParam: ref(0),
pages: ref(2),
getNextPageParam: ref(getNextPageParam),
select: ref(select),
})

expect(QueryClientOrigin.prototype.infiniteQuery).toBeCalledWith(
expect.objectContaining({
queryKey: queryKeyUnref,
initialPageParam: 0,
pages: 2,
getNextPageParam,
select,
}),
)
})

it('should properly unwrap getNextPageParam when using infiniteQueryOptions', () => {
const queryClient = new QueryClient()
const getNextPageParam = () => 12

const options = infiniteQueryOptions({
queryKey: queryKeyRef,
initialPageParam: ref(0),
getNextPageParam: ref(getNextPageParam),
})

queryClient.infiniteQuery({
...unref(options),
staleTime: 0,
pages: 1,
})

expect(QueryClientOrigin.prototype.infiniteQuery).toHaveBeenCalledWith(
expect.objectContaining({
queryKey: queryKeyUnref,
initialPageParam: 0,
pages: 1,
getNextPageParam,
}),
)
})
})

describe('prefetchInfiniteQuery', () => {
it('should properly unwrap parameters', () => {
const queryClient = new QueryClient()
Expand Down
Loading
Loading