Repository: tannerlinsley/react-query
Pull request #2074 (2021-04-02 15:26:24)

Add TQueryKey to QueryFunction generic type in QueryOptions, remove ensureArray from Query
This PR fixes the following edge case issue in typings:

```ts
import { QueryFunction, useQuery } from "react-query";

type MyData = number;
type MyQueryKey = readonly ["my-data", number];

const getMyData: QueryFunction<MyData, MyQueryKey> = async ({
  queryKey: [, n],
}) => {
  return n + 42;
};

// ...

const myData = useQuery({
  queryKey: ["my-data", 100],
  queryFn: getMyData,
});
```

**Before**

```
No overload matches this call.
  Overload 1 of 3, '(options: UseQueryOptions<number, unknown, number, (string | number)[]>): UseQueryResult<number, unknown>', gave the following error.
    Type 'QueryFunction<number, MyQueryKey>' is not assignable to type 'QueryFunction<number, QueryKey>'.
      Type 'QueryKey' is not assignable to type 'MyQueryKey'.
        Type 'string' is not assignable to type 'readonly ["my-data", number]'.
  Overload 2 of 3, '(queryKey: QueryKey, options?: UseQueryOptions<unknown, unknown, unknown, QueryKey> | undefined): UseQueryResult<unknown, unknown>', gave the following error.
    Argument of type '{ queryKey: (string | number)[]; queryFn: QueryFunction<number, MyQueryKey>; }' is not assignable to parameter of type 'QueryKey'.
      Object literal may only specify known properties, and 'queryKey' does not exist in type 'readonly unknown[]'.ts(2769)
types.d.ts(32, 5): The expected type comes from property 'queryFn' which is declared here on type 'UseQueryOptions<number, unknown, number, (string | number)[]>'
```

**After**

