Repository: tannerlinsley/react-query
Pull request #867 (2020-08-14 09:00:07)

feat: replace query instance with query observer
This MR addresses a few things:
- Replace `QueryInstance` with `QueryObserver`. The purpose of this entity is to observe a query while having the ability to switch to different queries.
- Add `keepPreviousData` query option to keep any previous data while loading new data when the query key changes. Would it be an idea to deprecate the `resolvedData` and `latestData` properties?
- Make sure a query is refetched on windows focus when any observer indicates if it should be refetched.
- Make sure when a user calls a refetch/clear/fetchMore hook callback, the configuration from that specific hook is used.
- Some preparation work to allow configuration to be a single object.


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.

Patch:
diff --git a/docs/src/pages/docs/api.md b/docs/src/pages/docs/api.md
index 5a5eeb625..a834280c1 100644
--- a/docs/src/pages/docs/api.md
+++ b/docs/src/pages/docs/api.md
@@ -27,6 +27,7 @@ const {
   retryDelay,
   staleTime,
   cacheTime,
+  keepPreviousData,
   refetchOnWindowFocus,
   refetchInterval,
   refetchIntervalInBackground,
@@ -112,6 +113,10 @@ const queryInfo = useQuery({
   - Optional
   - If set, this will mark any `initialData` provided as stale and will likely cause it to be refetched on mount
   - If a function is passed, it will be called only when appropriate to resolve the `initialStale` value. This can be useful if your `initialStale` value is costly to calculate.
+- `keepPreviousData: Boolean`
+  - Optional
+  - Defaults to `false`
+  - If set, any previous `data` will be kept when fetching new data because the query key changed.
 - `refetchOnMount: Boolean`
   - Optional
   - Defaults to `true`
diff --git a/src/core/config.ts b/src/core/config.ts
index d2807a667..67fa74af9 100644
--- a/src/core/config.ts
+++ b/src/core/config.ts
@@ -1,4 +1,4 @@
-import { stableStringify, identity } from './utils'
+import { stableStringify } from './utils'
 import {
   ArrayQueryKey,
   QueryKey,
@@ -30,9 +30,6 @@ export const defaultQueryKeySerializerFn: QueryKeySerializerFunction = (
 }
 
 export const DEFAULT_CONFIG: ReactQueryConfig = {
-  shared: {
-    suspense: false,
-  },
   queries: {
     queryKeySerializerFn: defaultQueryKeySerializerFn,
     enabled: true,
@@ -41,14 +38,7 @@ export const DEFAULT_CONFIG: ReactQueryConfig = {
     staleTime: 0,
     cacheTime: 5 * 60 * 1000,
     refetchOnWindowFocus: true,
-    refetchInterval: false,
-    queryFnParamsFilter: identity,
     refetchOnMount: true,
-    useErrorBoundary: false,
-  },
-  mutations: {
-    throwOnError: false,
-    useErrorBoundary: false,
   },
 }
 
diff --git a/src/core/query.ts b/src/core/query.ts
index fbb31e94b..8258baf83 100644
--- a/src/core/query.ts
+++ b/src/core/query.ts
@@ -9,7 +9,6 @@ import {
   Updater,
   replaceEqualDeep,
 } from './utils'
-import { QueryInstance, OnStateUpdateFunction } from './queryInstance'
 import {
   ArrayQueryKey,
   InfiniteQueryConfig,
@@ -19,7 +18,8 @@ import {
   QueryFunction,
   QueryStatus,
 } from './types'
-import { QueryCache } from './queryCache'
+import type { QueryCache } from './queryCache'
+import { QueryObserver, UpdateListener } from './queryObserver'
 
 // TYPES
 
@@ -39,7 +39,7 @@ export interface QueryState<TResult, TError> {
   isError: boolean
   isFetched: boolean
   isFetching: boolean
-  isFetchingMore?: IsFetchingMoreValue
+  isFetchingMore: IsFetchingMoreValue
   isIdle: boolean
   isLoading: boolean
   isStale: boolean
@@ -111,7 +111,7 @@ export class Query<TResult, TError> {
   queryKey: ArrayQueryKey
   queryHash: string
   config: QueryConfig<TResult, TError>
-  instances: QueryInstance<TResult, TError>[]
+  observers: QueryObserver<TResult, TError>[]
   state: QueryState<TResult, TError>
   shouldContinueRetryOnFocus?: boolean
   promise?: Promise<TResult | undefined>
@@ -131,17 +131,14 @@ export class Query<TResult, TError> {
     this.queryKey = init.queryKey
     this.queryHash = init.queryHash
     this.notifyGlobalListeners = init.notifyGlobalListeners
-    this.instances = []
+    this.observers = []
     this.state = getDefaultState(init.config)
 
     if (init.config.infinite) {
       const infiniteConfig = init.config as InfiniteQueryConfig<TResult, TError>
       const infiniteData = (this.state.data as unknown) as TResult[] | undefined
 
-      if (
-        typeof infiniteData !== 'undefined' &&
-        typeof this.state.canFetchMore === 'undefined'
-      ) {
+      if (typeof infiniteData !== 'undefined') {
         this.fetchMoreVariable = infiniteConfig.getFetchMore(
           infiniteData[infiniteData.length - 1],
           infiniteData
@@ -154,11 +151,28 @@ export class Query<TResult, TError> {
         this.pageVariables = [[...this.queryKey]]
       }
     }
+
+    // If the query started with data, schedule
+    // a stale timeout
+    if (!isServer && this.state.data) {
+      this.scheduleStaleTimeout()
+
+      // Simulate a query healing process
+      this.heal()
+
+      // Schedule for garbage collection in case
+      // nothing subscribes to this query
+      this.scheduleGarbageCollection()
+    }
+  }
+
+  updateConfig(config: QueryConfig<TResult, TError>): void {
+    this.config = config
   }
 
   private dispatch(action: Action<TResult, TError>): void {
     this.state = queryReducer(this.state, action)
-    this.instances.forEach(d => d.onStateUpdate(this.state, action))
+    this.observers.forEach(d => d.onQueryUpdate(this.state, action))
     this.notifyGlobalListeners(this)
   }
 
@@ -169,11 +183,7 @@ export class Query<TResult, TError> {
 
     this.clearStaleTimeout()
 
-    if (this.state.isStale) {
-      return
-    }
-
-    if (this.config.staleTime === Infinity) {
+    if (this.state.isStale || this.config.staleTime === Infinity) {
       return
     }
 
@@ -185,10 +195,6 @@ export class Query<TResult, TError> {
   invalidate(): void {
     this.clearStaleTimeout()
 
-    if (!this.queryCache.queries[this.queryHash]) {
-      return
-    }
-
     if (this.state.isStale) {
       return
     }
@@ -197,12 +203,12 @@ export class Query<TResult, TError> {
   }
 
   scheduleGarbageCollection(): void {
-    this.clearCacheTimeout()
-
-    if (!this.queryCache.queries[this.queryHash]) {
+    if (isServer) {
       return
     }
 
+    this.clearCacheTimeout()
+
     if (this.config.cacheTime === Infinity) {
       return
     }
@@ -244,9 +250,9 @@ export class Query<TResult, TError> {
     delete this.promise
   }
 
-  clearIntervals(): void {
-    this.instances.forEach(instance => {
-      instance.clearInterval()
+  private clearTimersObservers(): void {
+    this.observers.forEach(observer => {
+      observer.clearRefetchInterval()
     })
   }
 
@@ -310,19 +316,57 @@ export class Query<TResult, TError> {
     this.clearStaleTimeout()
     this.clearCacheTimeout()
     this.clearRetryTimeout()
-    this.clearIntervals()
+    this.clearTimersObservers()
     this.cancel()
     delete this.queryCache.queries[this.queryHash]
     this.notifyGlobalListeners(this)
   }
 
+  isEnabled(): boolean {
+    return this.observers.some(observer => observer.config.enabled)
+  }
+
+  shouldRefetchOnWindowFocus(): boolean {
+    return (
+      this.isEnabled() &&
+      this.state.isStale &&
+      this.observers.some(observer => observer.config.refetchOnWindowFocus)
+    )
+  }
+
   subscribe(
-    onStateUpdate?: OnStateUpdateFunction<TResult, TError>
-  ): QueryInstance<TResult, TError> {
-    const instance = new QueryInstance(this, onStateUpdate)
-    this.instances.push(instance)
+    listener?: UpdateListener<TResult, TError>
+  ): QueryObserver<TResult, TError> {
+    const observer = new QueryObserver<TResult, TError>({
+      queryCache: this.queryCache,
+      queryKey: this.queryKey,
+      ...this.config,
+    })
+
+    observer.subscribe(listener)
+
+    return observer
+  }
+
+  subscribeObserver(observer: QueryObserver<TResult, TError>): void {
+    this.observers.push(observer)
     this.heal()
-    return instance
+  }
+
+  unsubscribeObserver(
+    observer: QueryObserver<TResult, TError>,
+    preventGC?: boolean
+  ): void {
+    this.observers = this.observers.filter(x => x !== observer)
+
+    if (!this.observers.length) {
+      this.cancel()
+
+      if (!preventGC) {
+        // Schedule garbage collection
+        this.scheduleGarbageCollection()
+      }
+    }
   }
 
   // Set up the core fetcher function
@@ -332,7 +376,11 @@ export class Query<TResult, TError> {
   ): Promise<TResult> {
     try {
       // Perform the query
-      const promiseOrValue = fn(...this.config.queryFnParamsFilter!(args))
+      const filter = this.config.queryFnParamsFilter
+      const params = filter ? filter(args) : args
+
+      // Perform the query
+      const promiseOrValue = fn(...params)
 
       this.cancelPromises = () => (promiseOrValue as any)?.cancel?.()
 
@@ -584,6 +632,7 @@ function getDefaultState<TResult, TError>(
     error: null,
     isFetched: false,
     isFetching: initialStatus === QueryStatus.Loading,
+    isFetchingMore: false,
     failureCount: 0,
     isStale,
     data: initialData,
diff --git a/src/core/queryCache.ts b/src/core/queryCache.ts
index 975de3db2..1e6700ee2 100644
--- a/src/core/queryCache.ts
+++ b/src/core/queryCache.ts
@@ -121,6 +121,15 @@ export class QueryCache {
     return this.configRef.current
   }
 
+  getDefaultedConfig<TResult, TError>(config?: QueryConfig<TResult, TError>) {
+    return {
+      ...this.configRef.current.shared!,
+      ...this.configRef.current.queries!,
+      queryCache: this,
+      ...config,
+    } as QueryConfig<TResult, TError>
+  }
+
   subscribe(listener: QueryCacheListener): () => void {
     this.globalListeners.push(listener)
     return () => {
@@ -195,11 +204,8 @@ export class QueryCache {
     try {
       await Promise.all(
         this.getQueries(predicate, options).map(query => {
-          if (query.instances.length) {
-            if (
-              refetchActive &&
-              query.instances.some(instance => instance.config.enabled)
-            ) {
+          if (query.observers.length) {
+            if (refetchActive && query.isEnabled()) {
               return query.fetch()
             }
           } else {
@@ -226,13 +232,9 @@ export class QueryCache {
 
   buildQuery<TResult, TError = unknown>(
     userQueryKey: QueryKey,
-    queryConfig: QueryConfig<TResult, TError> = {}
+    queryConfig?: QueryConfig<TResult, TError>
   ): Query<TResult, TError> {
-    const config = {
-      ...this.configRef.current.shared!,
-      ...this.configRef.current.queries!,
-      ...queryConfig,
-    } as QueryConfig<TResult, TError>
+    const config = this.getDefaultedConfig(queryConfig)
 
     const [queryHash, queryKey] = config.queryKeySerializerFn!(userQueryKey)
 
@@ -240,7 +242,7 @@ export class QueryCache {
 
     if (this.queries[queryHash]) {
       query = this.queries[queryHash] as Query<TResult, TError>
-      query.config = config
+      query.updateConfig(config)
     }
 
     if (!query) {
@@ -254,18 +256,6 @@ export class QueryCache {
         },
       })
 
-      // If the query started with data, schedule
-      // a stale timeout
-      if (!isServer && query.state.data) {
-        query.scheduleStaleTimeout()
-
-        // Simulate a query healing process
-        query.heal()
-        // Schedule for garbage collection in case
-        // nothing subscribes to this query
-        query.scheduleGarbageCollection()
-      }
-
       if (!this.config.frozen) {
         this.queries[queryHash] = query
 
@@ -386,7 +376,7 @@ export class QueryCache {
   setQueryData<TResult, TError = unknown>(
     queryKey: QueryKey,
     updater: Updater<TResult | undefined, TResult>,
-    config: QueryConfig<TResult, TError> = {}
+    config?: QueryConfig<TResult, TError>
   ) {
     let query = this.getQuery<TResult, TError>(queryKey)
 
diff --git a/src/core/queryInstance.ts b/src/core/queryInstance.ts
deleted file mode 100644
index fe2471796..000000000
--- a/src/core/queryInstance.ts
+++ /dev/null
@@ -1,121 +0,0 @@
-import { uid, isServer, isDocumentVisible, Console } from './utils'
-import { Query, QueryState, Action, ActionType } from './query'
-import { BaseQueryConfig } from './types'
-
-// TYPES
-
-export type OnStateUpdateFunction<TResult, TError> = (
-  state: QueryState<TResult, TError>
-) => void
-
-// CLASS
-
-export class QueryInstance<TResult, TError> {
-  id: number
-  config: BaseQueryConfig<TResult, TError>
-
-  private query: Query<TResult, TError>
-  private refetchIntervalId?: number
-  private stateUpdateListener?: OnStateUpdateFunction<TResult, TError>
-
-  constructor(
-    query: Query<TResult, TError>,
-    onStateUpdate?: OnStateUpdateFunction<TResult, TError>
-  ) {
-    this.id = uid()
-    this.stateUpdateListener = onStateUpdate
-    this.query = query
-    this.config = {}
-  }
-
-  clearInterval(): void {
-    if (this.refetchIntervalId) {
-      clearInterval(this.refetchIntervalId)
-      this.refetchIntervalId = undefined
-    }
-  }
-
-  updateConfig(config: BaseQueryConfig<TResult, TError>): void {
-    const oldConfig = this.config
-
-    // Update the config
-    this.config = config
-
-    if (!isServer) {
-      if (oldConfig?.refetchInterval === config.refetchInterval) {
-        return
-      }
-
-      this.query.clearIntervals()
-
-      const minInterval = Math.min(
-        ...this.query.instances.map(d => d.config.refetchInterval || Infinity)
-      )
-
-      if (
-        !this.refetchIntervalId &&
-        minInterval > 0 &&
-        minInterval < Infinity
-      ) {
-        this.refetchIntervalId = setInterval(() => {
-          if (
-            this.query.instances.some(d => d.config.enabled) &&
-            (isDocumentVisible() ||
-              this.query.instances.some(
-                d => d.config.refetchIntervalInBackground
-              ))
-          ) {
-            this.query.fetch()
-          }
-        }, minInterval)
-      }
-    }
-  }
-
-  async run(): Promise<void> {
-    try {
-      // Perform the refetch for this query if necessary
-      if (
-        this.query.instances.some(d => d.config.enabled) && // Don't auto refetch if disabled
-        !(this.config.suspense && this.query.state.isFetched) && // Don't refetch if in suspense mode and the data is already fetched
-        this.query.state.isStale && // Only refetch if stale
-        (this.config.refetchOnMount || this.query.instances.length === 1)
-      ) {
-        await this.query.fetch()
-      }
-    } catch (error) {
-      Console.error(error)
-    }
-  }
-
-  unsubscribe(preventGC?: boolean): void {
-    this.query.instances = this.query.instances.filter(d => d.id !== this.id)
-
-    if (!this.query.instances.length) {
-      this.clearInterval()
-      this.query.cancel()
-
-      if (!preventGC && !isServer) {
-        // Schedule garbage collection
-        this.query.scheduleGarbageCollection()
-      }
-    }
-  }
-
-  onStateUpdate(
-    state: QueryState<TResult, TError>,
-    action: Action<TResult, TError>
-  ): void {
-    if (action.type === ActionType.Success && state.isSuccess) {
-      this.config.onSuccess?.(state.data!)
-      this.config.onSettled?.(state.data!, null)
-    }
-
-    if (action.type === ActionType.Error && state.isError) {
-      this.config.onError?.(state.error!)
-      this.config.onSettled?.(undefined, state.error!)
-    }
-
-    this.stateUpdateListener?.(state)
-  }
-}
diff --git a/src/core/queryObserver.ts b/src/core/queryObserver.ts
new file mode 100644
index 000000000..3e22560f8
--- /dev/null
+++ b/src/core/queryObserver.ts
@@ -0,0 +1,235 @@
+import { getStatusProps, isServer, isDocumentVisible, Console } from './utils'
+import type { QueryResult, QueryObserverConfig } from './types'
+import type { Query, QueryState, Action, FetchMoreOptions } from './query'
+
+export type UpdateListener<TResult, TError> = (
+  result: QueryResult<TResult, TError>
+) => void
+
+export class QueryObserver<TResult, TError> {
+  config: QueryObserverConfig<TResult, TError>
+
+  private currentQuery!: Query<TResult, TError>
+  private currentResult!: QueryResult<TResult, TError>
+  private previousResult?: QueryResult<TResult, TError>
+  private updateListener?: UpdateListener<TResult, TError>
+  private refetchIntervalId?: number
+  private started?: boolean
+
+  constructor(config: QueryObserverConfig<TResult, TError>) {
+    this.config = config
+
+    // Bind exposed methods
+    this.clear = this.clear.bind(this)
+    this.refetch = this.refetch.bind(this)
+    this.fetchMore = this.fetchMore.bind(this)
+
+    // Subscribe to query
+    this.updateQuery()
+  }
+
+  subscribe(listener?: UpdateListener<TResult, TError>): () => void {
+    this.started = true
+    this.updateListener = listener
+    this.optionalFetch()
+    this.updateRefetchInterval()
+    return this.unsubscribe.bind(this)
+  }
+
+  unsubscribe(preventGC?: boolean): void {
+    this.started = false
+    this.updateListener = undefined
+    this.clearRefetchInterval()
+    this.currentQuery.unsubscribeObserver(this, preventGC)
+  }
+
+  updateConfig(config: QueryObserverConfig<TResult, TError>): void {
+    const prevConfig = this.config
+    this.config = config
+
+    const updated = this.updateQuery()
+
+    // Take no further actions if the observer did not start yet
+    if (!this.started) {
+      return
+    }
+
+    // If we subscribed to a new query, optionally fetch and update refetch
+    if (updated) {
+      this.optionalFetch()
+      this.updateRefetchInterval()
+      return
+    }
+
+    // Optionally fetch if the query became enabled
+    if (config.enabled && !prevConfig.enabled) {
+      this.optionalFetch()
+    }
+
+    // Update refetch interval if needed
+    if (
+      config.enabled !== prevConfig.enabled ||
+      config.refetchInterval !== prevConfig.refetchInterval ||
+      config.refetchIntervalInBackground !==
+        prevConfig.refetchIntervalInBackground
+    ) {
+      this.updateRefetchInterval()
+    }
+  }
+
+  getCurrentResult(): QueryResult<TResult, TError> {
+    return this.currentResult
+  }
+
+  clear(): void {
+    return this.currentQuery.clear()
+  }
+
+  async refetch(): Promise<void> {
+    this.currentQuery.updateConfig(this.config)
+    return this.currentQuery.refetch()
+  }
+
+  async fetchMore(
+    fetchMoreVariable?: unknown,
+    options?: FetchMoreOptions
+  ): Promise<TResult | undefined> {
+    this.currentQuery.updateConfig(this.config)
+    return this.currentQuery.fetchMore(fetchMoreVariable, options)
+  }
+
+  async fetch(): Promise<TResult | undefined> {
+    this.currentQuery.updateConfig(this.config)
+    return this.currentQuery.fetch().catch(error => {
+      Console.error(error)
+      return undefined
+    })
+  }
+
+  private optionalFetch(): void {
+    if (
+      this.config.enabled && // Don't auto refetch if disabled
+      !(this.config.suspense && this.currentResult.isFetched) && // Don't refetch if in suspense mode and the data is already fetched
+      this.currentResult.isStale && // Only refetch if stale
+      (this.config.refetchOnMount || this.currentQuery.observers.length === 1)
+    ) {
+      this.fetch()
+    }
+  }
+
+  private updateRefetchInterval(): void {
+    if (isServer) {
+      return
+    }
+
+    this.clearRefetchInterval()
+
+    if (
+      !this.config.enabled ||
+      !this.config.refetchInterval ||
+      this.config.refetchInterval < 0 ||
+      this.config.refetchInterval === Infinity
+    ) {
+      return
+    }
+
+    this.refetchIntervalId = setInterval(() => {
+      if (this.config.refetchIntervalInBackground || isDocumentVisible()) {
+        this.fetch()
+      }
+    }, this.config.refetchInterval)
+  }
+
+  clearRefetchInterval(): void {
+    if (this.refetchIntervalId) {
+      clearInterval(this.refetchIntervalId)
+      this.refetchIntervalId = undefined
+    }
+  }
+
+  private createResult(): QueryResult<TResult, TError> {
+    const { currentQuery, previousResult, config } = this
+
+    const {
+      canFetchMore,
+      error,
+      failureCount,
+      isFetched,
+      isFetching,
+      isFetchingMore,
+      isLoading,
+      isStale,
+    } = currentQuery.state
+
+    let { data, status, updatedAt } = currentQuery.state
+
+    // Keep previous data if needed
+    if (config.keepPreviousData && isLoading && previousResult?.isSuccess) {
+      data = previousResult.data
+      updatedAt = previousResult.updatedAt
+      status = previousResult.status
+    }
+
+    return {
+      ...getStatusProps(status),
+      canFetchMore,
+      clear: this.clear,
+      data,
+      error,
+      failureCount,
+      fetchMore: this.fetchMore,
+      isFetched,
+      isFetching,
+      isFetchingMore,
+      isStale,
+      query: currentQuery,
+      refetch: this.refetch,
+      updatedAt,
+    }
+  }
+
+  private updateQuery(): boolean {
+    const prevQuery = this.currentQuery
+
+    // 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 newQuery = config.queryCache!.buildQuery(config.queryKey, config)
+
+    if (newQuery === prevQuery) {
+      return false
+    }
+
+    this.previousResult = this.currentResult
+    prevQuery?.unsubscribeObserver(this)
+    this.currentQuery = newQuery
+    newQuery.subscribeObserver(this)
+    this.currentResult = this.createResult()
+
+    return true
+  }
+
+  onQueryUpdate(
+    _state: QueryState<TResult, TError>,
+    action: Action<TResult, TError>
+  ): void {
+    this.currentResult = this.createResult()
+
+    const { data, error, isSuccess, isError } = this.currentResult
+
+    if (action.type === 'Success' && isSuccess) {
+      this.config.onSuccess?.(data!)
+      this.config.onSettled?.(data!, null)
+      this.updateRefetchInterval()
+    } else if (action.type === 'Error' && isError) {
+      this.config.onError?.(error!)
+      this.config.onSettled?.(undefined, error!)
+      this.updateRefetchInterval()
+    }
+
+    this.updateListener?.(this.currentResult)
+  }
+}
diff --git a/src/core/setFocusHandler.ts b/src/core/setFocusHandler.ts
index 08b1e6a7b..517e39924 100644
--- a/src/core/setFocusHandler.ts
+++ b/src/core/setFocusHandler.ts
@@ -11,15 +11,7 @@ const onWindowFocus: FocusHandler = () => {
     queryCaches.forEach(queryCache =>
       queryCache
         .invalidateQueries(query => {
-          if (!query.instances.length) {
-            return false
-          }
-
-          if (!query.instances.some(instance => instance.config.enabled)) {
-            return false
-          }
-
-          if (!query.state.isStale) {
+          if (!query.shouldRefetchOnWindowFocus()) {
             return false
           }
 
@@ -28,7 +20,7 @@ const onWindowFocus: FocusHandler = () => {
             delete query.promise
           }
 
-          return Boolean(query.config.refetchOnWindowFocus)
+          return true
         })
         .catch(Console.error)
     )
diff --git a/src/core/types.ts b/src/core/types.ts
index d470cccf7..a3f1ec25d 100644
--- a/src/core/types.ts
+++ b/src/core/types.ts
@@ -1,4 +1,5 @@
-import { Query, FetchMoreOptions } from './query'
+import type { Query, FetchMoreOptions } from './query'
+import type { QueryCache } from './queryCache'
 
 export type QueryKeyObject =
   | object
@@ -56,32 +57,86 @@ export interface BaseQueryConfig<TResult, TError = unknown> {
   retryDelay?: number | ((retryAttempt: number) => number)
   staleTime?: number
   cacheTime?: number
-  refetchInterval?: false | number
+  isDataEqual?: (oldData: unknown, newData: unknown) => boolean
+  queryFn?: QueryFunction<TResult>
+  queryKey?: QueryKey
+  queryKeySerializerFn?: QueryKeySerializerFunction
+  queryFnParamsFilter?: (args: ArrayQueryKey) => ArrayQueryKey
+  initialData?: TResult | InitialDataFunction<TResult>
+  initialStale?: boolean | InitialStaleFunction
+  infinite?: true
+}
+
+export interface QueryObserverConfig<TResult, TError = unknown>
+  extends BaseQueryConfig<TResult, TError> {
+  /**
+   * Set this to `false` to disable automatic refetching when the query mounts or changes query keys.
+   * To refetch the query, use the `refetch` method returned from the `useQuery` instance.
+   * Defaults to `true`.
+   */
+  enabled?: boolean | unknown
+  /**
+   * If set to a number, the query will continuously refetch at this frequency in milliseconds.
+   * Defaults to `false`.
+   */
+  refetchInterval?: number
+  /**
+   * If set to `true`, the query will continue to refetch while their tab/window is in the background.
+   * Defaults to `false`.
+   */
   refetchIntervalInBackground?: boolean
+  /**
+   * Set this to `true` or `false` to enable/disable automatic refetching on window focus for this query.
+   * Defaults to `true`.
+   */
   refetchOnWindowFocus?: boolean
+  /**
+   * If set to `false`, will disable additional instances of a query to trigger background refetches.
+   * Defaults to `true`.
+   */
   refetchOnMount?: boolean
+  /**
+   * This callback will fire any time the query successfully fetches new data.
+   */
   onSuccess?: (data: TResult) => void
+  /**
+   * This callback will fire if the query encounters an error and will be passed the error.
+   */
   onError?: (err: TError) => void
+  /**
+   * This callback will fire any time the query is either successfully fetched or errors and be passed either the data or error.
+   */
   onSettled?: (data: TResult | undefined, error: TError | null) => void
-  isDataEqual?: (oldData: unknown, newData: unknown) => boolean
+  /**
+   * Whether errors should be thrown instead of setting the `error` property.
+   * Defaults to `false`.
+   */
   useErrorBoundary?: boolean
-  queryFn?: QueryFunction<TResult>
-  queryKeySerializerFn?: QueryKeySerializerFunction
-  queryFnParamsFilter?: (args: ArrayQueryKey) => ArrayQueryKey
+  /**
+   * If set to `true`, the query will suspend when `status === 'loading'`
+   * and throw errors when `status === 'error'`.
+   * Defaults to `false`.
+   */
   suspense?: boolean
-  initialData?: TResult | InitialDataFunction<TResult>
-  initialStale?: boolean | InitialStaleFunction
-  infinite?: true
+  /**
+   * Set this to `true` to keep the previous `data` when fetching based on a new query key.
+   * Defaults to `false`.
+   */
+  keepPreviousData?: boolean
+  /**
+   * By default the query cache from the context is used, but a different cache can be specified.
+   */
+  queryCache?: QueryCache
 }
 
 export interface QueryConfig<TResult, TError = unknown>
-  extends BaseQueryConfig<TResult, TError> {}
+  extends QueryObserverConfig<TResult, TError> {}
 
 export interface PaginatedQueryConfig<TResult, TError = unknown>
-  extends BaseQueryConfig<TResult, TError> {}
+  extends QueryObserverConfig<TResult, TError> {}
 
 export interface InfiniteQueryConfig<TResult, TError = unknown>
-  extends BaseQueryConfig<TResult[], TError> {
+  extends QueryObserverConfig<TResult[], TError> {
   getFetchMore: (lastPage: TResult, allPages: TResult[]) => unknown
 }
 
@@ -95,25 +150,31 @@ export enum QueryStatus {
 }
 
 export interface QueryResultBase<TResult, TError = unknown> {
-  status: QueryStatus
+  canFetchMore: boolean | undefined
+  clear: () => void
+  data: TResult | undefined
   error: TError | null
-  isLoading: boolean
-  isSuccess: boolean
+  failureCount: number
+  fetchMore: (
+    fetchMoreVariable?: unknown,
+    options?: FetchMoreOptions
+  ) => Promise<TResult | undefined>
   isError: boolean
-  isIdle: boolean
+  isFetched: boolean
   isFetching: boolean
+  isFetchingMore?: IsFetchingMoreValue
+  isIdle: boolean
+  isLoading: boolean
   isStale: boolean
-  failureCount: number
+  isSuccess: boolean
   query: Query<TResult, TError>
-  updatedAt: number
   refetch: () => Promise<void>
-  clear: () => void
+  status: QueryStatus
+  updatedAt: number
 }
 
 export interface QueryResult<TResult, TError = unknown>
-  extends QueryResultBase<TResult, TError> {
-  data: TResult | undefined
-}
+  extends QueryResultBase<TResult, TError> {}
 
 export interface PaginatedQueryResult<TResult, TError = unknown>
   extends QueryResultBase<TResult, TError> {
@@ -122,15 +183,7 @@ export interface PaginatedQueryResult<TResult, TError = unknown>
 }
 
 export interface InfiniteQueryResult<TResult, TError = unknown>
-  extends QueryResultBase<TResult[], TError> {
-  data: TResult[] | undefined
-  isFetchingMore?: IsFetchingMoreValue
-  canFetchMore: boolean | undefined
-  fetchMore: (
-    fetchMoreVariable?: unknown,
-    options?: FetchMoreOptions
-  ) => Promise<TResult[] | undefined> | undefined
-}
+  extends QueryResultBase<TResult[], TError> {}
 
 export interface MutateConfig<
   TResult,
@@ -205,7 +258,7 @@ export interface ReactQuerySharedConfig {
 }
 
 export interface ReactQueryQueriesConfig<TResult, TError>
-  extends BaseQueryConfig<TResult, TError> {}
+  extends QueryObserverConfig<TResult, TError> {}
 
 export interface ReactQueryMutationsConfig<
   TResult,
diff --git a/src/core/utils.ts b/src/core/utils.ts
index 751029fcc..d857606f9 100644
--- a/src/core/utils.ts
+++ b/src/core/utils.ts
@@ -26,9 +26,6 @@ export const isServer = typeof window === 'undefined'
 export function noop(): void {
   return void 0
 }
-export function identity<T>(d: T): T {
-  return d
-}
 export let Console: ConsoleObject = console || {
   error: noop,
   warn: noop,
@@ -121,7 +118,7 @@ export function getQueryArgs<TResult, TError, TOptions = undefined>(
     options = args[3]
   }
 
-  config = config || {}
+  config = config ? { queryKey, ...config } : { queryKey }
 
   if (queryFn) {
     config = { ...config, queryFn }
diff --git a/src/react/useBaseQuery.ts b/src/react/useBaseQuery.ts
index 598f03e2b..1910b29db 100644
--- a/src/react/useBaseQuery.ts
+++ b/src/react/useBaseQuery.ts
@@ -1,70 +1,49 @@
 import React from 'react'
 
-import { useQueryCache } from './ReactQueryCacheProvider'
 import { useRerenderer } from './utils'
-import { QueryInstance } from '../core/queryInstance'
-import { QueryConfig, QueryKey, QueryResultBase } from '../core/types'
+import { QueryObserver } from '../core/queryObserver'
+import { QueryResultBase, QueryObserverConfig } from '../core/types'
 
 export function useBaseQuery<TResult, TError>(
-  queryKey: QueryKey,
-  config: QueryConfig<TResult, TError> = {}
+  config: QueryObserverConfig<TResult, TError> = {}
 ): QueryResultBase<TResult, TError> {
   // Make a rerender function
   const rerender = useRerenderer()
 
-  // Get the query cache
-  const queryCache = useQueryCache()
-
-  // Build the query for use
-  const query = queryCache.buildQuery<TResult, TError>(queryKey, config)
-  const state = query.state
-
-  // Create a query instance ref
-  const instanceRef = React.useRef<QueryInstance<TResult, TError>>()
-
-  // Subscribe to the query when the subscribe function changes
-  React.useEffect(() => {
-    const instance = query.subscribe(() => {
-      rerender()
-    })
-
-    instanceRef.current = instance
-
-    // Unsubscribe when things change
-    return () => instance.unsubscribe()
-  }, [query, rerender])
-
-  // Always update the config
-  React.useEffect(() => {
-    instanceRef.current?.updateConfig(config)
-  })
+  // Create query observer
+  const observerRef = React.useRef<QueryObserver<TResult, TError>>()
+  const firstRender = !observerRef.current
+  const observer = observerRef.current || new QueryObserver(config)
+  observerRef.current = observer
+
+  // Subscribe to the observer
+  React.useEffect(
+    () =>
+      observer.subscribe(() => {
+        Promise.resolve().then(rerender)
+      }),
+    [observer, rerender]
+  )
+
+  // Update config
+  if (!firstRender) {
+    observer.updateConfig(config)
+  }
 
-  const enabledBool = Boolean(config.enabled)
+  const result = observer.getCurrentResult()
 
-  // Run the instance when the query or enabled change
-  React.useEffect(() => {
-    if (enabledBool && query) {
-      // Just for change detection
+  // Handle suspense
+  if (config.suspense || config.useErrorBoundary) {
+    if (result.isError && result.query.state.throwInErrorBoundary) {
+      throw result.error
     }
-    instanceRef.current?.run()
-  }, [enabledBool, query])
-
-  const clear = React.useMemo(() => query.clear.bind(query), [query])
-  const refetch = React.useMemo(() => query.refetch.bind(query), [query])
 
-  return {
-    clear,
-    error: state.error,
-    failureCount: state.failureCount,
-    isError: state.isError,
-    isFetching: state.isFetching,
-    isIdle: state.isIdle,
-    isLoading: state.isLoading,
-    isStale: state.isStale,
-    isSuccess: state.isSuccess,
-    query,
-    refetch,
-    status: state.status,
-    updatedAt: state.updatedAt,
+    if (config.enabled && config.suspense && !result.isSuccess) {
+      throw observer.fetch().finally(() => {
+        observer.unsubscribe(true)
+      })
+    }
   }
+
+  return result
 }
diff --git a/src/react/useInfiniteQuery.ts b/src/react/useInfiniteQuery.ts
index 133f121ae..3dec137c4 100644
--- a/src/react/useInfiniteQuery.ts
+++ b/src/react/useInfiniteQuery.ts
@@ -1,7 +1,4 @@
-import React from 'react'
-
 import { useBaseQuery } from './useBaseQuery'
-import { handleSuspense } from './utils'
 import {
   InfiniteQueryConfig,
   InfiniteQueryResult,
@@ -72,24 +69,7 @@ export function useInfiniteQuery<TResult, TError, TKey extends TupleQueryKey>(
 export function useInfiniteQuery<TResult, TError>(
   ...args: any[]
 ): InfiniteQueryResult<TResult, TError> {
-  const [queryKey, config] = useQueryArgs<TResult[], TError>(args)
-
-  config.infinite = true
-
-  const result = useBaseQuery<TResult[], TError>(queryKey, config)
-  const query = result.query
-  const state = result.query.state
-
-  handleSuspense(config, result)
-
-  const fetchMore = React.useMemo(() => query.fetchMore.bind(query), [query])
-
-  return {
-    ...result,
-    data: state.data,
-    canFetchMore: state.canFetchMore,
-    fetchMore,
-    isFetching: state.isFetching,
-    isFetchingMore: state.isFetchingMore,
-  }
+  let config = useQueryArgs<TResult[], TError>(args)[1]
+  config = { ...config, infinite: true }
+  return useBaseQuery<TResult[], TError>(config)
 }
diff --git a/src/react/usePaginatedQuery.ts b/src/react/usePaginatedQuery.ts
index 53e08da5d..76048d2dc 100644
--- a/src/react/usePaginatedQuery.ts
+++ b/src/react/usePaginatedQuery.ts
@@ -1,8 +1,4 @@
-import React from 'react'
-
 import { useBaseQuery } from './useBaseQuery'
-import { handleSuspense } from './utils'
-import { getStatusProps } from '../core/utils'
 import {
   PaginatedQueryConfig,
   PaginatedQueryResult,
@@ -10,7 +6,6 @@ import {
   QueryKeyWithoutArray,
   QueryKeyWithoutObject,
   QueryKeyWithoutObjectAndArray,
-  QueryStatus,
   TupleQueryFunction,
   TupleQueryKey,
 } from '../core/types'
@@ -79,56 +74,13 @@ export function usePaginatedQuery<TResult, TError, TKey extends TupleQueryKey>(
 export function usePaginatedQuery<TResult, TError>(
   ...args: any[]
 ): PaginatedQueryResult<TResult, TError> {
-  const [queryKey, config] = useQueryArgs<TResult, TError>(args)
-
-  // Keep track of the latest data result
-  const lastDataRef = React.useRef<TResult>()
-
-  // If latestData is there, don't use initialData
-  if (typeof lastDataRef.current !== 'undefined') {
-    delete config.initialData
-  }
-
-  // Make the query as normal
-  const result = useBaseQuery<TResult, TError>(queryKey, config)
-
-  // If the query is disabled, get rid of the latest data
-  if (!result.query.config.enabled) {
-    lastDataRef.current = undefined
-  }
-
-  // Get the real data and status from the query
-  const { data: latestData, status } = result.query.state
-
-  // If the real query succeeds, and there is data in it,
-  // update the latest data
-  React.useEffect(() => {
-    if (status === QueryStatus.Success && typeof latestData !== 'undefined') {
-      lastDataRef.current = latestData
-    }
-  }, [latestData, status])
-
-  // Resolved data should be either the real data we're waiting on
-  // or the latest placeholder data
-  let resolvedData = latestData
-  if (typeof resolvedData === 'undefined') {
-    resolvedData = lastDataRef.current
-  }
-
-  // If we have any data at all from either, we
-  // need to make sure the status is success, even though
-  // the real query may still be loading
-  if (typeof resolvedData !== 'undefined') {
-    const overrides = getStatusProps(QueryStatus.Success)
-    Object.assign(result.query.state, overrides)
-    Object.assign(result, overrides)
-  }
-
-  handleSuspense(config, result)
-
+  let config = useQueryArgs<TResult, TError>(args)[1]
+  config = { ...config, keepPreviousData: true }
+  const result = useBaseQuery<TResult, TError>(config)
   return {
     ...result,
-    resolvedData,
-    latestData,
+    resolvedData: result.data,
+    latestData:
+      result.query.state.data === result.data ? result.data : undefined,
   }
 }
diff --git a/src/react/useQuery.ts b/src/react/useQuery.ts
index 164c2a327..c0d9fbd33 100644
--- a/src/react/useQuery.ts
+++ b/src/react/useQuery.ts
@@ -1,5 +1,4 @@
 import { useBaseQuery } from './useBaseQuery'
-import { handleSuspense } from './utils'
 import {
   QueryConfig,
   QueryKey,
@@ -63,13 +62,6 @@ export function useQuery<TResult, TError, TKey extends TupleQueryKey>(
 export function useQuery<TResult, TError>(
   ...args: any[]
 ): QueryResult<TResult, TError> {
-  const [queryKey, config] = useQueryArgs<TResult, TError>(args)
-  const result = useBaseQuery<TResult, TError>(queryKey, config)
-
-  handleSuspense(config, result)
-
-  return {
-    ...result,
-    data: result.query.state.data,
-  }
+  const config = useQueryArgs<TResult, TError>(args)[1]
+  return useBaseQuery<TResult, TError>(config)
 }
diff --git a/src/react/useQueryArgs.ts b/src/react/useQueryArgs.ts
index 502bbd70b..064c2afdb 100644
--- a/src/react/useQueryArgs.ts
+++ b/src/react/useQueryArgs.ts
@@ -1,10 +1,13 @@
 import { getQueryArgs } from '../core/utils'
 import { useConfigContext } from './ReactQueryConfigProvider'
 import { QueryConfig, QueryKey } from '../core/types'
+import { useQueryCache } from './ReactQueryCacheProvider'
 
 export function useQueryArgs<TResult, TError, TOptions = undefined>(
   args: any[]
 ): [QueryKey, QueryConfig<TResult, TError>, TOptions] {
+  const queryCache = useQueryCache()
+
   const configContext = useConfigContext()
 
   const [queryKey, config, options] = getQueryArgs<TResult, TError, TOptions>(
@@ -12,11 +15,12 @@ export function useQueryArgs<TResult, TError, TOptions = undefined>(
   )
 
   // Build the final config
-  const configWithContext = {
+  const resolvedConfig = {
     ...configContext.shared,
     ...configContext.queries,
+    queryCache,
     ...config,
   } as QueryConfig<TResult, TError>
 
-  return [queryKey, configWithContext, options]
+  return [queryKey, resolvedConfig, options]
 }
diff --git a/src/react/utils.ts b/src/react/utils.ts
index ce658fb7d..44d4f29b4 100644
--- a/src/react/utils.ts
+++ b/src/react/utils.ts
@@ -1,7 +1,6 @@
 import React from 'react'
 
 import { uid, isServer } from '../core/utils'
-import { QueryResultBase, BaseQueryConfig, QueryStatus } from '../core/types'
 
 export function useUid(): number {
   const ref = React.useRef(0)
@@ -39,35 +38,3 @@ export function useRerenderer() {
   const rerender = useMountedCallback(React.useState<unknown>()[1])
   return React.useCallback(() => rerender({}), [rerender])
 }
-
-export function handleSuspense(
-  config: BaseQueryConfig<any, any>,
-  result: QueryResultBase<any, any>
-) {
-  const { error, query } = result
-  const { state } = query
-
-  if (config.suspense || config.useErrorBoundary) {
-    if (state.status === QueryStatus.Error && state.throwInErrorBoundary) {
-      throw error
-    }
-
-    if (
-      config.suspense &&
-      state.status !== QueryStatus.Success &&
-      config.enabled
-    ) {
-      const instance = query.subscribe()
-
-      instance.updateConfig({
-        ...config,
-        onSettled: (data, error) => {
-          instance.unsubscribe(true)
-          config.onSettled?.(data, error)
-        },
-      })
-
-      throw query.fetch()
-    }
-  }
-}


Test patch:
diff --git a/src/react/tests/suspense.test.tsx b/src/react/tests/suspense.test.tsx
index 5dbff7af1..3bc14f3ae 100644
--- a/src/react/tests/suspense.test.tsx
+++ b/src/react/tests/suspense.test.tsx
@@ -57,12 +57,12 @@ describe("useQuery's in Suspense mode", () => {
     fireEvent.click(rendered.getByLabelText('toggle'))
     await waitFor(() => rendered.getByText('rendered'))
 
-    expect(queryCache.getQuery(key)?.instances.length).toBe(1)
+    expect(queryCache.getQuery(key)?.observers.length).toBe(1)
 
     fireEvent.click(rendered.getByLabelText('toggle'))
 
     expect(rendered.queryByText('rendered')).toBeNull()
-    expect(queryCache.getQuery(key)?.instances.length).toBe(0)
+    expect(queryCache.getQuery(key)?.observers.length).toBe(0)
   })
 
   it('should call onSuccess on the first successful call', async () => {
diff --git a/src/react/tests/useInfiniteQuery.test.tsx b/src/react/tests/useInfiniteQuery.test.tsx
index addf1266b..df837e611 100644
--- a/src/react/tests/useInfiniteQuery.test.tsx
+++ b/src/react/tests/useInfiniteQuery.test.tsx
@@ -60,14 +60,16 @@ describe('useInfiniteQuery', () => {
     await waitFor(() => rendered.getByText('Status: success'))
 
     expect(states[0]).toEqual({
+      canFetchmore: undefined,
       clear: expect.any(Function),
       data: undefined,
       error: null,
       failureCount: 0,
       fetchMore: expect.any(Function),
       isError: false,
+      isFetched: false,
       isFetching: true,
-      isFetchingMore: undefined,
+      isFetchingMore: false,
       isIdle: false,
       isLoading: true,
       isStale: true,
@@ -91,8 +93,9 @@ describe('useInfiniteQuery', () => {
       error: null,
       failureCount: 0,
       fetchMore: expect.any(Function),
-      isFetchingMore: undefined,
+      isFetchingMore: false,
       isError: false,
+      isFetched: true,
       isFetching: false,
       isIdle: false,
       isLoading: false,
@@ -105,6 +108,95 @@ describe('useInfiniteQuery', () => {
     })
   })
 
+  it('should keep the previous data when keepPreviousData is set', async () => {
+    const key = queryKey()
+    const states: InfiniteQueryResult<string>[] = []
+
+    function Page() {
+      const [order, setOrder] = React.useState('desc')
+
+      const state = useInfiniteQuery(
+        [key, order],
+        async (_key, order, page = 0) => {
+          await sleep(10)
+          return `${page}-${order}`
+        },
+        {
+          getFetchMore: (_lastGroup, _allGroups) => 1,
+          keepPreviousData: true,
+        }
+      )
+
+      states.push(state)
+
+      const { fetchMore } = state
+
+      React.useEffect(() => {
+        setTimeout(() => {
+          fetchMore()
+        }, 20)
+        setTimeout(() => {
+          setOrder('asc')
+        }, 40)
+      }, [fetchMore])
+
+      return null
+    }
+
+    render(<Page />)
+
+    await waitFor(() => expect(states.length).toBe(8))
+
+    expect(states[0]).toMatchObject({
+      data: undefined,
+      isFetching: true,
+      isFetchingMore: false,
+      isSuccess: false,
+    })
+    expect(states[1]).toMatchObject({
+      data: ['0-desc'],
+      isFetching: false,
+      isFetchingMore: false,
+      isSuccess: true,
+    })
+    expect(states[2]).toMatchObject({
+      data: ['0-desc'],
+      isFetching: true,
+      isFetchingMore: 'next',
+      isSuccess: true,
+    })
+    expect(states[3]).toMatchObject({
+      data: ['0-desc'],
+      isFetching: true,
+      isFetchingMore: 'next',
+      isSuccess: true,
+    })
+    expect(states[4]).toMatchObject({
+      data: ['0-desc'],
+      isFetching: true,
+      isFetchingMore: false,
+      isSuccess: true,
+    })
+    expect(states[5]).toMatchObject({
+      data: ['0-desc', '1-desc'],
+      isFetching: false,
+      isFetchingMore: false,
+      isSuccess: true,
+    })
+    expect(states[6]).toMatchObject({
+      data: ['0-desc', '1-desc'],
+      isFetching: true,
+      isFetchingMore: false,
+      isSuccess: true,
+    })
+    expect(states[7]).toMatchObject({
+      data: ['0-asc'],
+      isFetching: false,
+      isFetchingMore: false,
+      isSuccess: true,
+    })
+  })
+
   it('should allow you to fetch more pages', async () => {
     const key = queryKey()
 
@@ -182,7 +274,7 @@ describe('useInfiniteQuery', () => {
       rendered.getByText('Page 0: 0')
     })
 
-    await fireEvent.click(rendered.getByText('Load More'))
+    fireEvent.click(rendered.getByText('Load More'))
 
     await waitFor(() => rendered.getByText('Loading more...'))
 
diff --git a/src/react/tests/usePaginatedQuery.test.tsx b/src/react/tests/usePaginatedQuery.test.tsx
index 15ad1227f..def065b90 100644
--- a/src/react/tests/usePaginatedQuery.test.tsx
+++ b/src/react/tests/usePaginatedQuery.test.tsx
@@ -33,11 +33,16 @@ describe('usePaginatedQuery', () => {
     await waitFor(() => rendered.getByText('Status: success'))
 
     expect(states[0]).toEqual({
+      canFetchMore: undefined,
       clear: expect.any(Function),
+      data: undefined,
       error: null,
       failureCount: 0,
+      fetchMore: expect.any(Function),
       isError: false,
+      isFetched: false,
       isFetching: true,
+      isFetchingMore: false,
       isIdle: false,
       isLoading: true,
       isStale: true,
@@ -51,11 +56,16 @@ describe('usePaginatedQuery', () => {
     })
 
     expect(states[1]).toEqual({
+      canFetchMore: undefined,
       clear: expect.any(Function),
+      data: 1,
       error: null,
       failureCount: 0,
+      fetchMore: expect.any(Function),
       isError: false,
+      isFetched: true,
       isFetching: false,
+      isFetchingMore: false,
       isIdle: false,
       isLoading: false,
       isStale: true,
diff --git a/src/react/tests/useQuery.test.tsx b/src/react/tests/useQuery.test.tsx
index 2110bceeb..6d8e08e58 100644
--- a/src/react/tests/useQuery.test.tsx
+++ b/src/react/tests/useQuery.test.tsx
@@ -98,12 +98,16 @@ describe('useQuery', () => {
     await waitFor(() => rendered.getByText('Status: success'))
 
     expect(states[0]).toEqual({
+      canFetchMore: undefined,
       clear: expect.any(Function),
       data: undefined,
       error: null,
       failureCount: 0,
+      fetchMore: expect.any(Function),
       isError: false,
+      isFetched: false,
       isFetching: true,
+      isFetchingMore: false,
       isIdle: false,
       isLoading: true,
       isStale: true,
@@ -115,12 +119,16 @@ describe('useQuery', () => {
     })
 
     expect(states[1]).toEqual({
+      canFetchMore: undefined,
       clear: expect.any(Function),
       data: 'test',
       error: null,
       failureCount: 0,
+      fetchMore: expect.any(Function),
       isError: false,
+      isFetched: true,
       isFetching: false,
+      isFetchingMore: false,
       isIdle: false,
       isLoading: false,
       isStale: true,
@@ -163,12 +171,16 @@ describe('useQuery', () => {
     await waitFor(() => rendered.getByText('Status: error'))
 
     expect(states[0]).toEqual({
+      canFetchMore: undefined,
       clear: expect.any(Function),
       data: undefined,
       error: null,
       failureCount: 0,
+      fetchMore: expect.any(Function),
       isError: false,
+      isFetched: false,
       isFetching: true,
+      isFetchingMore: false,
       isIdle: false,
       isLoading: true,
       isStale: true,
@@ -180,12 +192,16 @@ describe('useQuery', () => {
     })
 
     expect(states[1]).toEqual({
+      canFetchMore: undefined,
       clear: expect.any(Function),
       data: undefined,
       error: null,
       failureCount: 1,
+      fetchMore: expect.any(Function),
       isError: false,
+      isFetched: false,
       isFetching: true,
+      isFetchingMore: false,
       isIdle: false,
       isLoading: true,
       isStale: true,
@@ -197,12 +213,16 @@ describe('useQuery', () => {
     })
 
     expect(states[2]).toEqual({
+      canFetchMore: undefined,
       clear: expect.any(Function),
       data: undefined,
       error: 'rejected',
       failureCount: 2,
+      fetchMore: expect.any(Function),
       isError: true,
+      isFetched: true,
       isFetching: false,
+      isFetchingMore: false,
       isIdle: false,
       isLoading: false,
       isStale: true,
@@ -266,6 +286,113 @@ describe('useQuery', () => {
     expect(newTodos).not.toBe(todos)
     expect(newTodo1).toBe(todo1)
     expect(newTodo2).not.toBe(todo2)
+
+    return null
+  })
+
+  it('should keep the previous data when keepPreviousData is set', async () => {
+    const key = queryKey()
+    const states: QueryResult<number>[] = []
+
+    function Page() {
+      const [count, setCount] = React.useState(0)
+
+      const state = useQuery(
+        [key, count],
+        async () => {
+          await sleep(10)
+          return count
+        },
+        { keepPreviousData: true }
+      )
+
+      states.push(state)
+
+      React.useEffect(() => {
+        setTimeout(() => {
+          setCount(1)
+        }, 20)
+      }, [])
+
+      return null
+    }
+
+    render(<Page />)
+
+    await waitFor(() => expect(states.length).toBe(4))
+
+    expect(states[0]).toMatchObject({
+      data: undefined,
+      isFetching: true,
+      isSuccess: false,
+    })
+    expect(states[1]).toMatchObject({
+      data: 0,
+      isFetching: false,
+      isSuccess: true,
+    })
+    expect(states[2]).toMatchObject({
+      data: 0,
+      isFetching: true,
+      isSuccess: true,
+    })
+    expect(states[3]).toMatchObject({
+      data: 1,
+      isFetching: false,
+      isSuccess: true,
+    })
+  })
+
+  it('should use the correct query function when components use different configurations', async () => {
+    const key = queryKey()
+    const states: QueryResult<number>[] = []
+
+    function FirstComponent() {
+      const state = useQuery(key, () => 1)
+      const refetch = state.refetch
+
+      states.push(state)
+
+      React.useEffect(() => {
+        setTimeout(() => {
+          refetch()
+        }, 10)
+      }, [refetch])
+
+      return null
+    }
+
+    function SecondComponent() {
+      useQuery(key, () => 2)
+      return null
+    }
+
+    function Page() {
+      return (
+        <>
+          <FirstComponent />
+          <SecondComponent />
+        </>
+      )
+    }
+
+    render(<Page />)
+
+    await waitFor(() => expect(states.length).toBe(4))
+
+    expect(states[0]).toMatchObject({
+      data: undefined,
+    })
+    expect(states[1]).toMatchObject({
+      data: 1,
+    })
+    expect(states[2]).toMatchObject({
+      data: 1,
+    })
+    // This state should be 1 instead of 2
+    expect(states[3]).toMatchObject({
+      data: 1,
+    })
   })
 
   // See https://github.com/tannerlinsley/react-query/issues/137