Repository: tannerlinsley/react-query
Pull request #917 (2020-08-24 09:35:33)

Refactor query timeouts
This PR refactors internals, specifically how `scheduleStaleTimeout` and `scheduleGarbageCollection` works behind the scenes, but leaves the documented public API unaffected. An earlier version was begun in #728, but has been fleshed out here and made sense as it's own PR.

The main goal is to remove scheduling of timeouts from the constructor of Query and instead let the consumer creating a new Query mark when timeouts are ready to be scheduled. This is needed to implement hydration, but will also make it easier to remove scheduling of timeouts from the render-phase in `useBaseQuery` as a next step.

* Remove scheduling timeouts from `constructor`
* Require explicit timeout activation via new `query.activateTimeouts()`
  - This only applies internally and specifically anyone using `buildQuery`. This is an undocumented API, so should be okay to change?
* Refactor all internal code to use `activateTimeouts`
* Rename schedule(GC/StaleTimeout) to rescheduleX
* Make reschedule-methods do the right thing based on query state
  - Previously we relied on these methods being called only at the correct times, now they can be called safely at any time
  - In a sense, this refactor mirrors how state and effects work in React, they "sync the timeout-sideeffects to what the current state is"
  - This gathers all the logic for stale/GC in their respective functions instead of being spread out
* Stale timeout is now based on updatedAt
  - Besides being necessary for the "reschedule"-approach, this is also necessary for hydration.
  - This could schedule queries to be stale a few ms earlier than previously (equivalent to execution time between `updatedAt` is set to when the timeout is scheduled, which is a sync process). Docs makes no promises about exact details here so this should be fine since the meaning of `staleTime` is preserved.

All tests are passing, I'm open to suggestions for adding new ones if there is something we need to verify further.

I'm not sure about the naming of `activateTimeouts()`, it could also be a more generic `initialize()` that could contain any future side effects for setting up queries. It could also be just `rescheduleTimeouts()` which would reflect that it actually schedules those timeouts and that it's is fine to use repeatedly, but that would loose the pretty important semantics that no timeouts will be scheduled until this function is called for the first time.

This is just an undocumented internal name though and easy to change, where it's used now I think `activateTimeouts()` makes sense, but I'm very open to suggestions. 😄 

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: *In Progress*  
✅ Preview: *In Progress*