No TS error



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/6sLvwoQh12Dbg3GwN2n5HK4cizyc](https://vercel.com/tannerlinsley/react-query/6sLvwoQh12Dbg3GwN2n5HK4cizyc)  
✅ Preview: *In Progress*


codesandbox[bot]: This pull request is automatically built and testable in [CodeSandbox](https://codesandbox.io).

  To see build info of the built libraries, click [here](https://ci.codesandbox.io/status/tannerlinsley/react-query/pr/2074/builds/115970) or the icon next to each commit SHA.

Latest deployment of this branch, based on commit 1a4a877431464ba74013c7faf24efd773891a1b1:

|Sandbox| Source |
|--|--|
|[tannerlinsley/react-query: basic](https://codesandbox.io/s/tannerlinsleyreact-query-basic-j04x3)| Configuration |
|[tannerlinsley/react-query: basic-typescript](https://codesandbox.io/s/tannerlinsleyreact-query-basic-typescript-mrf23)| Configuration |


Patch:
diff --git a/.eslintrc b/.eslintrc
index bddc97406..fcf0d28e8 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -29,7 +29,7 @@
     ],
     "no-shadow": "error",
     "import/no-cycle": "error",
-    "import/no-unresolved": "error",
+    "import/no-unresolved": ["error", { "ignore": ["react-query"] }],
     "import/no-unused-modules": ["off", { "unusedExports": true }],
     "no-redeclare": "off"
   }
diff --git a/docs/src/components/markdown.module.css b/docs/src/components/markdown.module.css
index e17f19756..f521614f7 100644
--- a/docs/src/components/markdown.module.css
+++ b/docs/src/components/markdown.module.css
@@ -156,6 +156,12 @@
   @apply text-xl;
 }
 
+.markdown > h2 > a,
+.markdown > h3 > a,
+.markdown > h4 > a {
+  @apply underline;
+}
+
 .markdown > .markdown ul {
   @apply list-disc;
 }
diff --git a/docs/src/pages/guides/infinite-queries.md b/docs/src/pages/guides/infinite-queries.md
index c469bab09..870dbfc6d 100644
--- a/docs/src/pages/guides/infinite-queries.md
+++ b/docs/src/pages/guides/infinite-queries.md
@@ -160,8 +160,8 @@ Manually removing a single 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 })
+  const newData = page.filter(val => val.id !== updatedId)
+  newPagesArray.push(newData)
 })
 queryClient.setQueryData('projects', data => ({
   pages: newPagesArray,
diff --git a/docs/src/pages/guides/migrating-to-react-query-3.md b/docs/src/pages/guides/migrating-to-react-query-3.md
index 4190cd3a6..9320a0abc 100644
--- a/docs/src/pages/guides/migrating-to-react-query-3.md
+++ b/docs/src/pages/guides/migrating-to-react-query-3.md
@@ -147,7 +147,7 @@ They were previously added as the last query key parameter in your query functio
 useInfiniteQuery(['posts'], (_key, pageParam = 0) => fetchPosts(pageParam))
 
 // New
-useInfiniteQuery(['posts'], ({ pageParam = 0 }) => fetchPost(pageParam))
+useInfiniteQuery(['posts'], ({ pageParam = 0 }) => fetchPosts(pageParam))
 ```
 
 ### usePaginatedQuery() has been deprecated in favor of the `keepPreviousData` option
@@ -416,6 +416,37 @@ setConsole({
 
 In version 3 **this is done automatically when React Query is used in React Native**.
 
+
+### Typescript
+#### `QueryStatus` has been changed from an [enum](https://www.typescriptlang.org/docs/handbook/enums.html#string-enums) to a [union type](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#union-types)
+
+So, if you were checking the status property of a query or mutation against a QueryStatus enum property you will have to check it now against the string literal the enum previously held for each property.
+
+Therefore you have to change the enum properties to their equivalent string literal, like this:
+- `QueryStatus.Idle` -> `'idle'`
+- `QueryStatus.Loading` -> `'loading'`
+- `QueryStatus.Error` -> `'error'`
+- `QueryStatus.Success` -> `'success'`
+
+Here is an example of the changes you would have to make:
+
+```diff
+- import { useQuery, QueryStatus } from 'react-query';
++ import { useQuery } from 'react-query';
+
+const { data, status } = useQuery(['post', id], () => fetchPost(id))
+
+- if (status === QueryStatus.Loading) {
++ if (status === 'loading') {
+  ...
+}
+
+- if (status === QueryStatus.Error) {
++ if (status === 'error') {
+  ...
+}
+```
+
 ## New features
 
 #### Query Data Selectors
diff --git a/docs/src/pages/guides/paginated-queries.md b/docs/src/pages/guides/paginated-queries.md
index 15e5081f4..e85651ce2 100644
--- a/docs/src/pages/guides/paginated-queries.md
+++ b/docs/src/pages/guides/paginated-queries.md
@@ -13,7 +13,7 @@ However, if you run this simple example, you might notice something strange:
 
 **The UI jumps in and out of the `success` and `loading` states because each new page is treated like a brand new query.**
 
-This experience is not optimal and unfortunately is how many tools today insist on working. But not React Query! As you may have guessed, React Query comes with an awesome featured called `keepPreviousData` that allows us to get around this.
+This experience is not optimal and unfortunately is how many tools today insist on working. But not React Query! As you may have guessed, React Query comes with an awesome feature called `keepPreviousData` that allows us to get around this.
 
 ## Better Paginated Queries with `keepPreviousData`
 
diff --git a/docs/src/pages/guides/updates-from-mutation-responses.md b/docs/src/pages/guides/updates-from-mutation-responses.md
index d9e78e63d..f9b98105a 100644
--- a/docs/src/pages/guides/updates-from-mutation-responses.md
+++ b/docs/src/pages/guides/updates-from-mutation-responses.md
@@ -19,7 +19,7 @@ mutation.mutate({
 
 // The query below will be updated with the response from the
 // successful mutation
-const { status, data, error } = useQuery(['todo', { id: 5 }], fetchTodoByID)
+const { status, data, error } = useQuery(['todo', { id: 5 }], fetchTodoById)
 ```
 
 You might want to tie the `onSuccess` logic into a reusable mutation, for that you can
diff --git a/docs/src/pages/reference/useQuery.md b/docs/src/pages/reference/useQuery.md
index 839b659f3..d0c8c4ac3 100644
--- a/docs/src/pages/reference/useQuery.md
+++ b/docs/src/pages/reference/useQuery.md
@@ -168,6 +168,9 @@ const result = useQuery({
   - Optional
   - Defaults to `true`
   - If set to `false`, structural sharing between query results will be disabled.
+- `useErrorBoundary: boolean`
+  - Defaults to the global query config's `useErrorBoundary` value, which is false
+  - Set this to true if you want errors to be thrown in the render phase and propagated to the nearest error boundary
 
 **Returns**
 
diff --git a/rollup.config.js b/rollup.config.js
index 7d54f852d..d02287653 100644
--- a/rollup.config.js
+++ b/rollup.config.js
@@ -7,11 +7,12 @@ import commonJS from 'rollup-plugin-commonjs'
 import visualizer from 'rollup-plugin-visualizer'
 import replace from '@rollup/plugin-replace'
 
-const external = ['react', 'react-dom']
+const external = ['react', 'react-dom', 'react-query']
 
 const globals = {
   react: 'React',
   'react-dom': 'ReactDOM',
+  'react-query': 'ReactQuery',
 }
 
 const inputSrcs = [
diff --git a/src/core/infiniteQueryObserver.ts b/src/core/infiniteQueryObserver.ts
index dd9169644..2dd199ba8 100644
--- a/src/core/infiniteQueryObserver.ts
+++ b/src/core/infiniteQueryObserver.ts
@@ -75,6 +75,21 @@ export class InfiniteQueryObserver<
     })
   }
 
+  getOptimisticResult(
+    options: InfiniteQueryObserverOptions<
+      TQueryFnData,
+      TError,
+      TData,
+      TQueryData
+    >
+  ): InfiniteQueryObserverResult<TData, TError> {
+    options.behavior = infiniteQueryBehavior()
+    return super.getOptimisticResult(options) as InfiniteQueryObserverResult<
+      TData,
+      TError
+    >
+  }
+
   fetchNextPage(
     options?: FetchNextPageOptions
   ): Promise<InfiniteQueryObserverResult<TData, TError>> {
diff --git a/src/core/query.ts b/src/core/query.ts
index 64098b539..4786aa6d6 100644
--- a/src/core/query.ts
+++ b/src/core/query.ts
@@ -1,6 +1,5 @@
 import {
   Updater,
-  ensureArray,
   functionalUpdate,
   isValidTimeout,
   noop,
@@ -378,9 +377,8 @@ export class Query<
     }
 
     // Create query function context
-    const queryKey = ensureArray(this.queryKey)
-    const queryFnContext: QueryFunctionContext<unknown[]> = {
-      queryKey,
+    const queryFnContext: QueryFunctionContext<TQueryKey> = {
+      queryKey: this.queryKey,
       pageParam: undefined,
     }
 
@@ -394,7 +392,7 @@ export class Query<
     const context: FetchContext<TQueryFnData, TError, TData, any> = {
       fetchOptions,
       options: this.options,
-      queryKey,
+      queryKey: this.queryKey,
       state: this.state,
       fetchFn,
     }
diff --git a/src/core/queryClient.ts b/src/core/queryClient.ts
index 7c7769dec..c8d809723 100644
--- a/src/core/queryClient.ts
+++ b/src/core/queryClient.ts
@@ -252,24 +252,24 @@ export class QueryClient {
     return promise
   }
 
-  fetchQuery<TQueryFnData = unknown, TError = unknown, TData = TQueryFnData>(
-    options: FetchQueryOptions<TQueryFnData, TError, TData>
+  fetchQuery<TQueryFnData = unknown, TError = unknown, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey>(
+    options: FetchQueryOptions<TQueryFnData, TError, TData, TQueryKey>
   ): Promise<TData>
-  fetchQuery<TQueryFnData = unknown, TError = unknown, TData = TQueryFnData>(
-    queryKey: QueryKey,
-    options?: FetchQueryOptions<TQueryFnData, TError, TData>
+  fetchQuery<TQueryFnData = unknown, TError = unknown, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey>(
+    queryKey: TQueryKey,
+    options?: FetchQueryOptions<TQueryFnData, TError, TData, TQueryKey>
   ): Promise<TData>
-  fetchQuery<TQueryFnData = unknown, TError = unknown, TData = TQueryFnData>(
-    queryKey: QueryKey,
-    queryFn: QueryFunction<TQueryFnData>,
-    options?: FetchQueryOptions<TQueryFnData, TError, TData>
+  fetchQuery<TQueryFnData = unknown, TError = unknown, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey>(
+    queryKey: TQueryKey,
+    queryFn: QueryFunction<TQueryFnData, TQueryKey>,
+    options?: FetchQueryOptions<TQueryFnData, TError, TData, TQueryKey>
   ): Promise<TData>
-  fetchQuery<TQueryFnData, TError, TData = TQueryFnData>(
-    arg1: QueryKey | FetchQueryOptions<TQueryFnData, TError, TData>,
+  fetchQuery<TQueryFnData, TError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey>(
+    arg1: TQueryKey | FetchQueryOptions<TQueryFnData, TError, TData, TQueryKey>,
     arg2?:
-      | QueryFunction<TQueryFnData>
-      | FetchQueryOptions<TQueryFnData, TError, TData>,
-    arg3?: FetchQueryOptions<TQueryFnData, TError, TData>
+      | QueryFunction<TQueryFnData, TQueryKey>
+      | FetchQueryOptions<TQueryFnData, TError, TData, TQueryKey>,
+    arg3?: FetchQueryOptions<TQueryFnData, TError, TData, TQueryKey>
   ): Promise<TData> {
     const parsedOptions = parseQueryArgs(arg1, arg2, arg3)
     const defaultedOptions = this.defaultQueryOptions(parsedOptions)
@@ -286,22 +286,22 @@ export class QueryClient {
       : Promise.resolve(query.state.data as TData)
   }
 
-  prefetchQuery<TQueryFnData = unknown, TError = unknown, TData = TQueryFnData>(
-    options: FetchQueryOptions<TQueryFnData, TError, TData>
+  prefetchQuery<TQueryFnData = unknown, TError = unknown, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey>(
+    options: FetchQueryOptions<TQueryFnData, TError, TData, TQueryKey>
   ): Promise<void>
-  prefetchQuery<TQueryFnData = unknown, TError = unknown, TData = TQueryFnData>(
-    queryKey: QueryKey,
-    options?: FetchQueryOptions<TQueryFnData, TError, TData>
+  prefetchQuery<TQueryFnData = unknown, TError = unknown, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey>(
+    queryKey: TQueryKey,
+    options?: FetchQueryOptions<TQueryFnData, TError, TData, TQueryKey>
   ): Promise<void>
-  prefetchQuery<TQueryFnData = unknown, TError = unknown, TData = TQueryFnData>(
-    queryKey: QueryKey,
-    queryFn: QueryFunction,
-    options?: FetchQueryOptions<TQueryFnData, TError, TData>
+  prefetchQuery<TQueryFnData = unknown, TError = unknown, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey>(
+    queryKey: TQueryKey,
+    queryFn: QueryFunction<TQueryFnData, TQueryKey>,
+    options?: FetchQueryOptions<TQueryFnData, TError, TData, TQueryKey>
   ): Promise<void>
-  prefetchQuery<TQueryFnData = unknown, TError = unknown, TData = TQueryFnData>(
-    arg1: QueryKey | FetchQueryOptions<TQueryFnData, TError, TData>,
-    arg2?: QueryFunction | FetchQueryOptions<TQueryFnData, TError, TData>,
-    arg3?: FetchQueryOptions<TQueryFnData, TError, TData>
+  prefetchQuery<TQueryFnData = unknown, TError = unknown, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey>(
+    arg1: TQueryKey | FetchQueryOptions<TQueryFnData, TError, TData, TQueryKey>,
+    arg2?: QueryFunction<TQueryFnData, TQueryKey> | FetchQueryOptions<TQueryFnData, TError, TData, TQueryKey>,
+    arg3?: FetchQueryOptions<TQueryFnData, TError, TData, TQueryKey>
   ): Promise<void> {
     return this.fetchQuery(arg1 as any, arg2 as any, arg3)
       .then(noop)
@@ -311,33 +311,36 @@ export class QueryClient {
   fetchInfiniteQuery<
     TQueryFnData = unknown,
     TError = unknown,
-    TData = TQueryFnData
+    TData = TQueryFnData,
+    TQueryKey extends QueryKey = QueryKey
   >(
-    options: FetchInfiniteQueryOptions<TQueryFnData, TError, TData>
+    options: FetchInfiniteQueryOptions<TQueryFnData, TError, TData, TQueryKey>
   ): Promise<InfiniteData<TData>>
   fetchInfiniteQuery<
     TQueryFnData = unknown,
     TError = unknown,
-    TData = TQueryFnData
+    TData = TQueryFnData,
+    TQueryKey extends QueryKey = QueryKey
   >(
-    queryKey: QueryKey,
-    options?: FetchInfiniteQueryOptions<TQueryFnData, TError, TData>
+    queryKey: TQueryKey,
+    options?: FetchInfiniteQueryOptions<TQueryFnData, TError, TData, TQueryKey>
   ): Promise<InfiniteData<TData>>
   fetchInfiniteQuery<
     TQueryFnData = unknown,
     TError = unknown,
-    TData = TQueryFnData
+    TData = TQueryFnData,
+    TQueryKey extends QueryKey = QueryKey
   >(
-    queryKey: QueryKey,
-    queryFn: QueryFunction<TQueryFnData>,
-    options?: FetchInfiniteQueryOptions<TQueryFnData, TError, TData>
+    queryKey: TQueryKey,
+    queryFn: QueryFunction<TQueryFnData, TQueryKey>,
+    options?: FetchInfiniteQueryOptions<TQueryFnData, TError, TData, TQueryKey>
   ): Promise<InfiniteData<TData>>
-  fetchInfiniteQuery<TQueryFnData, TError, TData = TQueryFnData>(
-    arg1: QueryKey | FetchInfiniteQueryOptions<TQueryFnData, TError, TData>,
+  fetchInfiniteQuery<TQueryFnData, TError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey>(
+    arg1: TQueryKey | FetchInfiniteQueryOptions<TQueryFnData, TError, TData, TQueryKey>,
     arg2?:
-      | QueryFunction<TQueryFnData>
-      | FetchInfiniteQueryOptions<TQueryFnData, TError, TData>,
-    arg3?: FetchInfiniteQueryOptions<TQueryFnData, TError, TData>
+      | QueryFunction<TQueryFnData, TQueryKey>
+      | FetchInfiniteQueryOptions<TQueryFnData, TError, TData, TQueryKey>,
+    arg3?: FetchInfiniteQueryOptions<TQueryFnData, TError, TData, TQueryKey>
   ): Promise<InfiniteData<TData>> {
     const parsedOptions = parseQueryArgs(arg1, arg2, arg3)
     parsedOptions.behavior = infiniteQueryBehavior<
@@ -348,20 +351,39 @@ export class QueryClient {
     return this.fetchQuery(parsedOptions)
   }
 
-  prefetchInfiniteQuery(options: FetchInfiniteQueryOptions): Promise<void>
-  prefetchInfiniteQuery(
-    queryKey: QueryKey,
-    options?: FetchInfiniteQueryOptions
+  prefetchInfiniteQuery<
+    TQueryFnData = unknown,
+    TError = unknown,
+    TData = TQueryFnData,
+    TQueryKey extends QueryKey = QueryKey
+    >(
+    options: FetchInfiniteQueryOptions<TQueryFnData, TError, TData, TQueryKey>
   ): Promise<void>
-  prefetchInfiniteQuery(
-    queryKey: QueryKey,
-    queryFn: QueryFunction,
-    options?: FetchInfiniteQueryOptions
+  prefetchInfiniteQuery<
+    TQueryFnData = unknown,
+    TError = unknown,
+    TData = TQueryFnData,
+    TQueryKey extends QueryKey = QueryKey
+    >(
+    queryKey: TQueryKey,
+    options?: FetchInfiniteQueryOptions<TQueryFnData, TError, TData, TQueryKey>
   ): Promise<void>
-  prefetchInfiniteQuery(
-    arg1: QueryKey | FetchInfiniteQueryOptions,
-    arg2?: QueryFunction | FetchInfiniteQueryOptions,
-    arg3?: FetchInfiniteQueryOptions
+  prefetchInfiniteQuery<
+    TQueryFnData = unknown,
+    TError = unknown,
+    TData = TQueryFnData,
+    TQueryKey extends QueryKey = QueryKey
+    >(
+    queryKey: TQueryKey,
+    queryFn: QueryFunction<TQueryFnData, TQueryKey>,
+    options?: FetchInfiniteQueryOptions<TQueryFnData, TError, TData, TQueryKey>
+  ): Promise<void>
+  prefetchInfiniteQuery<TQueryFnData, TError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey>(
+    arg1: TQueryKey | FetchInfiniteQueryOptions<TQueryFnData, TError, TData, TQueryKey>,
+    arg2?:
+      | QueryFunction<TQueryFnData, TQueryKey>
+      | FetchInfiniteQueryOptions<TQueryFnData, TError, TData, TQueryKey>,
+    arg3?: FetchInfiniteQueryOptions<TQueryFnData, TError, TData, TQueryKey>
   ): Promise<void> {
     return this.fetchInfiniteQuery(arg1 as any, arg2 as any, arg3)
       .then(noop)
diff --git a/src/core/types.ts b/src/core/types.ts
index 6d138bda3..e6df09785 100644
--- a/src/core/types.ts
+++ b/src/core/types.ts
@@ -59,7 +59,7 @@ export interface QueryOptions<
   retryDelay?: RetryDelayValue<TError>
   cacheTime?: number
   isDataEqual?: (oldData: TData | undefined, newData: TData) => boolean
-  queryFn?: QueryFunction<TQueryFnData>
+  queryFn?: QueryFunction<TQueryFnData, TQueryKey>
   queryHash?: string
   queryKey?: TQueryKey
   queryKeyHashFn?: QueryKeyHashFunction<TQueryKey>
@@ -210,8 +210,9 @@ export interface InfiniteQueryObserverOptions<
 export interface FetchQueryOptions<
   TQueryFnData = unknown,
   TError = unknown,
-  TData = TQueryFnData
-> extends QueryOptions<TQueryFnData, TError, TData> {
+  TData = TQueryFnData,
+  TQueryKey extends QueryKey = QueryKey
+> extends QueryOptions<TQueryFnData, TError, TData, TQueryKey> {
   /**
    * The time in milliseconds after data is considered stale.
    * If the data is fresh it will be returned from the cache.
@@ -222,8 +223,9 @@ export interface FetchQueryOptions<
 export interface FetchInfiniteQueryOptions<
   TQueryFnData = unknown,
   TError = unknown,
-  TData = TQueryFnData
-> extends FetchQueryOptions<TQueryFnData, TError, InfiniteData<TData>> {}
+  TData = TQueryFnData,
+  TQueryKey extends QueryKey = QueryKey
+> extends FetchQueryOptions<TQueryFnData, TError, InfiniteData<TData>, TQueryKey> {}
 
 export interface ResultOptions {
   throwOnError?: boolean
@@ -472,7 +474,7 @@ export interface MutationOptions<
   mutationFn?: MutationFunction<TData, TVariables>
   mutationKey?: MutationKey
   variables?: TVariables
-  onMutate?: (variables: TVariables) => Promise<TContext> | TContext
+  onMutate?: (variables: TVariables) => Promise<TContext> | Promise<undefined> | TContext | undefined
   onSuccess?: (
     data: TData,
     variables: TVariables,
diff --git a/src/devtools/devtools.tsx b/src/devtools/devtools.tsx
index 992df2e80..346bc247f 100644
--- a/src/devtools/devtools.tsx
+++ b/src/devtools/devtools.tsx
@@ -1,10 +1,11 @@
 // @ts-nocheck
 
 import React from 'react'
+
+import { useQueryClient } from 'react-query'
 import { matchSorter } from 'match-sorter'
-import { useQueryClient } from '../react'
 import useLocalStorage from './useLocalStorage'
-import { useSafeState, isStale } from './utils'
+import { useSafeState } from './utils'
 
 import {
   Panel,
@@ -305,7 +306,7 @@ export function ReactQueryDevtools({
 }
 
 const getStatusRank = q =>
-  q.state.isFetching ? 0 : !q.observers.length ? 3 : isStale(q) ? 2 : 1
+  q.state.isFetching ? 0 : !q.observers.length ? 3 : q.isStale() ? 2 : 1
 
 const sortFns = {
   'Status > Last Updated': (a, b) =>
diff --git a/src/devtools/utils.ts b/src/devtools/utils.ts
index 1d52634bd..9a5d94ba5 100644
--- a/src/devtools/utils.ts
+++ b/src/devtools/utils.ts
@@ -7,16 +7,10 @@ import useMediaQuery from './useMediaQuery'
 
 export const isServer = typeof window === 'undefined'
 
-export function isStale(query) {
-  return typeof query.isStale === 'function'
-    ? query.isStale()
-    : query.state.isStale
-}
-
 export function getQueryStatusColor(query, theme) {
   return query.state.isFetching
     ? theme.active
-    : isStale(query)
+    : query.isStale()
     ? theme.warning
     : !query.observers.length
     ? theme.gray
@@ -28,7 +22,7 @@ export function getQueryStatusLabel(query) {
     ? 'fetching'
     : !query.observers.length
     ? 'inactive'
-    : isStale(query)
+    : query.isStale()
     ? 'stale'
     : 'fresh'
 }
diff --git a/src/hydration/react.tsx b/src/hydration/react.tsx
index b9a21e860..62fe2964f 100644
--- a/src/hydration/react.tsx
+++ b/src/hydration/react.tsx
@@ -1,6 +1,6 @@
 import React from 'react'
 
-import { useQueryClient } from '../react'
+import { useQueryClient } from 'react-query'
 import { hydrate, HydrateOptions } from './hydration'
 
 export function useHydrate(state: unknown, options?: HydrateOptions) {
diff --git a/src/react/types.ts b/src/react/types.ts
index bdfeee0bf..aea5c78e2 100644
--- a/src/react/types.ts
+++ b/src/react/types.ts
@@ -75,7 +75,7 @@ export interface UseMutationOptions<
   TContext = unknown
 > {
   mutationKey?: string | unknown[]
-  onMutate?: (variables: TVariables) => Promise<TContext> | TContext
+  onMutate?: (variables: TVariables) => Promise<TContext> | Promise<undefined> | TContext | undefined
   onSuccess?: (
     data: TData,
     variables: TVariables,
diff --git a/tsconfig.json b/tsconfig.json
index da4415a41..cf0b28838 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -13,7 +13,10 @@
     "noUnusedParameters": true,
     "skipLibCheck": true,
     "strict": true,
-    "types": ["jest"]
+    "types": ["jest"],
+    "paths": {
+      "react-query": ["./src/index.ts"]
+    }
   },
   "include": ["./src"]
 }
diff --git a/tsconfig.types.json b/tsconfig.types.json
index 8474ccb0b..d09c81e05 100644
--- a/tsconfig.types.json
+++ b/tsconfig.types.json
@@ -4,7 +4,10 @@
     "declaration": true,
     "declarationDir": "./types",
     "emitDeclarationOnly": true,
-    "noEmit": false
+    "noEmit": false,
+    "paths": {
+      "react-query": ["./src/index.ts"]
+    }
   },
   "files": [
     "./src/index.ts",


Test patch:
diff --git a/src/core/tests/queryClient.test.tsx b/src/core/tests/queryClient.test.tsx
index 016324bf2..ac1787933 100644
--- a/src/core/tests/queryClient.test.tsx
+++ b/src/core/tests/queryClient.test.tsx
@@ -1,5 +1,5 @@
 import { sleep, queryKey, mockConsoleError } from '../../react/tests/utils'
-import { QueryCache, QueryClient, QueryObserver } from '../..'
+import { QueryCache, QueryClient, QueryFunction, QueryObserver } from '../..'
 
 describe('queryClient', () => {
   let queryClient: QueryClient
@@ -186,6 +186,23 @@ describe('queryClient', () => {
   })
 
   describe('fetchQuery', () => {
+    test('should not type-error with strict query key', async () => {
+      type StrictData = 'data'
+      type StrictQueryKey = ['strict', string]
+      const key: StrictQueryKey = ['strict', queryKey()]
+
+      const fetchFn: QueryFunction<StrictData, StrictQueryKey> = () => (
+        Promise.resolve('data')
+      )
+
+      await expect(
+        queryClient.fetchQuery<StrictData, any, StrictData, StrictQueryKey>(
+          key,
+          fetchFn,
+        )
+      ).resolves.toEqual('data')
+    })
+
     // https://github.com/tannerlinsley/react-query/issues/652
     test('should not retry by default', async () => {
       const consoleMock = mockConsoleError()
@@ -282,6 +299,28 @@ describe('queryClient', () => {
   })
 
   describe('fetchInfiniteQuery', () => {
+    test('should not type-error with strict query key', async () => {
+      type StrictData = string
+      type StrictQueryKey = ['strict', string]
+      const key: StrictQueryKey = ['strict', queryKey()]
+
+      const data = {
+        pages: ['data'],
+        pageParams: [undefined],
+      }
+
+      const fetchFn: QueryFunction<StrictData, StrictQueryKey> = () => (
+        Promise.resolve(data.pages[0])
+      )
+
+      await expect(
+        queryClient.fetchInfiniteQuery<StrictData, any, StrictData, StrictQueryKey>(
+          key,
+          fetchFn,
+        )
+      ).resolves.toEqual(data)
+    })
+
     test('should return infinite query data', async () => {
       const key = queryKey()
       const result = await queryClient.fetchInfiniteQuery(
@@ -301,6 +340,25 @@ describe('queryClient', () => {
   })
 
   describe('prefetchInfiniteQuery', () => {
+    test('should not type-error with strict query key', async () => {
+      type StrictData = 'data'
+      type StrictQueryKey = ['strict', string]
+      const key: StrictQueryKey = ['strict', queryKey()]
+
+      const fetchFn: QueryFunction<StrictData, StrictQueryKey> = () => (
+        Promise.resolve('data')
+      )
+
+      await queryClient.prefetchInfiniteQuery<StrictData, any, StrictData, StrictQueryKey>(key, fetchFn)
+
+      const result = queryClient.getQueryData(key)
+
+      expect(result).toEqual({
+        pages: ['data'],
+        pageParams: [undefined],
+      })
+    })
+
     test('should return infinite query data', async () => {
       const key = queryKey()
 
@@ -318,6 +376,22 @@ describe('queryClient', () => {
   })
 
   describe('prefetchQuery', () => {
+    test('should not type-error with strict query key', async () => {
+      type StrictData = 'data'
+      type StrictQueryKey = ['strict', string]
+      const key: StrictQueryKey = ['strict', queryKey()]
+
+      const fetchFn: QueryFunction<StrictData, StrictQueryKey> = () => (
+        Promise.resolve('data')
+      )
+
+      await queryClient.prefetchQuery<StrictData, any, StrictData, StrictQueryKey>(key, fetchFn);
+
+      const result = queryClient.getQueryData(key);
+
+      expect(result).toEqual('data')
+    })
+
     test('should return undefined when an error is thrown', async () => {
       const consoleMock = mockConsoleError()
 
diff --git a/src/core/tests/queryObserver.test.tsx b/src/core/tests/queryObserver.test.tsx
index 1467e50cb..1e4c6d36d 100644
--- a/src/core/tests/queryObserver.test.tsx
+++ b/src/core/tests/queryObserver.test.tsx
@@ -241,6 +241,7 @@ describe('queryObserver', () => {
   })
 
   test('should always run the selector again if selector throws an error', async () => {
+    const consoleMock = mockConsoleError()
     const key = queryKey()
     const results: QueryObserverResult[] = []
     const select = () => {
@@ -284,6 +285,7 @@ describe('queryObserver', () => {
       isFetching: false,
       data: undefined,
     })
+    consoleMock.mockRestore()
   })
 
   test('should structurally share the selector', async () => {
diff --git a/src/react/tests/suspense.test.tsx b/src/react/tests/suspense.test.tsx
index a52af6602..30450332d 100644
--- a/src/react/tests/suspense.test.tsx
+++ b/src/react/tests/suspense.test.tsx
@@ -72,19 +72,20 @@ describe("useQuery's in Suspense mode", () => {
     const states: UseInfiniteQueryResult<number>[] = []
 
     function Page() {
+      const [multiplier, setMultiplier] = React.useState(1)
       const state = useInfiniteQuery(
-        key,
-        ({ pageParam = 0 }) => Number(pageParam),
+        `${key}_${multiplier}`,
+        ({ pageParam = 1 }) => Number(pageParam * multiplier),
         {
           suspense: true,
           getNextPageParam: lastPage => lastPage + 1,
         }
       )
       states.push(state)
-      return null
+      return <button onClick={() => setMultiplier(2)}>next</button>
     }
 
-    renderWithClient(
+    const rendered = renderWithClient(
       queryClient,
       <React.Suspense fallback="loading">
         <Page />
@@ -95,7 +96,16 @@ describe("useQuery's in Suspense mode", () => {
 
     expect(states.length).toBe(1)
     expect(states[0]).toMatchObject({
-      data: { pages: [0], pageParams: [undefined] },
+      data: { pages: [1], pageParams: [undefined] },
+      status: 'success',
+    })
+
+    fireEvent.click(rendered.getByText('next'))
+    await sleep(10)
+
+    expect(states.length).toBe(3)
+    expect(states[2]).toMatchObject({
+      data: { pages: [2], pageParams: [undefined] },
       status: 'success',
     })
   })
diff --git a/src/react/tests/useQuery.test.tsx b/src/react/tests/useQuery.test.tsx
index de392c86c..dc8753251 100644
--- a/src/react/tests/useQuery.test.tsx
+++ b/src/react/tests/useQuery.test.tsx
@@ -16,6 +16,7 @@ import {
   QueryClient,
   UseQueryResult,
   QueryCache,
+  QueryFunction,
   QueryFunctionContext,
 } from '../..'
 
@@ -74,6 +75,20 @@ describe('useQuery', () => {
       })
       expectType<string | undefined>(fromGenericOptionsQueryFn.data)
       expectType<unknown>(fromGenericOptionsQueryFn.error)
+
+      type MyData = number
+      type MyQueryKey = readonly ['my-data', number]
+
+      const getMyData: QueryFunction<MyData, MyQueryKey> = async ({
+        queryKey: [, n],
+      }) => {
+        return n + 42
+      }
+
+      useQuery({
+        queryKey: ['my-data', 100],
+        queryFn: getMyData,
+      })
     }
   })
 
@@ -3611,14 +3626,16 @@ describe('useQuery', () => {
 
       states.push(state)
 
+      const { refetch } = state
+
       React.useEffect(() => {
         setActTimeout(() => {
-          state.refetch()
+          refetch()
         }, 0)
         setActTimeout(() => {
           queryClient.resetQueries(key)
         }, 50)
-      }, [])
+      }, [refetch])
 
       return null
     }