Repository: tannerlinsley/react-query
Pull request #1443 (2020-12-15 19:13:34)

feat: add imperative infinite query methods
Fixes https://github.com/tannerlinsley/react-query/issues/1426

vercel[bot]: This pull request is being automatically deployed with Vercel ([learn more](https://vercel.link/github-learn-more)).  
To see the status of your deployment, click below or on the icon next to each commit.

🔍 Inspect: [https://vercel.com/tannerlinsley/react-query/l7xjf14e2](https://vercel.com/tannerlinsley/react-query/l7xjf14e2)  
✅ Preview: *In Progress*



Patch:
diff --git a/docs/src/pages/reference/QueryClient.md b/docs/src/pages/reference/QueryClient.md
index f3e0694e6..e7e4220ad 100644
--- a/docs/src/pages/reference/QueryClient.md
+++ b/docs/src/pages/reference/QueryClient.md
@@ -23,8 +23,10 @@ await queryClient.prefetchQuery('posts', fetchPosts)
 
 Its available methods are:
 
-- [`fetchQuery`](#queryclientfetchquerydata)
+- [`fetchQuery`](#queryclientfetchquery)
+- [`fetchInfiniteQuery`](#queryclientfetchinfinitequery)
 - [`prefetchQuery`](#queryclientprefetchquery)
+- [`prefetchInfiniteQuery`](#queryclientprefetchinfinitequery)
 - [`getQueryData`](#queryclientgetquerydata)
 - [`setQueryData`](#queryclientsetquerydata)
 - [`getQueryState`](#queryclientgetquerystate)
@@ -92,6 +94,27 @@ The options for `fetchQuery` are exactly the same as those of [`useQuery`](#useq
 
 - `Promise<TData>`
 
+## `queryClient.fetchInfiniteQuery`
+
+`fetchInfiniteQuery` is similar to `fetchQuery` but can be used to fetch and cache an infinite query.
+
+```js
+try {
+  const data = await queryClient.fetchInfiniteQuery(queryKey, queryFn)
+  console.log(data.pages)
+} catch (error) {
+  console.log(error)
+}
+```
+
+**Options**
+
+The options for `fetchInfiniteQuery` are exactly the same as those of [`useInfiniteQuery`](#useinfinitequery).
+
+**Returns**
+
+- `Promise<InfiniteData<TData>>`
+
 ## `queryClient.prefetchQuery`
 
 `prefetchQuery` is an asynchronous method that can be used to prefetch a query before it is needed or rendered with `useQuery` and friends. The method works the same as `fetchQuery` except that is will not throw or return any data.
@@ -112,6 +135,23 @@ The options for `prefetchQuery` are exactly the same as those of [`useQuery`](#u
 
 **Returns**
 
+- `Promise<void>`
+  - A promise is returned that will either immediately resolve if no fetch is needed or after the query has been executed. It will not return any data or throw any errors.
+
+## `queryClient.prefetchInfiniteQuery`
+
+`prefetchInfiniteQuery` is similar to `prefetchQuery` but can be used to prefetch and cache an infinite query.
+
+```js
+await queryClient.prefetchInfiniteQuery(queryKey, queryFn)
+```
+
+**Options**
+
+The options for `prefetchInfiniteQuery` are exactly the same as those of [`useInfiniteQuery`](#useinfinitequery).
+
+**Returns**
+
 - `Promise<void>`
   - A promise is returned that will either immediately resolve if no fetch is needed or after the query has been executed. It will not return any data or throw any errors.
 
diff --git a/src/core/query.ts b/src/core/query.ts
index fec4bf4f6..c31a8c69a 100644
--- a/src/core/query.ts
+++ b/src/core/query.ts
@@ -168,9 +168,7 @@ export class Query<TData = unknown, TError = unknown, TQueryFnData = TData> {
 
     if (isValidTimeout(this.cacheTime)) {
       this.gcTimeout = setTimeout(() => {
-        if (!this.observers.length) {
-          this.cache.remove(this)
-        }
+        this.optionalRemove()
       }, this.cacheTime)
     }
   }
@@ -180,6 +178,12 @@ export class Query<TData = unknown, TError = unknown, TQueryFnData = TData> {
     this.gcTimeout = undefined
   }
 
+  private optionalRemove() {
+    if (!this.observers.length && !this.state.isFetching) {
+      this.cache.remove(this)
+    }
+  }
+
   setData(
     updater: Updater<TData | undefined, TData>,
     options?: SetDataOptions
@@ -412,6 +416,12 @@ export class Query<TData = unknown, TError = unknown, TQueryFnData = TData> {
         // Propagate error
         throw error
       })
+      .finally(() => {
+        // Remove query after fetching if cache time is 0
+        if (this.cacheTime === 0) {
+          this.optionalRemove()
+        }
+      })
 
     return this.promise
   }
diff --git a/src/core/queryClient.ts b/src/core/queryClient.ts
index 44b490490..61c131cab 100644
--- a/src/core/queryClient.ts
+++ b/src/core/queryClient.ts
@@ -9,7 +9,9 @@ import {
 } from './utils'
 import type {
   DefaultOptions,
+  FetchInfiniteQueryOptions,
   FetchQueryOptions,
+  InfiniteData,
   InvalidateOptions,
   InvalidateQueryFilters,
   MutationKey,
@@ -29,6 +31,7 @@ import { focusManager } from './focusManager'
 import { onlineManager } from './onlineManager'
 import { notifyManager } from './notifyManager'
 import { CancelOptions } from './retryer'
+import { infiniteQueryBehavior } from './infiniteQueryBehavior'
 
 // TYPES
 
@@ -134,8 +137,16 @@ export class QueryClient {
   }
 
   resetQueries(filters?: QueryFilters, options?: ResetOptions): Promise<void>
-  resetQueries(queryKey?: QueryKey, filters?: QueryFilters, options?: ResetOptions): Promise<void>
-  resetQueries(arg1?: QueryKey | QueryFilters, arg2?: QueryFilters | ResetOptions, arg3?: ResetOptions): Promise<void> {
+  resetQueries(
+    queryKey?: QueryKey,
+    filters?: QueryFilters,
+    options?: ResetOptions
+  ): Promise<void>
+  resetQueries(
+    arg1?: QueryKey | QueryFilters,
+    arg2?: QueryFilters | ResetOptions,
+    arg3?: ResetOptions
+  ): Promise<void> {
     const [filters, options] = parseFilterArgs(arg1, arg2, arg3)
     const queryCache = this.queryCache
 
@@ -286,6 +297,54 @@ export class QueryClient {
       .catch(noop)
   }
 
+  fetchInfiniteQuery<TData = unknown, TError = unknown, TQueryFnData = TData>(
+    options: FetchInfiniteQueryOptions<TData, TError, TQueryFnData>
+  ): Promise<InfiniteData<TData>>
+  fetchInfiniteQuery<TData = unknown, TError = unknown, TQueryFnData = TData>(
+    queryKey: QueryKey,
+    options?: FetchInfiniteQueryOptions<TData, TError, TQueryFnData>
+  ): Promise<InfiniteData<TData>>
+  fetchInfiniteQuery<TData = unknown, TError = unknown, TQueryFnData = TData>(
+    queryKey: QueryKey,
+    queryFn: QueryFunction<TQueryFnData | TData>,
+    options?: FetchInfiniteQueryOptions<TData, TError, TQueryFnData>
+  ): Promise<InfiniteData<TData>>
+  fetchInfiniteQuery<TData, TError, TQueryFnData = TData>(
+    arg1: QueryKey | FetchInfiniteQueryOptions<TData, TError, TQueryFnData>,
+    arg2?:
+      | QueryFunction<TQueryFnData | TData>
+      | FetchInfiniteQueryOptions<TData, TError, TQueryFnData>,
+    arg3?: FetchInfiniteQueryOptions<TData, TError, TQueryFnData>
+  ): Promise<InfiniteData<TData>> {
+    const parsedOptions = parseQueryArgs(arg1, arg2, arg3)
+    parsedOptions.behavior = infiniteQueryBehavior<
+      TData,
+      TError,
+      TQueryFnData
+    >()
+    return this.fetchQuery(parsedOptions)
+  }
+
+  prefetchInfiniteQuery(options: FetchInfiniteQueryOptions): Promise<void>
+  prefetchInfiniteQuery(
+    queryKey: QueryKey,
+    options?: FetchInfiniteQueryOptions
+  ): Promise<void>
+  prefetchInfiniteQuery(
+    queryKey: QueryKey,
+    queryFn: QueryFunction,
+    options?: FetchInfiniteQueryOptions
+  ): Promise<void>
+  prefetchInfiniteQuery(
+    arg1: QueryKey | FetchInfiniteQueryOptions,
+    arg2?: QueryFunction | FetchInfiniteQueryOptions,
+    arg3?: FetchInfiniteQueryOptions
+  ): Promise<void> {
+    return this.fetchInfiniteQuery(arg1 as any, arg2 as any, arg3)
+      .then(noop)
+      .catch(noop)
+  }
+
   cancelMutations(): Promise<void> {
     const promises = notifyManager.batch(() =>
       this.mutationCache.getAll().map(mutation => mutation.cancel())
diff --git a/src/core/types.ts b/src/core/types.ts
index 1a785cb4c..efacbbfcd 100644
--- a/src/core/types.ts
+++ b/src/core/types.ts
@@ -199,6 +199,12 @@ export interface FetchQueryOptions<
   staleTime?: number
 }
 
+export interface FetchInfiniteQueryOptions<
+  TData = unknown,
+  TError = unknown,
+  TQueryFnData = TData
+> extends FetchQueryOptions<InfiniteData<TData>, TError, TQueryFnData> {}
+
 export interface ResultOptions {
   throwOnError?: boolean
 }
diff --git a/src/hydration/hydration.ts b/src/hydration/hydration.ts
index 9842ffda3..b0aa9bc9d 100644
--- a/src/hydration/hydration.ts
+++ b/src/hydration/hydration.ts
@@ -30,7 +30,6 @@ interface DehydratedMutation {
 }
 
 interface DehydratedQuery {
-  cacheTime: number
   queryHash: string
   queryKey: QueryKey
   state: QueryState
@@ -47,14 +46,6 @@ export type ShouldDehydrateMutationFunction = (mutation: Mutation) => boolean
 
 // FUNCTIONS
 
-function serializePositiveNumber(value: number): number {
-  return value === Infinity ? -1 : value
-}
-
-function deserializePositiveNumber(value: number): number {
-  return value === -1 ? Infinity : value
-}
-
 function dehydrateMutation(mutation: Mutation): DehydratedMutation {
   return {
     mutationKey: mutation.options.mutationKey,
@@ -68,7 +59,6 @@ function dehydrateMutation(mutation: Mutation): DehydratedMutation {
 // in the html-payload, but not consume it on the initial render.
 function dehydrateQuery(query: Query): DehydratedQuery {
   return {
-    cacheTime: serializePositiveNumber(query.cacheTime),
     state: query.state,
     queryKey: query.queryKey,
     queryHash: query.queryHash,
@@ -167,7 +157,6 @@ export function hydrate(
         ...options?.defaultOptions?.queries,
         queryKey: dehydratedQuery.queryKey,
         queryHash: dehydratedQuery.queryHash,
-        cacheTime: deserializePositiveNumber(dehydratedQuery.cacheTime),
       },
       dehydratedQuery.state
     )


Test patch:
diff --git a/src/core/tests/queryCache.test.tsx b/src/core/tests/queryCache.test.tsx
index 8fa80a6fd..698eb6102 100644
--- a/src/core/tests/queryCache.test.tsx
+++ b/src/core/tests/queryCache.test.tsx
@@ -127,6 +127,42 @@ describe('queryCache', () => {
     expect(second).toBe(first)
   })
 
+  test('fetchQuery should be able to fetch when cache time is set to 0 and then be removed', async () => {
+    const testClient = new QueryClient({
+      defaultOptions: { queries: { cacheTime: 0 } },
+    })
+
+    const key1 = queryKey()
+
+    const result = await testClient.fetchQuery(key1, async () => {
+      await sleep(10)
+      return 1
+    })
+
+    const result2 = testClient.getQueryData(key1)
+
+    expect(result).toEqual(1)
+    expect(result2).toEqual(undefined)
+  })
+
+  test('fetchQuery should keep a query in cache if cache time is Infinity', async () => {
+    const testClient = new QueryClient({
+      defaultOptions: { queries: { cacheTime: Infinity } },
+    })
+
+    const key1 = queryKey()
+
+    const result = await testClient.fetchQuery(key1, async () => {
+      await sleep(10)
+      return 1
+    })
+
+    const result2 = testClient.getQueryData(key1)
+
+    expect(result).toEqual(1)
+    expect(result2).toEqual(1)
+  })
+
   test('fetchQuery should not force fetch', async () => {
     const key = queryKey()
 
@@ -166,6 +202,38 @@ describe('queryCache', () => {
     expect(fourth).toBe(2)
   })
 
+  test('fetchInfiniteQuery should return infinite query data', async () => {
+    const key = queryKey()
+    const result = await queryClient.fetchInfiniteQuery(
+      key,
+      ({ pageParam = 10 }) => Number(pageParam)
+    )
+    const result2 = queryClient.getQueryData(key)
+
+    const expected = {
+      pages: [10],
+      pageParams: [undefined],
+    }
+
+    expect(result).toEqual(expected)
+    expect(result2).toEqual(expected)
+  })
+
+  test('prefetchInfiniteQuery should return infinite query data', async () => {
+    const key = queryKey()
+
+    await queryClient.prefetchInfiniteQuery(key, ({ pageParam = 10 }) =>
+      Number(pageParam)
+    )
+
+    const result = queryClient.getQueryData(key)
+
+    expect(result).toEqual({
+      pages: [10],
+      pageParams: [undefined],
+    })
+  })
+
   test('prefetchQuery should return undefined when an error is thrown', async () => {
     const consoleMock = mockConsoleError()
 
diff --git a/src/hydration/tests/hydration.test.tsx b/src/hydration/tests/hydration.test.tsx
index 82ef5c088..3527e9ecb 100644
--- a/src/hydration/tests/hydration.test.tsx
+++ b/src/hydration/tests/hydration.test.tsx
@@ -62,7 +62,7 @@ describe('dehydration and rehydration', () => {
     hydrationClient.clear()
   })
 
-  test('should schedule garbage collection, measured from hydration', async () => {
+  test('should use the cache time from the client', async () => {
     const queryCache = new QueryCache()
     const queryClient = new QueryClient({ queryCache })
     await queryClient.prefetchQuery('string', () => fetchData('string'), {
@@ -80,28 +80,9 @@ describe('dehydration and rehydration', () => {
     const hydrationClient = new QueryClient({ queryCache: hydrationCache })
     hydrate(hydrationClient, parsed)
     expect(hydrationCache.find('string')?.state.data).toBe('string')
-    await sleep(10)
-    expect(hydrationCache.find('string')).toBeTruthy()
     await sleep(100)
-    expect(hydrationCache.find('string')).toBeFalsy()
-
-    queryClient.clear()
-    hydrationClient.clear()
-  })
+    expect(hydrationCache.find('string')).toBeTruthy()
 
-  test('should serialize the cacheTime correctly', async () => {
-    const queryCache = new QueryCache()
-    const queryClient = new QueryClient({ queryCache })
-    await queryClient.prefetchQuery('string', () => fetchData('string'), {
-      cacheTime: Infinity,
-    })
-    const dehydrated = dehydrate(queryClient)
-    const stringified = JSON.stringify(dehydrated)
-    const parsed = JSON.parse(stringified)
-    const hydrationCache = new QueryCache()
-    const hydrationClient = new QueryClient({ queryCache: hydrationCache })
-    hydrate(hydrationClient, parsed)
-    expect(hydrationCache.find('string')?.cacheTime).toBe(Infinity)
     queryClient.clear()
     hydrationClient.clear()
   })