Repository: tannerlinsley/react-query
Pull request #1742 (2021-02-04 12:35:02)

docs: fixes fetch example which throws error
I tried to run the example in a React Native project and it gave me the following error:

`Error: Objects are not valid as a React child (found: TypeError: Cannot read property 'text' of undefined). If you meant to render a collection of children, use an array instead.`

I guess the `useQuery` hook tries to access the text property of the response which is not available when returning `json()`.

vercel[bot]: This pull request is being automatically deployed with Vercel ([learn more](https://vercel.link/github-learn-more)).  
To see the status of your deployment, click below or on the icon next to each commit.

🔍 Inspect: [https://vercel.com/tannerlinsley/react-query/bbol5vdpa](https://vercel.com/tannerlinsley/react-query/bbol5vdpa)  
✅ Preview: *In Progress*



Patch:
diff --git a/docs/src/pages/guides/infinite-queries.md b/docs/src/pages/guides/infinite-queries.md
index bec40050d..458d4c13c 100644
--- a/docs/src/pages/guides/infinite-queries.md
+++ b/docs/src/pages/guides/infinite-queries.md
@@ -16,6 +16,8 @@ When using `useInfiniteQuery`, you'll notice a few things are different:
 - A `hasPreviousPage` boolean is now available and is `true` if `getPreviousPageParam` returns a value other than `undefined`.
 - The `isFetchingNextPage` and `isFetchingPreviousPage` booleans are now available to distinguish between a background refresh state and a loading more state
 
+> Note: When using options like `initialData` or `select` in your query, make sure when you restructure your data that it still includes `data.pages` and `data.pageParams` properties, otherwise your changes will be overwritten by the query in its return!
+
 ## Example
 
 Let's assume we have an API that returns pages of `projects` 3 at a time based on a `cursor` index along with a cursor that can be used to fetch the next group of projects:
@@ -152,3 +154,19 @@ queryClient.setQueryData('projects', data => ({
   pageParams: data.pageParams.slice(1),
 }))
 ```
+
+Manually removing a single of value from an individual page:
+
+```js
+const newPagesArray = []
+oldPagesArray?.pages.forEach(page => {
+  const newData = page.data.filter(val => val.id !== updatedId)
+  newPagesArray.push({ data: newData, pageParam: page.pageParam })
+})
+queryClient.setQueryData('projects', data => ({
+  pages: newPagesArray,
+  pageParams: data.pageParams,
+}))
+```
+
+Make sure to keep the same data structure of pages and pageParams!
diff --git a/docs/src/pages/guides/query-functions.md b/docs/src/pages/guides/query-functions.md
index a7a5fbc68..5136fae77 100644
--- a/docs/src/pages/guides/query-functions.md
+++ b/docs/src/pages/guides/query-functions.md
@@ -36,11 +36,11 @@ While most utilities like `axios` or `graphql-request` automatically throw error
 
 ```js
 useQuery(['todos', todoId], async () => {
-  const { ok, json } = await fetch('/todos/' + todoId)
-  if (!ok) {
+  const response = await fetch('/todos/' + todoId)
+  if (!response.ok) {
     throw new Error('Network response was not ok')
   }
-  return json()
+  return response.json()
 })
 ```
 
diff --git a/docs/src/pages/guides/ssr.md b/docs/src/pages/guides/ssr.md
index cfeeacc59..1e3ea5578 100644
--- a/docs/src/pages/guides/ssr.md
+++ b/docs/src/pages/guides/ssr.md
@@ -49,7 +49,7 @@ React Query supports prefetching multiple queries on the server in Next.js and t
 
 To support caching queries on the server and set up hydration:
 
-- Create a new `QueryClient` instance
+- Create a new `QueryClient` instance **inside of your app, and on an instance ref. This ensures that data is not shared between different users and requests.**
 - Wrap your app component with `<QueryClientProvider>` and pass it the client instance
 - Wrap your app component with `<Hydrate>` and pass it the `dehydratedState` prop from `pageProps`
 
@@ -58,11 +58,14 @@ To support caching queries on the server and set up hydration:
 import { QueryClient, QueryClientProvider } from 'react-query'
 import { Hydrate } from 'react-query/hydration'
 
-const queryClient = new QueryClient()
-
 export default function MyApp({ Component, pageProps }) {
+  const queryClientRef = React.useRef()
+  if (!queryClientRef.current) {
+    queryClientRef.current = new QueryClient()
+  }
+
   return (
-    <QueryClientProvider client={queryClient}>
+    <QueryClientProvider client={queryClientRef.current}>
       <Hydrate state={pageProps.dehydratedState}>
         <Component {...pageProps} />
       </Hydrate>
@@ -73,7 +76,7 @@ export default function MyApp({ Component, pageProps }) {
 
 Now you are ready to prefetch some data in your pages with either [`getStaticProps`](https://nextjs.org/docs/basic-features/data-fetching#getstaticprops-static-generation) (for SSG) or [`getServerSideProps`](https://nextjs.org/docs/basic-features/data-fetching#getserversideprops-server-side-rendering) (for SSR). From React Query's perspective, these integrate in the same way, `getStaticProps` is shown below.
 
-- Create a new `QueryClient` instance for each page request
+- Create a new `QueryClient` instance **for each page request. This ensures that data is not shared between users and requests.**
 - Prefetch the data using the clients `prefetchQuery` method and wait for it to complete
 - Use `dehydrate` to dehydrate the query cache and pass it to the page via the `dehydratedState` prop. This is the same prop that the cache will be picked up from in your `_app.js`
 
@@ -117,7 +120,7 @@ This guide is at-best, a high level overview of how SSR with React Query should
 
 ### On the Server
 
-- Create a new `QueryClient` instance
+- Create a new `QueryClient` instance **inside of your request handler. This ensures that data is not shared between different users and requests.**
 - Using the client, prefetch any data you need
 - Dehydrate the client
 - Render your app with the client provider and also **using the dehydrated state. This is extremely important! You must render both server and client using the same dehydrated state to ensure hydration on the client produces the exact same markup as the server.**
@@ -129,28 +132,30 @@ This guide is at-best, a high level overview of how SSR with React Query should
 import { QueryClient, QueryClientProvider } from 'react-query'
 import { dehydrate, Hydrate } from 'react-query/hydration'
 
-const queryClient = new QueryClient()
-await queryClient.prefetchQuery('key', fn)
-const dehydratedState = dehydrate(queryClient)
+function handleRequest (req, res) {
+  const queryClient = new QueryClient()
+  await queryClient.prefetchQuery('key', fn)
+  const dehydratedState = dehydrate(queryClient)
 
-const html = ReactDOM.renderToString(
-  <QueryClientProvider client={queryClient}>
-    <Hydrate state={dehydratedState}>
-      <App />
-    </Hydrate>
-  </QueryClientProvider>
-)
+  const html = ReactDOM.renderToString(
+    <QueryClientProvider client={queryClient}>
+      <Hydrate state={dehydratedState}>
+        <App />
+      </Hydrate>
+    </QueryClientProvider>
+  )
 
-res.send(`
-  <html>
-    <body>
-      <div id="root">${html}</div>
-      <script>
-        window.__REACT_QUERY_STATE__ = ${JSON.stringify(dehydratedState)};
-      </script>
-    </body>
-  </html>
+  res.send(`
+    <html>
+      <body>
+        <div id="root">${html}</div>
+        <script>
+          window.__REACT_QUERY_STATE__ = ${JSON.stringify(dehydratedState)};
+        </script>
+      </body>
+    </html>
   `)
+}
 ```
 
 ### Client
diff --git a/docs/src/pages/reference/QueryClient.md b/docs/src/pages/reference/QueryClient.md
index 5f86b4a9e..cd3d7e340 100644
--- a/docs/src/pages/reference/QueryClient.md
+++ b/docs/src/pages/reference/QueryClient.md
@@ -117,7 +117,7 @@ The options for `fetchInfiniteQuery` are exactly the same as those of [`fetchQue
 
 ## `queryClient.prefetchQuery`
 
-`prefetchQuery` is an asynchronous method that can be used to prefetch a query before it is needed or rendered with `useQuery` and friends. The method works the same as `fetchQuery` except that is will not throw or return any data.
+`prefetchQuery` is an asynchronous method that can be used to prefetch a query before it is needed or rendered with `useQuery` and friends. The method works the same as `fetchQuery` except that it will not throw or return any data.
 
 ```js
 await queryClient.prefetchQuery(queryKey, queryFn)
diff --git a/docs/src/pages/reference/useInfiniteQuery.md b/docs/src/pages/reference/useInfiniteQuery.md
index c59b69ff9..77d9198b7 100644
--- a/docs/src/pages/reference/useInfiniteQuery.md
+++ b/docs/src/pages/reference/useInfiniteQuery.md
@@ -24,12 +24,13 @@ const {
 The options for `useInfiniteQuery` are identical to the [`useQuery` hook](/reference/useQuery) with the addition of the following:
 
 - `queryFn: (context: QueryFunctionContext) => Promise<TData>`
-  - **Required, but only if no default query function has been defined**
+  - **Required, but only if no default query function has been defined** [`defaultQueryFn`](/guides/default-query-function)
   - The function that the query will use to request data.
   - Receives a `QueryFunctionContext` object with the following variables:
     - `queryKey: QueryKey`
     - `pageParam: unknown | undefined`
   - Must return a promise that will either resolves data or throws an error.
+  - Make sure you return the data *and* the `pageParam` if needed for use in the props below.
 - `getNextPageParam: (lastPage, allPages) => unknown | undefined`
   - When new data is received for this query, this function receives both the last page of the infinite list of data and the full array of all pages.
   - It should return a **single variable** that will be passed as the last optional parameter to your query function.
diff --git a/docs/src/pages/reference/useQuery.md b/docs/src/pages/reference/useQuery.md
index f68cc9209..ef71b1c0f 100644
--- a/docs/src/pages/reference/useQuery.md
+++ b/docs/src/pages/reference/useQuery.md
@@ -43,6 +43,7 @@ const {
   refetchOnReconnect,
   refetchOnWindowFocus,
   retry,
+  retryOnMount,
   retryDelay,
   select,
   staleTime,
@@ -68,7 +69,7 @@ const result = useQuery({
   - The query key will be hashed into a stable hash. See [Query Keys](../guides/query-keys) for more information.
   - The query will automatically update when this key changes (as long as `enabled` is not set to `false`).
 - `queryFn: (context: QueryFunctionContext) => Promise<TData>`
-  - **Required, but only if no default query function has been defined**
+  - **Required, but only if no default query function has been defined** See [Default Query Function](../guides/default-query-function) for more information.
   - The function that the query will use to request data.
   - Receives a `QueryFunctionContext` object with the following variables:
     - `queryKey: QueryKey`
@@ -80,6 +81,8 @@ const result = useQuery({
   - If `false`, failed queries will not retry by default.
   - If `true`, failed queries will retry infinitely.
   - If set to an `number`, e.g. `3`, failed queries will retry until the failed query count meets that number.
+- `retryOnMount: boolean`
+  - If set to `false`, the query will not be retried on mount if it contains an error. Defaults to `true`.
 - `retryDelay: (retryAttempt: number) => number`
   - This function receives a `retryAttempt` integer and returns the delay to apply before the next attempt in milliseconds.
   - A function like `attempt => Math.min(attempt > 1 ? 2 ** attempt * 1000 : 1000, 30 * 1000)` applies exponential backoff.
diff --git a/examples/nextjs/pages/_app.js b/examples/nextjs/pages/_app.js
index cf2ca77ad..56b91717b 100644
--- a/examples/nextjs/pages/_app.js
+++ b/examples/nextjs/pages/_app.js
@@ -3,11 +3,14 @@ import { QueryClient, QueryClientProvider } from 'react-query'
 import { Hydrate } from 'react-query/hydration'
 import { ReactQueryDevtools } from 'react-query/devtools'
 
-const queryClient = new QueryClient()
-
 export default function MyApp({ Component, pageProps }) {
+  const queryClientRef = React.useRef()
+  if (!queryClientRef.current) {
+    queryClientRef.current = new QueryClient()
+  }
+
   return (
-    <QueryClientProvider client={queryClient}>
+    <QueryClientProvider client={queryClientRef.current}>
       <Hydrate state={pageProps.dehydratedState}>
         <Component {...pageProps} />
       </Hydrate>
diff --git a/src/core/index.ts b/src/core/index.ts
index 89f6f659e..c3ecd0616 100644
--- a/src/core/index.ts
+++ b/src/core/index.ts
@@ -1,3 +1,4 @@
+export { CancelledError } from './retryer'
 export { QueryCache } from './queryCache'
 export { QueryClient } from './queryClient'
 export { QueryObserver } from './queryObserver'
@@ -14,6 +15,5 @@ export { isCancelledError } from './retryer'
 
 // Types
 export * from './types'
-export type { CancelledError } from './retryer'
 export type { Query } from './query'
 export type { Logger } from './logger'
diff --git a/src/core/infiniteQueryBehavior.ts b/src/core/infiniteQueryBehavior.ts
index 6bca69785..beed03d62 100644
--- a/src/core/infiniteQueryBehavior.ts
+++ b/src/core/infiniteQueryBehavior.ts
@@ -17,6 +17,7 @@ export function infiniteQueryBehavior<
         const oldPages = context.state.data?.pages || []
         const oldPageParams = context.state.data?.pageParams || []
         let newPageParams = oldPageParams
+        let cancelled = false
 
         // Get query function
         const queryFn =
@@ -29,6 +30,10 @@ export function infiniteQueryBehavior<
           param?: unknown,
           previous?: boolean
         ): Promise<unknown[]> => {
+          if (cancelled) {
+            return Promise.reject('Cancelled')
+          }
+
           if (typeof param === 'undefined' && !manual && pages.length) {
             return Promise.resolve(pages)
           }
@@ -38,11 +43,7 @@ export function infiniteQueryBehavior<
             pageParam: param,
           }
 
-          let cancelFn: undefined | (() => any)
           const queryFnResult = queryFn(queryFnContext)
-          if ((queryFnResult as any).cancel) {
-            cancelFn = (queryFnResult as any).cancel
-          }
 
           const promise = Promise.resolve(queryFnResult).then(page => {
             newPageParams = previous
@@ -51,15 +52,15 @@ export function infiniteQueryBehavior<
             return previous ? [page, ...pages] : [...pages, page]
           })
 
-          if (cancelFn) {
+          if (isCancelable(queryFnResult)) {
             const promiseAsAny = promise as any
-            promiseAsAny.cancel = cancelFn
+            promiseAsAny.cancel = queryFnResult.cancel
           }
 
           return promise
         }
 
-        let promise
+        let promise: Promise<unknown[]>
 
         // Fetch first page?
         if (!oldPages.length) {
@@ -109,9 +110,13 @@ export function infiniteQueryBehavior<
           pageParams: newPageParams,
         }))
 
-        if (isCancelable(promise)) {
-          const finalPromiseAsAny = finalPromise as any
-          finalPromiseAsAny.cancel = promise.cancel
+        const finalPromiseAsAny = finalPromise as any
+
+        finalPromiseAsAny.cancel = () => {
+          cancelled = true
+          if (isCancelable(promise)) {
+            promise.cancel()
+          }
         }
 
         return finalPromise
diff --git a/src/core/onlineManager.ts b/src/core/onlineManager.ts
index 922cc3e69..fc0c8362e 100644
--- a/src/core/onlineManager.ts
+++ b/src/core/onlineManager.ts
@@ -45,7 +45,14 @@ class OnlineManager extends Subscribable {
       return this.online
     }
 
-    return navigator.onLine === undefined || navigator.onLine
+    if (
+      typeof navigator === 'undefined' ||
+      typeof navigator.onLine === 'undefined'
+    ) {
+      return true
+    }
+
+    return navigator.onLine
   }
 
   private setDefaultEventListener() {
diff --git a/src/core/query.ts b/src/core/query.ts
index 6f8610c20..8e6aef538 100644
--- a/src/core/query.ts
+++ b/src/core/query.ts
@@ -135,7 +135,7 @@ export class Query<
   private cache: QueryCache
   private promise?: Promise<TData>
   private gcTimeout?: number
-  private retryer?: Retryer<unknown, TError>
+  private retryer?: Retryer<TData, TError>
   private observers: QueryObserver<any, any, any, any>[]
   private defaultOptions?: QueryOptions<TQueryFnData, TError, TData>
 
@@ -387,28 +387,21 @@ export class Query<
 
     // Try to fetch the data
     this.retryer = new Retryer({
-      fn: context.fetchFn,
-      onFail: () => {
-        this.dispatch({ type: 'failed' })
-      },
-      onPause: () => {
-        this.dispatch({ type: 'pause' })
-      },
-      onContinue: () => {
-        this.dispatch({ type: 'continue' })
-      },
-      retry: context.options.retry,
-      retryDelay: context.options.retryDelay,
-    })
+      fn: context.fetchFn as () => TData,
+      onSuccess: data => {
+        this.setData(data as TData)
 
-    this.promise = this.retryer.promise
-      .then(data => this.setData(data as TData))
-      .catch(error => {
-        // Set error state if needed
+        // Remove query after fetching if cache time is 0
+        if (this.cacheTime === 0) {
+          this.optionalRemove()
+        }
+      },
+      onError: error => {
+        // Optimistically update state if needed
         if (!(isCancelledError(error) && error.silent)) {
           this.dispatch({
             type: 'error',
-            error,
+            error: error as TError,
           })
         }
 
@@ -426,18 +419,21 @@ export class Query<
         if (this.cacheTime === 0) {
           this.optionalRemove()
         }
+      },
+      onFail: () => {
+        this.dispatch({ type: 'failed' })
+      },
+      onPause: () => {
+        this.dispatch({ type: 'pause' })
+      },
+      onContinue: () => {
+        this.dispatch({ type: 'continue' })
+      },
+      retry: context.options.retry,
+      retryDelay: context.options.retryDelay,
+    })
 
-        // Propagate error
-        throw error
-      })
-      .then(data => {
-        // Remove query after fetching if cache time is 0
-        if (this.cacheTime === 0) {
-          this.optionalRemove()
-        }
-
-        return data
-      })
+    this.promise = this.retryer.promise
 
     return this.promise
   }
diff --git a/src/core/queryCache.ts b/src/core/queryCache.ts
index 5c4829535..e323e1d6d 100644
--- a/src/core/queryCache.ts
+++ b/src/core/queryCache.ts
@@ -109,6 +109,11 @@ export class QueryCache extends Subscribable<QueryCacheListener> {
     arg2?: QueryFilters
   ): Query<TQueryFnData, TError, TData> | undefined {
     const [filters] = parseFilterArgs(arg1, arg2)
+
+    if (typeof filters.exact === 'undefined') {
+      filters.exact = true
+    }
+
     return this.queries.find(query => matchQuery(filters, query))
   }
 
diff --git a/src/core/queryObserver.ts b/src/core/queryObserver.ts
index 13938952c..6cbd29c92 100644
--- a/src/core/queryObserver.ts
+++ b/src/core/queryObserver.ts
@@ -21,6 +21,7 @@ import type { Query, QueryState, Action, FetchOptions } from './query'
 import type { QueryClient } from './queryClient'
 import { focusManager } from './focusManager'
 import { Subscribable } from './subscribable'
+import { getLogger } from './logger'
 
 type QueryObserverListener<TData, TError> = (
   result: QueryObserverResult<TData, TError>
@@ -49,6 +50,12 @@ export class QueryObserver<
   private currentQuery!: Query<TQueryFnData, TError, TQueryData>
   private currentResult!: QueryObserverResult<TData, TError>
   private currentResultState?: QueryState<TQueryData, TError>
+  private previousOptions?: QueryObserverOptions<
+    TQueryFnData,
+    TError,
+    TData,
+    TQueryData
+  >
   private previousQueryResult?: QueryObserverResult<TData, TError>
   private initialDataUpdateCount: number
   private initialErrorUpdateCount: number
@@ -155,8 +162,7 @@ export class QueryObserver<
   setOptions(
     options?: QueryObserverOptions<TQueryFnData, TError, TData, TQueryData>
   ): void {
-    const prevOptions = this.options
-
+    this.previousOptions = this.options
     this.options = this.client.defaultQueryObserverOptions(options)
 
     if (
@@ -168,39 +174,49 @@ export class QueryObserver<
 
     // Keep previous query key if the user does not supply one
     if (!this.options.queryKey) {
-      this.options.queryKey = prevOptions.queryKey
+      this.options.queryKey = this.previousOptions.queryKey
     }
 
     const didUpdateQuery = this.updateQuery()
 
     let optionalFetch
+    let updateResult
     let updateStaleTimeout
     let updateRefetchInterval
 
-    // If we subscribed to a new query, optionally fetch and update intervals
+    // If we subscribed to a new query, optionally fetch and update result and timers
     if (didUpdateQuery) {
       optionalFetch = true
+      updateResult = true
       updateStaleTimeout = true
       updateRefetchInterval = true
     }
 
     // Optionally fetch if the query became enabled
-    if (this.options.enabled !== false && prevOptions.enabled === false) {
+    if (
+      this.options.enabled !== false &&
+      this.previousOptions.enabled === false
+    ) {
       optionalFetch = true
     }
 
+    // Update result if the select function changed
+    if (this.options.select !== this.previousOptions.select) {
+      updateResult = true
+    }
+
     // Update stale interval if needed
     if (
-      this.options.enabled !== prevOptions.enabled ||
-      this.options.staleTime !== prevOptions.staleTime
+      this.options.enabled !== this.previousOptions.enabled ||
+      this.options.staleTime !== this.previousOptions.staleTime
     ) {
       updateStaleTimeout = true
     }
 
     // Update refetch interval if needed
     if (
-      this.options.enabled !== prevOptions.enabled ||
-      this.options.refetchInterval !== prevOptions.refetchInterval
+      this.options.enabled !== this.previousOptions.enabled ||
+      this.options.refetchInterval !== this.previousOptions.refetchInterval
     ) {
       updateRefetchInterval = true
     }
@@ -212,8 +228,7 @@ export class QueryObserver<
       }
     }
 
-    // Update result when subscribing to a new query
-    if (didUpdateQuery) {
+    if (updateResult) {
       this.updateResult()
     }
 
@@ -226,6 +241,9 @@ export class QueryObserver<
         this.updateRefetchInterval()
       }
     }
+
+    // Reset previous options after all code related to option changes has run
+    this.previousOptions = this.options
   }
 
   getCurrentResult(): QueryObserverResult<TData, TError> {
@@ -376,6 +394,8 @@ export class QueryObserver<
     let isPlaceholderData = false
     let data: TData | undefined
     let dataUpdatedAt = state.dataUpdatedAt
+    let error = state.error
+    let errorUpdatedAt = state.errorUpdatedAt
 
     // Optimistically set status to loading if we will start fetching
     if (!this.hasListeners() && this.willFetchOnMount()) {
@@ -399,13 +419,24 @@ export class QueryObserver<
     }
     // Select data if needed
     else if (this.options.select && typeof state.data !== 'undefined') {
-      // Use the previous select result if the query data did not change
-      if (this.currentResult && state.data === this.currentResultState?.data) {
+      // Use the previous select result if the query data and select function did not change
+      if (
+        this.currentResult &&
+        state.data === this.currentResultState?.data &&
+        this.options.select === this.previousOptions?.select
+      ) {
         data = this.currentResult.data
       } else {
-        data = this.options.select(state.data)
-        if (this.options.structuralSharing !== false) {
-          data = replaceEqualDeep(this.currentResult?.data, data)
+        try {
+          data = this.options.select(state.data)
+          if (this.options.structuralSharing !== false) {
+            data = replaceEqualDeep(this.currentResult?.data, data)
+          }
+        } catch (selectError) {
+          getLogger().error(selectError)
+          error = selectError
+          errorUpdatedAt = Date.now()
+          status = 'error'
         }
       }
     }
@@ -435,8 +466,8 @@ export class QueryObserver<
       ...getStatusProps(status),
       data,
       dataUpdatedAt,
-      error: state.error,
-      errorUpdatedAt: state.errorUpdatedAt,
+      error,
+      errorUpdatedAt,
       failureCount: state.fetchFailureCount,
       isFetched: state.dataUpdateCount > 0 || state.errorUpdateCount > 0,
       isFetchedAfterMount:
diff --git a/src/core/retryer.ts b/src/core/retryer.ts
index 8b4b7bfbf..f637e47d0 100644
--- a/src/core/retryer.ts
+++ b/src/core/retryer.ts
@@ -6,6 +6,8 @@ import { functionalUpdate, sleep } from './utils'
 
 interface RetryerConfig<TData = unknown, TError = unknown> {
   fn: () => TData | Promise<TData>
+  onError?: (error: TError) => void
+  onSuccess?: (data: TData) => void
   onFail?: (failureCount: number, error: TError) => void
   onPause?: () => void
   onContinue?: () => void
@@ -88,15 +90,21 @@ export class Retryer<TData = unknown, TError = unknown> {
     })
 
     const resolve = (value: any) => {
-      this.isResolved = true
-      continueFn?.()
-      promiseResolve(value)
+      if (!this.isResolved) {
+        this.isResolved = true
+        config.onSuccess?.(value)
+        continueFn?.()
+        promiseResolve(value)
+      }
     }
 
     const reject = (value: any) => {
-      this.isResolved = true
-      continueFn?.()
-      promiseReject(value)
+      if (!this.isResolved) {
+        this.isResolved = true
+        config.onError?.(value)
+        continueFn?.()
+        promiseReject(value)
+      }
     }
 
     const pause = () => {
@@ -129,13 +137,15 @@ export class Retryer<TData = unknown, TError = unknown> {
 
       // Create callback to cancel this fetch
       cancelFn = cancelOptions => {
-        reject(new CancelledError(cancelOptions))
-
-        // Cancel transport if supported
-        if (isCancelable(promiseOrValue)) {
-          try {
-            promiseOrValue.cancel()
-          } catch {}
+        if (!this.isResolved) {
+          reject(new CancelledError(cancelOptions))
+
+          // Cancel transport if supported
+          if (isCancelable(promiseOrValue)) {
+            try {
+              promiseOrValue.cancel()
+            } catch {}
+          }
         }
       }
 
diff --git a/src/react/useBaseQuery.ts b/src/react/useBaseQuery.ts
index c75338b5a..b004a3687 100644
--- a/src/react/useBaseQuery.ts
+++ b/src/react/useBaseQuery.ts
@@ -1,15 +1,18 @@
 import React from 'react'
 
+import { QueryObserverResult } from '../core/types'
 import { notifyManager } from '../core/notifyManager'
 import { QueryObserver } from '../core/queryObserver'
 import { useQueryErrorResetBoundary } from './QueryErrorResetBoundary'
 import { useQueryClient } from './QueryClientProvider'
 import { UseBaseQueryOptions } from './types'
+import { useIsMounted } from './useIsMounted'
 
 export function useBaseQuery<TQueryFnData, TError, TData, TQueryData>(
   options: UseBaseQueryOptions<TQueryFnData, TError, TData, TQueryData>,
   Observer: typeof QueryObserver
 ) {
+  const isMounted = useIsMounted()
   const queryClient = useQueryClient()
   const errorResetBoundary = useQueryErrorResetBoundary()
   const defaultedOptions = queryClient.defaultQueryObserverOptions(options)
@@ -63,8 +66,14 @@ export function useBaseQuery<TQueryFnData, TError, TData, TQueryData>(
   // Subscribe to the observer
   React.useEffect(() => {
     errorResetBoundary.clearReset()
-    return observer.subscribe(notifyManager.batchCalls(setCurrentResult))
-  }, [observer, errorResetBoundary])
+    return observer.subscribe(
+      notifyManager.batchCalls((result: QueryObserverResult) => {
+        if (isMounted()) {
+          setCurrentResult(result)
+        }
+      })
+    )
+  }, [observer, errorResetBoundary, isMounted])
 
   // Handle suspense
   if (observer.options.suspense || observer.options.useErrorBoundary) {
diff --git a/src/react/useIsFetching.ts b/src/react/useIsFetching.ts
index 1c6eb0c50..c7c919f14 100644
--- a/src/react/useIsFetching.ts
+++ b/src/react/useIsFetching.ts
@@ -4,6 +4,7 @@ import { notifyManager } from '../core/notifyManager'
 import { QueryKey } from '../core/types'
 import { parseFilterArgs, QueryFilters } from '../core/utils'
 import { useQueryClient } from './QueryClientProvider'
+import { useIsMounted } from './useIsMounted'
 
 export function useIsFetching(filters?: QueryFilters): number
 export function useIsFetching(
@@ -14,6 +15,7 @@ export function useIsFetching(
   arg1?: QueryKey | QueryFilters,
   arg2?: QueryFilters
 ): number {
+  const isMounted = useIsMounted()
   const queryClient = useQueryClient()
   const [filters] = parseFilterArgs(arg1, arg2)
   const [isFetching, setIsFetching] = React.useState(
@@ -29,13 +31,15 @@ export function useIsFetching(
     () =>
       queryClient.getQueryCache().subscribe(
         notifyManager.batchCalls(() => {
-          const newIsFetching = queryClient.isFetching(filtersRef.current)
-          if (isFetchingRef.current !== newIsFetching) {
-            setIsFetching(newIsFetching)
+          if (isMounted()) {
+            const newIsFetching = queryClient.isFetching(filtersRef.current)
+            if (isFetchingRef.current !== newIsFetching) {
+              setIsFetching(newIsFetching)
+            }
           }
         })
       ),
-    [queryClient]
+    [queryClient, isMounted]
   )
 
   return isFetching
diff --git a/src/react/useIsMounted.ts b/src/react/useIsMounted.ts
new file mode 100644
index 000000000..f1f620f72
--- /dev/null
+++ b/src/react/useIsMounted.ts
@@ -0,0 +1,17 @@
+import React from 'react'
+
+import { isServer } from '../core/utils'
+
+export function useIsMounted() {
+  const mountedRef = React.useRef(false)
+  const isMounted = React.useCallback(() => mountedRef.current, [])
+
+  React[isServer ? 'useEffect' : 'useLayoutEffect'](() => {
+    mountedRef.current = true
+    return () => {
+      mountedRef.current = false
+    }
+  }, [])
+
+  return isMounted
+}
diff --git a/src/react/useMutation.ts b/src/react/useMutation.ts
index 7e65706b7..d5fb6374e 100644
--- a/src/react/useMutation.ts
+++ b/src/react/useMutation.ts
@@ -14,6 +14,7 @@ import {
   MutationKey,
   MutationObserverResult,
 } from '../core/types'
+import { useIsMounted } from './useIsMounted'
 
 // HOOK
 
@@ -68,6 +69,7 @@ export function useMutation<
     | UseMutationOptions<TData, TError, TVariables, TContext>,
   arg3?: UseMutationOptions<TData, TError, TVariables, TContext>
 ): UseMutationResult<TData, TError, TVariables, TContext> {
+  const isMounted = useIsMounted()
   const options = parseMutationArgs(arg1, arg2, arg3)
   const queryClient = useQueryClient()
 
@@ -96,14 +98,13 @@ export function useMutation<
           (
             result: MutationObserverResult<TData, TError, TVariables, TContext>
           ) => {
-            // Check if the component is still mounted
-            if (observer.hasListeners()) {
+            if (isMounted()) {
               setCurrentResult(result)
             }
           }
         )
       ),
-    [observer]
+    [observer, isMounted]
   )
 
   const mutate = React.useCallback<
diff --git a/src/react/useQueries.ts b/src/react/useQueries.ts
index 81774b9ae..1a85857bd 100644
--- a/src/react/useQueries.ts
+++ b/src/react/useQueries.ts
@@ -1,11 +1,14 @@
 import React from 'react'
 
+import { QueryObserverResult } from '../core/types'
 import { notifyManager } from '../core/notifyManager'
 import { QueriesObserver } from '../core/queriesObserver'
 import { useQueryClient } from './QueryClientProvider'
 import { UseQueryOptions, UseQueryResult } from './types'
+import { useIsMounted } from './useIsMounted'
 
 export function useQueries(queries: UseQueryOptions[]): UseQueryResult[] {
+  const isMounted = useIsMounted()
   const queryClient = useQueryClient()
 
   // Create queries observer
@@ -25,8 +28,15 @@ export function useQueries(queries: UseQueryOptions[]): UseQueryResult[] {
 
   // Subscribe to the observer
   React.useEffect(
-    () => observer.subscribe(notifyManager.batchCalls(setCurrentResult)),
-    [observer]
+    () =>
+      observer.subscribe(
+        notifyManager.batchCalls((result: QueryObserverResult[]) => {
+          if (isMounted()) {
+            setCurrentResult(result)
+          }
+        })
+      ),
+    [observer, isMounted]
   )
 
   return currentResult


Test patch:
diff --git a/.github/workflows/test-and-publish.yml b/.github/workflows/test-and-publish.yml
index 958b97ced..d8ef950e2 100644
--- a/.github/workflows/test-and-publish.yml
+++ b/.github/workflows/test-and-publish.yml
@@ -4,6 +4,7 @@ on:
   push:
     branches:
       - 'master'
+      - 'next'
       - 'beta'
       - '1.x'
       - '2.x'
@@ -29,7 +30,7 @@ jobs:
     name: 'Publish Module to NPM'
     needs: test
     # publish only when merged in master on original repo, not on PR
-    if: github.repository == 'tannerlinsley/react-query' && (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/beta' || github.ref == 'refs/heads/1.x' || github.ref == 'refs/heads/2.x')
+    if: github.repository == 'tannerlinsley/react-query' && (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/next' || github.ref == 'refs/heads/beta' || github.ref == 'refs/heads/1.x' || github.ref == 'refs/heads/2.x')
     runs-on: ubuntu-latest
     steps:
       - uses: actions/checkout@v2
diff --git a/docs/src/pages/guides/testing.md b/docs/src/pages/guides/testing.md
index 9b0b5bdcc..a3bb0cfb7 100644
--- a/docs/src/pages/guides/testing.md
+++ b/docs/src/pages/guides/testing.md
@@ -21,8 +21,7 @@ Once installed, a simple test can be written. Given the following custom hook:
 
 ```
 export function useCustomHook() {
-  const { data } = useQuery('customHook', () => 'Hello');
-  return data;
+  return useQuery('customHook', () => 'Hello');
 }
 ```
 
@@ -36,9 +35,11 @@ const wrapper = ({ children }) => (
   </QueryClientProvider>
 );
 
-const { result } = renderHook(() => useCustomHook(), { wrapper });
+const { result, waitFor } = renderHook(() => useCustomHook(), { wrapper });
 
-expect(result.current).toEqual('Hello');
+await waitFor(() => result.current.isSuccess);
+
+expect(result.current.data).toEqual("Hello");
 ```
 
 Note that we provide a custom wrapper that builds the `QueryClient` and `QueryClientProvider`. This helps to ensure that our test is completely isolated from any other tests.
diff --git a/src/core/tests/queryClient.test.tsx b/src/core/tests/queryClient.test.tsx
index a754a7d0b..1de12ad59 100644
--- a/src/core/tests/queryClient.test.tsx
+++ b/src/core/tests/queryClient.test.tsx
@@ -153,6 +153,25 @@ describe('queryClient', () => {
     })
   })
 
+  describe('getQueryData', () => {
+    test('should return the query data if the query is found', () => {
+      const key = queryKey()
+      queryClient.setQueryData([key, 'id'], 'bar')
+      expect(queryClient.getQueryData([key, 'id'])).toBe('bar')
+    })
+
+    test('should return undefined if the query is not found', () => {
+      const key = queryKey()
+      expect(queryClient.getQueryData(key)).toBeUndefined()
+    })
+
+    test('should match exact by default', () => {
+      const key = queryKey()
+      queryClient.setQueryData([key, 'id'], 'bar')
+      expect(queryClient.getQueryData([key])).toBeUndefined()
+    })
+  })
+
   describe('fetchQuery', () => {
     // https://github.com/tannerlinsley/react-query/issues/652
     test('should not retry by default', async () => {
diff --git a/src/core/tests/queryObserver.test.tsx b/src/core/tests/queryObserver.test.tsx
index a540f98ea..91dcbc673 100644
--- a/src/core/tests/queryObserver.test.tsx
+++ b/src/core/tests/queryObserver.test.tsx
@@ -130,6 +130,105 @@ describe('queryObserver', () => {
     expect(observerResult2.data).toMatchObject({ myCount: 1 })
   })
 
+  test('should run the selector again if the selector changed', async () => {
+    const key = queryKey()
+    let count = 0
+    const results: QueryObserverResult[] = []
+    const queryFn = () => ({ count: 1 })
+    const select1 = (data: ReturnType<typeof queryFn>) => {
+      count++
+      return { myCount: data.count }
+    }
+    const select2 = (_data: ReturnType<typeof queryFn>) => {
+      count++
+      return { myCount: 99 }
+    }
+    const observer = new QueryObserver(queryClient, {
+      queryKey: key,
+      queryFn,
+      select: select1,
+    })
+    const unsubscribe = observer.subscribe(result => {
+      results.push(result)
+    })
+    await sleep(1)
+    observer.setOptions({
+      queryKey: key,
+      queryFn,
+      select: select2,
+    })
+    await sleep(1)
+    await observer.refetch()
+    unsubscribe()
+    expect(count).toBe(2)
+    expect(results.length).toBe(4)
+    expect(results[0]).toMatchObject({
+      status: 'success',
+      isFetching: false,
+      data: { myCount: 1 },
+    })
+    expect(results[1]).toMatchObject({
+      status: 'success',
+      isFetching: false,
+      data: { myCount: 99 },
+    })
+    expect(results[2]).toMatchObject({
+      status: 'success',
+      isFetching: true,
+      data: { myCount: 99 },
+    })
+    expect(results[3]).toMatchObject({
+      status: 'success',
+      isFetching: false,
+      data: { myCount: 99 },
+    })
+  })
+
+  test('should not run the selector again if the data and selector did not change', async () => {
+    const key = queryKey()
+    let count = 0
+    const results: QueryObserverResult[] = []
+    const queryFn = () => ({ count: 1 })
+    const select = (data: ReturnType<typeof queryFn>) => {
+      count++
+      return { myCount: data.count }
+    }
+    const observer = new QueryObserver(queryClient, {
+      queryKey: key,
+      queryFn,
+      select,
+    })
+    const unsubscribe = observer.subscribe(result => {
+      results.push(result)
+    })
+    await sleep(1)
+    observer.setOptions({
+      queryKey: key,
+      queryFn,
+      select,
+    })
+    await sleep(1)
+    await observer.refetch()
+    unsubscribe()
+    expect(count).toBe(1)
+    expect(results.length).toBe(3)
+    expect(results[0]).toMatchObject({
+      status: 'success',
+      isFetching: false,
+      data: { myCount: 1 },
+    })
+    expect(results[1]).toMatchObject({
+      status: 'success',
+      isFetching: true,
+      data: { myCount: 1 },
+    })
+    expect(results[2]).toMatchObject({
+      status: 'success',
+      isFetching: false,
+      data: { myCount: 1 },
+    })
+  })
+
   test('should not run the selector again if the data did not change', async () => {
     const key = queryKey()
     let count = 0
diff --git a/src/hydration/tests/ssr.test.tsx b/src/hydration/tests/ssr.test.tsx
index aff7e8642..4a764d59b 100644
--- a/src/hydration/tests/ssr.test.tsx
+++ b/src/hydration/tests/ssr.test.tsx
@@ -143,7 +143,7 @@ describe('Server side rendering with de/rehydration', () => {
     expect(consoleMock).toHaveBeenCalledTimes(1)
     expect(fetchDataError).toHaveBeenCalledTimes(1)
     expect(el.innerHTML).toBe(expectedMarkup)
-    await sleep(10)
+    await sleep(50)
     expect(fetchDataError).toHaveBeenCalledTimes(2)
     expect(el.innerHTML).toBe(
       'ErrorComponent - status:error fetching:false data:undefined'
@@ -208,7 +208,7 @@ describe('Server side rendering with de/rehydration', () => {
     expect(consoleMock).not.toHaveBeenCalled()
     expect(fetchDataSuccess).toHaveBeenCalledTimes(0)
     expect(el.innerHTML).toBe(expectedMarkup)
-    await sleep(10)
+    await sleep(50)
     expect(fetchDataSuccess).toHaveBeenCalledTimes(1)
     expect(el.innerHTML).toBe(
       'SuccessComponent - status:success fetching:false data:success!'
diff --git a/src/react/tests/useInfiniteQuery.test.tsx b/src/react/tests/useInfiniteQuery.test.tsx
index ddc1e25f2..e22967bd7 100644
--- a/src/react/tests/useInfiniteQuery.test.tsx
+++ b/src/react/tests/useInfiniteQuery.test.tsx
@@ -15,6 +15,7 @@ import {
   QueryClient,
   QueryCache,
 } from '../..'
+import { CancelledError } from '../../core'
 
 interface Result {
   items: number[]
@@ -720,6 +721,54 @@ describe('useInfiniteQuery', () => {
     })
   })
 
+  it('should stop fetching additional pages when the component is unmounted', async () => {
+    const key = queryKey()
+    const states: UseInfiniteQueryResult<number>[] = []
+    let fetches = 0
+
+    function List() {
+      const state = useInfiniteQuery(
+        key,
+        async ({ pageParam }) => {
+          fetches++
+          await sleep(50)
+          return Number(pageParam)
+        },
+        {
+          initialData: { pages: [1, 2, 3, 4], pageParams: [1, 2, 3, 4] },
+          getNextPageParam: lastPage => lastPage + 1,
+        }
+      )
+
+      states.push(state)
+
+      return null
+    }
+
+    function Page() {
+      const [show, setShow] = React.useState(true)
+
+      React.useEffect(() => {
+        setActTimeout(() => {
+          setShow(false)
+        }, 75)
+      }, [])
+
+      return show ? <List /> : null
+    }
+
+    renderWithClient(queryClient, <Page />)
+
+    await sleep(300)
+
+    expect(states.length).toBe(1)
+    expect(fetches).toBe(2)
+    expect(queryClient.getQueryState(key)).toMatchObject({
+      status: 'error',
+      error: expect.any(CancelledError),
+    })
+  })
+
   it('should be able to override the cursor in the fetchNextPage callback', async () => {
     const key = queryKey()
     const states: UseInfiniteQueryResult<number>[] = []
@@ -874,6 +923,71 @@ describe('useInfiniteQuery', () => {
     })
   })
 
+  it('should only refetch the first page when initialData is provided', async () => {
+    const key = queryKey()
+    const states: UseInfiniteQueryResult<number>[] = []
+
+    function Page() {
+      const state = useInfiniteQuery(
+        key,
+        async ({ pageParam }) => {
+          await sleep(10)
+          return pageParam
+        },
+        {
+          initialData: { pages: [1], pageParams: [1] },
+          getNextPageParam: lastPage => lastPage + 1,
+        }
+      )
+
+      states.push(state)
+
+      const { fetchNextPage } = state
+
+      React.useEffect(() => {
+        setActTimeout(() => {
+          fetchNextPage()
+        }, 20)
+      }, [fetchNextPage])
+
+      return null
+    }
+
+    renderWithClient(queryClient, <Page />)
+
+    await sleep(100)
+
+    expect(states.length).toBe(4)
+    expect(states[0]).toMatchObject({
+      data: { pages: [1] },
+      hasNextPage: true,
+      isFetching: true,
+      isFetchingNextPage: false,
+      isSuccess: true,
+    })
+    expect(states[1]).toMatchObject({
+      data: { pages: [1] },
+      hasNextPage: true,
+      isFetching: false,
+      isFetchingNextPage: false,
+      isSuccess: true,
+    })
+    expect(states[2]).toMatchObject({
+      data: { pages: [1] },
+      hasNextPage: true,
+      isFetching: true,
+      isFetchingNextPage: true,
+      isSuccess: true,
+    })
+    expect(states[3]).toMatchObject({
+      data: { pages: [1, 2] },
+      hasNextPage: true,
+      isFetching: false,
+      isFetchingNextPage: false,
+      isSuccess: true,
+    })
+  })
+
   it('should set hasNextPage to false if getNextPageParam returns undefined', async () => {
     const key = queryKey()
     const states: UseInfiniteQueryResult<number>[] = []
diff --git a/src/react/tests/useQuery.test.tsx b/src/react/tests/useQuery.test.tsx
index 41ef4c869..9dac880bb 100644
--- a/src/react/tests/useQuery.test.tsx
+++ b/src/react/tests/useQuery.test.tsx
@@ -13,6 +13,7 @@ import {
 } from './utils'
 import {
   useQuery,
+  CancelledError,
   QueryClient,
   UseQueryResult,
   QueryCache,
@@ -728,6 +729,35 @@ describe('useQuery', () => {
     expect(states[1]).toMatchObject({ data: 'test' })
   })
 
+  it('should throw an error when a selector throws', async () => {
+    const consoleMock = mockConsoleError()
+    const key = queryKey()
+    const states: UseQueryResult<string>[] = []
+    const error = new Error('Select Error')
+
+    function Page() {
+      const state = useQuery(key, () => ({ name: 'test' }), {
+        select: () => {
+          throw error
+        },
+      })
+      states.push(state)
+      return null
+    }
+
+    renderWithClient(queryClient, <Page />)
+
+    await sleep(10)
+
+    expect(consoleMock).toHaveBeenCalledWith(error)
+    expect(states.length).toBe(2)
+
+    expect(states[0]).toMatchObject({ status: 'loading', data: undefined })
+    expect(states[1]).toMatchObject({ status: 'error', error })
+
+    consoleMock.mockRestore()
+  })
+
   it('should re-render when dataUpdatedAt changes but data remains the same', async () => {
     const key = queryKey()
     const states: UseQueryResult<string>[] = []
@@ -900,7 +930,7 @@ describe('useQuery', () => {
 
     renderWithClient(queryClient, <Page />)
 
-    await waitFor(() => renderCount > 1)
+    await sleep(10)
     expect(renderCount).toBe(2)
     expect(states.length).toBe(2)
     expect(states[0]).toMatchObject({ data: undefined })
@@ -1051,7 +1081,14 @@ describe('useQuery', () => {
     queryClient.setQueryData(key, 'set')
 
     function Page() {
-      const result = useQuery(key, () => 'fetched', { enabled: false })
+      const result = useQuery(
+        key,
+        async () => {
+          await sleep(1)
+          return 'fetched'
+        },
+        { enabled: false }
+      )
 
       results.push(result)
 
@@ -1082,7 +1119,8 @@ describe('useQuery', () => {
     function Page() {
       const state = useQuery(
         key,
-        () => {
+        async () => {
+          await sleep(1)
           count++
           return count
         },
@@ -2214,10 +2252,17 @@ describe('useQuery', () => {
     let count = 0
 
     function Page() {
-      const state = useQuery(key, () => count++, {
-        staleTime: Infinity,
-        refetchOnWindowFocus: 'always',
-      })
+      const state = useQuery(
+        key,
+        async () => {
+          await sleep(1)
+          return count++
+        },
+        {
+          staleTime: Infinity,
+          refetchOnWindowFocus: 'always',
+        }
+      )
       states.push(state)
       return null
     }
@@ -2725,7 +2770,10 @@ describe('useQuery', () => {
     queryClient.setQueryData(key, 'prefetched')
 
     function Page() {
-      const state = useQuery(key, () => 'data')
+      const state = useQuery(key, async () => {
+        await sleep(1)
+        return 'data'
+      })
       states.push(state)
       return null
     }
@@ -3269,6 +3317,70 @@ describe('useQuery', () => {
     expect(cancelFn).toHaveBeenCalled()
   })
 
+  it('should refetch when quickly switching to a failed query', async () => {
+    const key = queryKey()
+    const states: UseQueryResult<string>[] = []
+
+    const queryFn = () => {
+      let cancelFn = jest.fn()
+
+      const promise = new Promise<string>((resolve, reject) => {
+        cancelFn = jest.fn(() => reject('Cancelled'))
+        sleep(50).then(() => resolve('OK'))
+      })
+
+      ;(promise as any).cancel = cancelFn
+
+      return promise
+    }
+
+    function Page() {
+      const [id, setId] = React.useState(1)
+      const [hasChanged, setHasChanged] = React.useState(false)
+
+      const state = useQuery([key, id], queryFn)
+
+      states.push(state)
+
+      React.useEffect(() => {
+        setId(prevId => (prevId === 1 ? 2 : 1))
+        setHasChanged(true)
+      }, [hasChanged])
+
+      return null
+    }
+
+    renderWithClient(queryClient, <Page />)
+
+    await sleep(100)
+    expect(states.length).toBe(5)
+    // Load query 1
+    expect(states[0]).toMatchObject({
+      status: 'loading',
+      error: null,
+    })
+    // Load query 2
+    expect(states[1]).toMatchObject({
+      status: 'loading',
+      error: null,
+    })
+    // Load query 1
+    expect(states[2]).toMatchObject({
+      status: 'loading',
+      error: expect.any(CancelledError),
+    })
+    // State update
+    expect(states[3]).toMatchObject({
+      status: 'loading',
+      error: expect.any(CancelledError),
+    })
+    // Loaded query 1
+    expect(states[4]).toMatchObject({
+      status: 'success',
+      error: null,
+    })
+  })
+
   it('should update query state and refetch when reset with resetQueries', async () => {
     const key = queryKey()
     const states: UseQueryResult<number>[] = []
@@ -3277,7 +3389,8 @@ describe('useQuery', () => {
     function Page() {
       const state = useQuery(
         key,
-        () => {
+        async () => {
+          await sleep(1)
           count++
           return count
         },