Repository: tannerlinsley/react-query
Pull request #1727 (2021-02-02 17:37:11)

hotfix: devtools hydration error
Closes https://github.com/tannerlinsley/react-query/issues/1726

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/1d55i14ny](https://vercel.com/tannerlinsley/react-query/1d55i14ny)  
✅ 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/initial-query-data.md b/docs/src/pages/guides/initial-query-data.md
index 01d62899f..502f4a69d 100644
--- a/docs/src/pages/guides/initial-query-data.md
+++ b/docs/src/pages/guides/initial-query-data.md
@@ -8,8 +8,8 @@ There are many ways to supply initial data for a query to the cache before you n
 - Declaratively:
   - Provide `initialData` to a query to prepopulate its cache if empty
 - Imperatively:
-  - [Prefetch the data using `queryClient.prefetchQuery`](../prefetching)
-  - [Manually place the data into the cache using `queryClient.setQueryData`](../prefetching)
+  - [Prefetch the data using `queryClient.prefetchQuery`](./prefetching)
+  - [Manually place the data into the cache using `queryClient.setQueryData`](./prefetching)
 
 ## Using `initialData` to prepopulate a query
 
diff --git a/docs/src/pages/guides/queries.md b/docs/src/pages/guides/queries.md
index 0c4e7f592..1eb68e439 100644
--- a/docs/src/pages/guides/queries.md
+++ b/docs/src/pages/guides/queries.md
@@ -37,7 +37,7 @@ The `result` object contains a few very important states you'll need to be aware
 - `isSuccess` or `status === 'success'` - The query was successful and data is available
 - `isIdle` or `status === 'idle'` - The query is currently disabled (you'll learn more about this in a bit)
 
-Beyond those primary state, more information is available depending on the state the query:
+Beyond those primary states, more information is available depending on the state of the query:
 
 - `error` - If the query is in an `isError` state, the error is available via the `error` property.
 - `data` - If the query is in a `success` state, the data is available via the `data` property.
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/guides/window-focus-refetching.md b/docs/src/pages/guides/window-focus-refetching.md
index a385f0d27..9b0de8f26 100644
--- a/docs/src/pages/guides/window-focus-refetching.md
+++ b/docs/src/pages/guides/window-focus-refetching.md
@@ -67,15 +67,11 @@ Instead of event listeners on `window`, React Native provides focus information
 import { AppState } from 'react-native'
 import { focusManager } from 'react-query'
 
-focusManager.setEventListener(setFocus => {
-  const handleAppStateChange = appState => {
-    setFocus(appState === 'active')
-  }
-
-  AppState.addEventListener('change', handleAppStateChange)
+focusManager.setEventListener(handleFocus => {
+  AppState.addEventListener('change', handleFocus)
 
   return () => {
-    AppState.removeEventListener('change', handleAppStateChange)
+   AppState.removeEventListener('change', handleFocus)
   }
 })
 ```
diff --git a/docs/src/pages/reference/InfiniteQueryObserver.md b/docs/src/pages/reference/InfiniteQueryObserver.md
index aa64f7f83..71d85f581 100644
--- a/docs/src/pages/reference/InfiniteQueryObserver.md
+++ b/docs/src/pages/reference/InfiniteQueryObserver.md
@@ -23,4 +23,4 @@ const unsubscribe = observer.subscribe(result => {
 
 **Options**
 
-The options for the `InfiniteQueryObserver` are exactly the same as those of [`useInfiniteQuery`](#useinfinitequery).
+The options for the `InfiniteQueryObserver` are exactly the same as those of [`useInfiniteQuery`](/reference/useInfiniteQuery).
diff --git a/docs/src/pages/reference/QueryClient.md b/docs/src/pages/reference/QueryClient.md
index b6847850a..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)
@@ -185,7 +185,7 @@ queryClient.setQueryData(queryKey, updater)
 
 **Options**
 
-- `queryKey: QueryKey` [Query Keys](./guides/query-keys)
+- `queryKey: QueryKey`: [Query Keys](../guides/query-keys)
 - `updater: unknown | (oldData: TData | undefined) => TData`
   - If non-function is passed, the data will be updated to this value
   - If a function is passed, it will receive the old data value and be expected to return a new one.
@@ -228,8 +228,8 @@ The `invalidateQueries` method can be used to invalidate and refetch single or m
 ```js
 await queryClient.invalidateQueries('posts', {
   exact,
-  refetchActive = true,
-  refetchInactive = false
+  refetchActive: true,
+  refetchInactive: false
 }, { throwOnError })
 ```
 
diff --git a/docs/src/pages/reference/QueryObserver.md b/docs/src/pages/reference/QueryObserver.md
index 70ccfba12..51813453d 100644
--- a/docs/src/pages/reference/QueryObserver.md
+++ b/docs/src/pages/reference/QueryObserver.md
@@ -18,4 +18,4 @@ const unsubscribe = observer.subscribe(result => {
 
 **Options**
 
-The options for the `QueryObserver` are exactly the same as those of [`useQuery`](#usequery).
+The options for the `QueryObserver` are exactly the same as those of [`useQuery`](/reference/useQuery).
diff --git a/docs/src/pages/reference/focusManager.md b/docs/src/pages/reference/focusManager.md
index 323b39134..7aaa00798 100644
--- a/docs/src/pages/reference/focusManager.md
+++ b/docs/src/pages/reference/focusManager.md
@@ -37,7 +37,7 @@ focusManager.setEventListener(handleFocus => {
 
 ## `focusManager.setFocused`
 
-`setFocsued` can be used to manually set the focus state. Set `undefined` to fallback to the default focus check.
+`setFocused` can be used to manually set the focus state. Set `undefined` to fallback to the default focus check.
 
 ```js
 import { focusManager } from 'react-query'
diff --git a/docs/src/pages/reference/useInfiniteQuery.md b/docs/src/pages/reference/useInfiniteQuery.md
index 62ee6ebd1..77d9198b7 100644
--- a/docs/src/pages/reference/useInfiniteQuery.md
+++ b/docs/src/pages/reference/useInfiniteQuery.md
@@ -21,15 +21,16 @@ const {
 
 **Options**
 
-The options for `useInfiniteQuery` are identical to the [`useQuery` hook](#usequery) with the addition of the following:
+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.
@@ -41,7 +42,7 @@ The options for `useInfiniteQuery` are identical to the [`useQuery` hook](#usequ
 
 **Returns**
 
-The returned properties for `useInfiniteQuery` are identical to the [`useQuery` hook](#usequery), with the addition of the following:
+The returned properties for `useInfiniteQuery` are identical to the [`useQuery` hook](/reference/useQuery), with the addition of the following:
 
 - `data.pages: TData[]`
   - Array containing all pages.
diff --git a/docs/src/pages/reference/useMutation.md b/docs/src/pages/reference/useMutation.md
index 5448bce41..d182480c9 100644
--- a/docs/src/pages/reference/useMutation.md
+++ b/docs/src/pages/reference/useMutation.md
@@ -17,6 +17,7 @@ const {
   reset,
   status,
 } = useMutation(mutationFn, {
+  mutationKey,
   onError,
   onMutate,
   onSettled,
diff --git a/docs/src/pages/reference/useQueries.md b/docs/src/pages/reference/useQueries.md
index c83a1261b..f69e5fe3a 100644
--- a/docs/src/pages/reference/useQueries.md
+++ b/docs/src/pages/reference/useQueries.md
@@ -14,7 +14,7 @@ const results = useQueries([
 
 **Options**
 
-The `useQueries` hook accepts an array with query option objects identical to the [`useQuery` hook](#usequery).
+The `useQueries` hook accepts an array with query option objects identical to the [`useQuery` hook](/reference/useQuery).
 
 **Returns**
 
diff --git a/docs/src/pages/reference/useQuery.md b/docs/src/pages/reference/useQuery.md
index 9cb8be065..ef71b1c0f 100644
--- a/docs/src/pages/reference/useQuery.md
+++ b/docs/src/pages/reference/useQuery.md
@@ -6,15 +6,19 @@ title: useQuery
 ```js
 const {
   data,
+  dataUpdatedAt,
   error,
+  errorUpdatedAt,
   failureCount,
   isError,
   isFetchedAfterMount,
   isFetching,
   isIdle,
   isLoading,
+  isLoadingError,
   isPlaceholderData,
   isPreviousData,
+  isRefetchError,
   isStale,
   isSuccess,
   refetch,
@@ -24,6 +28,7 @@ const {
   cacheTime,
   enabled,
   initialData,
+  initialDataUpdatedAt
   isDataEqual,
   keepPreviousData,
   notifyOnChangeProps,
@@ -38,6 +43,7 @@ const {
   refetchOnReconnect,
   refetchOnWindowFocus,
   retry,
+  retryOnMount,
   retryDelay,
   select,
   staleTime,
@@ -63,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`
@@ -75,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.
@@ -141,6 +149,9 @@ const result = useQuery({
   - If set to a function, the function will be called **once** during the shared/root query initialization, and be expected to synchronously return the initialData
   - Initial data is considered stale by default unless a `staleTime` has been set.
   - `initialData` **is persisted** to the cache
+- `initialDataUpdatedAt: number | (() => number | undefined)`
+  - Optional
+  - If set, this value will be used as the time (in milliseconds) of when the `initialData` itself was last updated.
 - `placeholderData: TData | () => TData`
   - Optional
   - If set, this value will be used as the placeholder data for this particular query observer while the query is still in the `loading` data and no initialData has been provided.
@@ -177,9 +188,13 @@ const result = useQuery({
 - `data: TData`
   - Defaults to `undefined`.
   - The last successfully resolved data for the query.
+- `dataUpdatedAt: number`
+  - The timestamp for when the query most recently returned the `status` as `"success"`.
 - `error: null | TError`
   - Defaults to `null`
   - The error object for the query, if an error was thrown.
+- `errorUpdatedAt: number`
+  - The timestamp for when the query most recently returned the `status` as `"error"`.
 - `isStale: boolean`
   - Will be `true` if the data in the cache is invalidated or if the data is older than the given `staleTime`.
 - `isPlaceholderData: boolean`
@@ -196,8 +211,9 @@ const result = useQuery({
   - The failure count for the query.
   - Incremented every time the query fails.
   - Reset to `0` when the query succeeds.
-- `refetch: (options: { throwOnError: boolean }) => Promise<UseQueryResult>`
+- `refetch: (options: { throwOnError: boolean, cancelRefetch: boolean }) => Promise<UseQueryResult>`
   - A function to manually refetch the query.
   - If the query errors, the error will only be logged. If you want an error to be thrown, pass the `throwOnError: true` option
+  - If `cancelRefetch` is `true`, then the current request will be cancelled before a new request is made
 - `remove: () => void`
   - A function to remove the query from the cache.
diff --git a/examples/auto-refetching/package.json b/examples/auto-refetching/package.json
index 0571377f4..925d81daa 100644
--- a/examples/auto-refetching/package.json
+++ b/examples/auto-refetching/package.json
@@ -4,7 +4,7 @@
   "main": "index.js",
   "license": "MIT",
   "dependencies": {
-    "axios": "^0.19.2",
+    "axios": "^0.21.1",
     "isomorphic-unfetch": "3.0.0",
     "next": "9.3.2",
     "react": "16.13.0",
diff --git a/examples/basic-graphql-request/package.json b/examples/basic-graphql-request/package.json
index 94838147b..d8081ff1e 100644
--- a/examples/basic-graphql-request/package.json
+++ b/examples/basic-graphql-request/package.json
@@ -7,7 +7,7 @@
     "eject": "rescripts eject"
   },
   "dependencies": {
-    "axios": "0.19.2",
+    "axios": "^0.21.1",
     "graphql": "^15.3.0",
     "graphql-request": "^3.1.0",
     "react": "^16.8.6",
diff --git a/examples/basic/package.json b/examples/basic/package.json
index c7148c0d0..487cfbe41 100644
--- a/examples/basic/package.json
+++ b/examples/basic/package.json
@@ -7,7 +7,7 @@
     "eject": "rescripts eject"
   },
   "dependencies": {
-    "axios": "0.19.2",
+    "axios": "^0.21.1",
     "react": "^16.8.6",
     "react-dom": "^16.8.6",
     "react-query": "^3.5.0",
diff --git a/examples/custom-hooks/package.json b/examples/custom-hooks/package.json
index 9c99e6dcb..4dddacc1a 100644
--- a/examples/custom-hooks/package.json
+++ b/examples/custom-hooks/package.json
@@ -7,7 +7,7 @@
     "eject": "rescripts eject"
   },
   "dependencies": {
-    "axios": "0.19.2",
+    "axios": "^0.21.1",
     "react": "^16.8.6",
     "react-dom": "^16.8.6",
     "react-query": "^3.5.0",
diff --git a/examples/default-query-function/package.json b/examples/default-query-function/package.json
index c7148c0d0..487cfbe41 100644
--- a/examples/default-query-function/package.json
+++ b/examples/default-query-function/package.json
@@ -7,7 +7,7 @@
     "eject": "rescripts eject"
   },
   "dependencies": {
-    "axios": "0.19.2",
+    "axios": "^0.21.1",
     "react": "^16.8.6",
     "react-dom": "^16.8.6",
     "react-query": "^3.5.0",
diff --git a/examples/focus-refetching/package.json b/examples/focus-refetching/package.json
index 183bf731d..1dd00ef7c 100644
--- a/examples/focus-refetching/package.json
+++ b/examples/focus-refetching/package.json
@@ -4,7 +4,7 @@
   "main": "index.js",
   "license": "MIT",
   "dependencies": {
-    "axios": "0.19.2",
+    "axios": "^0.21.1",
     "isomorphic-unfetch": "3.0.0",
     "next": "9.3.2",
     "react": "16.13.0",
diff --git a/examples/load-more-infinite-scroll/package.json b/examples/load-more-infinite-scroll/package.json
index 92f0b4d7f..c420f6835 100644
--- a/examples/load-more-infinite-scroll/package.json
+++ b/examples/load-more-infinite-scroll/package.json
@@ -4,7 +4,7 @@
   "main": "index.js",
   "license": "MIT",
   "dependencies": {
-    "axios": "^0.19.2",
+    "axios": "^0.21.1",
     "isomorphic-unfetch": "3.0.0",
     "next": "9.3.2",
     "react": "16.13.0",
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/examples/optimistic-updates-typescript/package.json b/examples/optimistic-updates-typescript/package.json
index 0a5ff5ba0..2b779d5af 100755
--- a/examples/optimistic-updates-typescript/package.json
+++ b/examples/optimistic-updates-typescript/package.json
@@ -6,7 +6,7 @@
   "dependencies": {
     "@types/react": "^17.0.0",
     "@types/node": "14.14.14",
-    "axios": "^0.19.2",
+    "axios": "^0.21.1",
     "isomorphic-unfetch": "3.0.0",
     "next": "9.2.2",
     "react": "^17.0.1",
diff --git a/examples/optimistic-updates/package.json b/examples/optimistic-updates/package.json
index e74ca4d97..97e918c87 100755
--- a/examples/optimistic-updates/package.json
+++ b/examples/optimistic-updates/package.json
@@ -4,7 +4,7 @@
   "main": "index.js",
   "license": "MIT",
   "dependencies": {
-    "axios": "^0.19.2",
+    "axios": "^0.21.1",
     "isomorphic-unfetch": "3.0.0",
     "next": "9.2.2",
     "react": "16.13.0",
diff --git a/examples/pagination/package.json b/examples/pagination/package.json
index 92f0b4d7f..c420f6835 100644
--- a/examples/pagination/package.json
+++ b/examples/pagination/package.json
@@ -4,7 +4,7 @@
   "main": "index.js",
   "license": "MIT",
   "dependencies": {
-    "axios": "^0.19.2",
+    "axios": "^0.21.1",
     "isomorphic-unfetch": "3.0.0",
     "next": "9.3.2",
     "react": "16.13.0",
diff --git a/examples/prefetching/package.json b/examples/prefetching/package.json
index 2d25bff33..c1f017579 100644
--- a/examples/prefetching/package.json
+++ b/examples/prefetching/package.json
@@ -4,7 +4,7 @@
   "main": "index.js",
   "license": "MIT",
   "dependencies": {
-    "axios": "0.19.2",
+    "axios": "^0.21.1",
     "isomorphic-unfetch": "3.0.0",
     "next": "9.3.2",
     "react": "16.13.0",
diff --git a/examples/simple/package.json b/examples/simple/package.json
index c7148c0d0..487cfbe41 100644
--- a/examples/simple/package.json
+++ b/examples/simple/package.json
@@ -7,7 +7,7 @@
     "eject": "rescripts eject"
   },
   "dependencies": {
-    "axios": "0.19.2",
+    "axios": "^0.21.1",
     "react": "^16.8.6",
     "react-dom": "^16.8.6",
     "react-query": "^3.5.0",
diff --git a/examples/star-wars/package.json b/examples/star-wars/package.json
index aa4cf7e79..f5c1f6072 100644
--- a/examples/star-wars/package.json
+++ b/examples/star-wars/package.json
@@ -8,7 +8,7 @@
   },
   "dependencies": {
     "@material-ui/core": "^4.9.7",
-    "axios": "0.19.2",
+    "axios": "^0.21.1",
     "react": "^16.8.6",
     "react-dom": "^16.8.6",
     "react-query": "^3.5.0",
diff --git a/examples/suspense/package.json b/examples/suspense/package.json
index 0d491409b..4bc8162fa 100644
--- a/examples/suspense/package.json
+++ b/examples/suspense/package.json
@@ -5,7 +5,7 @@
   "keywords": [],
   "main": "src/index.js",
   "dependencies": {
-    "axios": "0.19.2",
+    "axios": "^0.21.1",
     "react": "0.0.0-experimental-5faf377df",
     "react-dom": "0.0.0-experimental-5faf377df",
     "react-error-boundary": "^2.2.3",
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/infiniteQueryObserver.ts b/src/core/infiniteQueryObserver.ts
index 22458612d..959572a08 100644
--- a/src/core/infiniteQueryObserver.ts
+++ b/src/core/infiniteQueryObserver.ts
@@ -98,11 +98,9 @@ export class InfiniteQueryObserver<
     })
   }
 
-  protected getNewResult(
-    willFetch?: boolean
-  ): InfiniteQueryObserverResult<TData, TError> {
+  protected getNewResult(): InfiniteQueryObserverResult<TData, TError> {
     const { state } = this.getCurrentQuery()
-    const result = super.getNewResult(willFetch)
+    const result = super.getNewResult()
     return {
       ...result,
       fetchNextPage: this.fetchNextPage,
diff --git a/src/core/mutationObserver.ts b/src/core/mutationObserver.ts
index 05476efda..da52a321c 100644
--- a/src/core/mutationObserver.ts
+++ b/src/core/mutationObserver.ts
@@ -116,7 +116,8 @@ export class MutationObserver<
 
     this.currentMutation = this.client.getMutationCache().build(this.client, {
       ...this.options,
-      variables: variables ?? this.options.variables,
+      variables:
+        typeof variables !== 'undefined' ? variables : this.options.variables,
     })
 
     this.currentMutation.addObserver(this)
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 986326aab..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
@@ -87,6 +94,7 @@ export class QueryObserver<
         this.executeFetch()
       }
 
+      this.updateResult()
       this.updateTimers()
     }
   }
@@ -154,9 +162,7 @@ export class QueryObserver<
   setOptions(
     options?: QueryObserverOptions<TQueryFnData, TError, TData, TQueryData>
   ): void {
-    const prevOptions = this.options
-    const prevQuery = this.currentQuery
-
+    this.previousOptions = this.options
     this.options = this.client.defaultQueryObserverOptions(options)
 
     if (
@@ -168,43 +174,76 @@ 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
     }
 
-    this.updateQuery()
+    const didUpdateQuery = this.updateQuery()
 
-    // Take no further actions if there are no subscribers
-    if (!this.listeners.length) {
-      return
-    }
+    let optionalFetch
+    let updateResult
+    let updateStaleTimeout
+    let updateRefetchInterval
 
-    // If we subscribed to a new query, optionally fetch and update refetch
-    if (this.currentQuery !== prevQuery) {
-      this.optionalFetch()
-      this.updateTimers()
-      return
+    // 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) {
-      this.optionalFetch()
+    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
     ) {
-      this.updateStaleTimeout()
+      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
     ) {
-      this.updateRefetchInterval()
+      updateRefetchInterval = true
+    }
+
+    // Fetch only if there are subscribers
+    if (this.hasListeners()) {
+      if (optionalFetch) {
+        this.optionalFetch()
+      }
+    }
+
+    if (updateResult) {
+      this.updateResult()
+    }
+
+    // Update intervals only if there are subscribers
+    if (this.hasListeners()) {
+      if (updateStaleTimeout) {
+        this.updateStaleTimeout()
+      }
+      if (updateRefetchInterval) {
+        this.updateRefetchInterval()
+      }
     }
+
+    // Reset previous options after all code related to option changes has run
+    this.previousOptions = this.options
   }
 
   getCurrentResult(): QueryObserverResult<TData, TError> {
@@ -302,12 +341,7 @@ export class QueryObserver<
 
     this.staleTimeoutId = setTimeout(() => {
       if (!this.currentResult.isStale) {
-        const prevResult = this.currentResult
         this.updateResult()
-        this.notify({
-          listeners: this.shouldNotifyListeners(prevResult, this.currentResult),
-          cache: true,
-        })
       }
     }, timeout)
   }
@@ -353,18 +387,18 @@ export class QueryObserver<
     this.refetchIntervalId = undefined
   }
 
-  protected getNewResult(
-    willFetch?: boolean
-  ): QueryObserverResult<TData, TError> {
+  protected getNewResult(): QueryObserverResult<TData, TError> {
     const { state } = this.currentQuery
     let { isFetching, status } = state
     let isPreviousData = false
     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 (willFetch) {
+    if (!this.hasListeners() && this.willFetchOnMount()) {
       isFetching = true
       if (!dataUpdatedAt) {
         status = 'loading'
@@ -385,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'
         }
       }
     }
@@ -421,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:
@@ -442,7 +487,7 @@ export class QueryObserver<
   }
 
   private shouldNotifyListeners(
-    prevResult: QueryObserverResult,
+    prevResult: QueryObserverResult | undefined,
     result: QueryObserverResult
   ): boolean {
     const { notifyOnChangeProps, notifyOnChangePropsExclusions } = this.options
@@ -451,6 +496,10 @@ export class QueryObserver<
       return false
     }
 
+    if (!prevResult) {
+      return true
+    }
+
     if (!notifyOnChangeProps && !notifyOnChangePropsExclusions) {
       return true
     }
@@ -485,39 +534,60 @@ export class QueryObserver<
     return false
   }
 
-  private updateResult(willFetch?: boolean): void {
-    const result = this.getNewResult(willFetch)
+  private updateResult(action?: Action<TData, TError>): void {
+    const prevResult = this.currentResult as
+      | QueryObserverResult<TData, TError>
+      | undefined
+
+    const result = this.getNewResult()
 
     // Keep reference to the current state on which the current result is based on
     this.currentResultState = this.currentQuery.state
 
     // Only update if something has changed
-    if (!shallowEqualObjects(result, this.currentResult)) {
-      this.currentResult = result
+    if (shallowEqualObjects(result, prevResult)) {
+      return
+    }
 
-      if (this.options.notifyOnChangeProps === 'tracked') {
-        const addTrackedProps = (prop: keyof QueryObserverResult) => {
-          if (!this.trackedProps.includes(prop)) {
-            this.trackedProps.push(prop)
-          }
+    this.currentResult = result
+
+    if (this.options.notifyOnChangeProps === 'tracked') {
+      const addTrackedProps = (prop: keyof QueryObserverResult) => {
+        if (!this.trackedProps.includes(prop)) {
+          this.trackedProps.push(prop)
         }
-        this.trackedCurrentResult = {} as QueryObserverResult<TData, TError>
-
-        Object.keys(result).forEach(key => {
-          Object.defineProperty(this.trackedCurrentResult, key, {
-            configurable: false,
-            enumerable: true,
-            get() {
-              addTrackedProps(key as keyof QueryObserverResult)
-              return result[key as keyof QueryObserverResult]
-            },
-          })
-        })
       }
+      this.trackedCurrentResult = {} as QueryObserverResult<TData, TError>
+
+      Object.keys(result).forEach(key => {
+        Object.defineProperty(this.trackedCurrentResult, key, {
+          configurable: false,
+          enumerable: true,
+          get() {
+            addTrackedProps(key as keyof QueryObserverResult)
+            return result[key as keyof QueryObserverResult]
+          },
+        })
+      })
+    }
+
+    // Determine which callbacks to trigger
+    const notifyOptions: NotifyOptions = { cache: true }
+
+    if (action?.type === 'success') {
+      notifyOptions.onSuccess = true
+    } else if (action?.type === 'error') {
+      notifyOptions.onError = true
     }
+
+    if (this.shouldNotifyListeners(prevResult, result)) {
+      notifyOptions.listeners = true
+    }
+
+    this.notify(notifyOptions)
   }
 
-  private updateQuery(): void {
+  private updateQuery(): boolean {
     const prevQuery = this.currentQuery
 
     const query = this.client
@@ -528,7 +598,7 @@ export class QueryObserver<
       )
 
     if (query === prevQuery) {
-      return
+      return false
     }
 
     this.previousQueryResult = this.currentResult
@@ -536,54 +606,19 @@ export class QueryObserver<
     this.initialDataUpdateCount = query.state.dataUpdateCount
     this.initialErrorUpdateCount = query.state.errorUpdateCount
 
-    const willFetch = prevQuery
-      ? this.willFetchOptionally()
-      : this.willFetchOnMount()
-
-    this.updateResult(willFetch)
-
-    if (!this.hasListeners()) {
-      return
+    if (this.hasListeners()) {
+      prevQuery?.removeObserver(this)
+      this.currentQuery.addObserver(this)
     }
 
-    prevQuery?.removeObserver(this)
-    this.currentQuery.addObserver(this)
-
-    if (
-      this.shouldNotifyListeners(this.previousQueryResult, this.currentResult)
-    ) {
-      this.notify({ listeners: true })
-    }
+    return true
   }
 
   onQueryUpdate(action: Action<TData, TError>): void {
-    // Store current result and get new result
-    const prevResult = this.currentResult
-    this.updateResult()
-    const currentResult = this.currentResult
-
-    // Update timers
-    this.updateTimers()
-
-    // Do not notify if the nothing has changed
-    if (prevResult === currentResult) {
-      return
-    }
-
-    // Determine which callbacks to trigger
-    const notifyOptions: NotifyOptions = {}
-
-    if (action.type === 'success') {
-      notifyOptions.onSuccess = true
-    } else if (action.type === 'error') {
-      notifyOptions.onError = true
-    }
-
-    if (this.shouldNotifyListeners(prevResult, currentResult)) {
-      notifyOptions.listeners = true
+    this.updateResult(action)
+    if (this.hasListeners()) {
+      this.updateTimers()
     }
-
-    this.notify(notifyOptions)
   }
 
   private notify(notifyOptions: NotifyOptions): void {
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/core/types.ts b/src/core/types.ts
index 9cabdcc2a..40ecfec39 100644
--- a/src/core/types.ts
+++ b/src/core/types.ts
@@ -216,7 +216,9 @@ export interface ResultOptions {
   throwOnError?: boolean
 }
 
-export interface RefetchOptions extends ResultOptions {}
+export interface RefetchOptions extends ResultOptions {
+  cancelRefetch?: boolean;
+}
 
 export interface InvalidateQueryFilters extends QueryFilters {
   refetchActive?: boolean
diff --git a/src/devtools/useLocalStorage.ts b/src/devtools/useLocalStorage.ts
index b26a7f45d..a7f46e955 100644
--- a/src/devtools/useLocalStorage.ts
+++ b/src/devtools/useLocalStorage.ts
@@ -11,13 +11,17 @@ const getItem = key => {
 }
 
 export default function useLocalStorage(key, defaultValue) {
-  const [value, setValue] = React.useState(() => {
-    const val = getItem(key)
-    if (typeof val === 'undefined' || val === null) {
-      return typeof defaultValue === 'function' ? defaultValue() : defaultValue
+  const [value, setValue] = React.useState()
+
+  useEffect(() => {
+    const initialValue = getItem(key)
+
+    if (typeof initialValue === 'undefined' || initialValue === null) {
+      setValue(typeof defaultValue === 'function' ? defaultValue() : defaultValue)
+    } else {
+      setValue(initialValue)
     }
-    return val
-  })
+  }, []);
 
   const setter = React.useCallback(
     updater => {
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/mutations.test.tsx b/src/core/tests/mutations.test.tsx
index 30264f895..45dcdd547 100644
--- a/src/core/tests/mutations.test.tsx
+++ b/src/core/tests/mutations.test.tsx
@@ -24,6 +24,23 @@ describe('mutations', () => {
     expect(result).toBe(result)
   })
 
+  test('mutate should accept null values', async () => {
+    let variables
+
+    const mutation = new MutationObserver(queryClient, {
+      mutationFn: async (vars: unknown) => {
+        variables = vars
+        return vars
+      },
+    })
+
+    mutation.mutate(null)
+
+    await sleep(10)
+
+    expect(variables).toBe(null)
+  })
+
   test('setMutationDefaults should be able to set defaults', async () => {
     const key = queryKey()
 
diff --git a/src/core/tests/queryClient.test.tsx b/src/core/tests/queryClient.test.tsx
index 8b7c241a4..1de12ad59 100644
--- a/src/core/tests/queryClient.test.tsx
+++ b/src/core/tests/queryClient.test.tsx
@@ -77,6 +77,7 @@ describe('queryClient', () => {
       const observer = new QueryObserver(queryClient, {
         queryKey: [key],
         retry: false,
+        enabled: false,
       })
       const { status } = await observer.refetch()
       expect(status).toBe('error')
@@ -152,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 abdd49320..91dcbc673 100644
--- a/src/core/tests/queryObserver.test.tsx
+++ b/src/core/tests/queryObserver.test.tsx
@@ -49,6 +49,24 @@ describe('queryObserver', () => {
     expect(results[2]).toMatchObject({ data: 2, status: 'success' })
   })
 
+  test('should notify when the query has updated before subscribing', async () => {
+    const key = queryKey()
+    const results: QueryObserverResult[] = []
+    const observer = new QueryObserver(queryClient, {
+      queryKey: key,
+      queryFn: () => 1,
+      staleTime: Infinity,
+    })
+    queryClient.setQueryData(key, 2)
+    const unsubscribe = observer.subscribe(result => {
+      results.push(result)
+    })
+    await sleep(1)
+    unsubscribe()
+    expect(results.length).toBe(1)
+    expect(results[0]).toMatchObject({ data: 2, status: 'success' })
+  })
+
   test('should be able to fetch with a selector', async () => {
     const key = queryKey()
     const observer = new QueryObserver(queryClient, {
@@ -112,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 472bd6166..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,
@@ -333,6 +334,34 @@ describe('useQuery', () => {
     expect(onSuccess).toHaveBeenCalledWith('data')
   })
 
+  it('should call onSuccess after a query has been refetched', async () => {
+    const key = queryKey()
+    const states: UseQueryResult<string>[] = []
+    const onSuccess = jest.fn()
+
+    function Page() {
+      const state = useQuery(key, () => 'data', { onSuccess })
+
+      states.push(state)
+
+      const { refetch } = state
+
+      React.useEffect(() => {
+        setActTimeout(() => {
+          refetch()
+        }, 10)
+      }, [refetch])
+
+      return null
+    }
+
+    renderWithClient(queryClient, <Page />)
+
+    await sleep(50)
+    expect(states.length).toBe(4)
+    expect(onSuccess).toHaveBeenCalledTimes(2)
+  })
+
   it('should call onSuccess after a disabled query has been fetched', async () => {
     const key = queryKey()
     const states: UseQueryResult<string>[] = []
@@ -700,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>[] = []
@@ -872,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 })
@@ -1023,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)
 
@@ -1054,7 +1119,8 @@ describe('useQuery', () => {
     function Page() {
       const state = useQuery(
         key,
-        () => {
+        async () => {
+          await sleep(1)
           count++
           return count
         },
@@ -1794,11 +1860,13 @@ describe('useQuery', () => {
     unsubscribe()
 
     // 1. Subscribe observer
-    // 2. Query init
-    // 3. Query fetch
-    // 4. Query stale
-    // 5. Unsubscribe observer
-    expect(fn).toHaveBeenCalledTimes(5)
+    // 2. Query loading
+    // 3. Observer loading
+    // 4. Query success
+    // 5. Observer success
+    // 6. Query stale
+    // 7. Unsubscribe observer
+    expect(fn).toHaveBeenCalledTimes(7)
   })
 
   it('should not re-render when it should only re-render on data changes and the data did not change', async () => {
@@ -1904,6 +1972,38 @@ describe('useQuery', () => {
     expect(queryCache.find(key)!.options.queryFn).toBe(queryFn1)
   })
 
+  it('should render correct states even in case of useEffect triggering delays', async () => {
+    const key = queryKey()
+    const states: UseQueryResult<string>[] = []
+
+    const originalUseEffect = React.useEffect
+
+    // Try to simulate useEffect timing delay
+    React.useEffect = (...args: any[]) => {
+      originalUseEffect(() => {
+        setTimeout(() => {
+          args[0]()
+        }, 10)
+      }, args[1])
+    }
+
+    function Page() {
+      const state = useQuery(key, () => 'data', { staleTime: Infinity })
+      states.push(state)
+      return null
+    }
+
+    renderWithClient(queryClient, <Page />)
+    queryClient.setQueryData(key, 'data')
+    await sleep(50)
+
+    React.useEffect = originalUseEffect
+
+    expect(states.length).toBe(2)
+    expect(states[0]).toMatchObject({ status: 'loading' })
+    expect(states[1]).toMatchObject({ status: 'success' })
+  })
+
   it('should batch re-renders', async () => {
     const key = queryKey()
 
@@ -2152,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
     }
@@ -2663,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
     }
@@ -3207,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>[] = []
@@ -3215,7 +3389,8 @@ describe('useQuery', () => {
     function Page() {
       const state = useQuery(
         key,
-        () => {
+        async () => {
+          await sleep(1)
           count++
           return count
         },