Repository: tannerlinsley/react-query
Pull request #840 (2020-08-06 15:40:58)

feat: add notify flag for controlling re-renders
Some applications might not have indicators in the UI to indicate if data is stale or if some query is refetching/revalidating. In these cases, the UI also does not need to re-render if any of these states change. It would be sufficient to only re-render when the actual `data` or `error` properties change. This flag resembles the `notifyOnNetworkStatusChange` flag from Apollo.

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 on the icon next to each commit.
tannerlinsley: Conflicts ;)
boschni: Resolved :)

Patch:
diff --git a/docs/src/pages/docs/api.md b/docs/src/pages/docs/api.md
index 8e6353187..ed71f8c25 100644
--- a/docs/src/pages/docs/api.md
+++ b/docs/src/pages/docs/api.md
@@ -7,40 +7,41 @@ title: API Reference
 
 ```js
 const {
-  status,
-  isIdle,
-  isLoading,
-  isSuccess,
-  isError,
+  clear,
   data,
   error,
-  isStale,
-  isFetching,
   failureCount,
+  isError,
+  isFetching,
+  isIdle,
+  isLoading,
+  isStale,
+  isSuccess,
   refetch,
-  clear,
+  status,
 } = useQuery(queryKey, queryFn?, {
-  suspense,
-  queryKeySerializerFn,
-  enabled,
-  retry,
-  retryDelay,
-  staleTime,
   cacheTime,
+  enabled,
+  initialData,
+  initialStale,
+  isDataEqual,
   keepPreviousData,
-  refetchOnWindowFocus,
-  refetchOnReconnect,
+  notifyOnStatusChange,
+  onError,
+  onSettled,
+  onSuccess,
+  queryFnParamsFilter,
+  queryKeySerializerFn,
   refetchInterval,
   refetchIntervalInBackground,
-  queryFnParamsFilter,
   refetchOnMount,
+  refetchOnReconnect,
+  refetchOnWindowFocus,
+  retry,
+  retryDelay,
+  staleTime,
   structuralSharing,
-  isDataEqual,
-  onError,
-  onSuccess,
-  onSettled,
-  initialData,
-  initialStale,
+  suspense,
   useErrorBoundary,
 })
 
@@ -96,6 +97,11 @@ const queryInfo = useQuery({
 - `refetchOnReconnect: Boolean`
   - Optional
   - Set this to `true` or `false` to enable/disable automatic refetching on reconnect for this query.
+- `notifyOnStatusChange: Boolean`
+  - Optional
+  - Whether a change to the query status should re-render a component.
+  - If set to `false`, the component will only re-render when the actual `data` or `error` changes.
+  - Defaults to `true`.
 - `onSuccess: Function(data) => data`
   - Optional
   - This function will fire any time the query successfully fetches new data.
@@ -792,13 +798,13 @@ setConsole({
 
 ## `hydration/dehydrate`
 
-`dehydrate` creates a frozen representation of a `queryCache` that can later be hydrated with `useHydrate`, `hydrate` or by passing it into `hydration/ReactQueryCacheProvider`. This is useful for passing prefetched queries from server to client or persisting queries to localstorage. It only includes currently successful queries by default.
+`dehydrate` creates a frozen representation of a `queryCache` that can later be hydrated with `useHydrate`, `hydrate` or `Hydrate`. This is useful for passing prefetched queries from server to client or persisting queries to localstorage. It only includes currently successful queries by default.
 
 ```js
 import { dehydrate } from 'react-query/hydration'
 
 const dehydratedState = dehydrate(queryCache, {
-  shouldDehydrate
+  shouldDehydrate,
 })
 ```
 
@@ -824,7 +830,7 @@ const dehydratedState = dehydrate(queryCache, {
 `hydrate` adds a previously dehydrated state into a `queryCache`. If the queries included in dehydration already exist in the cache, `hydrate` does not overwrite them.
 
 ```js
-import { hydrate } from 'react-query/hydration'
+import { hydrate } from 'react-query/hydration'
 
 hydrate(queryCache, dehydratedState)
 ```
@@ -854,31 +860,19 @@ useHydrate(dehydratedState)
   - **Required**
   - The state to hydrate
 
-## `hydration/ReactQueryCacheProvider`
+## `hydration/Hydrate`
 
-`hydration/ReactQueryCacheProvider` does the same thing as `ReactQueryCacheProvider` but also supports hydrating an initial state into the cache.
+`hydration/Hydrate` does the same thing as `useHydrate` but exposed as a component.
 
 ```js
-import { ReactQueryCacheProvider } from 'react-query/hydration'
+import { Hydrate } from 'react-query/hydration'
 
 function App() {
-  return (
-    <ReactQueryCacheProvider
-      queryCache={queryCache}
-      dehydratedState={dehydratedState}
-      hydrationConfig={hydrationConfig}>
-      ...
-    </ReactQueryCacheProvider>
-  )
+  return <Hydrate state={dehydratedState}>...</Hydrate>
 }
 ```
 
 **Options**
 
-- `queryCache: QueryCache`
-  - In instance of queryCache, you can use the `makeQueryCache` factory to create this.
-  - If not provided, a new cache will be generated.
-- `dehydratedState: DehydratedState`
+- `state: DehydratedState`
   - The state to hydrate
-- `hydrationConfig`
-  - Same config as for `hydrate`
diff --git a/docs/src/pages/docs/comparison.md b/docs/src/pages/docs/comparison.md
index 68c7985a0..53619eee3 100644
--- a/docs/src/pages/docs/comparison.md
+++ b/docs/src/pages/docs/comparison.md
@@ -19,7 +19,6 @@ Feature/Capability Key:
 | 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 |
-| Stale While Revalidate                       | Server-Side + Client-Side              | Server-Side                | None                                |
 | Bundle Size                                  | [![][bp-react-query]][bpl-react-query] | [![][bp-swr]][bpl-swr]     | [![][bp-apollo]][bpl-apollo]        |
 | Queries                                      | ✅                                     | ✅                         | ✅                                  |
 | Caching                                      | ✅                                     | ✅                         | ✅                                  |
@@ -38,6 +37,8 @@ Feature/Capability Key:
 | Prefetching APIs                             | ✅                                     | 🔶                         | ✅                                  |
 | Query Cancellation                           | ✅                                     | 🛑                         | 🛑                                  |
 | Partial Query Matching<sup>2</sup>           | ✅                                     | 🛑                         | 🛑                                  |
+| Stale While Revalidate                       | ✅                                     | ✅                         | 🛑                                  |
+| Stale Time Configuration                     | ✅                                     | 🛑                         | 🛑                                  |
 | Window Focus Refetching                      | ✅                                     | ✅                         | 🛑                                  |
 | Network Status Refetching                    | ✅                                     | ✅                         | ✅                                  |
 | Automatic Refetch after Mutation<sup>3</sup> | 🔶                                     | 🔶                         | ✅                                  |
diff --git a/docs/src/pages/docs/guides/ssr.md b/docs/src/pages/docs/guides/ssr.md
index cc1a30203..338ee2eb6 100644
--- a/docs/src/pages/docs/guides/ssr.md
+++ b/docs/src/pages/docs/guides/ssr.md
@@ -13,11 +13,11 @@ This approach works well for applications or user-specific pages that might cont
 
 React Query supports two ways of prefetching data on the server and passing that to the client.
 
-* Prefetch the data yourself and pass it in as `initialData`
-  * Quick to set up for simple cases
-  * Has some caveats
-* Prefetch the query via React Query and use de/rehydration
-  * Requires slightly more setup up front
+- Prefetch the data yourself and pass it in as `initialData`
+  - Quick to set up for simple cases
+  - Has some caveats
+- Prefetch the query via React Query and use de/rehydration
+  - Requires slightly more setup up front
 
 The exact implementation of these mechanisms may vary from platform to platform, but we recommend starting with Next.js which supports [2 forms of pre-rendering](https://nextjs.org/docs/basic-features/data-fetching):
 
@@ -45,9 +45,9 @@ function Posts(props) {
 
 The setup is minimal and this can be a perfect solution for some cases, but there are a few tradeoffs compared to the full approach:
 
-* If you are calling `useQuery` in a component deeper down in the tree you need to pass the `initialData` down to that point
-* If you are calling `useQuery` with the same query in multiple locations, you need to pass `initialData` to all of them
-* There is no way to know at what time the query was fetched on the server, so `updatedAt` and determining if the query needs refetching is based on when the page loaded instead
+- If you are calling `useQuery` in a component deeper down in the tree you need to pass the `initialData` down to that point
+- If you are calling `useQuery` with the same query in multiple locations, you need to pass `initialData` to all of them
+- There is no way to know at what time the query was fetched on the server, so `updatedAt` and determining if the query needs refetching is based on when the page loaded instead
 
 ## Prefetch the query via React Query and use de/rehydration
 
@@ -55,18 +55,19 @@ React Query supports prefetching a query on the server and handing off or _dehyd
 
 ### Integrating with Next.js
 
-To support caching queries on the server and set up hydration, you start with wrapping your application with `<ReactQueryCacheProvider>` in `_app.js`.
-
-> Note: You need to import `ReactQueryCacheProvider` from `'react-query/hydration'` for it to support hydration!
+To support caching queries on the server and set up hydration, you start with wrapping your application with `<ReactQueryCacheProvider>` and `<Hydrate>` in `_app.js`.
 
 ```jsx
 // _app.jsx
