Repository: tannerlinsley/react-query
Pull request #1011 (2020-09-14 15:53:42)

feat: add always option to refetch options

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



Patch:
diff --git a/docs/src/pages/docs/api.md b/docs/src/pages/docs/api.md
index 32babb187..2efec1dfe 100644
--- a/docs/src/pages/docs/api.md
+++ b/docs/src/pages/docs/api.md
@@ -94,12 +94,24 @@ const queryInfo = useQuery({
 - `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
-- `refetchOnWindowFocus: Boolean`
+- `refetchOnMount: boolean | "always"`
   - Optional
-  - Set this to `true` or `false` to enable/disable automatic refetching on window focus for this query.
-- `refetchOnReconnect: Boolean`
+  - Defaults to `true`
+  - If set to `true`, the query will refetch on mount if the data is stale.
+  - If set to `false`, will disable additional instances of a query to trigger background refetches.
+  - If set to `"always"`, the query will always refetch on mount.
+- `refetchOnWindowFocus: boolean | "always"`
+  - Optional
+  - Defaults to `true`
+  - If set to `true`, the query will refetch on window focus if the data is stale.
+  - If set to `false`, the query will not refetch on window focus.
+  - If set to `"always"`, the query will always refetch on window focus.
+- `refetchOnReconnect: boolean | "always"`
   - Optional
-  - Set this to `true` or `false` to enable/disable automatic refetching on reconnect for this query.
+  - Defaults to `true`
+  - If set to `true`, the query will refetch on reconnect if the data is stale.
+  - If set to `false`, the query will not refetch on reconnect.
+  - If set to `"always"`, the query will always refetch on reconnect.
 - `notifyOnStatusChange: Boolean`
   - Optional
   - Set this to `false` to only re-render when there are changes to `data` or `error`.
@@ -134,10 +146,6 @@ const queryInfo = useQuery({
   - Optional
   - Defaults to `false`
   - Set this to `true` to always fetch when the component mounts (regardless of staleness).
-- `refetchOnMount: Boolean`
-  - Optional
-  - Defaults to `true`
-  - If set to `false`, will disable additional instances of a query to trigger background refetches
 - `queryFnParamsFilter: Function(args) => filteredArgs`
   - Optional
   - This function will filter the params that get passed to `queryFn`.
diff --git a/src/core/query.ts b/src/core/query.ts
index ea4ed582d..b8d50fa30 100644
--- a/src/core/query.ts
+++ b/src/core/query.ts
@@ -234,13 +234,19 @@ export class Query<TResult, TError> {
   onInteraction(type: 'focus' | 'online'): void {
     // Execute the first observer which is enabled,
     // stale and wants to refetch on this interaction.
-    const staleObserver = this.observers.find(
-      observer =>
-        observer.getCurrentResult().isStale &&
-        observer.config.enabled &&
-        ((observer.config.refetchOnWindowFocus && type === 'focus') ||
-          (observer.config.refetchOnReconnect && type === 'online'))
-    )
+    const staleObserver = this.observers.find(observer => {
+      const { config } = observer
+      const { isStale } = observer.getCurrentResult()
+      return (
+        config.enabled &&
+        ((type === 'focus' &&
+          (config.refetchOnWindowFocus === 'always' ||
+            (config.refetchOnWindowFocus && isStale))) ||
+          (type === 'online' &&
+            (config.refetchOnReconnect === 'always' ||
+              (config.refetchOnReconnect && isStale))))
+      )
+    })
 
     if (staleObserver) {
       staleObserver.fetch()
diff --git a/src/core/queryObserver.ts b/src/core/queryObserver.ts
index d46c2a4ae..cbe1366f9 100644
--- a/src/core/queryObserver.ts
+++ b/src/core/queryObserver.ts
@@ -42,7 +42,10 @@ export class QueryObserver<TResult, TError> {
     this.listener = listener
     this.currentQuery.subscribeObserver(this)
 
-    if (this.config.enabled && this.config.forceFetchOnMount) {
+    if (
+      this.config.enabled &&
+      (this.config.forceFetchOnMount || this.config.refetchOnMount === 'always')
+    ) {
       this.fetch()
     } else {
       this.optionalFetch()
diff --git a/src/core/types.ts b/src/core/types.ts
index bc30aa9ec..0f3f636d0 100644
--- a/src/core/types.ts
+++ b/src/core/types.ts
@@ -95,20 +95,26 @@ export interface QueryObserverConfig<
    */
   refetchIntervalInBackground?: boolean
   /**
-   * Set this to `true` or `false` to enable/disable automatic refetching on window focus for this query.
+   * If set to `true`, the query will refetch on window focus if the data is stale.
+   * If set to `false`, the query will not refetch on window focus.
+   * If set to `'always'`, the query will always refetch on window focus.
    * Defaults to `true`.
    */
-  refetchOnWindowFocus?: boolean
+  refetchOnWindowFocus?: boolean | 'always'
   /**
-   * Set this to `true` or `false` to enable/disable automatic refetching on reconnect for this query.
+   * If set to `true`, the query will refetch on reconnect if the data is stale.
+   * If set to `false`, the query will not refetch on reconnect.
+   * If set to `'always'`, the query will always refetch on reconnect.
    * Defaults to `true`.
    */
-  refetchOnReconnect?: boolean
+  refetchOnReconnect?: boolean | 'always'
   /**
+   * If set to `true`, the query will refetch on mount if the data is stale.
    * If set to `false`, will disable additional instances of a query to trigger background refetches.
+   * If set to `'always'`, the query will always refetch on mount.
    * Defaults to `true`.
    */
-  refetchOnMount?: boolean
+  refetchOnMount?: boolean | 'always'
   /**
    * Set this to `true` to always fetch when the component mounts (regardless of staleness).
    * Defaults to `false`.


Test patch:
diff --git a/src/react/tests/useQuery.test.tsx b/src/react/tests/useQuery.test.tsx
index 323839962..8420627a8 100644
--- a/src/react/tests/useQuery.test.tsx
+++ b/src/react/tests/useQuery.test.tsx
@@ -1129,6 +1129,171 @@ describe('useQuery', () => {
     expect(queryFn).not.toHaveBeenCalled()
   })
 
+  it('should not refetch stale query on focus when `refetchOnWindowFocus` is set to `false`', async () => {
+    const key = queryKey()
+    const states: QueryResult<number>[] = []
+    let count = 0
+
+    function Page() {
+      const state = useQuery(key, () => count++, {
+        staleTime: 0,
+        refetchOnWindowFocus: false,
+      })
+      states.push(state)
+      return null
+    }
+
+    render(<Page />)
+
+    await waitForMs(10)
+
+    act(() => {
+      window.dispatchEvent(new FocusEvent('focus'))
+    })
+
+    await waitForMs(10)
+
+    expect(states.length).toBe(2)
+    expect(states[0]).toMatchObject({ data: undefined, isFetching: true })
+    expect(states[1]).toMatchObject({ data: 0, isFetching: false })
+  })
+
+  it('should not refetch fresh query on focus when `refetchOnWindowFocus` is set to `true`', async () => {
+    const key = queryKey()
+    const states: QueryResult<number>[] = []
+    let count = 0
+
+    function Page() {
+      const state = useQuery(key, () => count++, {
+        staleTime: Infinity,
+        refetchOnWindowFocus: true,
+      })
+      states.push(state)
+      return null
+    }
+
+    render(<Page />)
+
+    await waitForMs(10)
+
+    act(() => {
+      window.dispatchEvent(new FocusEvent('focus'))
+    })
+
+    await waitForMs(10)
+
+    expect(states.length).toBe(2)
+    expect(states[0]).toMatchObject({ data: undefined, isFetching: true })
+    expect(states[1]).toMatchObject({ data: 0, isFetching: false })
+  })
+
+  it('should refetch fresh query on focus when `refetchOnWindowFocus` is set to `always`', async () => {
+    const key = queryKey()
+    const states: QueryResult<number>[] = []
+    let count = 0
+
+    function Page() {
+      const state = useQuery(key, () => count++, {
+        staleTime: Infinity,
+        refetchOnWindowFocus: 'always',
+      })
+      states.push(state)
+      return null
+    }
+
+    render(<Page />)
+
+    await waitForMs(10)
+
+    act(() => {
+      window.dispatchEvent(new FocusEvent('focus'))
+    })
+
+    await waitForMs(10)
+
+    expect(states.length).toBe(4)
+    expect(states[0]).toMatchObject({ data: undefined, isFetching: true })
+    expect(states[1]).toMatchObject({ data: 0, isFetching: false })
+    expect(states[2]).toMatchObject({ data: 0, isFetching: true })
+    expect(states[3]).toMatchObject({ data: 1, isFetching: false })
+  })
+
+  it('should refetch fresh query when refetchOnMount is set to always', async () => {
+    const key = queryKey()
+    const states: QueryResult<string>[] = []
+
+    await queryCache.prefetchQuery(key, () => 'prefetched')
+
+    function Page() {
+      const state = useQuery(key, () => 'data', {
+        refetchOnMount: 'always',
+        staleTime: Infinity,
+      })
+      states.push(state)
+      return null
+    }
+
+    render(<Page />)
+
+    await waitForMs(10)
+
+    expect(states.length).toBe(3)
+    expect(states[0]).toMatchObject({
+      data: 'prefetched',
+      isStale: false,
+      isFetching: false,
+    })
+    expect(states[1]).toMatchObject({
+      data: 'prefetched',
+      isStale: false,
+      isFetching: true,
+    })
+    expect(states[2]).toMatchObject({
+      data: 'data',
+      isStale: false,
+      isFetching: false,
+    })
+  })
+
+  it('should refetch stale query when refetchOnMount is set to true', async () => {
+    const key = queryKey()
+    const states: QueryResult<string>[] = []
+
+    await queryCache.prefetchQuery(key, () => 'prefetched')
+
+    await sleep(10)
+
+    function Page() {
+      const state = useQuery(key, () => 'data', {
+        refetchOnMount: true,
+        staleTime: 0,
+      })
+      states.push(state)
+      return null
+    }
+
+    render(<Page />)
+
+    await waitForMs(10)
+
+    expect(states.length).toBe(3)
+    expect(states[0]).toMatchObject({
+      data: 'prefetched',
+      isStale: true,
+      isFetching: false,
+    })
+    expect(states[1]).toMatchObject({
+      data: 'prefetched',
+      isStale: true,
+      isFetching: true,
+    })
+    expect(states[2]).toMatchObject({
+      data: 'data',
+      isStale: true,
+      isFetching: false,
+    })
+  })
+
   it('should set status to error if queryFn throws', async () => {
     const key = queryKey()
     const consoleMock = mockConsoleError()