Patch:
diff --git a/docs/src/pages/docs/api.md b/docs/src/pages/docs/api.md
index 052bdcfb0..d673e48ef 100644
--- a/docs/src/pages/docs/api.md
+++ b/docs/src/pages/docs/api.md
@@ -29,6 +29,7 @@ const {
   cacheTime,
   keepPreviousData,
   refetchOnWindowFocus,
+  refetchOnReconnect,
   refetchInterval,
   refetchIntervalInBackground,
   queryFnParamsFilter,
@@ -92,6 +93,9 @@ const queryInfo = useQuery({
 - `refetchOnWindowFocus: Boolean`
   - Optional
   - Set this to `true` or `false` to enable/disable automatic refetching on window focus for this query.
+- `refetchOnReconnect: Boolean`
+  - Optional
+  - Set this to `true` or `false` to enable/disable automatic refetching on reconnect for this query.
 - `onSuccess: Function(data) => data`
   - Optional
   - This function will fire any time the query successfully fetches new data.
diff --git a/docs/src/pages/docs/comparison.md b/docs/src/pages/docs/comparison.md
index 911c3f338..e20f65b55 100644
--- a/docs/src/pages/docs/comparison.md
+++ b/docs/src/pages/docs/comparison.md
@@ -12,36 +12,36 @@ Feature/Capability Key:
 - 🔶 Supported and documented, but requires extra user-code to implement
 - 🛑 Not officially supported or documented.
 
-|                                              | React Query                            | SWR [(Website)](https://github.com/vercel/swr) | Apollo Client [(Website)](https://github.com/apollographql/apollo-client) |
-| -------------------------------------------- | -------------------------------------- | ---------------------------------------------- | ------------------------------------------------------------------------- |
-| Supported Protocol                           | HTTP                                   | HTTP                                           | GraphQL                                                                   |
-| Supported Query Signatures                   | Promise                                | Promise                                        | GraphQL Query                                                             |
-| Supported Query Keys                         | JSON                                   | JSON                                           | GraphQL Query                                                             |
-| Query Key Change Detection                   | Deep Compare (Serialization)           | Referential Equality (===)                     | Deep Compare (Serialization)                                              |
-| Query Data Memoization Level                 | Query + Structural Sharing             | Query                                          | Query + Entity + Structural Sharing                                       |
-| Bundle Size                                  | [![][bp-react-query]][bpl-react-query] | [![][bp-swr]][bpl-swr]                         | [![][bp-apollo]][bpl-apollo]                                              |
-| Queries                                      | ✅                                     | ✅                                             | ✅                                                                        |
-| Caching                                      | ✅                                     | ✅                                             | ✅                                                                        |
-| Devtools                                     | ✅                                     | 🟡                                             | ✅                                                                        |
-| Polling/Intervals                            | ✅                                     | ✅                                             | ✅                                                                        |
-| Parallel Queries                             | ✅                                     | ✅                                             | ✅                                                                        |
-| Dependent Queries                            | ✅                                     | ✅                                             | ✅                                                                        |
-| Paginated Queries                            | ✅                                     | 🛑<sup>1</sup>                                 | ✅                                                                        |
-| Infinite Queries                             | ✅                                     | ✅                                             | ✅                                                                        |
-| Initial Data                                 | ✅                                     | ✅                                             | ✅                                                                        |
-| Scroll Recovery                              | ✅                                     | ✅                                             | ✅                                                                        |
-| Cache Manipulation                           | ✅                                     | ✅                                             | ✅                                                                        |
-| Outdated Query Dismissal                     | ✅                                     | ✅                                             | ✅                                                                        |
-| Auto Garbage Collection                      | ✅                                     | 🛑                                             | 🛑                                                                        |
-| Mutation Hooks                               | ✅                                     | 🟡                                             | ✅                                                                        |
-| Prefetching APIs                             | ✅                                     | 🔶                                             | ✅                                                                        |
-| Query Cancellation                           | ✅                                     | 🛑                                             | 🛑                                                                        |
-| Partial Query Matching<sup>2</sup>           | ✅                                     | 🛑                                             | 🛑                                                                        |
-| Window Focus Refetching                      | ✅                                     | ✅                                             | 🛑                                                                        |
-| Network Status Refetching                    | 🛑                                     | ✅                                             | ✅                                                                        |
-| Automatic Refetch after Mutation<sup>3</sup> | 🔶                                     | 🔶                                             | ✅                                                                        |
-| Cache Dehydration/Rehydration                | 🛑                                     | 🛑                                             | ✅                                                                        |
-| React Suspense (Experimental)                | ✅                                     | ✅                                             | 🛑                                                                        |
+|                                              | React Query                            | SWR [_(Website)_](https://github.com/vercel/swr) | Apollo Client [_(Website)_](https://github.com/apollographql/apollo-client) |
+| -------------------------------------------- | -------------------------------------- | ------------------------------------------------ | --------------------------------------------------------------------------- |
+| Supported Protocol                           | HTTP                                   | HTTP                                             | GraphQL                                                                     |
+| Supported Query Signatures                   | Promise                                | Promise                                          | GraphQL Query                                                               |
+| Supported Query Keys                         | JSON                                   | JSON                                             | GraphQL Query                                                               |
+| Query Key Change Detection                   | Deep Compare (Serialization)           | Referential Equality (===)                       | Deep Compare (Serialization)                                                |
+| Query Data Memoization Level                 | Query + Structural Sharing             | Query                                            | Query + Entity + Structural Sharing                                         |
+| Bundle Size                                  | [![][bp-react-query]][bpl-react-query] | [![][bp-swr]][bpl-swr]                           | [![][bp-apollo]][bpl-apollo]                                                |
+| Queries                                      | ✅                                     | ✅                                               | ✅                                                                          |
+| Caching                                      | ✅                                     | ✅                                               | ✅                                                                          |
+| Devtools                                     | ✅                                     | 🟡                                               | ✅                                                                          |
+| Polling/Intervals                            | ✅                                     | ✅                                               | ✅                                                                          |
+| Parallel Queries                             | ✅                                     | ✅                                               | ✅                                                                          |
+| Dependent Queries                            | ✅                                     | ✅                                               | ✅                                                                          |
+| Paginated Queries                            | ✅                                     | 🛑<sup>1</sup>                                   | ✅                                                                          |
+| Infinite Queries                             | ✅                                     | ✅                                               | ✅                                                                          |
+| Initial Data                                 | ✅                                     | ✅                                               | ✅                                                                          |
+| Scroll Recovery                              | ✅                                     | ✅                                               | ✅                                                                          |
+| Cache Manipulation                           | ✅                                     | ✅                                               | ✅                                                                          |
+| Outdated Query Dismissal                     | ✅                                     | ✅                                               | ✅                                                                          |
+| Auto Garbage Collection                      | ✅                                     | 🛑                                               | 🛑                                                                          |
+| Mutation Hooks                               | ✅                                     | 🟡                                               | ✅                                                                          |
+| Prefetching APIs                             | ✅                                     | 🔶                                               | ✅                                                                          |
+| Query Cancellation                           | ✅                                     | 🛑                                               | 🛑                                                                          |
+| Partial Query Matching<sup>2</sup>           | ✅                                     | 🛑                                               | 🛑                                                                          |
+| Window Focus Refetching                      | ✅                                     | ✅                                               | 🛑                                                                          |
+| Network Status Refetching                    | ✅                                     | ✅                                               | ✅                                                                          |
+| Automatic Refetch after Mutation<sup>3</sup> | 🔶                                     | 🔶                                               | ✅                                                                          |
+| Cache Dehydration/Rehydration                | 🛑 (Coming Soon!)                      | 🛑                                               | ✅                                                                          |
+| React Suspense (Experimental)                | ✅                                     | ✅                                               | 🛑                                                                          |
 
 ### Notes
 
diff --git a/docs/src/pages/docs/guides/important-defaults.md b/docs/src/pages/docs/guides/important-defaults.md
index 9fd5036ea..61223a841 100644
--- a/docs/src/pages/docs/guides/important-defaults.md
+++ b/docs/src/pages/docs/guides/important-defaults.md
@@ -7,6 +7,6 @@ Out of the box, React Query is configured with **aggressive but sane** defaults.
 
 - Query results that are _currently rendered on the screen_ (via `useQuery` and similar hooks) will become "stale" immediately after they are resolved and will be refetched automatically in the background when they are rendered or used again. To change this, you can alter the default `staleTime` for queries to something other than `0` milliseconds.
 - Query results that become unused (all instances of the query are unmounted) will still be cached in case they are used again for a default of 5 minutes before they are garbage collected. To change this, you can alter the default `cacheTime` for queries to something other than `1000 * 60 * 5` milliseconds.
-- Stale queries will automatically be refetched in the background **when the browser window is refocused by the user**. You can disable this using the `refetchOnWindowFocus` option in queries or the global config.
+- Stale queries will automatically be refetched in the background **when the browser window is refocused by the user or when the browser reconnects**. You can disable this using the `refetchOnWindowFocus` and `refetchOnReconnect` options in queries or the global config.
 - Queries that fail will silently be retried **3 times, with exponential backoff delay** before capturing and displaying an error to the UI. To change this, you can alter the default `retry` and `retryDelay` options for queries to something other than `3` and the default exponential backoff function.
 - Query results by default are structurally shared to detect if data has actually changed and if not, the data reference remains unchanged to better help with value stabilization with regards to useMemo and useCallback. Structural sharing only works with JSON-compatible values, any other value types will always be considered as changed. If you are seeing performance issues because of large responses for example, you can disable this feature with the `config.structuralSharing` flag. If you are dealing with non-JSON compatible values in your query responses and still want to detect if data has changed or not, you can define a data compare function with `config.isDataEqual`.
diff --git a/src/core/config.ts b/src/core/config.ts
index 2fa181002..74a1cc71d 100644
--- a/src/core/config.ts
+++ b/src/core/config.ts
@@ -56,6 +56,7 @@ export const DEFAULT_CONFIG: ReactQueryConfig = {
     staleTime: 0,
     cacheTime: 5 * 60 * 1000,
     refetchOnWindowFocus: true,
+    refetchOnReconnect: true,
     refetchOnMount: true,
     structuralSharing: true,
   },
diff --git a/src/core/index.ts b/src/core/index.ts
index fdb7dc2b3..a4636e68f 100644
--- a/src/core/index.ts
+++ b/src/core/index.ts
@@ -1,13 +1,7 @@
 export { queryCache, queryCaches, makeQueryCache } from './queryCache'
 export { setFocusHandler } from './setFocusHandler'
-export {
-  CancelledError,
-  deepIncludes,
-  isCancelledError,
-  isError,
-  setConsole,
-  stableStringify,
-} from './utils'
+export { setOnlineHandler } from './setOnlineHandler'
+export { CancelledError, isCancelledError, isError, setConsole } from './utils'
 
 // Types
 export * from './types'
diff --git a/src/core/query.ts b/src/core/query.ts
index 9da09f017..3de938ad9 100644
--- a/src/core/query.ts
+++ b/src/core/query.ts
@@ -7,6 +7,7 @@ import {
   isCancelable,
   isCancelledError,
   isDocumentVisible,
+  isOnline,
   isServer,
   replaceEqualDeep,
   sleep,
@@ -120,6 +121,7 @@ export class Query<TResult, TError> {
   private continueFetch?: () => void
   private isTransportCancelable?: boolean
   private notifyGlobalListeners: (query: Query<TResult, TError>) => void
+  private enableTimeouts: boolean
 
   constructor(init: QueryInitConfig<TResult, TError>) {
     this.config = init.config
@@ -129,16 +131,13 @@ export class Query<TResult, TError> {
     this.notifyGlobalListeners = init.notifyGlobalListeners
     this.observers = []
     this.state = getDefaultState(init.config)
+    this.enableTimeouts = false
+  }
 
-    // If the query started with data, schedule
-    // a stale timeout
-    if (!isServer && this.state.data) {
-      this.scheduleStaleTimeout()
-
-      // Schedule for garbage collection in case
-      // nothing subscribes to this query
-      this.scheduleCacheTimeout()
-    }
+  activateTimeouts(): void {
+    this.enableTimeouts = true
+    this.rescheduleStaleTimeout()
+    this.rescheduleGarbageCollection()
   }
 
   updateConfig(config: QueryConfig<TResult, TError>): void {
@@ -151,20 +150,33 @@ export class Query<TResult, TError> {
     this.notifyGlobalListeners(this)
   }
 
-  private scheduleStaleTimeout(): void {
+  private rescheduleStaleTimeout(): void {
     if (isServer) {
       return
     }
 
     this.clearStaleTimeout()
 
-    if (this.state.isStale || this.config.staleTime === Infinity) {
+    if (
+      !this.enableTimeouts ||
+      this.state.isStale ||
+      this.state.status !== QueryStatus.Success ||
+      this.config.staleTime === Infinity
+    ) {
       return
     }
 
+    const staleTime = this.config.staleTime || 0
+    let timeout = staleTime
+    if (this.state.updatedAt) {
+      const timeElapsed = Date.now() - this.state.updatedAt
+      const timeUntilStale = staleTime - timeElapsed
+      timeout = Math.max(timeUntilStale, 0)
+    }
+
     this.staleTimeout = setTimeout(() => {
       this.invalidate()
-    }, this.config.staleTime)
+    }, timeout)
   }
 
   invalidate(): void {
@@ -177,14 +189,18 @@ export class Query<TResult, TError> {
     this.dispatch({ type: ActionType.MarkStale })
   }
 
-  private scheduleCacheTimeout(): void {
+  private rescheduleGarbageCollection(): void {
     if (isServer) {
       return
     }
 
     this.clearCacheTimeout()
 
-    if (this.config.cacheTime === Infinity) {
+    if (
+      !this.enableTimeouts ||
+      this.config.cacheTime === Infinity ||
+      this.observers.length > 0
+    ) {
       return
     }
 
@@ -208,7 +224,7 @@ export class Query<TResult, TError> {
     this.cancelFetch?.()
   }
 
-  continue(): void {
+  private continue(): void {
     this.continueFetch?.()
   }
 
@@ -261,10 +277,7 @@ export class Query<TResult, TError> {
       canFetchMore,
     })
 
-    if (!isStale) {
-      // Schedule a fresh invalidation!
-      this.scheduleStaleTimeout()
-    }
+    this.rescheduleStaleTimeout()
   }
 
   clear(): void {
@@ -280,12 +293,30 @@ export class Query<TResult, TError> {
     return this.observers.some(observer => observer.config.enabled)
   }
 
-  shouldRefetchOnWindowFocus(): boolean {
-    return (
-      this.isEnabled() &&
+  onWindowFocus(): void {
+    if (
       this.state.isStale &&
-      this.observers.some(observer => observer.config.refetchOnWindowFocus)
-    )
+      this.observers.some(
+        observer =>
+          observer.config.enabled && observer.config.refetchOnWindowFocus
+      )
+    ) {
+      this.fetch()
+    }
+    this.continue()
+  }
+
+  onOnline(): void {
+    if (
+      this.state.isStale &&
+      this.observers.some(
+        observer =>
+          observer.config.enabled && observer.config.refetchOnReconnect
+      )
+    ) {
+      this.fetch()
+    }
+    this.continue()
   }
 
   subscribe(
@@ -318,10 +349,9 @@ export class Query<TResult, TError> {
       if (this.isTransportCancelable) {
         this.cancel()
       }
-
-      // Schedule garbage collection
-      this.scheduleCacheTimeout()
     }
+
+    this.rescheduleGarbageCollection()
   }
 
   private async tryFetchData<T>(
@@ -410,8 +440,8 @@ export class Query<TResult, TError> {
           // Delay
           await sleep(functionalUpdate(retryDelay, failureCount) || 0)
 
-          // Pause retry if the document is not visible
-          if (!isDocumentVisible()) {
+          // Pause retry if the document is not visible or when the device is offline
+          if (!isDocumentVisible() || !isOnline()) {
             await new Promise(continueResolve => {
               continueLoop = continueResolve
             })
diff --git a/src/core/queryCache.ts b/src/core/queryCache.ts
index 267b79bae..d6ee1eac6 100644
--- a/src/core/queryCache.ts
+++ b/src/core/queryCache.ts
@@ -3,7 +3,9 @@ import {
   deepIncludes,
   functionalUpdate,
   getQueryArgs,
+  isDocumentVisible,
   isObject,
+  isOnline,
   isServer,
 } from './utils'
 import { getDefaultedQueryConfig } from './config'
@@ -300,11 +302,9 @@ export class QueryCache {
     // https://github.com/tannerlinsley/react-query/issues/652
     const configWithoutRetry = { retry: false, ...config }
 
+    let query
     try {
-      const query = this.buildQuery<TResult, TError>(
-        queryKey,
-        configWithoutRetry
-      )
+      query = this.buildQuery<TResult, TError>(queryKey, configWithoutRetry)
       if (options?.force || query.state.isStale) {
         await query.fetch()
       }
@@ -314,6 +314,14 @@ export class QueryCache {
         throw error
       }
       return
+    } finally {
+      if (query) {
+        // When prefetching, no observer is tied to the query,
+        // so to avoid immediate garbage collection of the still
+        // empty query, we wait with activating timeouts until
+        // the prefetch is done
+        query.activateTimeouts()
+      }
     }
   }
 
@@ -329,11 +337,13 @@ export class QueryCache {
       return
     }
 
-    this.buildQuery<TResult, TError>(queryKey, {
+    const newQuery = this.buildQuery<TResult, TError>(queryKey, {
       initialStale: typeof config?.staleTime === 'undefined',
       initialData: functionalUpdate(updater, undefined),
       ...config,
     })
+
+    newQuery.activateTimeouts()
   }
 }
 
@@ -344,3 +354,17 @@ export const queryCaches = [defaultQueryCache]
 export function makeQueryCache(config?: QueryCacheConfig) {
   return new QueryCache(config)
 }
+
+export function onVisibilityOrOnlineChange(isOnlineChange: boolean) {
+  if (isDocumentVisible() && isOnline()) {
+    queryCaches.forEach(queryCache => {
+      queryCache.getQueries(query => {
+        if (isOnlineChange) {
+          query.onOnline()
+        } else {
+          query.onWindowFocus()
+        }
+      })
+    })
+  }
+}
diff --git a/src/core/queryObserver.ts b/src/core/queryObserver.ts
index 5bfad4174..bed80161a 100644
--- a/src/core/queryObserver.ts
+++ b/src/core/queryObserver.ts
@@ -211,6 +211,8 @@ export class QueryObserver<TResult, TError> {
       return false
     }
 
+    newQuery.activateTimeouts()
+
     this.previousResult = this.currentResult
     this.currentQuery = newQuery
     this.currentResult = this.createResult()
diff --git a/src/core/setFocusHandler.ts b/src/core/setFocusHandler.ts
index 05fecb1c3..2fdb02795 100644
--- a/src/core/setFocusHandler.ts
+++ b/src/core/setFocusHandler.ts
@@ -1,49 +1,22 @@
-import { isOnline, isDocumentVisible, Console, isServer } from './utils'
-import { queryCaches } from './queryCache'
+import { createSetHandler, isServer } from './utils'
+import { onVisibilityOrOnlineChange } from './queryCache'
 
-type FocusHandler = () => void
+export const setFocusHandler = createSetHandler(() =>
+  onVisibilityOrOnlineChange(false)
+)
 
-const visibilityChangeEvent = 'visibilitychange'
-const focusEvent = 'focus'
-
-const onWindowFocus: FocusHandler = () => {
-  if (isDocumentVisible() && isOnline()) {
-    queryCaches.forEach(queryCache => {
-      // Continue any paused queries
-      queryCache.getQueries(query => {
-        query.continue()
-      })
-
-      // Invalidate queries which should refetch on window focus
-      queryCache
-        .invalidateQueries(query => query.shouldRefetchOnWindowFocus())
-        .catch(Console.error)
-    })
-  }
-}
-
-let removePreviousHandler: (() => void) | void
-
-export function setFocusHandler(callback: (callback: FocusHandler) => void) {
-  // Unsub the old watcher
-  if (removePreviousHandler) {
-    removePreviousHandler()
+setFocusHandler(handleFocus => {
+  if (isServer || !window?.addEventListener) {
+    return
   }
-  // Sub the new watcher
-  removePreviousHandler = callback(onWindowFocus)
-}
 
-setFocusHandler((handleFocus: FocusHandler) => {
   // Listen to visibillitychange and focus
-  if (!isServer && window?.addEventListener) {
-    window.addEventListener(visibilityChangeEvent, handleFocus, false)
-    window.addEventListener(focusEvent, handleFocus, false)
+  window.addEventListener('visibilitychange', handleFocus, false)
+  window.addEventListener('focus', handleFocus, false)
 
-    return () => {
-      // Be sure to unsubscribe if a new handler is set
-      window.removeEventListener(visibilityChangeEvent, handleFocus)
-      window.removeEventListener(focusEvent, handleFocus)
-    }
+  return () => {
+    // Be sure to unsubscribe if a new handler is set
+    window.removeEventListener('visibilitychange', handleFocus)
+    window.removeEventListener('focus', handleFocus)
   }
-  return
 })
diff --git a/src/core/setOnlineHandler.ts b/src/core/setOnlineHandler.ts
new file mode 100644
index 000000000..853bf08cb
--- /dev/null
+++ b/src/core/setOnlineHandler.ts
@@ -0,0 +1,20 @@
+import { createSetHandler, isServer } from './utils'
+import { onVisibilityOrOnlineChange } from './queryCache'
+
+export const setOnlineHandler = createSetHandler(() =>
+  onVisibilityOrOnlineChange(true)
+)
+
+setOnlineHandler(handleOnline => {
+  if (isServer || !window?.addEventListener) {
+    return
+  }
+
+  // Listen to online
+  window.addEventListener('online', handleOnline, false)
+
+  return () => {
+    // Be sure to unsubscribe if a new handler is set
+    window.removeEventListener('online', handleOnline)
+  }
+})
diff --git a/src/core/types.ts b/src/core/types.ts
index 1e8a7168e..4883d4068 100644
--- a/src/core/types.ts
+++ b/src/core/types.ts
@@ -1,4 +1,4 @@
-import type { Query, FetchMoreOptions, RefetchOptions } from './query';
+import type { Query, FetchMoreOptions, RefetchOptions } from './query'
 import type { QueryCache } from './queryCache'
 
 export type QueryKey =
@@ -95,6 +95,11 @@ export interface QueryObserverConfig<
    * Defaults to `true`.
    */
   refetchOnWindowFocus?: boolean
+  /**
+   * Set this to `true` or `false` to enable/disable automatic refetching on reconnect for this query.
+   * Defaults to `true`.
+   */
+  refetchOnReconnect?: boolean
   /**
    * If set to `false`, will disable additional instances of a query to trigger background refetches.
    * Defaults to `true`.
diff --git a/src/core/utils.ts b/src/core/utils.ts
index fdc9c9714..2d8cb6e24 100644
--- a/src/core/utils.ts
+++ b/src/core/utils.ts
@@ -235,3 +235,15 @@ export function getStatusProps<T extends QueryStatus>(status: T) {
     isIdle: status === QueryStatus.Idle,
   }
 }
+
+export function createSetHandler(fn: () => void) {
+  let removePreviousHandler: (() => void) | void
+  return (callback: (handler: () => void) => void) => {
+    // Unsub the old handler
+    if (removePreviousHandler) {
+      removePreviousHandler()
+    }
+    // Sub the new handler
+    removePreviousHandler = callback(fn)
+  }
+}


Test patch:
diff --git a/src/core/tests/queryCache.test.tsx b/src/core/tests/queryCache.test.tsx
index e5235c7c1..c84f3b2f2 100644
--- a/src/core/tests/queryCache.test.tsx
+++ b/src/core/tests/queryCache.test.tsx
@@ -3,6 +3,7 @@ import {
   queryKey,
   mockVisibilityState,
   mockConsoleError,
+  mockNavigatorOnLine,
 } from '../../react/tests/utils'
 import { makeQueryCache, queryCache as defaultQueryCache } from '..'
 import { isCancelledError, isError } from '../utils'
@@ -435,6 +436,54 @@ describe('queryCache', () => {
       expect(result).toBe('data3')
     })
 
+    it('should continue retry after reconnect and resolve all promises', async () => {
+      const key = queryKey()
+
+      mockNavigatorOnLine(false)
+
+      let count = 0
+      let result
+
+      const promise = defaultQueryCache.prefetchQuery(
+        key,
+        async () => {
+          count++
+
+          if (count === 3) {
+            return `data${count}`
+          }
+
+          throw new Error(`error${count}`)
+        },
+        {
+          retry: 3,
+          retryDelay: 1,
+        }
+      )
+
+      promise.then(data => {
+        result = data
+      })
+
+      // Check if we do not have a result
+      expect(result).toBeUndefined()
+
+      // Check if the query is really paused
+      await sleep(50)
+      expect(result).toBeUndefined()
+
+      // Reset navigator to original value
+      mockNavigatorOnLine(true)
+      window.dispatchEvent(new Event('online'))
+
+      // There should not be a result yet
+      expect(result).toBeUndefined()
+
+      // By now we should have a value
+      await sleep(50)
+      expect(result).toBe('data3')
+    })
+
     it('should throw a CancelledError when a paused query is cancelled', async () => {
       const key = queryKey()
 
diff --git a/src/react/tests/useQuery.test.tsx b/src/react/tests/useQuery.test.tsx
index 78a4e94e7..6e4966eb5 100644
--- a/src/react/tests/useQuery.test.tsx
+++ b/src/react/tests/useQuery.test.tsx
@@ -800,7 +800,10 @@ describe('useQuery', () => {
     queryFn.mockImplementation(() => 'data')
 
     const prefetchQueryFn = jest.fn()
-    prefetchQueryFn.mockImplementation(() => 'not yet...')
+    prefetchQueryFn.mockImplementation(async () => {
+      await sleep(10)
+      return 'not yet...'
+    })
 
     await queryCache.prefetchQuery(key, prefetchQueryFn, {
       staleTime: 1000,
@@ -809,7 +812,9 @@ describe('useQuery', () => {
     await sleep(0)
 
     function Page() {
-      useQuery(key, queryFn)
+      useQuery(key, queryFn, {
+        staleTime: 1000,
+      })
       return null
     }
 
diff --git a/src/react/tests/utils.tsx b/src/react/tests/utils.tsx
index 481b1836f..4e00ff1a3 100644
--- a/src/react/tests/utils.tsx
+++ b/src/react/tests/utils.tsx
@@ -7,6 +7,13 @@ export function mockVisibilityState(value: string) {
   })
 }
 
+export function mockNavigatorOnLine(value: boolean) {
+  Object.defineProperty(navigator, 'onLine', {
+    value,
+    configurable: true,
+  })
+}
+
 export function mockConsoleError() {
   const consoleMock = jest.spyOn(console, 'error')
   consoleMock.mockImplementation(() => undefined)