-import { ReactQueryCacheProvider } from 'react-query/hydration'
+import { ReactQueryCacheProvider } from 'react-query'
+import { Hydrate } from 'react-query/hydration'
 
 export default function MyApp({ Component, pageProps }) {
   return (
-    <ReactQueryCacheProvider dehydratedState={pageProps.dehydratedState}>
-      <Component {...pageProps} />
+    <ReactQueryCacheProvider>
+      <Hydrate state={pageProps.dehydratedState}>
+        <Component {...pageProps} />
+      </Hydrate>
     </ReactQueryCacheProvider>
   )
 }
@@ -86,8 +87,8 @@ export async function getStaticProps() {
 
   return {
     props: {
-      dehydratedState: dehydrate(queryCache)
-    }
+      dehydratedState: dehydrate(queryCache),
+    },
   }
 }
 
@@ -119,7 +120,7 @@ Since there are many different possible setups for SSR, it's hard to give a deta
   - Call `prefetchQueryCache.prefetchQuery(...)` to prefetch queries
   - Dehydrate by using `const dehydratedState = dehydrate(prefetchQueryCache)`
 - Render
-  - Wrap the app in `<ReactQueryCacheProvider>` from `'react-query/hydration'` and pass in `dehydratedState`
+  - Wrap the app in `<ReactQueryCacheProvider>` and `<Hydrate>` to create a new cache and hydrate the state
     - This makes sure a separate `queryCache` is created specifically for rendering
     - **Do not** pass in the `prefetchQueryCache` from the last step, the server and client both needs to render from the dehydrated data to avoid React hydration mismatches. This is because queries with errors are excluded from dehydration by default.
 - Serialize and embed `dehydratedState` in the markup
@@ -129,7 +130,7 @@ Since there are many different possible setups for SSR, it's hard to give a deta
 
 - Parse `dehydratedState` from where you put it in the markup
 - Render
-  - Wrap the app in `<ReactQueryCacheProvider>` from `'react-query/hydration'` and pass in `dehydratedState`
+  - Wrap the app in `<Hydrate>` from `'react-query/hydration'` and pass in the dehyrated state.
 
 This list aims to be exhaustive, but depending on your current setup, the above steps can take more or less work. Here is a barebones example:
 
@@ -140,15 +141,19 @@ await prefetchCache.prefetchQuery('key', fn)
 const dehydratedState = dehydrate(prefetchCache)
 
 const html = ReactDOM.renderToString(
-  <ReactQueryCacheProvider dehydratedState={dehydratedState}>
-    <App />
+  <ReactQueryCacheProvider>
+    <Hydrate state={dehyratedState}>
+      <App />
+    </Hydrate>
   </ReactQueryCacheProvider>
 )
 res.send(`
 <html>
   <body>
     <div id="app">${html}</div>
-    <script>window.__REACT_QUERY_INITIAL_QUERIES__ = ${JSON.stringify(dehydratedState)};</script>
+    <script>window.__REACT_QUERY_INITIAL_QUERIES__ = ${JSON.stringify(
+      dehydratedState
+    )};</script>
   </body>
 </html>
 `)
