Repository: tannerlinsley/react-query
Pull request #942 (2020-08-31 17:20:25)

fix: always use config from last query execution
Currently the query configuration is dependent on the order in which hooks are rendered and this could result in unexpected behaviour. This MR will make sure the query configuration is only changed on execution. Related to https://github.com/tannerlinsley/react-query/discussions/932

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.



Patch:
diff --git a/docs/src/pages/docs/api.md b/docs/src/pages/docs/api.md
index 8e6353187..595350c4e 100644
--- a/docs/src/pages/docs/api.md
+++ b/docs/src/pages/docs/api.md
@@ -792,13 +792,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 +824,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 +854,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/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/query.ts b/src/core/query.ts
index eaad23a61..578cf6690 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,11 +122,13 @@ 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 {
@@ -141,18 +144,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 +216,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,119 +302,21 @@ 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) {
@@ -530,16 +440,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..4f737361a 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,11 +82,12 @@ 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
     )
@@ -108,24 +110,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 +143,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 +156,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 } =
@@ -200,7 +214,7 @@ export class QueryCache {
   }
 
   resetErrorBoundaries(): void {
-    this.getQueries(true).forEach(query => {
+    this.getQueries().forEach(query => {
       query.state.throwInErrorBoundary = false
     })
   }
@@ -210,31 +224,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,13 +309,16 @@ 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) {
@@ -348,7 +360,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..9d32d8f49 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
     }
@@ -277,11 +277,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
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>
-  )
-}


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/useQuery.test.tsx b/src/react/tests/useQuery.test.tsx
index 2307b4efe..ecc3d7e0e 100644
--- a/src/react/tests/useQuery.test.tsx
+++ b/src/react/tests/useQuery.test.tsx
@@ -577,6 +577,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 +722,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()