Repository: tannerlinsley/react-query
Pull request #2622 (2021-08-31 20:27:29)

feat(useQuery): make refetchInterval accept a function
so that we can have intervals dependent on current data / error

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/6MNBvxNZEJChkWw1CCf9JT79tjng](https://vercel.com/tannerlinsley/react-query/6MNBvxNZEJChkWw1CCf9JT79tjng)  
✅ Preview: *In Progress*


codesandbox[bot]: This pull request is automatically built and testable in [CodeSandbox](https://codesandbox.io).

  To see build info of the built libraries, click [here](https://ci.codesandbox.io/status/tannerlinsley/react-query/pr/2622/builds/164967) or the icon next to each commit SHA.

Patch:
diff --git a/docs/src/pages/reference/useQuery.md b/docs/src/pages/reference/useQuery.md
index 5789c36cd..38499afbe 100644
--- a/docs/src/pages/reference/useQuery.md
+++ b/docs/src/pages/reference/useQuery.md
@@ -100,9 +100,10 @@ const result = useQuery({
 - `queryKeyHashFn: (queryKey: QueryKey) => string`
   - Optional
   - If specified, this function is used to hash the `queryKey` to a string.
-- `refetchInterval: false | number`
+- `refetchInterval: number | false | ((data: TData | undefined, query: Query) => number | false)`
   - Optional
   - If set to a number, all queries will continuously refetch at this frequency in milliseconds
+  - If set to a function, the function will be executed with the latest data and query to compute a frequency
 - `refetchIntervalInBackground: boolean`
   - Optional
   - If set to `true`, queries that are set to continuously refetch with a `refetchInterval` will continue to refetch while their tab/window is in the background
diff --git a/src/broadcastQueryClient-experimental/index.ts b/src/broadcastQueryClient-experimental/index.ts
index 8bf618474..9f6181553 100644
--- a/src/broadcastQueryClient-experimental/index.ts
+++ b/src/broadcastQueryClient-experimental/index.ts
@@ -3,7 +3,7 @@ import { QueryClient } from '../core'
 
 interface BroadcastQueryClientOptions {
   queryClient: QueryClient
-  broadcastChannel: string
+  broadcastChannel?: string
 }
 
 export function broadcastQueryClient({
diff --git a/src/core/queryObserver.ts b/src/core/queryObserver.ts
index 5a833f2c0..254a19446 100644
--- a/src/core/queryObserver.ts
+++ b/src/core/queryObserver.ts
@@ -71,6 +71,7 @@ export class QueryObserver<
   private previousSelectError: Error | null
   private staleTimeoutId?: number
   private refetchIntervalId?: number
+  private currentRefetchInterval?: number | false
   private trackedProps!: Array<keyof QueryObserverResult>
 
   constructor(
@@ -187,14 +188,16 @@ export class QueryObserver<
       this.updateStaleTimeout()
     }
 
+    const nextRefetchInterval = this.computeRefetchInterval()
+
     // Update refetch interval if needed
     if (
       mounted &&
       (this.currentQuery !== prevQuery ||
         this.options.enabled !== prevOptions.enabled ||
-        this.options.refetchInterval !== prevOptions.refetchInterval)
+        nextRefetchInterval !== this.currentRefetchInterval)
     ) {
-      this.updateRefetchInterval()
+      this.updateRefetchInterval(nextRefetchInterval)
     }
   }
 
@@ -365,13 +368,22 @@ export class QueryObserver<
     }, timeout)
   }
 
-  private updateRefetchInterval(): void {
+  private computeRefetchInterval() {
+    return typeof this.options.refetchInterval === 'function'
+      ? this.options.refetchInterval(this.currentResult.data, this.currentQuery)
+      : this.options.refetchInterval ?? false
+  }
+
+  private updateRefetchInterval(nextInterval: number | false): void {
     this.clearRefetchInterval()
 
+    this.currentRefetchInterval = nextInterval
+
     if (
       isServer ||
       this.options.enabled === false ||
-      !isValidTimeout(this.options.refetchInterval)
+      !isValidTimeout(this.currentRefetchInterval) ||
+      this.currentRefetchInterval === 0
     ) {
       return
     }
@@ -383,12 +395,12 @@ export class QueryObserver<
       ) {
         this.executeFetch()
       }
-    }, this.options.refetchInterval)
+    }, this.currentRefetchInterval)
   }
 
   private updateTimers(): void {
     this.updateStaleTimeout()
-    this.updateRefetchInterval()
+    this.updateRefetchInterval(this.computeRefetchInterval())
   }
 
   private clearTimers(): void {
diff --git a/src/core/types.ts b/src/core/types.ts
index 887a26adf..4de75759d 100644
--- a/src/core/types.ts
+++ b/src/core/types.ts
@@ -1,5 +1,5 @@
 import type { MutationState } from './mutation'
-import type { QueryBehavior } from './query'
+import type { QueryBehavior, Query } from './query'
 import type { RetryValue, RetryDelayValue } from './retryer'
 import type { QueryFilters } from './utils'
 
@@ -105,9 +105,16 @@ export interface QueryObserverOptions<
   staleTime?: number
   /**
    * If set to a number, the query will continuously refetch at this frequency in milliseconds.
+   * If set to a function, the function will be executed with the latest data and query to compute a frequency
    * Defaults to `false`.
    */
-  refetchInterval?: number | false
+  refetchInterval?:
+    | number
+    | false
+    | ((
+        data: TData | undefined,
+        query: Query<TQueryFnData, TError, TQueryData, TQueryKey>
+      ) => number | false)
   /**
    * If set to `true`, the query will continue to refetch while their tab/window is in the background.
    * Defaults to `false`.
diff --git a/src/core/utils.ts b/src/core/utils.ts
index f31a160e2..3e23dd357 100644
--- a/src/core/utils.ts
+++ b/src/core/utils.ts
@@ -87,7 +87,7 @@ export function functionalUpdate<TInput, TOutput>(
     : updater
 }
 
-export function isValidTimeout(value: any): value is number {
+export function isValidTimeout(value: unknown): value is number {
   return typeof value === 'number' && value >= 0 && value !== Infinity
 }
 


Test patch:
diff --git a/src/react/tests/useQuery.test.tsx b/src/react/tests/useQuery.test.tsx
index 997ba275f..c892f1f49 100644
--- a/src/react/tests/useQuery.test.tsx
+++ b/src/react/tests/useQuery.test.tsx
@@ -3520,6 +3520,97 @@ describe('useQuery', () => {
     await waitFor(() => rendered.getByText('count: 2'))
   })
 
+  it('should refetch in an interval depending on function result', async () => {
+    const key = queryKey()
+    let count = 0
+    const states: UseQueryResult<number>[] = []
+
+    function Page() {
+      const queryInfo = useQuery(key, () => count++, {
+        refetchInterval: (data = 0) => (data < 2 ? 10 : false),
+      })
+
+      states.push(queryInfo)
+
+      return <div>count: {queryInfo.data}</div>
+    }
+
+    const rendered = renderWithClient(queryClient, <Page />)
+
+    await waitFor(() => rendered.getByText('count: 2'))
+
+    expect(states.length).toEqual(6)
+
+    expect(states).toMatchObject([
+      {
+        status: 'loading',
+        isFetching: true,
+        data: undefined,
+      },
+      {
+        status: 'success',
+        isFetching: false,
+        data: 0,
+      },
+      {
+        status: 'success',
+        isFetching: true,
+        data: 0,
+      },
+      {
+        status: 'success',
+        isFetching: false,
+        data: 1,
+      },
+      {
+        status: 'success',
+        isFetching: true,
+        data: 1,
+      },
+      {
+        status: 'success',
+        isFetching: false,
+        data: 2,
+      },
+    ])
+  })
+
+  it('should not interval fetch with a refetchInterval of 0', async () => {
+    const key = queryKey()
+    const states: UseQueryResult<number>[] = []
+
+    function Page() {
+      const queryInfo = useQuery(key, () => 1, {
+        refetchInterval: 0,
+      })
+
+      states.push(queryInfo)
+
+      return <div>count: {queryInfo.data}</div>
+    }
+
+    const rendered = renderWithClient(queryClient, <Page />)
+
+    await waitFor(() => rendered.getByText('count: 1'))
+
+    await sleep(10) //extra sleep to make sure we're not re-fetching
+
+    expect(states.length).toEqual(2)
+
+    expect(states).toMatchObject([
+      {
+        status: 'loading',
+        isFetching: true,
+        data: undefined,
+      },
+      {
+        status: 'success',
+        isFetching: false,
+        data: 1,
+      },
+    ])
+  })
+
   it('should accept an empty string as query key', async () => {
     function Page() {
       const result = useQuery('', ctx => ctx.queryKey)