@@ -156,8 +161,10 @@ res.send(`
 // Client
 const dehydratedState = JSON.parse(window.__REACT_QUERY_INITIAL_QUERIES__)
 ReactDOM.hydrate(
-  <ReactQueryCacheProvider dehydratedState={dehydratedState}>
-    <App />
+  <ReactQueryCacheProvider>
+    <Hydrate state={dehyratedState}>
+      <App />
+    </Hydrate>
   </ReactQueryCacheProvider>
 )
 ```
diff --git a/package.json b/package.json
index d80fb026c..93825fa3a 100644
--- a/package.json
+++ b/package.json
@@ -41,7 +41,8 @@
   "files": [
     "dist",
     "types",
-    "scripts"
+    "scripts",
+    "hydration.js"
   ],
   "dependencies": {},
   "peerDependencies": {
diff --git a/src/core/config.ts b/src/core/config.ts
index 55b639c20..801a2cb26 100644
--- a/src/core/config.ts
+++ b/src/core/config.ts
@@ -60,6 +60,7 @@ export const DEFAULT_CONFIG: ReactQueryConfig = {
     refetchOnWindowFocus: true,
     refetchOnReconnect: true,
     refetchOnMount: true,
+    notifyOnStatusChange: true,
     structuralSharing: true,
   },
 }
diff --git a/src/core/query.ts b/src/core/query.ts
index eaad23a61..e731472aa 100644
--- a/src/core/query.ts
+++ b/src/core/query.ts
@@ -104,6 +104,7 @@ export class Query<TResult, TError> {
   config: QueryConfig<TResult, TError>
   observers: QueryObserver<TResult, TError>[]
   state: QueryState<TResult, TError>
+  cacheTime: number
 
   private queryCache: QueryCache
   private promise?: Promise<TResult | undefined>
@@ -121,16 +122,22 @@ export class Query<TResult, TError> {
     this.notifyGlobalListeners = init.notifyGlobalListeners
     this.observers = []
     this.state = getDefaultState(init.config)
+    this.cacheTime = init.config.cacheTime!
     this.scheduleGc()
   }
 
-  updateConfig(config: QueryConfig<TResult, TError>): void {
+  private updateConfig(config: QueryConfig<TResult, TError>): void {
     this.config = config
+    this.cacheTime = Math.max(this.cacheTime, config.cacheTime || 0)
   }
 
   private dispatch(action: Action<TResult, TError>): void {
     this.state = queryReducer(this.state, action)
-    this.observers.forEach(d => d.onQueryUpdate(this.state, action))
+
+    this.observers.forEach(observer => {
+      observer.onQueryUpdate(this.state, action)
+    })
+
     this.notifyGlobalListeners(this)
   }
 
@@ -141,18 +148,21 @@ export class Query<TResult, TError> {
 
     this.clearGcTimeout()
 
-    if (this.config.cacheTime === Infinity || this.observers.length > 0) {
+    if (this.cacheTime === Infinity || this.observers.length > 0) {
       return
     }
 
     this.gcTimeout = setTimeout(() => {
       this.clear()
-    }, this.config.cacheTime)
+    }, this.cacheTime)
   }
 
-  async refetch(options?: RefetchOptions): Promise<TResult | undefined> {
+  async refetch(
+    options?: RefetchOptions,
+    config?: QueryConfig<TResult, TError>
+  ): Promise<TResult | undefined> {
     try {
-      return await this.fetch()
+      return await this.fetch(undefined, config)
     } catch (error) {
       if (options?.throwOnError === true) {
         throw error
@@ -210,11 +220,13 @@ export class Query<TResult, TError> {
   }
 
   clear(): void {
+    this.queryCache.removeQuery(this)
+  }
+
+  destroy(): void {
     this.clearGcTimeout()
     this.clearTimersObservers()
     this.cancel()
-    delete this.queryCache.queries[this.queryHash]
-    this.notifyGlobalListeners(this)
   }
 
   isEnabled(): boolean {
@@ -294,122 +306,24 @@ export class Query<TResult, TError> {
     this.scheduleGc()
   }
 
-  private async tryFetchData<T>(
-    config: QueryConfig<TResult, TError>,
-    fn: QueryFunction<T>
-  ): Promise<T> {
-    return new Promise<T>((outerResolve, outerReject) => {
-      let resolved = false
-      let continueLoop: () => void
-      let cancelTransport: () => void
-
-      const done = () => {
-        resolved = true
-
-        delete this.cancelFetch
-        delete this.continueFetch
-        delete this.isTransportCancelable
-
-        // End loop if currently paused
-        continueLoop?.()
-      }
-
-      const resolve = (value: any) => {
-        done()
-        outerResolve(value)
-      }
-
-      const reject = (value: any) => {
-        done()
-        outerReject(value)
-      }
-
-      // Create callback to cancel this fetch
-      this.cancelFetch = () => {
-        reject(new CancelledError())
-        try {
-          cancelTransport?.()
-        } catch {}
-      }
-
-      // Create callback to continue this fetch
-      this.continueFetch = () => {
-        continueLoop?.()
-      }
-
-      // Create loop function
-      const run = async () => {
-        try {
-          // Execute query
-          const promiseOrValue = fn()
-
-          // Check if the transport layer support cancellation
-          if (isCancelable(promiseOrValue)) {
-            cancelTransport = () => {
-              promiseOrValue.cancel()
-            }
-            this.isTransportCancelable = true
-          }
-
-          // Await data
-          resolve(await promiseOrValue)
-        } catch (error) {
-          // Stop if the fetch is already resolved
-          if (resolved) {
-            return
-          }
-
-          // Do we need to retry the request?
-          const { failureCount } = this.state
-          const { retry, retryDelay } = config
-
-          const shouldRetry =
-            retry === true ||
-            failureCount < retry! ||
-            (typeof retry === 'function' && retry(failureCount, error))
-
-          if (!shouldRetry) {
-            // We are done if the query does not need to be retried
-            reject(error)
-            return
-          }
-
-          // Increase the failureCount
-          this.dispatch({ type: ActionType.Failed })
-
-          // Delay
-          await sleep(functionalUpdate(retryDelay, failureCount) || 0)
-
-          // Pause retry if the document is not visible or when the device is offline
-          if (!isDocumentVisible() || !isOnline()) {
-            await new Promise(continueResolve => {
-              continueLoop = continueResolve
-            })
-          }
-
-          // Try again if not resolved yet
-          if (!resolved) {
-            run()
-          }
-        }
-      }
-
-      // Start loop
-      run()
-    })
-  }
-
-  async fetch(options?: FetchOptions): Promise<TResult | undefined> {
+  async fetch(
+    options?: FetchOptions,
+    config?: QueryConfig<TResult, TError>
+  ): Promise<TResult | undefined> {
     // If we are already fetching, return current promise
     if (this.promise) {
       return this.promise
     }
 
-    // Store reference to the config that initiated this fetch
-    const config = this.config
+    // Update config if passed, otherwise the config from the last execution is used
+    if (config) {
+      this.updateConfig(config)
+    }
+
+    config = this.config
 
     // Check if there is a query function
-    if (!config.queryFn) {
+    if (typeof config.queryFn !== 'function') {
       return
     }
 
@@ -530,16 +444,125 @@ export class Query<TResult, TError> {
     return this.tryFetchData(config, fetchData)
   }
 
+  private async tryFetchData<T>(
+    config: QueryConfig<TResult, TError>,
+    fn: QueryFunction<T>
+  ): Promise<T> {
+    return new Promise<T>((outerResolve, outerReject) => {
+      let resolved = false
+      let continueLoop: () => void
+      let cancelTransport: () => void
+
+      const done = () => {
+        resolved = true
+
+        delete this.cancelFetch
+        delete this.continueFetch
+        delete this.isTransportCancelable
+
+        // End loop if currently paused
+        continueLoop?.()
+      }
+
+      const resolve = (value: any) => {
+        done()
+        outerResolve(value)
+      }
+
+      const reject = (value: any) => {
+        done()
+        outerReject(value)
+      }
+
+      // Create callback to cancel this fetch
+      this.cancelFetch = () => {
+        reject(new CancelledError())
+        try {
+          cancelTransport?.()
+        } catch {}
+      }
+
+      // Create callback to continue this fetch
+      this.continueFetch = () => {
+        continueLoop?.()
+      }
+
+      // Create loop function
+      const run = async () => {
+        try {
+          // Execute query
+          const promiseOrValue = fn()
+
+          // Check if the transport layer support cancellation
+          if (isCancelable(promiseOrValue)) {
+            cancelTransport = () => {
+              promiseOrValue.cancel()
+            }
+            this.isTransportCancelable = true
+          }
+
+          // Await data
+          resolve(await promiseOrValue)
+        } catch (error) {
+          // Stop if the fetch is already resolved
+          if (resolved) {
+            return
+          }
+
+          // Do we need to retry the request?
+          const { failureCount } = this.state
+          const { retry, retryDelay } = config
+
+          const shouldRetry =
+            retry === true ||
+            failureCount < retry! ||
+            (typeof retry === 'function' && retry(failureCount, error))
+
+          if (!shouldRetry) {
+            // We are done if the query does not need to be retried
+            reject(error)
+            return
+          }
+
+          // Increase the failureCount
+          this.dispatch({ type: ActionType.Failed })
+
+          // Delay
+          await sleep(functionalUpdate(retryDelay, failureCount) || 0)
+
+          // Pause retry if the document is not visible or when the device is offline
+          if (!isDocumentVisible() || !isOnline()) {
+            await new Promise(continueResolve => {
+              continueLoop = continueResolve
+            })
+          }
+
+          // Try again if not resolved yet
+          if (!resolved) {
+            run()
+          }
+        }
+      }
+
+      // Start loop
+      run()
+    })
+  }
+
   fetchMore(
     fetchMoreVariable?: unknown,
-    options?: FetchMoreOptions
+    options?: FetchMoreOptions,
+    config?: QueryConfig<TResult, TError>
   ): Promise<TResult | undefined> {
-    return this.fetch({
-      fetchMore: {
-        fetchMoreVariable,
-        previous: options?.previous || false,
+    return this.fetch(
+      {
+        fetchMore: {
+          fetchMoreVariable,
+          previous: options?.previous || false,
+        },
       },
-    })
+      config
+    )
   }
 }
 
diff --git a/src/core/queryCache.ts b/src/core/queryCache.ts
index 3c8c63dde..17fd17cc3 100644
--- a/src/core/queryCache.ts
+++ b/src/core/queryCache.ts
@@ -68,11 +68,12 @@ type QueryCacheListener = (
 // CLASS
 
 export class QueryCache {
-  queries: QueryHashMap
   isFetching: number
 
   private config: QueryCacheConfig
   private globalListeners: QueryCacheListener[]
+  private queries: QueryHashMap
+  private queriesArray: Query<any, any>[]
 
   constructor(config?: QueryCacheConfig) {
     this.config = config || {}
@@ -81,16 +82,19 @@ export class QueryCache {
     this.globalListeners = []
 
     this.queries = {}
+    this.queriesArray = []
     this.isFetching = 0
   }
 
   private notifyGlobalListeners(query?: Query<any, any>) {
-    this.isFetching = Object.values(this.queries).reduce(
+    this.isFetching = this.getQueries().reduce(
       (acc, query) => (query.state.isFetching ? acc + 1 : acc),
       0
     )
 
-    this.globalListeners.forEach(d => d(this, query))
+    this.globalListeners.forEach(listener => {
+      listener(this, query)
+    })
   }
 
   getDefaultConfig() {
@@ -108,24 +112,23 @@ export class QueryCache {
   subscribe(listener: QueryCacheListener): () => void {
     this.globalListeners.push(listener)
     return () => {
-      this.globalListeners.splice(this.globalListeners.indexOf(listener), 1)
+      this.globalListeners = this.globalListeners.filter(x => x !== listener)
     }
   }
 
   clear(options?: ClearOptions): void {
-    Object.values(this.queries).forEach(query => query.clear())
-    this.queries = {}
+    this.removeQueries()
     if (options?.notify) {
       this.notifyGlobalListeners()
     }
   }
 
   getQueries<TResult = unknown, TError = unknown>(
-    predicate: QueryPredicate,
+    predicate?: QueryPredicate,
     options?: QueryPredicateOptions
   ): Query<TResult, TError>[] {
-    if (predicate === true) {
-      return Object.values(this.queries)
+    if (predicate === true || typeof predicate === 'undefined') {
+      return this.queriesArray
     }
 
     let predicateFn: QueryPredicateFn
@@ -142,7 +145,7 @@ export class QueryCache {
           : deepIncludes(d.queryKey, queryKey)
     }
 
-    return Object.values(this.queries).filter(predicateFn)
+    return this.queriesArray.filter(predicateFn)
   }
 
   getQuery<TResult, TError = unknown>(
@@ -155,22 +158,35 @@ export class QueryCache {
     return this.getQuery<TResult>(predicate)?.state.data
   }
 
+  removeQuery(query: Query<any, any>): void {
+    if (this.queries[query.queryHash]) {
+      query.destroy()
+      delete this.queries[query.queryHash]
+      this.queriesArray = this.queriesArray.filter(x => x !== query)
+      this.notifyGlobalListeners(query)
+    }
+  }
+
   removeQueries(
-    predicate: QueryPredicate,
+    predicate?: QueryPredicate,
     options?: QueryPredicateOptions
   ): void {
-    this.getQueries(predicate, options).forEach(query => query.clear())
+    this.getQueries(predicate, options).forEach(query => {
+      this.removeQuery(query)
+    })
   }
 
   cancelQueries(
-    predicate: QueryPredicate,
+    predicate?: QueryPredicate,
     options?: QueryPredicateOptions
   ): void {
-    this.getQueries(predicate, options).forEach(query => query.cancel())
+    this.getQueries(predicate, options).forEach(query => {
+      query.cancel()
+    })
   }
 
   async invalidateQueries(
-    predicate: QueryPredicate,
+    predicate?: QueryPredicate,
     options?: InvalidateQueriesOptions
   ): Promise<void> {
     const { refetchActive = true, refetchInactive = false, throwOnError } =
@@ -179,14 +195,10 @@ export class QueryCache {
     try {
       await Promise.all(
         this.getQueries(predicate, options).map(query => {
-          if (query.observers.length) {
-            if (refetchActive && query.isEnabled()) {
-              return query.fetch()
-            }
-          } else {
-            if (refetchInactive) {
-              return query.fetch()
-            }
+          const enabled = query.isEnabled()
+
+          if ((enabled && refetchActive) || (!enabled && refetchInactive)) {
+            return query.fetch()
           }
 
           return undefined
@@ -200,7 +212,7 @@ export class QueryCache {
   }
 
   resetErrorBoundaries(): void {
-    this.getQueries(true).forEach(query => {
+    this.getQueries().forEach(query => {
       query.state.throwInErrorBoundary = false
     })
   }
@@ -210,31 +222,26 @@ export class QueryCache {
     queryConfig?: QueryConfig<TResult, TError>
   ): Query<TResult, TError> {
     const config = this.getDefaultedQueryConfig(queryConfig)
-
     const [queryHash, queryKey] = config.queryKeySerializerFn!(userQueryKey)
 
-    let query
-
     if (this.queries[queryHash]) {
-      query = this.queries[queryHash] as Query<TResult, TError>
-      query.updateConfig(config)
+      return this.queries[queryHash] as Query<TResult, TError>
     }
 
-    if (!query) {
-      query = new Query<TResult, TError>({
-        queryCache: this,
-        queryKey,
-        queryHash,
-        config,
-        notifyGlobalListeners: query => {
-          this.notifyGlobalListeners(query)
-        },
-      })
-
-      if (!this.config.frozen) {
-        this.queries[queryHash] = query
+    const query = new Query<TResult, TError>({
+      queryCache: this,
+      queryKey,
+      queryHash,
+      config,
+      notifyGlobalListeners: query => {
         this.notifyGlobalListeners(query)
-      }
+      },
+    })
+
+    if (!this.config.frozen) {
+      this.queries[queryHash] = query
+      this.queriesArray.push(query)
+      this.notifyGlobalListeners(query)
     }
 
     return query
@@ -300,20 +307,22 @@ export class QueryCache {
     >(args)
 
     // https://github.com/tannerlinsley/react-query/issues/652
-    const configWithoutRetry = { retry: false, ...config }
+    const configWithoutRetry = this.getDefaultedQueryConfig({
+      retry: false,
+      ...config,
+    })
 
     let query
     try {
       query = this.buildQuery<TResult, TError>(queryKey, configWithoutRetry)
       if (options?.force || query.isStaleByTime(config.staleTime)) {
-        await query.fetch()
+        await query.fetch(undefined, configWithoutRetry)
       }
       return query.state.data
     } catch (error) {
       if (options?.throwOnError) {
         throw error
       }
-      return
     }
   }
 
@@ -348,7 +357,7 @@ export function makeQueryCache(config?: QueryCacheConfig) {
 export function onVisibilityOrOnlineChange(isOnlineChange: boolean) {
   if (isDocumentVisible() && isOnline()) {
     queryCaches.forEach(queryCache => {
-      queryCache.getQueries(query => {
+      queryCache.getQueries().forEach(query => {
         if (isOnlineChange) {
           query.onOnline()
         } else {
diff --git a/src/core/queryObserver.ts b/src/core/queryObserver.ts
index 505308ed0..e8cb6c5f4 100644
--- a/src/core/queryObserver.ts
+++ b/src/core/queryObserver.ts
@@ -7,6 +7,7 @@ import type {
   FetchMoreOptions,
   RefetchOptions,
 } from './query'
+import type { QueryCache } from './queryCache'
 
 export type UpdateListener<TResult, TError> = (
   result: QueryResult<TResult, TError>
@@ -15,6 +16,7 @@ export type UpdateListener<TResult, TError> = (
 export class QueryObserver<TResult, TError> {
   config: QueryObserverConfig<TResult, TError>
 
+  private queryCache: QueryCache
   private currentQuery!: Query<TResult, TError>
   private currentResult!: QueryResult<TResult, TError>
   private previousResult?: QueryResult<TResult, TError>
@@ -25,6 +27,7 @@ export class QueryObserver<TResult, TError> {
 
   constructor(config: QueryObserverConfig<TResult, TError>) {
     this.config = config
+    this.queryCache = config.queryCache!
 
     // Bind exposed methods
     this.clear = this.clear.bind(this)
@@ -106,22 +109,19 @@ export class QueryObserver<TResult, TError> {
   }
 
   async refetch(options?: RefetchOptions): Promise<TResult | undefined> {
-    this.currentQuery.updateConfig(this.config)
-    return this.currentQuery.refetch(options)
+    return this.currentQuery.refetch(options, this.config)
   }
 
   async fetchMore(
     fetchMoreVariable?: unknown,
     options?: FetchMoreOptions
   ): Promise<TResult | undefined> {
-    this.currentQuery.updateConfig(this.config)
-    return this.currentQuery.fetchMore(fetchMoreVariable, options)
+    return this.currentQuery.fetchMore(fetchMoreVariable, options, this.config)
   }
 
   async fetch(): Promise<TResult | undefined> {
-    this.currentQuery.updateConfig(this.config)
     try {
-      return await this.currentQuery.fetch()
+      return await this.currentQuery.fetch(undefined, this.config)
     } catch (error) {
       return undefined
     }
@@ -217,22 +217,16 @@ export class QueryObserver<TResult, TError> {
   }
 
   private createResult(): QueryResult<TResult, TError> {
-    const { currentResult, currentQuery, previousResult, config } = this
-
-    const {
-      canFetchMore,
-      error,
-      failureCount,
-      isFetched,
-      isFetching,
-      isFetchingMore,
-      isLoading,
-    } = currentQuery.state
-
-    let { data, status, updatedAt } = currentQuery.state
+    const { currentQuery, currentResult, previousResult, config } = this
+    const { state } = currentQuery
+    let { data, status, updatedAt } = state
 
     // Keep previous data if needed
-    if (config.keepPreviousData && isLoading && previousResult?.isSuccess) {
+    if (
+      config.keepPreviousData &&
+      state.isLoading &&
+      previousResult?.isSuccess
+    ) {
       data = previousResult.data
       updatedAt = previousResult.updatedAt
       status = previousResult.status
@@ -256,15 +250,15 @@ export class QueryObserver<TResult, TError> {
 
     return {
       ...getStatusProps(status),
-      canFetchMore,
+      canFetchMore: state.canFetchMore,
       clear: this.clear,
       data,
-      error,
-      failureCount,
+      error: state.error,
+      failureCount: state.failureCount,
       fetchMore: this.fetchMore,
-      isFetched,
-      isFetching,
-      isFetchingMore,
+      isFetched: state.isFetched,
+      isFetching: state.isFetching,
+      isFetchingMore: state.isFetchingMore,
       isStale,
       query: currentQuery,
       refetch: this.refetch,
@@ -277,11 +271,12 @@ export class QueryObserver<TResult, TError> {
 
     // Remove the initial data when there is an existing query
     // because this data should not be used for a new query
-    const config = prevQuery
-      ? { ...this.config, initialData: undefined }
-      : this.config
+    const config =
+      this.config.keepPreviousData && prevQuery
+        ? { ...this.config, initialData: undefined }
+        : this.config
 
-    const newQuery = config.queryCache!.buildQuery(config.queryKey, config)
+    const newQuery = this.queryCache.buildQuery(config.queryKey, config)
 
     if (newQuery === prevQuery) {
       return false
@@ -303,20 +298,35 @@ export class QueryObserver<TResult, TError> {
     _state: QueryState<TResult, TError>,
     action: Action<TResult, TError>
   ): void {
-    this.currentResult = this.createResult()
+    const { config } = this
 
-    const { data, error, isSuccess, isError } = this.currentResult
+    // Store current result and get new result
+    const prevResult = this.currentResult
+    this.currentResult = this.createResult()
+    const result = this.currentResult
 
-    if (action.type === 'Success' && isSuccess) {
-      this.config.onSuccess?.(data!)
-      this.config.onSettled?.(data!, null)
+    // We need to check the action because the state could have
+    // transitioned from success to success in case of `setQueryData`.
+    if (action.type === 'Success' && result.isSuccess) {
+      config.onSuccess?.(result.data!)
+      config.onSettled?.(result.data!, null)
       this.updateTimers()
-    } else if (action.type === 'Error' && isError) {
-      this.config.onError?.(error!)
-      this.config.onSettled?.(undefined, error!)
+    } else if (action.type === 'Error' && result.isError) {
+      config.onError?.(result.error!)
+      config.onSettled?.(undefined, result.error!)
       this.updateTimers()
     }
 
-    this.updateListener?.(this.currentResult)
+    // Decide if we need to notify the listener
+    const notify =
+      // Always notify on data or error change
+      result.data !== prevResult.data ||
+      result.error !== prevResult.error ||
+      // Maybe notify on other changes
+      config.notifyOnStatusChange
+
+    if (notify) {
+      this.updateListener?.(result)
+    }
   }
 }
diff --git a/src/core/types.ts b/src/core/types.ts
index 4883d4068..fccead863 100644
--- a/src/core/types.ts
+++ b/src/core/types.ts
@@ -105,6 +105,12 @@ export interface QueryObserverConfig<
    * Defaults to `true`.
    */
   refetchOnMount?: boolean
+  /**
+   * Whether a change to the query status should re-render a component.
+   * If set to `false`, the component will only re-render when the actual `data` or `error` changes.
+   * Defaults to `true`.
+   */
+  notifyOnStatusChange?: boolean
   /**
    * This callback will fire any time the query successfully fetches new data.
    */
diff --git a/src/hydration/hydration.ts b/src/hydration/hydration.ts
index a3c41ba33..64b424230 100644
--- a/src/hydration/hydration.ts
+++ b/src/hydration/hydration.ts
@@ -40,8 +40,8 @@ function dehydrateQuery<TResult, TError = unknown>(
   // in the html-payload, but not consume it on the initial render.
   // We still schedule stale and garbage collection right away, which means
   // we need to specifically include staleTime and cacheTime in dehydration.
-  if (query.config.cacheTime !== DEFAULT_CACHE_TIME) {
-    dehydratedQuery.config.cacheTime = query.config.cacheTime
+  if (query.cacheTime !== DEFAULT_CACHE_TIME) {
+    dehydratedQuery.config.cacheTime = query.cacheTime
   }
   if (query.state.data !== undefined) {
     dehydratedQuery.config.initialData = query.state.data
@@ -62,7 +62,7 @@ export function dehydrate(
   const dehydratedState: DehydratedState = {
     queries: [],
   }
-  for (const query of Object.values(queryCache.queries)) {
+  for (const query of queryCache.getQueries()) {
     if (shouldDehydrate(query)) {
       dehydratedState.queries.push(dehydrateQuery(query))
     }
diff --git a/src/hydration/index.ts b/src/hydration/index.ts
index 183124753..7e387c506 100644
--- a/src/hydration/index.ts
+++ b/src/hydration/index.ts
@@ -1,5 +1,5 @@
 export { dehydrate, hydrate } from './hydration'
-export { useHydrate, ReactQueryCacheProvider } from './react'
+export { useHydrate, Hydrate } from './react'
 
 // Types
 export type {
@@ -9,4 +9,4 @@ export type {
   ShouldDehydrateFunction,
   DehydrateConfig,
 } from './hydration'
-export type { HydrationCacheProviderProps } from './react'
+export type { HydrateProps } from './react'
diff --git a/src/hydration/react.tsx b/src/hydration/react.tsx
index f8f60b449..2a0339b04 100644
--- a/src/hydration/react.tsx
+++ b/src/hydration/react.tsx
@@ -1,11 +1,7 @@
 import React from 'react'
-import {
-  useQueryCache,
-  ReactQueryCacheProvider as CacheProvider,
-} from 'react-query'
-import { hydrate } from './hydration'
+import { useQueryCache } from 'react-query'
 
-import type { ReactQueryCacheProviderProps } from '../react'
+import { hydrate } from './hydration'
 
 export function useHydrate(queries: unknown) {
   const queryCache = useQueryCache()
@@ -21,28 +17,11 @@ export function useHydrate(queries: unknown) {
   }, [queryCache, queries])
 }
 
-interface HydratorProps {
-  dehydratedState?: unknown
+export interface HydrateProps {
+  state?: unknown
 }
 
-const Hydrator: React.FC<HydratorProps> = ({ dehydratedState, children }) => {
-  useHydrate(dehydratedState)
+export const Hydrate: React.FC<HydrateProps> = ({ state, children }) => {
+  useHydrate(state)
   return children as React.ReactElement<any>
 }
-
-export interface HydrationCacheProviderProps
-  extends ReactQueryCacheProviderProps {
-  dehydratedState?: unknown
-}
-
-export const ReactQueryCacheProvider: React.FC<HydrationCacheProviderProps> = ({
-  dehydratedState,
-  children,
-  ...rest
-}) => {
-  return (
-    <CacheProvider {...rest}>
-      <Hydrator dehydratedState={dehydratedState}>{children}</Hydrator>
-    </CacheProvider>
-  )
-}
diff --git a/src/react/useMutation.ts b/src/react/useMutation.ts
index 2a641f2ba..349cfbacc 100644
--- a/src/react/useMutation.ts
+++ b/src/react/useMutation.ts
@@ -172,8 +172,6 @@ export function useMutation<
         if (mutateConfig.throwOnError ?? config.throwOnError) {
           throw error
         }
-
-        return
       }
     },
     [dispatch, getConfig, getMutationFn]
diff --git a/tsconfig.json b/tsconfig.json
index 5d737de6c..287685b3f 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -6,7 +6,7 @@
     "target": "es5",
     "noEmit": true,
     "noImplicitAny": true,
-    "noImplicitReturns": true,
+    "noImplicitReturns": false,
     "noImplicitThis": true,
     "noUnusedLocals": true,
     "noUnusedParameters": true,


Test patch:
diff --git a/src/core/tests/queryCache.test.tsx b/src/core/tests/queryCache.test.tsx
index 1a7df97ad..4cb3d5018 100644
--- a/src/core/tests/queryCache.test.tsx
+++ b/src/core/tests/queryCache.test.tsx
@@ -267,7 +267,6 @@ describe('queryCache', () => {
     })
     const query = defaultQueryCache.getQuery(key)!
     const instance = query.subscribe()
-    instance.updateConfig(query.config)
     // @ts-expect-error
     expect(instance.refetchIntervalId).not.toBeUndefined()
     instance.unsubscribe()
@@ -362,6 +361,21 @@ describe('queryCache', () => {
       unsubscribe()
     })
 
+    test('query should use the longest cache time it has seen', async () => {
+      const key = queryKey()
+      await defaultQueryCache.prefetchQuery(key, () => 'data', {
+        cacheTime: 100,
+      })
+      await defaultQueryCache.prefetchQuery(key, () => 'data', {
+        cacheTime: 200,
+      })
+      await defaultQueryCache.prefetchQuery(key, () => 'data', {
+        cacheTime: 10,
+      })
+      const query = defaultQueryCache.getQuery(key)!
+      expect(query.cacheTime).toBe(200)
+    })
+
     it('should continue retry after focus regain and resolve all promises', async () => {
       const key = queryKey()
 
diff --git a/src/hydration/tests/react.test.tsx b/src/hydration/tests/react.test.tsx
index 21945693c..ba2a319de 100644
--- a/src/hydration/tests/react.test.tsx
+++ b/src/hydration/tests/react.test.tsx
@@ -1,12 +1,8 @@
 import React from 'react'
 import { render } from '@testing-library/react'
 
-import {
-  ReactQueryCacheProvider as OriginalCacheProvider,
-  makeQueryCache,
-  useQuery,
-} from '../..'
-import { dehydrate, useHydrate, ReactQueryCacheProvider } from '../'
+import { ReactQueryCacheProvider, makeQueryCache, useQuery } from '../..'
+import { dehydrate, useHydrate, Hydrate } from '../'
 import { waitForMs } from '../../react/tests/utils'
 
 describe('React hydration', () => {
@@ -58,9 +54,9 @@ describe('React hydration', () => {
       }
 
       const rendered = render(
-        <OriginalCacheProvider queryCache={clientQueryCache}>
+        <ReactQueryCacheProvider queryCache={clientQueryCache}>
           <Page />
-        </OriginalCacheProvider>
+        </ReactQueryCacheProvider>
       )
 
       await waitForMs(10)
@@ -84,11 +80,10 @@ describe('React hydration', () => {
       }
 
       const rendered = render(
-        <ReactQueryCacheProvider
-          queryCache={clientQueryCache}
-          dehydratedState={dehydratedState}
-        >
-          <Page queryKey={'string'} />
+        <ReactQueryCacheProvider queryCache={clientQueryCache}>
+          <Hydrate state={dehydratedState}>
+            <Page queryKey={'string'} />
+          </Hydrate>
         </ReactQueryCacheProvider>
       )
 
@@ -104,12 +99,11 @@ describe('React hydration', () => {
       intermediateCache.clear({ notify: false })
 
       rendered.rerender(
-        <ReactQueryCacheProvider
-          queryCache={clientQueryCache}
-          dehydratedState={dehydrated}
-        >
-          <Page queryKey={'string'} />
-          <Page queryKey={'added string'} />
+        <ReactQueryCacheProvider queryCache={clientQueryCache}>
+          <Hydrate state={dehydrated}>
+            <Page queryKey={'string'} />
+            <Page queryKey={'added string'} />
+          </Hydrate>
         </ReactQueryCacheProvider>
       )
 
@@ -137,11 +131,10 @@ describe('React hydration', () => {
       }
 
       const rendered = render(
-        <ReactQueryCacheProvider
-          queryCache={clientQueryCache}
-          dehydratedState={dehydratedState}
-        >
-          <Page />
+        <ReactQueryCacheProvider queryCache={clientQueryCache}>
+          <Hydrate state={dehydratedState}>
+            <Page />
+          </Hydrate>
         </ReactQueryCacheProvider>
       )
 
@@ -151,11 +144,10 @@ describe('React hydration', () => {
       const newClientQueryCache = makeQueryCache()
 
       rendered.rerender(
-        <ReactQueryCacheProvider
-          queryCache={newClientQueryCache}
-          dehydratedState={dehydratedState}
-        >
-          <Page />
+        <ReactQueryCacheProvider queryCache={newClientQueryCache}>
+          <Hydrate state={dehydratedState}>
+            <Page />
+          </Hydrate>
         </ReactQueryCacheProvider>
       )
 
diff --git a/src/hydration/tests/ssr.test.tsx b/src/hydration/tests/ssr.test.tsx
index e1f5a82f6..5d83153dc 100644
--- a/src/hydration/tests/ssr.test.tsx
+++ b/src/hydration/tests/ssr.test.tsx
@@ -2,8 +2,14 @@ import * as React from 'react'
 import ReactDOM from 'react-dom'
 import ReactDOMServer from 'react-dom/server'
 import { waitFor } from '@testing-library/react'
-import { makeQueryCache, useQuery, setConsole } from '../..'
-import { dehydrate, ReactQueryCacheProvider } from '../'
+
+import {
+  makeQueryCache,
+  useQuery,
+  setConsole,
+  ReactQueryCacheProvider,
+} from '../..'
+import { dehydrate, Hydrate } from '../'
 import * as utils from '../../core/utils'
 import { sleep } from '../../react/tests/utils'
 
@@ -73,8 +79,10 @@ describe('Server side rendering with de/rehydration', () => {
     await prefetchPromise
     const dehydratedStateServer = dehydrate(serverPrefetchCache)
     const markup = ReactDOMServer.renderToString(
-      <ReactQueryCacheProvider dehydratedState={dehydratedStateServer}>
-        <SuccessComponent />
+      <ReactQueryCacheProvider>
+        <Hydrate state={dehydratedStateServer}>
+          <SuccessComponent />
+        </Hydrate>
       </ReactQueryCacheProvider>
     )
     const stringifiedState = JSON.stringify(dehydratedStateServer)
@@ -87,8 +95,10 @@ describe('Server side rendering with de/rehydration', () => {
     el.innerHTML = markup
     const dehydratedStateClient = JSON.parse(stringifiedState)
     ReactDOM.hydrate(
-      <ReactQueryCacheProvider dehydratedState={dehydratedStateClient}>
-        <SuccessComponent />
+      <ReactQueryCacheProvider>
+        <Hydrate state={dehydratedStateClient}>
+          <SuccessComponent />
+        </Hydrate>
       </ReactQueryCacheProvider>,
       el
     )
@@ -139,8 +149,10 @@ describe('Server side rendering with de/rehydration', () => {
     await prefetchPromise
     const dehydratedStateServer = dehydrate(serverQueryCache)
     const markup = ReactDOMServer.renderToString(
-      <ReactQueryCacheProvider dehydratedState={dehydratedStateServer}>
-        <ErrorComponent />
+      <ReactQueryCacheProvider>
+        <Hydrate state={dehydratedStateServer}>
+          <ErrorComponent />
+        </Hydrate>
       </ReactQueryCacheProvider>
     )
     const stringifiedState = JSON.stringify(dehydratedStateServer)
@@ -153,8 +165,10 @@ describe('Server side rendering with de/rehydration', () => {
     el.innerHTML = markup
     const dehydratedStateClient = JSON.parse(stringifiedState)
     ReactDOM.hydrate(
-      <ReactQueryCacheProvider dehydratedState={dehydratedStateClient}>
-        <ErrorComponent />
+      <ReactQueryCacheProvider>
+        <Hydrate state={dehydratedStateClient}>
+          <ErrorComponent />
+        </Hydrate>
       </ReactQueryCacheProvider>,
       el
     )
@@ -204,8 +218,10 @@ describe('Server side rendering with de/rehydration', () => {
     const serverPrefetchCache = makeQueryCache()
     const dehydratedStateServer = dehydrate(serverPrefetchCache)
     const markup = ReactDOMServer.renderToString(
-      <ReactQueryCacheProvider dehydratedState={dehydratedStateServer}>
-        <SuccessComponent />
+      <ReactQueryCacheProvider>
+        <Hydrate state={dehydratedStateServer}>
+          <SuccessComponent />
+        </Hydrate>
       </ReactQueryCacheProvider>
     )
     const stringifiedState = JSON.stringify(dehydratedStateServer)
@@ -218,8 +234,10 @@ describe('Server side rendering with de/rehydration', () => {
     el.innerHTML = markup
     const dehydratedStateClient = JSON.parse(stringifiedState)
     ReactDOM.hydrate(
-      <ReactQueryCacheProvider dehydratedState={dehydratedStateClient}>
-        <SuccessComponent />
+      <ReactQueryCacheProvider>
+        <Hydrate state={dehydratedStateClient}>
+          <SuccessComponent />
+        </Hydrate>
       </ReactQueryCacheProvider>,
       el
     )
diff --git a/src/react/tests/ssr.test.tsx b/src/react/tests/ssr.test.tsx
index 2c2f67b82..68f4b1aff 100644
--- a/src/react/tests/ssr.test.tsx
+++ b/src/react/tests/ssr.test.tsx
@@ -96,7 +96,7 @@ describe('Server Side Rendering', () => {
 
       renderToString(<Page />)
 
-      expect(queryCache.queries).toEqual({})
+      expect(queryCache.getQueries().length).toEqual(0)
     })
 
     it('should not add prefetched data to the cache', async () => {
@@ -209,7 +209,9 @@ describe('Server Side Rendering', () => {
         </ReactQueryCacheProvider>
       )
 
-      expect(Object.keys(queryCache.queries)).toEqual([`["${key}",1]`])
+      const keys = queryCache.getQueries().map(query => query.queryHash)
+
+      expect(keys).toEqual([`["${key}",1]`])
     })
 
     it('should not call setTimeout', async () => {
diff --git a/src/react/tests/useInfiniteQuery.test.tsx b/src/react/tests/useInfiniteQuery.test.tsx
index b5d3552cb..86e0d0677 100644
--- a/src/react/tests/useInfiniteQuery.test.tsx
+++ b/src/react/tests/useInfiniteQuery.test.tsx
@@ -60,7 +60,7 @@ describe('useInfiniteQuery', () => {
     await waitFor(() => rendered.getByText('Status: success'))
 
     expect(states[0]).toEqual({
-      canFetchmore: undefined,
+      canFetchMore: undefined,
       clear: expect.any(Function),
       data: undefined,
       error: null,
diff --git a/src/react/tests/useQuery.test.tsx b/src/react/tests/useQuery.test.tsx
index 2307b4efe..abc2c9c41 100644
--- a/src/react/tests/useQuery.test.tsx
+++ b/src/react/tests/useQuery.test.tsx
@@ -7,6 +7,7 @@ import {
   queryKey,
   mockVisibilityState,
   mockConsoleError,
+  waitForMs,
 } from './utils'
 import { useQuery } from '..'
 import { queryCache, QueryResult } from '../../core'
@@ -356,6 +357,56 @@ describe('useQuery', () => {
     return null
   })
 
+  it.only('should update disabled query when updated with invalidateQueries', async () => {
+    const key = queryKey()
+    const states: QueryResult<number>[] = []
+    let count = 0
+
+    function Page() {
+      const state = useQuery(
+        key,
+        async () => {
+          await sleep(10)
+          count++
+          return count
+        },
+        { enabled: false }
+      )
+
+      states.push(state)
+
+      React.useEffect(() => {
+        setTimeout(() => {
+          queryCache.invalidateQueries(key, { refetchInactive: true })
+        }, 20)
+      }, [])
+
+      return null
+    }
+
+    render(<Page />)
+
+    await waitFor(() => expect(states.length).toBe(3))
+
+    expect(states).toMatchObject([
+      {
+        data: undefined,
+        isFetching: false,
+        isSuccess: false,
+      },
+      {
+        data: undefined,
+        isFetching: true,
+        isSuccess: false,
+      },
+      {
+        data: 1,
+        isFetching: false,
+        isSuccess: true,
+      },
+    ])
+  })
+
   it('should keep the previous data when keepPreviousData is set', async () => {
     const key = queryKey()
     const states: QueryResult<number>[] = []
@@ -468,7 +519,7 @@ describe('useQuery', () => {
 
     await queryCache.prefetchQuery(key, () => 'prefetch')
 
-    await sleep(10)
+    await sleep(40)
 
     function FirstComponent() {
       const state = useQuery(key, () => 'one', {
@@ -480,7 +531,7 @@ describe('useQuery', () => {
 
     function SecondComponent() {
       const state = useQuery(key, () => 'two', {
-        staleTime: 5,
+        staleTime: 20,
       })
       states2.push(state)
       return null
@@ -543,6 +594,76 @@ describe('useQuery', () => {
     })
   })
 
+  it('should re-render when a query becomes stale', async () => {
+    const key = queryKey()
+    const states: QueryResult<string>[] = []
+
+    function Page() {
+      const state = useQuery(key, () => 'test', {
+        staleTime: 50,
+      })
+      states.push(state)
+      return null
+    }
+
+    render(<Page />)
+
+    await waitFor(() => expect(states.length).toBe(3))
+
+    expect(states[0]).toMatchObject({
+      isStale: true,
+    })
+    expect(states[1]).toMatchObject({
+      isStale: false,
+    })
+    expect(states[2]).toMatchObject({
+      isStale: true,
+    })
+  })
+
+  it('should not re-render when a query status changes and notifyOnStatusChange is false', async () => {
+    const key = queryKey()
+    const states: QueryResult<string>[] = []
+
+    function Page() {
+      const state = useQuery(
+        key,
+        async () => {
+          await sleep(5)
+          return 'test'
+        },
+        {
+          notifyOnStatusChange: false,
+        }
+      )
+
+      states.push(state)
+
+      const { refetch } = state
+
+      React.useEffect(() => {
+        setTimeout(refetch, 10)
+      }, [refetch])
+      return null
+    }
+
+    render(<Page />)
+
+    await waitForMs(30)
+
+    expect(states.length).toBe(2)
+    expect(states[0]).toMatchObject({
+      data: undefined,
+      status: 'loading',
+      isFetching: true,
+    })
+    expect(states[1]).toMatchObject({
+      data: 'test',
+      status: 'success',
+      isFetching: false,
+    })
+  })
+
   // See https://github.com/tannerlinsley/react-query/issues/137
   it('should not override initial data in dependent queries', async () => {
     const key1 = queryKey()
@@ -577,6 +698,30 @@ describe('useQuery', () => {
     rendered.getByText('Second Status: success')
   })
 
+  it('should not override query configuration on render', async () => {
+    const key = queryKey()
+
+    const queryFn1 = async () => {
+      await sleep(10)
+      return 'data1'
+    }
+
+    const queryFn2 = async () => {
+      await sleep(10)
+      return 'data2'
+    }
+
+    function Page() {
+      useQuery(key, queryFn1)
+      useQuery(key, queryFn2)
+      return null
+    }
+
+    render(<Page />)
+
+    expect(queryCache.getQuery(key)!.config.queryFn).toBe(queryFn1)
+  })
+
   // See https://github.com/tannerlinsley/react-query/issues/170
   it('should start with status idle if enabled is false', async () => {
     const key1 = queryKey()
@@ -698,6 +843,38 @@ describe('useQuery', () => {
     consoleMock.mockRestore()
   })
 
+  it('should keep initial stale and initial data when the query key changes', async () => {
+    const key = queryKey()
+    const states: QueryResult<{ count: number }>[] = []
+
+    function Page() {
+      const [count, setCount] = React.useState(0)
+      const state = useQuery([key, count], () => ({ count: 10 }), {
+        initialStale: () => false,
+        initialData: () => ({ count }),
+      })
+      states.push(state)
+
+      React.useEffect(() => {
+        setTimeout(() => setCount(1), 10)
+      }, [])
+
+      return null
+    }
+
+    render(<Page />)
+
+    await waitFor(() => expect(states.length).toBe(5))
+
+    expect(states).toMatchObject([
+      { data: { count: 0 } },
+      { data: { count: 0 } },
+      { data: { count: 1 } },
+      { data: { count: 1 } },
+      { data: { count: 10 } },
+    ])
+  })
+
   it('should retry specified number of times', async () => {
     const key = queryKey()
     const consoleMock = mockConsoleError()