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
21 changes: 21 additions & 0 deletions docs/src/pages/guides/infinite-queries.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,27 @@ function Projects() {

When an infinite query becomes `stale` and needs to be refetched, each group is fetched `sequentially`, starting from the first one. This ensures that even if the underlying data is mutated, we're not using stale cursors and potentially getting duplicates or skipping records. If an infinite query's results are ever removed from the queryCache, the pagination restarts at the initial state with only the initial group being requested.

### refetchPage

If you only want to actively refetch a subset of all pages, you can pass the `refetchPage` function to `refetch` returned from `useInfiniteQuery`.

```js
const { refetch } = useInfiniteQuery('projects', fetchProjects, {
getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
})

// only refetch the first page
refetch({ refetchPage: (page, index) => index === 0 })
```

You can also pass this function as part of the 2nd argument (`queryFilters`) to [queryClient.refetchQueries](/reference/QueryClient#queryclientrefetchqueries), [queryClient.invalidateQueries](/reference/QueryClient#queryclientinvalidatequeries) or [queryClient.resetQueries](/reference/QueryClient#queryclientresetqueries).

**Signature**

- `refetchPage: (page: TData, index: number, allPages: TData[]) => boolean`

The function is executed for each page, and only pages where this function returns `true` will be refetched.

## What if I need to pass custom information to my query function?

By default, the variable returned from `getNextPageParam` will be supplied to the query function, but in some cases, you may want to override this. You can pass custom variables to the `fetchNextPage` function which will override the default variable like so:
Expand Down
11 changes: 10 additions & 1 deletion docs/src/pages/reference/QueryClient.md
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ const data = queryClient.getQueriesData(queryKey | filters)

- `[queryKey:QueryKey, data:TData | unknown][]`
- An array of tuples for the matched query keys, or `[]` if there are no matches. The tuples are the query key and its associated data.

**Caveats**

Because the returned data in each tuple can be of varying structures (i.e. using a filter to return "active" queries can return different data types), the `TData` generic defaults to `unknown`. If you provide a more specific type to `TData` it is assumed that you are certain each tuple's data entry is all the same type.
Expand Down Expand Up @@ -287,6 +287,9 @@ await queryClient.invalidateQueries('posts', {
- `refetchInactive: Boolean`
- Defaults to `false`
- When set to `true`, queries that match the refetch predicate and are not being rendered via `useQuery` and friends will be both marked as invalid and also refetched in the background
- `refetchPage: (page: TData, index: number, allPages: TData[]) => boolean`
- Only for [Infinite Queries](../guides/infinite-queries#refetchpage)
- Use this function to specify which pages should be refetched
- `refetchOptions?: RefetchOptions`:
- `throwOnError?: boolean`
- When set to `true`, this method will throw if any of the query refetch tasks fail.
Expand Down Expand Up @@ -315,6 +318,9 @@ await queryClient.refetchQueries(['posts', 1], { active: true, exact: true })

- `queryKey?: QueryKey`: [Query Keys](../guides/query-keys)
- `filters?: QueryFilters`: [Query Filters](../guides/filters#query-filters)
- `refetchPage: (page: TData, index: number, allPages: TData[]) => boolean`
- Only for [Infinite Queries](../guides/infinite-queries#refetchpage)
- Use this function to specify which pages should be refetched
- `refetchOptions?: RefetchOptions`:
- `throwOnError?: boolean`
- When set to `true`, this method will throw if any of the query refetch tasks fail.
Expand Down Expand Up @@ -378,6 +384,9 @@ queryClient.resetQueries(queryKey, { exact: true })

- `queryKey?: QueryKey`: [Query Keys](../guides/query-keys)
- `filters?: QueryFilters`: [Query Filters](../guides/filters#query-filters)
- `refetchPage: (page: TData, index: number, allPages: TData[]) => boolean`
- Only for [Infinite Queries](../guides/infinite-queries#refetchpage)
- Use this function to specify which pages should be refetched
- `resetOptions?: ResetOptions`:
- `throwOnError?: boolean`
- When set to `true`, this method will throw if any of the query refetch tasks fail.
Expand Down
57 changes: 45 additions & 12 deletions src/core/infiniteQueryBehavior.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import type { QueryBehavior } from './query'
import { isCancelable } from './retryer'
import type { InfiniteData, QueryFunctionContext, QueryOptions } from './types'
import type {
InfiniteData,
QueryFunctionContext,
QueryOptions,
RefetchQueryFilters,
} from './types'

export function infiniteQueryBehavior<
TQueryFnData,
Expand All @@ -10,6 +15,8 @@ export function infiniteQueryBehavior<
return {
onFetch: context => {
context.fetchFn = () => {
const refetchPage: RefetchQueryFilters['refetchPage'] | undefined =
context.fetchOptions?.meta?.refetchPage
const fetchMore = context.fetchOptions?.meta?.fetchMore
const pageParam = fetchMore?.pageParam
const isFetchingNextPage = fetchMore?.direction === 'forward'
Expand All @@ -23,6 +30,18 @@ export function infiniteQueryBehavior<
const queryFn =
context.options.queryFn || (() => Promise.reject('Missing queryFn'))

const buildNewPages = (
pages: unknown[],
param: unknown,
page: unknown,
previous?: boolean
) => {
newPageParams = previous
? [param, ...newPageParams]
: [...newPageParams, param]
return previous ? [page, ...pages] : [...pages, page]
}

// Create function to fetch a page
const fetchPage = (
pages: unknown[],
Expand All @@ -45,12 +64,9 @@ export function infiniteQueryBehavior<

const queryFnResult = queryFn(queryFnContext)

const promise = Promise.resolve(queryFnResult).then(page => {
newPageParams = previous
? [param, ...newPageParams]
: [...newPageParams, param]
return previous ? [page, ...pages] : [...pages, page]
})
const promise = Promise.resolve(queryFnResult).then(page =>
buildNewPages(pages, param, page, previous)
)

if (isCancelable(queryFnResult)) {
const promiseAsAny = promise as any
Expand Down Expand Up @@ -91,16 +107,33 @@ export function infiniteQueryBehavior<

const manual = typeof context.options.getNextPageParam === 'undefined'

const shouldFetchFirstPage =
refetchPage && oldPages[0]
? refetchPage(oldPages[0], 0, oldPages)
: true

// Fetch first page
promise = fetchPage([], manual, oldPageParams[0])
promise = shouldFetchFirstPage
? fetchPage([], manual, oldPageParams[0])
: Promise.resolve(buildNewPages([], oldPageParams[0], oldPages[0]))

// Fetch remaining pages
for (let i = 1; i < oldPages.length; i++) {
promise = promise.then(pages => {
const param = manual
? oldPageParams[i]
: getNextPageParam(context.options, pages)
return fetchPage(pages, manual, param)
const shouldFetchNextPage =
refetchPage && oldPages[i]
? refetchPage(oldPages[i], i, oldPages)
: true

if (shouldFetchNextPage) {
const param = manual
? oldPageParams[i]
: getNextPageParam(context.options, pages)
return fetchPage(pages, manual, param)
}
return Promise.resolve(
buildNewPages(pages, oldPageParams[i], oldPages[i])
)
})
}
}
Expand Down
31 changes: 20 additions & 11 deletions src/core/queryClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ import type {
QueryObserverOptions,
QueryOptions,
RefetchOptions,
RefetchQueryFilters,
ResetOptions,
ResetQueryFilters,
} from './types'
import type { QueryState, SetDataOptions } from './query'
import { QueryCache } from './queryCache'
Expand Down Expand Up @@ -182,21 +184,24 @@ export class QueryClient {
})
}

resetQueries(filters?: QueryFilters, options?: ResetOptions): Promise<void>
resetQueries(
filters?: ResetQueryFilters,
options?: ResetOptions
): Promise<void>
resetQueries(
queryKey?: QueryKey,
filters?: QueryFilters,
filters?: ResetQueryFilters,
options?: ResetOptions
): Promise<void>
resetQueries(
arg1?: QueryKey | QueryFilters,
arg2?: QueryFilters | ResetOptions,
arg1?: QueryKey | ResetQueryFilters,
arg2?: ResetQueryFilters | ResetOptions,
arg3?: ResetOptions
): Promise<void> {
const [filters, options] = parseFilterArgs(arg1, arg2, arg3)
const queryCache = this.queryCache

const refetchFilters: QueryFilters = {
const refetchFilters: RefetchQueryFilters = {
...filters,
active: true,
}
Expand Down Expand Up @@ -249,7 +254,7 @@ export class QueryClient {
): Promise<void> {
const [filters, options] = parseFilterArgs(arg1, arg2, arg3)

const refetchFilters: QueryFilters = {
const refetchFilters: RefetchQueryFilters = {
...filters,
// if filters.refetchActive is not provided and filters.active is explicitly false,
// e.g. invalidateQueries({ active: false }), we don't want to refetch active queries
Expand All @@ -266,23 +271,27 @@ export class QueryClient {
}

refetchQueries(
filters?: QueryFilters,
filters?: RefetchQueryFilters,
options?: RefetchOptions
): Promise<void>
refetchQueries(
queryKey?: QueryKey,
filters?: QueryFilters,
filters?: RefetchQueryFilters,
options?: RefetchOptions
): Promise<void>
refetchQueries(
arg1?: QueryKey | QueryFilters,
arg2?: QueryFilters | RefetchOptions,
arg1?: QueryKey | RefetchQueryFilters,
arg2?: RefetchQueryFilters | RefetchOptions,
arg3?: RefetchOptions
): Promise<void> {
const [filters, options] = parseFilterArgs(arg1, arg2, arg3)

const promises = notifyManager.batch(() =>
this.queryCache.findAll(filters).map(query => query.fetch())
this.queryCache.findAll(filters).map(query =>
query.fetch(undefined, {
meta: { refetchPage: filters?.refetchPage },
})
)
)

let promise = Promise.all(promises).then(noop)
Expand Down
8 changes: 6 additions & 2 deletions src/core/queryObserver.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { RefetchQueryFilters } from './types'
import {
isServer,
isValidTimeout,
Expand Down Expand Up @@ -275,9 +276,12 @@ export class QueryObserver<
}

refetch(
options?: RefetchOptions
options?: RefetchOptions & RefetchQueryFilters<TData>
): Promise<QueryObserverResult<TData, TError>> {
return this.fetch(options)
return this.fetch({
...options,
meta: { refetchPage: options?.refetchPage },
})
}

fetchOptimistic(
Expand Down
100 changes: 99 additions & 1 deletion src/core/tests/queryClient.test.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import { sleep, queryKey, mockConsoleError } from '../../react/tests/utils'
import { QueryCache, QueryClient, QueryFunction, QueryObserver } from '../..'
import {
InfiniteQueryObserver,
QueryCache,
QueryClient,
QueryFunction,
QueryObserver,
} from '../..'

describe('queryClient', () => {
let queryClient: QueryClient
Expand Down Expand Up @@ -918,4 +924,96 @@ describe('queryClient', () => {
expect(queryFn2).toHaveBeenCalledTimes(0)
})
})

describe('refetch only certain pages of an infinite query', () => {
test('refetchQueries', async () => {
const key = queryKey()
let multiplier = 1
const observer = new InfiniteQueryObserver<number>(queryClient, {
queryKey: key,
queryFn: ({ pageParam = 10 }) => Number(pageParam) * multiplier,
getNextPageParam: lastPage => lastPage + 1,
})

await observer.fetchNextPage()
await observer.fetchNextPage()

expect(queryClient.getQueryData(key)).toMatchObject({
pages: [10, 11],
})

multiplier = 2

await queryClient.refetchQueries({
queryKey: key,
refetchPage: (_, index) => index === 0,
})

expect(queryClient.getQueryData(key)).toMatchObject({
pages: [20, 11],
})
})
test('invalidateQueries', async () => {
const key = queryKey()
let multiplier = 1
const observer = new InfiniteQueryObserver<number>(queryClient, {
queryKey: key,
queryFn: ({ pageParam = 10 }) => Number(pageParam) * multiplier,
getNextPageParam: lastPage => lastPage + 1,
})

await observer.fetchNextPage()
await observer.fetchNextPage()

expect(queryClient.getQueryData(key)).toMatchObject({
pages: [10, 11],
})

multiplier = 2

await queryClient.invalidateQueries({
queryKey: key,
refetchInactive: true,
refetchPage: (page, _, allPages) => {
return page === allPages[0]
},
})

expect(queryClient.getQueryData(key)).toMatchObject({
pages: [20, 11],
})
})

test('resetQueries', async () => {
const key = queryKey()
let multiplier = 1
new InfiniteQueryObserver<number>(queryClient, {
queryKey: key,
queryFn: ({ pageParam = 10 }) => Number(pageParam) * multiplier,
getNextPageParam: lastPage => lastPage + 1,
initialData: () => ({
pages: [10, 11],
pageParams: [10, 11],
}),
})

expect(queryClient.getQueryData(key)).toMatchObject({
pages: [10, 11],
})

multiplier = 2

await queryClient.resetQueries({
queryKey: key,
inactive: true,
refetchPage: (page, _, allPages) => {
return page === allPages[0]
},
})

expect(queryClient.getQueryData(key)).toMatchObject({
pages: [20, 11],
})
})
})
})
Loading