Repository: tannerlinsley/react-query
Pull request #3246 (2022-01-29 23:47:42)

feat(*): add optional logger prop to QueryClient config
See the commit message for full(ish) list of changes. 

Addresses https://github.com/tannerlinsley/react-query/discussions/3240.

I'll update docs if we are happy with these changes.

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/tanstack/react-query/2ukJhhpHKYw7o6A15kUZjejjKugw](https://vercel.com/tanstack/react-query/2ukJhhpHKYw7o6A15kUZjejjKugw)  
✅ 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/3246/builds/213331) or the icon next to each commit SHA.

Latest deployment of this branch, based on commit 490aab0790fc623aa3881ba5de6969e9a1e63d12:

|Sandbox| Source |
|--|--|
|[tannerlinsley/react-query](https://codesandbox.io/s/tannerlinsley-react-query-5dgzs)| Configuration |
|[tannerlinsley/react-query: basic-typescript](https://codesandbox.io/s/tannerlinsley-react-query-basic-typescript-fz1sr)| Configuration |

TkDodo: I think we can remove `setLogger` from the side-effects, right?

https://github.com/tannerlinsley/react-query/blob/5e2f2c2b644c03433c15e3ba61f884e142098fc7/package.json#L17-L26
codecov[bot]: # [Codecov](https://codecov.io/gh/tannerlinsley/react-query/pull/3246?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Tanner+Linsley) Report
> Merging [#3246](https://codecov.io/gh/tannerlinsley/react-query/pull/3246?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Tanner+Linsley) (490aab0) into [alpha](https://codecov.io/gh/tannerlinsley/react-query/commit/c0fc916cb3e7d32a05e9356b2f1359c3cd5de573?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Tanner+Linsley) (c0fc916) will **decrease** coverage by `0.00%`.
> The diff coverage is `100.00%`.

[![Impacted file tree graph](https://codecov.io/gh/tannerlinsley/react-query/pull/3246/graphs/tree.svg?width=650&height=150&src=pr&token=jqEbswkEDQ&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Tanner+Linsley)](https://codecov.io/gh/tannerlinsley/react-query/pull/3246?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Tanner+Linsley)

```diff
@@            Coverage Diff             @@
##            alpha    #3246      +/-   ##
==========================================
- Coverage   96.82%   96.82%   -0.01%     
==========================================
  Files          46       43       -3     
  Lines        2302     2298       -4     
  Branches      668      667       -1     
==========================================
- Hits         2229     2225       -4     
  Misses         70       70              
  Partials        3        3              
```


| [Impacted Files](https://codecov.io/gh/tannerlinsley/react-query/pull/3246?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Tanner+Linsley) | Coverage Δ | |
|---|---|---|
| [src/core/mutationCache.ts](https://codecov.io/gh/tannerlinsley/react-query/pull/3246/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Tanner+Linsley#diff-c3JjL2NvcmUvbXV0YXRpb25DYWNoZS50cw==) | `100.00% <ø> (ø)` | |
| [src/core/queryCache.ts](https://codecov.io/gh/tannerlinsley/react-query/pull/3246/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Tanner+Linsley#diff-c3JjL2NvcmUvcXVlcnlDYWNoZS50cw==) | `100.00% <ø> (ø)` | |
| [src/core/logger.ts](https://codecov.io/gh/tannerlinsley/react-query/pull/3246/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Tanner+Linsley#diff-c3JjL2NvcmUvbG9nZ2VyLnRz) | `100.00% <100.00%> (ø)` | |
| [src/core/mutation.ts](https://codecov.io/gh/tannerlinsley/react-query/pull/3246/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Tanner+Linsley#diff-c3JjL2NvcmUvbXV0YXRpb24udHM=) | `100.00% <100.00%> (ø)` | |
| [src/core/query.ts](https://codecov.io/gh/tannerlinsley/react-query/pull/3246/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Tanner+Linsley#diff-c3JjL2NvcmUvcXVlcnkudHM=) | `100.00% <100.00%> (ø)` | |
| [src/core/queryClient.ts](https://codecov.io/gh/tannerlinsley/react-query/pull/3246/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Tanner+Linsley#diff-c3JjL2NvcmUvcXVlcnlDbGllbnQudHM=) | `100.00% <100.00%> (ø)` | |
| [src/core/queryObserver.ts](https://codecov.io/gh/tannerlinsley/react-query/pull/3246/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Tanner+Linsley#diff-c3JjL2NvcmUvcXVlcnlPYnNlcnZlci50cw==) | `100.00% <100.00%> (ø)` | |
| [src/reactjs/tests/utils.tsx](https://codecov.io/gh/tannerlinsley/react-query/pull/3246/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Tanner+Linsley#diff-c3JjL3JlYWN0anMvdGVzdHMvdXRpbHMudHN4) | `97.29% <100.00%> (-0.08%)` | :arrow_down: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/tannerlinsley/react-query/pull/3246?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Tanner+Linsley).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Tanner+Linsley)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/tannerlinsley/react-query/pull/3246?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Tanner+Linsley). Last update [c0fc916...490aab0](https://codecov.io/gh/tannerlinsley/react-query/pull/3246?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Tanner+Linsley). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Tanner+Linsley).

TkDodo: The size of `react-query.production.min.js` seems to go up from `11.74KB` on the latest `alpha` release to `11.88KB` on this PR.
This seems significant for such a tiny change 🤔 
anilanar: > The size of react-query.production.min.js seems to go up from 11.74KB on the latest alpha release to 11.88KB on this PR.

I think `console` vs `{ log: console.log, warn: console.warn. error: console.error }` by itself adds ~ 50bytes by itself, so once I revert that, it should be a less significant number, but I can probably reduce it further, I'll check. 
TkDodo: With the old approach, log statements were removing completely in production due to the env check. Now they stay in the bundle and do nothing per default, and it will increase with every log statement we make. The string itself also can't be minified. 
The key warning for example is similar to the react key warning that only shows up in dev mode. Not sure why we'd ever want that in production. 
TkDodo: any updates here please, especially in regards to my last statement? thank you
anilanar: @TkDodo I'll check today and get back to you.
anilanar: @TkDodo I wrapped all log usages with production check. I'll add docs and this should be good to go.

The increase in bundle size comes from `logger` class fields and `getLogger` getter, which cannot be shortened to a single character by the uglifier unlike `getLogger` function. 

If you prefer bundle size over code readability, you can rename all private class fields to single character names and that can save some bytes.
TkDodo: > @TkDodo I wrapped all log usages with production check. I'll add docs and this should be good to go.

Thanks. Have you removed the `sideEffects` form `package.json` ?

> The increase in bundle size comes from logger class fields and getLogger getter, which cannot be shortened to a single character by the uglifier unlike getLogger function.

> If you prefer bundle size over code readability, you can rename all private class fields to single character names and that can save some bytes.

Thanks, I do not prefer that, but I think there are ways to let terser know to mangle private fields, but it's out of scope for here.

The gain is also minimal now since you updated with alpha:

```
alpha: 11.64KB
here:  11.68KB
```

so it's all good 👍 
anilanar: @TkDodo 

 I updated the docs :tada: 
 
 I wanted to document the `Logger` interface but noticed that there's no documentation convention for pure types/interfaces so I created a section in `Guides` instead.
TkDodo: @anilanar the side-effects still need to be removed from package.json, right?

https://github.com/tannerlinsley/react-query/pull/3246#issuecomment-1025151854
TkDodo: thank you for your effort in contributing this 🙇 

Patch:
diff --git a/docs/src/manifests/manifest.json b/docs/src/manifests/manifest.json
index 748b8ba7a..ac3a02988 100644
--- a/docs/src/manifests/manifest.json
+++ b/docs/src/manifests/manifest.json
@@ -195,6 +195,11 @@
           "path": "/guides/suspense",
           "editUrl": "/guides/suspense.md"
         },
+        {
+          "title": "Custom Logger",
+          "path": "/guides/custom-logger",
+          "editUrl": "/guides/custom-logger.md"
+        },
         {
           "title": "Testing",
           "path": "/guides/testing",
@@ -444,11 +449,6 @@
           "path": "/reference/onlineManager",
           "editUrl": "/reference/onlineManager.md"
         },
-        {
-          "title": "setLogger",
-          "path": "/reference/setLogger",
-          "editUrl": "/reference/setLogger.md"
-        },
         {
           "title": "hydration",
           "path": "/reference/hydration",
diff --git a/docs/src/pages/guides/custom-logger.md b/docs/src/pages/guides/custom-logger.md
new file mode 100644
index 000000000..8730ac330
--- /dev/null
+++ b/docs/src/pages/guides/custom-logger.md
@@ -0,0 +1,22 @@
+---
+id: custom-logger
+title: Custom Logger
+---
+
+If you want to change how information is logged by React Query, you can set a custom logger when creating a `QueryClient`.
+
+```js
+const queryClient = new QueryClient({
+  logger: {
+    log: (...args) => {
+      // Log debugging information
+    },
+    warn: (...args) => {
+      // Log warning
+    },
+    error: (...args) => {
+      // Log error
+    },
+  },
+})
+```
diff --git a/docs/src/pages/guides/migrating-to-react-query-4.md b/docs/src/pages/guides/migrating-to-react-query-4.md
index ac5cedc7f..a5a77d2e3 100644
--- a/docs/src/pages/guides/migrating-to-react-query-4.md
+++ b/docs/src/pages/guides/migrating-to-react-query-4.md
@@ -260,11 +260,24 @@ Additionally, `query.setDefaultOptions` was removed because it was also unused.
 
 Types now require using TypeScript v4.1 or greater
 
-### Logging
+### Logging in production
 
 Starting with v4, react-query will no longer log errors (e.g. failed fetches) to the console in production mode, as this was confusing to many.
 Errors will still show up in development mode.
 
+### `setLogger` is removed
+
+It was possible to change the logger globally by calling `setLogger`. In v4, that function is replaced with an optional field when creating a `QueryClient`.
+
+```diff
+- import { QueryClient, setLogger } from 'react-query';
++ import { QueryClient } from 'react-query';
+
+- setLogger(customLogger)
+- const queryClient = new QueryClient();
++ const queryClient = new QueryClient({ logger: customLogger })
+```
+
 ### Undefined is an illegale cache value for successful queries
 
 In order to make bailing out of updates possible by returning `undefined`, we had to make `undefined` an illegal cache value. This is in-line with other concepts of react-query, for example, returning `undefined` from the [initialData function](guides/initial-query-data#initial-data-function) will also _not_ set data.
diff --git a/docs/src/pages/reference/QueryClient.md b/docs/src/pages/reference/QueryClient.md
index 2c4ab7046..3e4c2daf7 100644
--- a/docs/src/pages/reference/QueryClient.md
+++ b/docs/src/pages/reference/QueryClient.md
@@ -39,6 +39,7 @@ Its available methods are:
 - [`queryClient.resetQueries`](#queryclientresetqueries)
 - [`queryClient.isFetching`](#queryclientisfetching)
 - [`queryClient.isMutating`](#queryclientismutating)
+- [`queryClient.getLogger`](#queryclientgetlogger)
 - [`queryClient.getDefaultOptions`](#queryclientgetdefaultoptions)
 - [`queryClient.setDefaultOptions`](#queryclientsetdefaultoptions)
 - [`queryClient.getQueryDefaults`](#queryclientgetquerydefaults)
@@ -57,6 +58,9 @@ Its available methods are:
 - `mutationCache?: MutationCache`
   - Optional
   - The mutation cache this client is connected to.
+- `logger?: Logger`
+  - Optional
+  - The logger this client uses to log debugging information, warnings and errors. If not set, `console` is the default logger.
 - `defaultOptions?: DefaultOptions`
   - Optional
   - Define defaults for all queries and mutations using this queryClient.
@@ -203,7 +207,7 @@ This distinction is more a "convenience" for ts devs that know which structure w
 
 ## `queryClient.setQueryData`
 
-`setQueryData` is a synchronous function that can be used to immediately update a query's cached data. If the query does not exist, it will be created. **If the query is not utilized by a query hook in the default `cacheTime` of 5 minutes, the query will be garbage collected**. To update multiple queries at once and match query keys partially, you need to use [`queryClient.setQueriesData`](#queryclientsetqueriesdata) instead. 
+`setQueryData` is a synchronous function that can be used to immediately update a query's cached data. If the query does not exist, it will be created. **If the query is not utilized by a query hook in the default `cacheTime` of 5 minutes, the query will be garbage collected**. To update multiple queries at once and match query keys partially, you need to use [`queryClient.setQueriesData`](#queryclientsetqueriesdata) instead.
 
 > The difference between using `setQueryData` and `fetchQuery` is that `setQueryData` is sync and assumes that you already synchronously have the data available. If you need to fetch the data asynchronously, it's suggested that you either refetch the query key or use `fetchQuery` to handle the asynchronous fetch.
 
@@ -450,6 +454,14 @@ React Query also exports a handy [`useIsMutating`](./useIsMutating) hook that wi
 **Returns**
 
 This method returns the number of fetching mutations.
+## `queryClient.getLogger`
+
+The `getLogger` method returns the logger which have been set when creating the client.
+
+```js
+const logger = queryClient.getLogger()
+```
+
 ## `queryClient.getDefaultOptions`
 
 The `getDefaultOptions` method returns the default options which have been set when creating the client or with `setDefaultOptions`.
diff --git a/docs/src/pages/reference/setLogger.md b/docs/src/pages/reference/setLogger.md
deleted file mode 100644
index fddb136a6..000000000
--- a/docs/src/pages/reference/setLogger.md
+++ /dev/null
@@ -1,43 +0,0 @@
----
-id: setLogger
-title: setLogger
----
-
-## `setLogger`
-
-`setLogger` is an optional function that allows you to replace the default `logger` used by React Query to log errors. By default, the `window.console` object is used. If no global `console` object is found in the environment, nothing will be logged.
-
-Examples:
-
-```js
-import { setLogger } from 'react-query'
-import { printLog, printWarn, printError } from 'custom-logger'
-
-// Custom logger
-setLogger({
-  log: printLog,
-  warn: printWarn,
-  error: printError,
-})
-
-// Sentry logger
-setLogger({
-  log: message => {
-    Sentry.captureMessage(message)
-  },
-  warn: message => {
-    Sentry.captureMessage(message)
-  },
-  error: error => {
-    Sentry.captureException(error)
-  },
-})
-
-// Winston logger
-setLogger(winston.createLogger())
-```
-
-**Options**
-
-- `logger: Logger`
-  - Must implement the `log`, `warn`, and `error` methods.
diff --git a/jest.config.js b/jest.config.js
index 2bb4897dd..a23185598 100644
--- a/jest.config.js
+++ b/jest.config.js
@@ -7,4 +7,5 @@ module.exports = {
   moduleNameMapper: {
     'react-query': '<rootDir>/src/index.ts',
   },
+  clearMocks: true,
 }
diff --git a/package.json b/package.json
index 326031572..26e4f5652 100644
--- a/package.json
+++ b/package.json
@@ -18,11 +18,9 @@
     "es/index.js",
     "es/reactjs/index.js",
     "es/reactjs/setBatchUpdatesFn.js",
-    "es/reactjs/setLogger.js",
     "lib/index.js",
     "lib/reactjs/index.js",
-    "lib/reactjs/setBatchUpdatesFn.js",
-    "lib/reactjs/setLogger.js"
+    "lib/reactjs/setBatchUpdatesFn.js"
   ],
   "scripts": {
     "test": "is-ci \"test:ci\" \"test:dev\"",
diff --git a/src/core/index.ts b/src/core/index.ts
index 5a6035735..d6b2d64f0 100644
--- a/src/core/index.ts
+++ b/src/core/index.ts
@@ -6,7 +6,6 @@ export { QueriesObserver } from './queriesObserver'
 export { InfiniteQueryObserver } from './infiniteQueryObserver'
 export { MutationCache } from './mutationCache'
 export { MutationObserver } from './mutationObserver'
-export { setLogger } from './logger'
 export { notifyManager } from './notifyManager'
 export { focusManager } from './focusManager'
 export { onlineManager } from './onlineManager'
diff --git a/src/core/logger.native.ts b/src/core/logger.native.ts
new file mode 100644
index 000000000..161b5b72f
--- /dev/null
+++ b/src/core/logger.native.ts
@@ -0,0 +1,11 @@
+import type { Logger } from './logger'
+
+/**
+ * See https://github.com/tannerlinsley/react-query/issues/795
+ * and https://github.com/tannerlinsley/react-query/pull/3246/#discussion_r795105707
+ */
+export const defaultLogger: Logger = {
+  log: console.log,
+  warn: console.warn,
+  error: console.warn,
+}
diff --git a/src/core/logger.ts b/src/core/logger.ts
index eb33ebc98..8025b966b 100644
--- a/src/core/logger.ts
+++ b/src/core/logger.ts
@@ -1,5 +1,3 @@
-// TYPES
-
 export interface Logger {
   log: LogFunction
   warn: LogFunction
@@ -8,14 +6,4 @@ export interface Logger {
 
 type LogFunction = (...args: any[]) => void
 
-// FUNCTIONS
-
-let logger: Logger = console
-
-export function getLogger(): Logger {
-  return logger
-}
-
-export function setLogger(newLogger: Logger) {
-  logger = newLogger
-}
+export const defaultLogger: Logger = console
diff --git a/src/core/mutation.ts b/src/core/mutation.ts
index f8cecc5d3..2db6a609b 100644
--- a/src/core/mutation.ts
+++ b/src/core/mutation.ts
@@ -1,7 +1,7 @@
 import type { MutationOptions, MutationStatus, MutationMeta } from './types'
 import type { MutationCache } from './mutationCache'
 import type { MutationObserver } from './mutationObserver'
-import { getLogger } from './logger'
+import { defaultLogger, Logger } from './logger'
 import { notifyManager } from './notifyManager'
 import { Removable } from './removable'
 import { canFetch, Retryer, createRetryer } from './retryer'
@@ -12,6 +12,7 @@ interface MutationConfig<TData, TError, TVariables, TContext> {
   mutationId: number
   mutationCache: MutationCache
   options: MutationOptions<TData, TError, TVariables, TContext>
+  logger?: Logger
   defaultOptions?: MutationOptions<TData, TError, TVariables, TContext>
   state?: MutationState<TData, TError, TVariables, TContext>
   meta?: MutationMeta
@@ -89,6 +90,7 @@ export class Mutation<
 
   private observers: MutationObserver<TData, TError, TVariables, TContext>[]
   private mutationCache: MutationCache
+  private logger: Logger
   private retryer?: Retryer<TData>
 
   constructor(config: MutationConfig<TData, TError, TVariables, TContext>) {
@@ -100,6 +102,7 @@ export class Mutation<
     }
     this.mutationId = config.mutationId
     this.mutationCache = config.mutationCache
+    this.logger = config.logger || defaultLogger
     this.observers = []
     this.state = config.state || getDefaultState()
     this.meta = config.meta
@@ -252,7 +255,7 @@ export class Mutation<
         )
 
         if (process.env.NODE_ENV !== 'production') {
-          getLogger().error(error)
+          this.logger.error(error)
         }
 
         return Promise.resolve()
diff --git a/src/core/mutationCache.ts b/src/core/mutationCache.ts
index 91eb0ac9b..11664cbeb 100644
--- a/src/core/mutationCache.ts
+++ b/src/core/mutationCache.ts
@@ -85,6 +85,7 @@ export class MutationCache extends Subscribable<MutationCacheListener> {
   ): Mutation<TData, TError, TVariables, TContext> {
     const mutation = new Mutation({
       mutationCache: this,
+      logger: client.getLogger(),
       mutationId: ++this.mutationId,
       options: client.defaultMutationOptions(options),
       state,
diff --git a/src/core/query.ts b/src/core/query.ts
index 0389d4527..6eb070787 100644
--- a/src/core/query.ts
+++ b/src/core/query.ts
@@ -17,8 +17,8 @@ import type {
 } from './types'
 import type { QueryCache } from './queryCache'
 import type { QueryObserver } from './queryObserver'
+import { defaultLogger, Logger } from './logger'
 import { notifyManager } from './notifyManager'
-import { getLogger } from './logger'
 import { Retryer, isCancelledError, canFetch, createRetryer } from './retryer'
 import { Removable } from './removable'
 
@@ -33,6 +33,7 @@ interface QueryConfig<
   cache: QueryCache
   queryKey: TQueryKey
   queryHash: string
+  logger?: Logger
   options?: QueryOptions<TQueryFnData, TError, TData, TQueryKey>
   defaultOptions?: QueryOptions<TQueryFnData, TError, TData, TQueryKey>
   state?: QueryState<TData, TError>
@@ -155,6 +156,7 @@ export class Query<
   isFetchingOptimistic?: boolean
 
   private cache: QueryCache
+  private logger: Logger
   private promise?: Promise<TData>
   private retryer?: Retryer<TData>
   private observers: QueryObserver<any, any, any, any, any>[]
@@ -169,6 +171,7 @@ export class Query<
     this.setOptions(config.options)
     this.observers = []
     this.cache = config.cache
+    this.logger = config.logger || defaultLogger
     this.queryKey = config.queryKey
     this.queryHash = config.queryHash
     this.initialState = config.state || getDefaultState(this.options)
@@ -360,14 +363,13 @@ export class Query<
       }
     }
 
-    if (
-      process.env.NODE_ENV !== 'production' &&
-      !Array.isArray(this.options.queryKey)
-    ) {
-      getLogger().error(
-        'As of v4, queryKey needs to be an Array, but the queryKey used was:',
-        JSON.stringify(this.options.queryKey)
-      )
+    if (!Array.isArray(this.options.queryKey)) {
+      if (process.env.NODE_ENV !== 'production') {
+        this.logger.error(
+          'As of v4, queryKey needs to be an Array, but the queryKey used was:',
+          JSON.stringify(this.options.queryKey)
+        )
+      }
     }
 
     const abortController = getAbortController()
@@ -447,7 +449,7 @@ export class Query<
         this.cache.config.onError?.(error, this as Query<any, any, any, any>)
 
         if (process.env.NODE_ENV !== 'production') {
-          getLogger().error(error)
+          this.logger.error(error)
         }
       }
 
diff --git a/src/core/queryCache.ts b/src/core/queryCache.ts
index 730e42a7d..8f91147e7 100644
--- a/src/core/queryCache.ts
+++ b/src/core/queryCache.ts
@@ -93,6 +93,7 @@ export class QueryCache extends Subscribable<QueryCacheListener> {
     if (!query) {
       query = new Query({
         cache: this,
+        logger: client.getLogger(),
         queryKey,
         queryHash,
         options: client.defaultQueryOptions(options),
diff --git a/src/core/queryClient.ts b/src/core/queryClient.ts
index af0cf460a..cd96553ef 100644
--- a/src/core/queryClient.ts
+++ b/src/core/queryClient.ts
@@ -39,7 +39,7 @@ import { onlineManager } from './onlineManager'
 import { notifyManager } from './notifyManager'
 import { infiniteQueryBehavior } from './infiniteQueryBehavior'
 import { CancelOptions, DefaultedQueryObserverOptions } from './types'
-import { getLogger } from './logger'
+import { defaultLogger, Logger } from './logger'
 
 // TYPES
 
@@ -58,6 +58,7 @@ interface MutationDefaults {
 export class QueryClient {
   private queryCache: QueryCache
   private mutationCache: MutationCache
+  private logger: Logger
   private defaultOptions: DefaultOptions
   private queryDefaults: QueryDefaults[]
   private mutationDefaults: MutationDefaults[]
@@ -67,6 +68,7 @@ export class QueryClient {
   constructor(config: QueryClientConfig = {}) {
     this.queryCache = config.queryCache || new QueryCache()
     this.mutationCache = config.mutationCache || new MutationCache()
+    this.logger = config.logger || defaultLogger
     this.defaultOptions = config.defaultOptions || {}
     this.queryDefaults = []
     this.mutationDefaults = []
@@ -520,6 +522,10 @@ export class QueryClient {
     return this.mutationCache
   }
 
+  getLogger(): Logger {
+    return this.logger
+  }
+
   getDefaultOptions(): DefaultOptions {
     return this.defaultOptions
   }
@@ -562,11 +568,13 @@ export class QueryClient {
       )
       // It is ok not having defaults, but it is error prone to have more than 1 default for a given key
       if (matchingDefaults.length > 1) {
-        getLogger().error(
-          `[QueryClient] Several query defaults match with key '${JSON.stringify(
-            queryKey
-          )}'. The first matching query defaults are used. Please check how query defaults are registered. Order does matter here. cf. https://react-query.tanstack.com/reference/QueryClient#queryclientsetquerydefaults.`
-        )
+        if (process.env.NODE_ENV !== 'production') {
+          this.logger.error(
+            `[QueryClient] Several query defaults match with key '${JSON.stringify(
+              queryKey
+            )}'. The first matching query defaults are used. Please check how query defaults are registered. Order does matter here. cf. https://react-query.tanstack.com/reference/QueryClient#queryclientsetquerydefaults.`
+          )
+        }
       }
     }
 
@@ -607,11 +615,13 @@ export class QueryClient {
       )
       // It is ok not having defaults, but it is error prone to have more than 1 default for a given key
       if (matchingDefaults.length > 1) {
-        getLogger().error(
-          `[QueryClient] Several mutation defaults match with key '${JSON.stringify(
-            mutationKey
-          )}'. The first matching mutation defaults are used. Please check how mutation defaults are registered. Order does matter here. cf. https://react-query.tanstack.com/reference/QueryClient#queryclientsetmutationdefaults.`
-        )
+        if (process.env.NODE_ENV !== 'production') {
+          this.logger.error(
+            `[QueryClient] Several mutation defaults match with key '${JSON.stringify(
+              mutationKey
+            )}'. The first matching mutation defaults are used. Please check how mutation defaults are registered. Order does matter here. cf. https://react-query.tanstack.com/reference/QueryClient#queryclientsetmutationdefaults.`
+          )
+        }
       }
     }
 
diff --git a/src/core/queryObserver.ts b/src/core/queryObserver.ts
index 704c9cb88..8cc02210c 100644
--- a/src/core/queryObserver.ts
+++ b/src/core/queryObserver.ts
@@ -21,7 +21,6 @@ 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'
 import { canFetch, isCancelledError } from './retryer'
 
 type QueryObserverListener<TData, TError> = (
@@ -471,7 +470,7 @@ export class QueryObserver<
           this.previousSelectError = null
         } catch (selectError) {
           if (process.env.NODE_ENV !== 'production') {
-            getLogger().error(selectError)
+            this.client.getLogger().error(selectError)
           }
           error = selectError as TError
           this.previousSelectError = selectError as TError
@@ -516,7 +515,7 @@ export class QueryObserver<
             this.previousSelectError = null
           } catch (selectError) {
             if (process.env.NODE_ENV !== 'production') {
-              getLogger().error(selectError)
+              this.client.getLogger().error(selectError)
             }
             error = selectError as TError
             this.previousSelectError = selectError as TError
diff --git a/src/core/types.ts b/src/core/types.ts
index f630423d3..0dab1232c 100644
--- a/src/core/types.ts
+++ b/src/core/types.ts
@@ -4,6 +4,7 @@ import type { RetryValue, RetryDelayValue } from './retryer'
 import type { QueryFilters, QueryTypeFilter } from './utils'
 import type { QueryCache } from './queryCache'
 import type { MutationCache } from './mutationCache'
+import { Logger } from './logger'
 
 export type QueryKey = readonly unknown[]
 export type QueryFunctionData<T> = T extends undefined ? never : T
@@ -656,6 +657,7 @@ export type MutationObserverResult<
 export interface QueryClientConfig {
   queryCache?: QueryCache
   mutationCache?: MutationCache
+  logger?: Logger
   defaultOptions?: DefaultOptions
 }
 
diff --git a/src/persistQueryClient/index.ts b/src/persistQueryClient/index.ts
index d38ed924b..407e58f4e 100644
--- a/src/persistQueryClient/index.ts
+++ b/src/persistQueryClient/index.ts
@@ -1,5 +1,4 @@
 import { QueryClient } from '../core'
-import { getLogger } from '../core/logger'
 import {
   dehydrate,
   DehydratedState,
@@ -84,10 +83,14 @@ export async function persistQueryClientRestore({
         }
       }
     } catch (err) {
-      getLogger().error(err)
-      getLogger().warn(
-        'Encountered an error attempting to restore client cache from persisted location. As a precaution, the persisted cache will be discarded.'
-      )
+      if (process.env.NODE_ENV !== 'production') {
+        queryClient.getLogger().error(err)
+        queryClient
+          .getLogger()
+          .warn(
+            'Encountered an error attempting to restore client cache from persisted location. As a precaution, the persisted cache will be discarded.'
+          )
+      }
       persister.removeClient()
     }
   }
diff --git a/src/reactjs/index.ts b/src/reactjs/index.ts
index 54d5ba0d2..b428b5fe8 100644
--- a/src/reactjs/index.ts
+++ b/src/reactjs/index.ts
@@ -1,6 +1,5 @@
 // Side effects
 import './setBatchUpdatesFn'
-import './setLogger'
 
 export { QueryClientProvider, useQueryClient } from './QueryClientProvider'
 export {
diff --git a/src/reactjs/logger.native.ts b/src/reactjs/logger.native.ts
deleted file mode 100644
index b0fc86bf0..000000000
--- a/src/reactjs/logger.native.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-import type { Logger } from '../core/logger'
-
-export const logger: Logger = {
-  log: console.log,
-  warn: console.warn,
-  error: console.warn,
-}
diff --git a/src/reactjs/logger.ts b/src/reactjs/logger.ts
deleted file mode 100644
index b0b275d9f..000000000
--- a/src/reactjs/logger.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-import type { Logger } from '../core/logger'
-
-export const logger: Logger = console
diff --git a/src/reactjs/setLogger.ts b/src/reactjs/setLogger.ts
deleted file mode 100644
index ce65933fc..000000000
--- a/src/reactjs/setLogger.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-import { setLogger } from '../core'
-import { logger } from './logger'
-
-setLogger(logger)


Test patch:
diff --git a/docs/src/pages/guides/testing.md b/docs/src/pages/guides/testing.md
index 3f1ed6610..fc3a32356 100644
--- a/docs/src/pages/guides/testing.md
+++ b/docs/src/pages/guides/testing.md
@@ -71,17 +71,18 @@ This will set the defaults for all queries in the component tree to "no retries"
 ## Turn off network error logging
 
 When testing we want to suppress network errors being logged to the console.
-To do this, we can use `react-query`'s `setLogger()` function.
+To do this, we can pass a custom logger to `QueryClient`:
 
 ```ts
-// as part of your test setup
-import { setLogger } from 'react-query'
-
-setLogger({
-  log: console.log,
-  warn: console.warn,
-  // ✅ no more errors on the console
-  error: () => {},
+import { QueryClient } from 'react-query'
+
+const queryClient = new QueryClient({
+  logger: {
+    log: console.log,
+    warn: console.warn,
+    // ✅ no more errors on the console for tests
+    error: process.env.NODE_ENV === 'test' ? () => {} : console.error,
+  },
 })
 ```
 
@@ -181,6 +182,7 @@ expect(result.current.data.pages).toStrictEqual([
 
 expectation.done();
 ```
+
 ## Further reading
 
 For additional tips and an alternative setup using `mock-service-worker`, have a look at [Testing React Query](../community/tkdodos-blog#5-testing-react-query) from
diff --git a/src/core/tests/hydration.test.tsx b/src/core/tests/hydration.test.tsx
index 77a5e5892..aeda28738 100644
--- a/src/core/tests/hydration.test.tsx
+++ b/src/core/tests/hydration.test.tsx
@@ -1,10 +1,10 @@
 import {
+  createQueryClient,
   executeMutation,
   mockNavigatorOnLine,
   sleep,
 } from '../../reactjs/tests/utils'
 import { QueryCache } from '../queryCache'
-import { QueryClient } from '../queryClient'
 import { dehydrate, hydrate } from '../hydration'
 
 async function fetchData<TData>(value: TData, ms?: number): Promise<TData> {
@@ -15,7 +15,7 @@ async function fetchData<TData>(value: TData, ms?: number): Promise<TData> {
 describe('dehydration and rehydration', () => {
   test('should work with serializeable values', async () => {
     const queryCache = new QueryCache()
-    const queryClient = new QueryClient({ queryCache })
+    const queryClient = createQueryClient({ queryCache })
     await queryClient.prefetchQuery(['string'], () => fetchData('string'))
     await queryClient.prefetchQuery(['number'], () => fetchData(1))
     await queryClient.prefetchQuery(['boolean'], () => fetchData(true))
@@ -31,7 +31,9 @@ describe('dehydration and rehydration', () => {
 
     const parsed = JSON.parse(stringified)
     const hydrationCache = new QueryCache()
-    const hydrationClient = new QueryClient({ queryCache: hydrationCache })
+    const hydrationClient = createQueryClient({
+      queryCache: hydrationCache,
+    })
     hydrate(hydrationClient, parsed)
     expect(hydrationCache.find(['string'])?.state.data).toBe('string')
     expect(hydrationCache.find(['number'])?.state.data).toBe(1)
@@ -69,7 +71,7 @@ describe('dehydration and rehydration', () => {
 
   test('should not dehydrate queries if dehydrateQueries is set to false', async () => {
     const queryCache = new QueryCache()
-    const queryClient = new QueryClient({ queryCache })
+    const queryClient = createQueryClient({ queryCache })
     await queryClient.prefetchQuery(['string'], () => fetchData('string'))
 
     const dehydrated = dehydrate(queryClient, { dehydrateQueries: false })
@@ -81,7 +83,7 @@ describe('dehydration and rehydration', () => {
 
   test('should use the cache time from the client', async () => {
     const queryCache = new QueryCache()
-    const queryClient = new QueryClient({ queryCache })
+    const queryClient = createQueryClient({ queryCache })
     await queryClient.prefetchQuery(['string'], () => fetchData('string'), {
       cacheTime: 50,
     })
@@ -94,7 +96,7 @@ describe('dehydration and rehydration', () => {
 
     const parsed = JSON.parse(stringified)
     const hydrationCache = new QueryCache()
-    const hydrationClient = new QueryClient({ queryCache: hydrationCache })
+    const hydrationClient = createQueryClient({ queryCache: hydrationCache })
     hydrate(hydrationClient, parsed)
     expect(hydrationCache.find(['string'])?.state.data).toBe('string')
     await sleep(100)
@@ -106,13 +108,13 @@ describe('dehydration and rehydration', () => {
 
   test('should be able to provide default options for the hydrated queries', async () => {
     const queryCache = new QueryCache()
-    const queryClient = new QueryClient({ queryCache })
+    const queryClient = createQueryClient({ queryCache })
     await queryClient.prefetchQuery(['string'], () => fetchData('string'))
     const dehydrated = dehydrate(queryClient)
     const stringified = JSON.stringify(dehydrated)
     const parsed = JSON.parse(stringified)
     const hydrationCache = new QueryCache()
-    const hydrationClient = new QueryClient({ queryCache: hydrationCache })
+    const hydrationClient = createQueryClient({ queryCache: hydrationCache })
     hydrate(hydrationClient, parsed, {
       defaultOptions: { queries: { retry: 10 } },
     })
@@ -123,7 +125,7 @@ describe('dehydration and rehydration', () => {
 
   test('should work with complex keys', async () => {
     const queryCache = new QueryCache()
-    const queryClient = new QueryClient({ queryCache })
+    const queryClient = createQueryClient({ queryCache })
     await queryClient.prefetchQuery(
       ['string', { key: ['string'], key2: 0 }],
       () => fetchData('string')
@@ -135,7 +137,7 @@ describe('dehydration and rehydration', () => {
 
     const parsed = JSON.parse(stringified)
     const hydrationCache = new QueryCache()
-    const hydrationClient = new QueryClient({ queryCache: hydrationCache })
+    const hydrationClient = createQueryClient({ queryCache: hydrationCache })
     hydrate(hydrationClient, parsed)
     expect(
       hydrationCache.find(['string', { key: ['string'], key2: 0 }])?.state.data
@@ -158,7 +160,7 @@ describe('dehydration and rehydration', () => {
     consoleMock.mockImplementation(() => undefined)
 
     const queryCache = new QueryCache()
-    const queryClient = new QueryClient({ queryCache })
+    const queryClient = createQueryClient({ queryCache })
     await queryClient.prefetchQuery(['success'], () => fetchData('success'))
     queryClient.prefetchQuery(['loading'], () => fetchData('loading', 10000))
     await queryClient.prefetchQuery(['error'], () => {
@@ -171,7 +173,7 @@ describe('dehydration and rehydration', () => {
 
     const parsed = JSON.parse(stringified)
     const hydrationCache = new QueryCache()
-    const hydrationClient = new QueryClient({ queryCache: hydrationCache })
+    const hydrationClient = createQueryClient({ queryCache: hydrationCache })
     hydrate(hydrationClient, parsed)
 
     expect(hydrationCache.find(['success'])).toBeTruthy()
@@ -185,7 +187,7 @@ describe('dehydration and rehydration', () => {
 
   test('should filter queries via shouldDehydrateQuery', async () => {
     const queryCache = new QueryCache()
-    const queryClient = new QueryClient({ queryCache })
+    const queryClient = createQueryClient({ queryCache })
     await queryClient.prefetchQuery(['string'], () => fetchData('string'))
     await queryClient.prefetchQuery(['number'], () => fetchData(1))
     const dehydrated = dehydrate(queryClient, {
@@ -205,7 +207,7 @@ describe('dehydration and rehydration', () => {
 
     const parsed = JSON.parse(stringified)
     const hydrationCache = new QueryCache()
-    const hydrationClient = new QueryClient({ queryCache: hydrationCache })
+    const hydrationClient = createQueryClient({ queryCache: hydrationCache })
     hydrate(hydrationClient, parsed)
     expect(hydrationCache.find(['string'])).toBeUndefined()
     expect(hydrationCache.find(['number'])?.state.data).toBe(1)
@@ -216,7 +218,7 @@ describe('dehydration and rehydration', () => {
 
   test('should not overwrite query in cache if hydrated query is older', async () => {
     const queryCache = new QueryCache()
-    const queryClient = new QueryClient({ queryCache })
+    const queryClient = createQueryClient({ queryCache })
     await queryClient.prefetchQuery(['string'], () =>
       fetchData('string-older', 5)
     )
@@ -227,7 +229,7 @@ describe('dehydration and rehydration', () => {
 
     const parsed = JSON.parse(stringified)
     const hydrationCache = new QueryCache()
-    const hydrationClient = new QueryClient({ queryCache: hydrationCache })
+    const hydrationClient = createQueryClient({ queryCache: hydrationCache })
     await hydrationClient.prefetchQuery(['string'], () =>
       fetchData('string-newer', 5)
     )
@@ -241,7 +243,7 @@ describe('dehydration and rehydration', () => {
 
   test('should overwrite query in cache if hydrated query is newer', async () => {
     const hydrationCache = new QueryCache()
-    const hydrationClient = new QueryClient({ queryCache: hydrationCache })
+    const hydrationClient = createQueryClient({ queryCache: hydrationCache })
     await hydrationClient.prefetchQuery(['string'], () =>
       fetchData('string-older', 5)
     )
@@ -249,7 +251,7 @@ describe('dehydration and rehydration', () => {
     // ---
 
     const queryCache = new QueryCache()
-    const queryClient = new QueryClient({ queryCache })
+    const queryClient = createQueryClient({ queryCache })
     await queryClient.prefetchQuery(['string'], () =>
       fetchData('string-newer', 5)
     )
@@ -280,7 +282,7 @@ describe('dehydration and rehydration', () => {
     })
     const serverOnSuccess = jest.fn()
 
-    const serverClient = new QueryClient()
+    const serverClient = createQueryClient()
 
     serverClient.setMutationDefaults(['addTodo'], {
       mutationFn: serverAddTodo,
@@ -307,7 +309,7 @@ describe('dehydration and rehydration', () => {
     onlineMock.mockReturnValue(true)
 
     const parsed = JSON.parse(stringified)
-    const client = new QueryClient()
+    const client = createQueryClient()
 
     const clientAddTodo = jest.fn().mockImplementation(variables => {
       return { id: 2, text: variables.text }
@@ -352,7 +354,7 @@ describe('dehydration and rehydration', () => {
       .fn()
       .mockImplementation(() => Promise.reject('offline'))
 
-    const queryClient = new QueryClient()
+    const queryClient = createQueryClient()
 
     queryClient.setMutationDefaults(['addTodo'], {
       mutationFn: serverAddTodo,
@@ -381,7 +383,7 @@ describe('dehydration and rehydration', () => {
       .fn()
       .mockImplementation(() => Promise.reject('offline'))
 
-    const queryClient = new QueryClient()
+    const queryClient = createQueryClient()
 
     queryClient.setMutationDefaults(['addTodo'], {
       mutationFn: serverAddTodo,
@@ -407,7 +409,7 @@ describe('dehydration and rehydration', () => {
 
   test('should not hydrate if the hydratedState is null or is not an object', async () => {
     const queryCache = new QueryCache()
-    const queryClient = new QueryClient({ queryCache })
+    const queryClient = createQueryClient({ queryCache })
 
     expect(() => hydrate(queryClient, null)).not.toThrow()
     expect(() => hydrate(queryClient, 'invalid')).not.toThrow()
@@ -417,7 +419,7 @@ describe('dehydration and rehydration', () => {
 
   test('should support hydratedState with undefined queries and mutations', async () => {
     const queryCache = new QueryCache()
-    const queryClient = new QueryClient({ queryCache })
+    const queryClient = createQueryClient({ queryCache })
 
     expect(() => hydrate(queryClient, {})).not.toThrow()
     expect(() => hydrate(queryClient, {})).not.toThrow()
diff --git a/src/core/tests/infiniteQueryBehavior.test.tsx b/src/core/tests/infiniteQueryBehavior.test.tsx
index a3e57e30e..6a056e172 100644
--- a/src/core/tests/infiniteQueryBehavior.test.tsx
+++ b/src/core/tests/infiniteQueryBehavior.test.tsx
@@ -1,5 +1,5 @@
 import { waitFor } from '@testing-library/react'
-import { queryKey, mockConsoleError } from '../../reactjs/tests/utils'
+import { createQueryClient, queryKey } from '../../reactjs/tests/utils'
 import {
   QueryClient,
   InfiniteQueryObserver,
@@ -10,7 +10,7 @@ describe('InfiniteQueryBehavior', () => {
   let queryClient: QueryClient
 
   beforeEach(() => {
-    queryClient = new QueryClient()
+    queryClient = createQueryClient()
     queryClient.mount()
   })
 
@@ -19,7 +19,6 @@ describe('InfiniteQueryBehavior', () => {
   })
 
   test('InfiniteQueryBehavior should throw an error if the queryFn is not defined', async () => {
-    const consoleMock = mockConsoleError()
     const key = queryKey()
 
     const observer = new InfiniteQueryObserver(queryClient, {
@@ -43,7 +42,6 @@ describe('InfiniteQueryBehavior', () => {
     })
 
     unsubscribe()
-    consoleMock.mockRestore()
   })
 
   test('InfiniteQueryBehavior should not refetch the first page if another page refetched', async () => {
diff --git a/src/core/tests/infiniteQueryObserver.test.tsx b/src/core/tests/infiniteQueryObserver.test.tsx
index 27094fe61..5d367f3bf 100644
--- a/src/core/tests/infiniteQueryObserver.test.tsx
+++ b/src/core/tests/infiniteQueryObserver.test.tsx
@@ -1,11 +1,11 @@
-import { sleep, queryKey } from '../../reactjs/tests/utils'
+import { sleep, queryKey, createQueryClient } from '../../reactjs/tests/utils'
 import { QueryClient, InfiniteQueryObserver } from '../..'
 
 describe('InfiniteQueryObserver', () => {
   let queryClient: QueryClient
 
   beforeEach(() => {
-    queryClient = new QueryClient()
+    queryClient = createQueryClient()
     queryClient.mount()
   })
 
diff --git a/src/core/tests/mutationCache.test.tsx b/src/core/tests/mutationCache.test.tsx
index 5bafcc2e3..8c1b8098a 100644
--- a/src/core/tests/mutationCache.test.tsx
+++ b/src/core/tests/mutationCache.test.tsx
@@ -1,20 +1,19 @@
 import { waitFor } from '@testing-library/react'
 import {
   queryKey,
-  mockConsoleError,
   sleep,
   executeMutation,
+  createQueryClient,
 } from '../../reactjs/tests/utils'
-import { MutationCache, MutationObserver, QueryClient } from '../..'
+import { MutationCache, MutationObserver } from '../..'
 
 describe('mutationCache', () => {
   describe('MutationCacheConfig.onError', () => {
     test('should be called when a mutation errors', async () => {
-      const consoleMock = mockConsoleError()
       const key = queryKey()
       const onError = jest.fn()
       const testCache = new MutationCache({ onError })
-      const testClient = new QueryClient({ mutationCache: testCache })
+      const testClient = createQueryClient({ mutationCache: testCache })
 
       try {
         await executeMutation(testClient, {
@@ -23,9 +22,7 @@ describe('mutationCache', () => {
           mutationFn: () => Promise.reject('error'),
           onMutate: () => 'context',
         })
-      } catch {
-        consoleMock.mockRestore()
-      }
+      } catch {}
 
       const mutation = testCache.getAll()[0]
       expect(onError).toHaveBeenCalledWith('error', 'vars', 'context', mutation)
@@ -33,11 +30,10 @@ describe('mutationCache', () => {
   })
   describe('MutationCacheConfig.onSuccess', () => {
     test('should be called when a mutation is successful', async () => {
-      const consoleMock = mockConsoleError()
       const key = queryKey()
       const onSuccess = jest.fn()
       const testCache = new MutationCache({ onSuccess })
-      const testClient = new QueryClient({ mutationCache: testCache })
+      const testClient = createQueryClient({ mutationCache: testCache })
 
       try {
         await executeMutation(testClient, {
@@ -46,9 +42,7 @@ describe('mutationCache', () => {
           mutationFn: () => Promise.resolve({ data: 5 }),
           onMutate: () => 'context',
         })
-      } catch {
-        consoleMock.mockRestore()
-      }
+      } catch {}
 
       const mutation = testCache.getAll()[0]
       expect(onSuccess).toHaveBeenCalledWith(
@@ -61,11 +55,10 @@ describe('mutationCache', () => {
   })
   describe('MutationCacheConfig.onMutate', () => {
     test('should be called before a mutation executes', async () => {
-      const consoleMock = mockConsoleError()
       const key = queryKey()
       const onMutate = jest.fn()
       const testCache = new MutationCache({ onMutate })
-      const testClient = new QueryClient({ mutationCache: testCache })
+      const testClient = createQueryClient({ mutationCache: testCache })
 
       try {
         await executeMutation(testClient, {
@@ -74,9 +67,7 @@ describe('mutationCache', () => {
           mutationFn: () => Promise.resolve({ data: 5 }),
           onMutate: () => 'context',
         })
-      } catch {
-        consoleMock.mockRestore()
-      }
+      } catch {}
 
       const mutation = testCache.getAll()[0]
       expect(onMutate).toHaveBeenCalledWith('vars', mutation)
@@ -86,7 +77,7 @@ describe('mutationCache', () => {
   describe('find', () => {
     test('should filter correctly', async () => {
       const testCache = new MutationCache()
-      const testClient = new QueryClient({ mutationCache: testCache })
+      const testClient = createQueryClient({ mutationCache: testCache })
       const key = ['mutation', 'vars']
       await executeMutation(testClient, {
         mutationKey: key,
@@ -108,7 +99,7 @@ describe('mutationCache', () => {
   describe('findAll', () => {
     test('should filter correctly', async () => {
       const testCache = new MutationCache()
-      const testClient = new QueryClient({ mutationCache: testCache })
+      const testClient = createQueryClient({ mutationCache: testCache })
       await executeMutation(testClient, {
         mutationKey: ['a', 1],
         variables: 1,
@@ -139,7 +130,7 @@ describe('mutationCache', () => {
   describe('garbage collection', () => {
     test('should remove unused mutations after cacheTime has elapsed', async () => {
       const testCache = new MutationCache()
-      const testClient = new QueryClient({ mutationCache: testCache })
+      const testClient = createQueryClient({ mutationCache: testCache })
       const onSuccess = jest.fn()
       await executeMutation(testClient, {
         mutationKey: ['a', 1],
@@ -158,7 +149,7 @@ describe('mutationCache', () => {
     })
 
     test('should not remove mutations if there are active observers', async () => {
-      const queryClient = new QueryClient()
+      const queryClient = createQueryClient()
       const observer = new MutationObserver(queryClient, {
         variables: 1,
         cacheTime: 10,
@@ -180,7 +171,7 @@ describe('mutationCache', () => {
     })
 
     test('should only remove when the last observer unsubscribes', async () => {
-      const queryClient = new QueryClient()
+      const queryClient = createQueryClient()
       const observer1 = new MutationObserver(queryClient, {
         variables: 1,
         cacheTime: 10,
@@ -220,7 +211,7 @@ describe('mutationCache', () => {
     })
 
     test('should be garbage collected later when unsubscribed and mutation is loading', async () => {
-      const queryClient = new QueryClient()
+      const queryClient = createQueryClient()
       const onSuccess = jest.fn()
       const observer = new MutationObserver(queryClient, {
         variables: 1,
@@ -247,7 +238,7 @@ describe('mutationCache', () => {
     })
 
     test('should call callbacks even with cacheTime 0 and mutation still loading', async () => {
-      const queryClient = new QueryClient()
+      const queryClient = createQueryClient()
       const onSuccess = jest.fn()
       const observer = new MutationObserver(queryClient, {
         variables: 1,
diff --git a/src/core/tests/mutationObserver.test.tsx b/src/core/tests/mutationObserver.test.tsx
index c1f577f93..d011beda4 100644
--- a/src/core/tests/mutationObserver.test.tsx
+++ b/src/core/tests/mutationObserver.test.tsx
@@ -1,12 +1,12 @@
 import { waitFor } from '@testing-library/react'
-import { sleep } from '../../reactjs/tests/utils'
+import { createQueryClient, sleep } from '../../reactjs/tests/utils'
 import { QueryClient, MutationObserver } from '../..'
 
 describe('mutationObserver', () => {
   let queryClient: QueryClient
 
   beforeEach(() => {
-    queryClient = new QueryClient()
+    queryClient = createQueryClient()
     queryClient.mount()
   })
 
diff --git a/src/core/tests/mutations.test.tsx b/src/core/tests/mutations.test.tsx
index 71e491bde..acf9fb79b 100644
--- a/src/core/tests/mutations.test.tsx
+++ b/src/core/tests/mutations.test.tsx
@@ -1,7 +1,7 @@
 import { QueryClient } from '../..'
 import {
+  createQueryClient,
   executeMutation,
-  mockConsoleError,
   queryKey,
   sleep,
 } from '../../reactjs/tests/utils'
@@ -12,7 +12,7 @@ describe('mutations', () => {
   let queryClient: QueryClient
 
   beforeEach(() => {
-    queryClient = new QueryClient()
+    queryClient = createQueryClient()
     queryClient.mount()
   })
 
@@ -151,8 +151,6 @@ describe('mutations', () => {
   })
 
   test('mutation should set correct error states', async () => {
-    const consoleMock = mockConsoleError()
-
     const mutation = new MutationObserver(queryClient, {
       mutationFn: async () => {
         await sleep(20)
@@ -243,8 +241,6 @@ describe('mutations', () => {
       status: 'error',
       variables: 'todo',
     })
-
-    consoleMock.mockRestore()
   })
 
   test('should be able to restore a mutation', async () => {
@@ -351,8 +347,6 @@ describe('mutations', () => {
   })
 
   test('mutate should throw an error if no mutationFn found', async () => {
-    const consoleMock = mockConsoleError()
-
     const mutation = new MutationObserver(queryClient, {
       mutationFn: undefined,
       retry: false,
@@ -365,7 +359,5 @@ describe('mutations', () => {
       error = err
     }
     expect(error).toEqual('No mutationFn found')
-
-    consoleMock.mockRestore()
   })
 })
diff --git a/src/core/tests/queriesObserver.test.tsx b/src/core/tests/queriesObserver.test.tsx
index e8cd4ca55..b75f25e12 100644
--- a/src/core/tests/queriesObserver.test.tsx
+++ b/src/core/tests/queriesObserver.test.tsx
@@ -1,12 +1,15 @@
 import { waitFor } from '@testing-library/react'
-import { sleep, queryKey } from '../../reactjs/tests/utils'
+import {
+  sleep,
+  queryKey,
+  createQueryClient,
+  mockLogger,
+} from '../../reactjs/tests/utils'
 import {
   QueryClient,
   QueriesObserver,
   QueryObserverResult,
   QueryObserver,
-  Logger,
-  setLogger,
 } from '../..'
 import { QueryKey } from '..'
 
@@ -14,7 +17,7 @@ describe('queriesObserver', () => {
   let queryClient: QueryClient
 
   beforeEach(() => {
-    queryClient = new QueryClient()
+    queryClient = createQueryClient()
     queryClient.mount()
   })
 
@@ -41,14 +44,6 @@ describe('queriesObserver', () => {
   })
 
   test('should still return value for undefined query key', async () => {
-    const logger: Logger = {
-      error: jest.fn(),
-      log: jest.fn(),
-      warn: jest.fn(),
-    }
-
-    setLogger(logger)
-
     const key1 = queryKey()
     const queryFn1 = jest.fn().mockReturnValue(1)
     const queryFn2 = jest.fn().mockReturnValue(2)
@@ -64,8 +59,7 @@ describe('queriesObserver', () => {
     unsubscribe()
     expect(observerResult).toMatchObject([{ data: 1 }, { data: 2 }])
 
-    expect(logger.error).toHaveBeenCalledTimes(1)
-    setLogger(console)
+    expect(mockLogger.error).toHaveBeenCalledTimes(1)
   })
 
   test('should update when a query updates', async () => {
diff --git a/src/core/tests/query.test.tsx b/src/core/tests/query.test.tsx
index e60f2b622..f53ff9ad8 100644
--- a/src/core/tests/query.test.tsx
+++ b/src/core/tests/query.test.tsx
@@ -2,7 +2,8 @@ import {
   sleep,
   queryKey,
   mockVisibilityState,
-  mockConsoleError,
+  mockLogger,
+  createQueryClient,
 } from '../../reactjs/tests/utils'
 import {
   QueryCache,
@@ -21,7 +22,7 @@ describe('query', () => {
   let queryCache: QueryCache
 
   beforeEach(() => {
-    queryClient = new QueryClient()
+    queryClient = createQueryClient()
     queryCache = queryClient.getQueryCache()
     queryClient.mount()
   })
@@ -429,8 +430,6 @@ describe('query', () => {
   })
 
   test('cancelling a rejected query should not have any effect', async () => {
-    const consoleMock = mockConsoleError()
-
     const key = queryKey()
 
     await queryClient.prefetchQuery(key, async () => {
@@ -442,12 +441,9 @@ describe('query', () => {
 
     expect(isError(query.state.error)).toBe(true)
     expect(isCancelledError(query.state.error)).toBe(false)
-
-    consoleMock.mockRestore()
   })
 
   test('the previous query status should be kept when refetching', async () => {
-    const consoleMock = mockConsoleError()
     const key = queryKey()
 
     await queryClient.prefetchQuery(key, () => 'data')
@@ -471,8 +467,6 @@ describe('query', () => {
 
     await sleep(100)
     expect(query.state.status).toBe('error')
-
-    consoleMock.mockRestore()
   })
 
   test('queries with cacheTime 0 should be removed immediately after unsubscribing', async () => {
@@ -789,7 +783,6 @@ describe('query', () => {
   })
 
   test('fetch should throw an error if the queryFn is not defined', async () => {
-    const consoleMock = mockConsoleError()
     const key = queryKey()
 
     const observer = new QueryObserver(queryClient, {
@@ -800,15 +793,12 @@ describe('query', () => {
 
     const unsubscribe = observer.subscribe(() => undefined)
     await sleep(10)
-    expect(consoleMock).toHaveBeenCalledWith('Missing queryFn')
+    expect(mockLogger.error).toHaveBeenCalledWith('Missing queryFn')
 
     unsubscribe()
-    consoleMock.mockRestore()
   })
 
   test('fetch should dispatch an error if the queryFn returns undefined', async () => {
-    const consoleMock = mockConsoleError()
-
     const key = queryKey()
 
     const observer = new QueryObserver(queryClient, {
@@ -832,9 +822,8 @@ describe('query', () => {
       error,
     })
 
-    expect(consoleMock).toHaveBeenCalledWith(error)
+    expect(mockLogger.error).toHaveBeenCalledWith(error)
     unsubscribe()
-    consoleMock.mockRestore()
   })
 
   test('fetch should dispatch fetch if is fetching and current promise is undefined', async () => {
diff --git a/src/core/tests/queryCache.test.tsx b/src/core/tests/queryCache.test.tsx
index e9fcba22c..1097164d2 100644
--- a/src/core/tests/queryCache.test.tsx
+++ b/src/core/tests/queryCache.test.tsx
@@ -1,4 +1,4 @@
-import { sleep, queryKey, mockConsoleError } from '../../reactjs/tests/utils'
+import { sleep, queryKey, createQueryClient } from '../../reactjs/tests/utils'
 import { QueryCache, QueryClient } from '../..'
 import { Query } from '.././query'
 
@@ -7,7 +7,7 @@ describe('queryCache', () => {
   let queryCache: QueryCache
 
   beforeEach(() => {
-    queryClient = new QueryClient()
+    queryClient = createQueryClient()
     queryCache = queryClient.getQueryCache()
   })
 
@@ -171,13 +171,11 @@ describe('queryCache', () => {
 
   describe('QueryCacheConfig.onError', () => {
     test('should be called when a query errors', async () => {
-      const consoleMock = mockConsoleError()
       const key = queryKey()
       const onError = jest.fn()
       const testCache = new QueryCache({ onError })
-      const testClient = new QueryClient({ queryCache: testCache })
+      const testClient = createQueryClient({ queryCache: testCache })
       await testClient.prefetchQuery(key, () => Promise.reject('error'))
-      consoleMock.mockRestore()
       const query = testCache.find(key)
       expect(onError).toHaveBeenCalledWith('error', query)
     })
@@ -185,13 +183,11 @@ describe('queryCache', () => {
 
   describe('QueryCacheConfig.onSuccess', () => {
     test('should be called when a query is successful', async () => {
-      const consoleMock = mockConsoleError()
       const key = queryKey()
       const onSuccess = jest.fn()
       const testCache = new QueryCache({ onSuccess })
-      const testClient = new QueryClient({ queryCache: testCache })
+      const testClient = createQueryClient({ queryCache: testCache })
       await testClient.prefetchQuery(key, () => Promise.resolve({ data: 5 }))
-      consoleMock.mockRestore()
       const query = testCache.find(key)
       expect(onSuccess).toHaveBeenCalledWith({ data: 5 }, query)
     })
diff --git a/src/core/tests/queryClient.test.tsx b/src/core/tests/queryClient.test.tsx
index aca0a7726..dde571992 100644
--- a/src/core/tests/queryClient.test.tsx
+++ b/src/core/tests/queryClient.test.tsx
@@ -5,8 +5,9 @@ import React from 'react'
 import {
   sleep,
   queryKey,
-  mockConsoleError,
   renderWithClient,
+  mockLogger,
+  createQueryClient,
 } from '../../reactjs/tests/utils'
 import {
   useQuery,
@@ -23,7 +24,7 @@ describe('queryClient', () => {
   let queryCache: QueryCache
 
   beforeEach(() => {
-    queryClient = new QueryClient()
+    queryClient = createQueryClient()
     queryCache = queryClient.getQueryCache()
     queryClient.mount()
   })
@@ -37,7 +38,7 @@ describe('queryClient', () => {
       const key = queryKey()
 
       const queryFn = () => 'data'
-      const testClient = new QueryClient({
+      const testClient = createQueryClient({
         defaultOptions: { queries: { queryFn } },
       })
 
@@ -47,7 +48,7 @@ describe('queryClient', () => {
     test('should merge defaultOptions when query is added to cache', async () => {
       const key = queryKey()
 
-      const testClient = new QueryClient({
+      const testClient = createQueryClient({
         defaultOptions: {
           queries: { cacheTime: Infinity },
         },
@@ -62,7 +63,7 @@ describe('queryClient', () => {
     test('should get defaultOptions', async () => {
       const queryFn = () => 'data'
       const defaultOptions = { queries: { queryFn } }
-      const testClient = new QueryClient({
+      const testClient = createQueryClient({
         defaultOptions,
       })
       expect(testClient.getDefaultOptions()).toMatchObject(defaultOptions)
@@ -97,7 +98,6 @@ describe('queryClient', () => {
     })
 
     test('should not match if the query key is a subset', async () => {
-      const consoleMock = mockConsoleError()
       const key = queryKey()
       queryClient.setQueryDefaults([key, 'a'], { queryFn: () => 'data' })
       const observer = new QueryObserver(queryClient, {
@@ -107,7 +107,6 @@ describe('queryClient', () => {
       })
       const { status } = await observer.refetch()
       expect(status).toBe('error')
-      consoleMock.mockRestore()
     })
 
     test('should also set defaults for observers', async () => {
@@ -134,9 +133,6 @@ describe('queryClient', () => {
 
     test('should warn in dev if several query defaults match a given key', () => {
       // Check discussion here: https://github.com/tannerlinsley/react-query/discussions/3199
-      const consoleErrorMock = jest.spyOn(console, 'error')
-      consoleErrorMock.mockImplementation(() => true)
-
       const keyABCD = [
         {
           a: 'a',
@@ -172,7 +168,7 @@ describe('queryClient', () => {
       // No defaults, no warning
       const noDefaults = queryClient.getQueryDefaults(keyABCD)
       expect(noDefaults).toBeUndefined()
-      expect(consoleErrorMock).not.toHaveBeenCalled()
+      expect(mockLogger.error).not.toHaveBeenCalled()
 
       // If defaults for key ABCD are registered **before** the ones of key ABC (more generic)…
       queryClient.setQueryDefaults(keyABCD, defaultsOfABCD)
@@ -181,10 +177,10 @@ describe('queryClient', () => {
       const goodDefaults = queryClient.getQueryDefaults(keyABCD)
       expect(goodDefaults).toBe(defaultsOfABCD)
       // The warning is still raised since several defaults are matching
-      expect(consoleErrorMock).toHaveBeenCalledTimes(1)
+      expect(mockLogger.error).toHaveBeenCalledTimes(1)
 
       // Let's create another queryClient and change the order of registration
-      const newQueryClient = new QueryClient()
+      const newQueryClient = createQueryClient()
       // The defaults for key ABC (more generic) are registered **before** the ones of key ABCD…
       newQueryClient.setQueryDefaults(keyABC, defaultsOfABC)
       newQueryClient.setQueryDefaults(keyABCD, defaultsOfABCD)
@@ -192,16 +188,11 @@ describe('queryClient', () => {
       const badDefaults = newQueryClient.getQueryDefaults(keyABCD)
       expect(badDefaults).not.toBe(defaultsOfABCD)
       expect(badDefaults).toBe(defaultsOfABC)
-      expect(consoleErrorMock).toHaveBeenCalledTimes(2)
-
-      consoleErrorMock.mockRestore()
+      expect(mockLogger.error).toHaveBeenCalledTimes(2)
     })
 
     test('should warn in dev if several mutation defaults match a given key', () => {
       // Check discussion here: https://github.com/tannerlinsley/react-query/discussions/3199
-      const consoleErrorMock = jest.spyOn(console, 'error')
-      consoleErrorMock.mockImplementation(() => true)
-
       const keyABCD = [
         {
           a: 'a',
@@ -233,7 +224,7 @@ describe('queryClient', () => {
       // No defaults, no warning
       const noDefaults = queryClient.getMutationDefaults(keyABCD)
       expect(noDefaults).toBeUndefined()
-      expect(consoleErrorMock).not.toHaveBeenCalled()
+      expect(mockLogger.error).not.toHaveBeenCalled()
 
       // If defaults for key ABCD are registered **before** the ones of key ABC (more generic)…
       queryClient.setMutationDefaults(keyABCD, defaultsOfABCD)
@@ -242,10 +233,10 @@ describe('queryClient', () => {
       const goodDefaults = queryClient.getMutationDefaults(keyABCD)
       expect(goodDefaults).toBe(defaultsOfABCD)
       // The warning is still raised since several defaults are matching
-      expect(consoleErrorMock).toHaveBeenCalledTimes(1)
+      expect(mockLogger.error).toHaveBeenCalledTimes(1)
 
       // Let's create another queryClient and change the order of registration
-      const newQueryClient = new QueryClient()
+      const newQueryClient = createQueryClient()
       // The defaults for key ABC (more generic) are registered **before** the ones of key ABCD…
       newQueryClient.setMutationDefaults(keyABC, defaultsOfABC)
       newQueryClient.setMutationDefaults(keyABCD, defaultsOfABCD)
@@ -253,9 +244,7 @@ describe('queryClient', () => {
       const badDefaults = newQueryClient.getMutationDefaults(keyABCD)
       expect(badDefaults).not.toBe(defaultsOfABCD)
       expect(badDefaults).toBe(defaultsOfABC)
-      expect(consoleErrorMock).toHaveBeenCalledTimes(2)
-
-      consoleErrorMock.mockRestore()
+      expect(mockLogger.error).toHaveBeenCalledTimes(2)
     })
   })
 
@@ -281,7 +270,7 @@ describe('queryClient', () => {
 
     test('should use default options', () => {
       const key = queryKey()
-      const testClient = new QueryClient({
+      const testClient = createQueryClient({
         defaultOptions: { queries: { queryKeyHashFn: () => 'someKey' } },
       })
       const testCache = testClient.getQueryCache()
@@ -538,8 +527,6 @@ describe('queryClient', () => {
 
     // https://github.com/tannerlinsley/react-query/issues/652
     test('should not retry by default', async () => {
-      const consoleMock = mockConsoleError()
-
       const key = queryKey()
 
       await expect(
@@ -547,8 +534,6 @@ describe('queryClient', () => {
           throw new Error('error')
         })
       ).rejects.toEqual(new Error('error'))
-
-      consoleMock.mockRestore()
     })
 
     test('should return the cached data on cache hit', async () => {
@@ -736,8 +721,6 @@ describe('queryClient', () => {
     })
 
     test('should return undefined when an error is thrown', async () => {
-      const consoleMock = mockConsoleError()
-
       const key = queryKey()
 
       const result = await queryClient.prefetchQuery(
@@ -751,9 +734,7 @@ describe('queryClient', () => {
       )
 
       expect(result).toBeUndefined()
-      expect(consoleMock).toHaveBeenCalled()
-
-      consoleMock.mockRestore()
+      expect(mockLogger.error).toHaveBeenCalled()
     })
 
     test('should be garbage collected after cacheTime if unused', async () => {
@@ -794,7 +775,6 @@ describe('queryClient', () => {
 
   describe('cancelQueries', () => {
     test('should revert queries to their previous state', async () => {
-      const consoleMock = mockConsoleError()
       const key1 = queryKey()
       const key2 = queryKey()
       const key3 = queryKey()
@@ -839,11 +819,9 @@ describe('queryClient', () => {
         status: 'loading',
         fetchStatus: 'idle',
       })
-      consoleMock.mockRestore()
     })
 
     test('should not revert if revert option is set to false', async () => {
-      const consoleMock = mockConsoleError()
       const key1 = queryKey()
       await queryClient.fetchQuery(key1, async () => {
         return 'data'
@@ -858,7 +836,6 @@ describe('queryClient', () => {
       expect(state1).toMatchObject({
         status: 'error',
       })
-      consoleMock.mockRestore()
     })
   })
 
@@ -1062,7 +1039,6 @@ describe('queryClient', () => {
     })
 
     test('should throw an error if throwOnError option is set to true', async () => {
-      const consoleMock = mockConsoleError()
       const key1 = queryKey()
       const queryFnError = () => Promise.reject('error')
       try {
@@ -1082,7 +1058,6 @@ describe('queryClient', () => {
         error = err
       }
       expect(error).toEqual('error')
-      consoleMock.mockRestore()
     })
   })
 
@@ -1416,7 +1391,7 @@ describe('queryClient', () => {
 
   describe('focusManager and onlineManager', () => {
     test('should notify queryCache and mutationCache if focused', async () => {
-      const testClient = new QueryClient()
+      const testClient = createQueryClient()
       testClient.mount()
 
       const queryCacheOnFocusSpy = jest.spyOn(
@@ -1449,7 +1424,7 @@ describe('queryClient', () => {
     })
 
     test('should notify queryCache and mutationCache if online', async () => {
-      const testClient = new QueryClient()
+      const testClient = createQueryClient()
       testClient.mount()
 
       const queryCacheOnFocusSpy = jest.spyOn(
diff --git a/src/core/tests/queryObserver.test.tsx b/src/core/tests/queryObserver.test.tsx
index af89f3d57..2bee1ca21 100644
--- a/src/core/tests/queryObserver.test.tsx
+++ b/src/core/tests/queryObserver.test.tsx
@@ -1,8 +1,9 @@
 import {
   sleep,
   queryKey,
-  mockConsoleError,
   expectType,
+  mockLogger,
+  createQueryClient,
 } from '../../reactjs/tests/utils'
 import {
   QueryClient,
@@ -15,7 +16,7 @@ describe('queryObserver', () => {
   let queryClient: QueryClient
 
   beforeEach(() => {
-    queryClient = new QueryClient()
+    queryClient = createQueryClient()
     queryClient.mount()
   })
 
@@ -246,7 +247,6 @@ 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 = () => {
@@ -290,7 +290,6 @@ describe('queryObserver', () => {
       isFetching: false,
       data: undefined,
     })
-    consoleMock.mockRestore()
   })
 
   test('should structurally share the selector', async () => {
@@ -395,7 +394,6 @@ describe('queryObserver', () => {
   })
 
   test('should stop retry when unsubscribing', async () => {
-    const consoleMock = mockConsoleError()
     const key = queryKey()
     let count = 0
     const observer = new QueryObserver(queryClient, {
@@ -412,7 +410,6 @@ describe('queryObserver', () => {
     unsubscribe()
     await sleep(200)
     expect(count).toBe(2)
-    consoleMock.mockRestore()
   })
 
   test('should clear interval when unsubscribing to a refetchInterval query', async () => {
@@ -463,7 +460,6 @@ describe('queryObserver', () => {
   })
 
   test('the retrier should not throw an error when reject if the retrier is already resolved', async () => {
-    const consoleMock = mockConsoleError()
     const key = queryKey()
     let count = 0
 
@@ -490,9 +486,7 @@ describe('queryObserver', () => {
     // Should not log an error
     queryClient.clear()
     await sleep(40)
-    expect(consoleMock).not.toHaveBeenNthCalledWith(1, 'reject 1')
-
-    consoleMock.mockRestore()
+    expect(mockLogger.error).not.toHaveBeenNthCalledWith(1, 'reject 1')
   })
 
   test('should throw an error if enabled option type is not valid', async () => {
@@ -522,7 +516,6 @@ describe('queryObserver', () => {
 
   test('should throw an error if throwOnError option is true', async () => {
     const key = queryKey()
-    const consoleMock = mockConsoleError()
 
     const observer = new QueryObserver(queryClient, {
       queryKey: key,
@@ -538,8 +531,6 @@ describe('queryObserver', () => {
     }
 
     expect(error).toEqual('error')
-
-    consoleMock.mockRestore()
   })
 
   test('should not refetch in background if refetchIntervalInBackground is false', async () => {
@@ -596,7 +587,6 @@ describe('queryObserver', () => {
 
   test('select function error using placeholderdata should log an error', () => {
     const key = queryKey()
-    const consoleMock = mockConsoleError()
 
     new QueryObserver(queryClient, {
       queryKey: key,
@@ -607,9 +597,7 @@ describe('queryObserver', () => {
       },
     })
 
-    expect(consoleMock).toHaveBeenNthCalledWith(1, new Error('error'))
-
-    consoleMock.mockRestore()
+    expect(mockLogger.error).toHaveBeenNthCalledWith(1, new Error('error'))
   })
 
   test('should not use replaceEqualDeep for select value when structuralSharing option is true and placeholderdata is defined', () => {
diff --git a/src/core/tests/utils.test.tsx b/src/core/tests/utils.test.tsx
index af8b618a8..4e4079063 100644
--- a/src/core/tests/utils.test.tsx
+++ b/src/core/tests/utils.test.tsx
@@ -6,41 +6,11 @@ import {
   matchMutation,
   scheduleMicrotask,
 } from '../utils'
-import { QueryClient, QueryCache, setLogger, Logger } from '../..'
-import { queryKey } from '../../reactjs/tests/utils'
 import { Mutation } from '../mutation'
 import { waitFor } from '@testing-library/dom'
+import { createQueryClient } from '../../reactjs/tests/utils'
 
 describe('core/utils', () => {
-  it('setLogger should override the default logger', async () => {
-    const key = queryKey()
-
-    const queryCache = new QueryCache()
-    const queryClient = new QueryClient({ queryCache })
-
-    const logger: Logger = {
-      error: jest.fn(),
-      log: jest.fn(),
-      warn: jest.fn(),
-    }
-
-    setLogger(logger)
-
-    await queryClient.prefetchQuery(
-      key,
-      async () => {
-        throw new Error('Test')
-      },
-      {
-        retry: 0,
-      }
-    )
-
-    expect(logger.error).toHaveBeenCalled()
-
-    setLogger(console)
-  })
-
   describe('isPlainObject', () => {
     it('should return `true` for a plain object', () => {
       expect(isPlainObject({})).toEqual(true)
@@ -349,7 +319,7 @@ describe('core/utils', () => {
   describe('matchMutation', () => {
     it('should return false if mutationKey options is undefined', () => {
       const filters = { mutationKey: ['key1'] }
-      const queryClient = new QueryClient()
+      const queryClient = createQueryClient()
       const mutation = new Mutation({
         mutationId: 1,
         mutationCache: queryClient.getMutationCache(),
diff --git a/src/reactjs/tests/Hydrate.test.tsx b/src/reactjs/tests/Hydrate.test.tsx
index 12924afa9..c3a827848 100644
--- a/src/reactjs/tests/Hydrate.test.tsx
+++ b/src/reactjs/tests/Hydrate.test.tsx
@@ -2,7 +2,6 @@ import React from 'react'
 import { render } from '@testing-library/react'
 
 import {
-  QueryClient,
   QueryClientProvider,
   QueryCache,
   useQuery,
@@ -10,7 +9,7 @@ import {
   useHydrate,
   Hydrate,
 } from '../..'
-import { sleep } from './utils'
+import { createQueryClient, sleep } from './utils'
 import * as coreModule from '../../core/index'
 
 describe('React hydration', () => {
@@ -21,7 +20,7 @@ describe('React hydration', () => {
 
   beforeAll(async () => {
     const queryCache = new QueryCache()
-    const queryClient = new QueryClient({ queryCache })
+    const queryClient = createQueryClient({ queryCache })
     await queryClient.prefetchQuery(['string'], () => dataQuery(['string']))
     const dehydrated = dehydrate(queryClient)
     stringifiedState = JSON.stringify(dehydrated)
@@ -32,7 +31,7 @@ describe('React hydration', () => {
     test('should hydrate queries to the cache on context', async () => {
       const dehydratedState = JSON.parse(stringifiedState)
       const queryCache = new QueryCache()
-      const queryClient = new QueryClient({ queryCache })
+      const queryClient = createQueryClient({ queryCache })
 
       function Page() {
         useHydrate(dehydratedState)
@@ -60,7 +59,7 @@ describe('React hydration', () => {
     test('should hydrate new queries if queries change', async () => {
       const dehydratedState = JSON.parse(stringifiedState)
       const queryCache = new QueryCache()
-      const queryClient = new QueryClient({ queryCache })
+      const queryClient = createQueryClient({ queryCache })
 
       function Page({ queryKey }: { queryKey: [string] }) {
         const { data } = useQuery(queryKey, () => dataQuery(queryKey))
@@ -83,7 +82,7 @@ describe('React hydration', () => {
       rendered.getByText('string')
 
       const intermediateCache = new QueryCache()
-      const intermediateClient = new QueryClient({
+      const intermediateClient = createQueryClient({
         queryCache: intermediateCache,
       })
       await intermediateClient.prefetchQuery(['string'], () =>
@@ -117,7 +116,7 @@ describe('React hydration', () => {
     test('should hydrate queries to new cache if cache changes', async () => {
       const dehydratedState = JSON.parse(stringifiedState)
       const queryCache = new QueryCache()
-      const queryClient = new QueryClient({ queryCache })
+      const queryClient = createQueryClient({ queryCache })
 
       function Page() {
         const { data } = useQuery(['string'], () => dataQuery(['string']))
@@ -140,7 +139,7 @@ describe('React hydration', () => {
       rendered.getByText('string')
 
       const newClientQueryCache = new QueryCache()
-      const newClientQueryClient = new QueryClient({
+      const newClientQueryClient = createQueryClient({
         queryCache: newClientQueryCache,
       })
 
@@ -162,7 +161,7 @@ describe('React hydration', () => {
 
   test('should not hydrate queries if state is null', async () => {
     const queryCache = new QueryCache()
-    const queryClient = new QueryClient({ queryCache })
+    const queryClient = createQueryClient({ queryCache })
 
     const hydrateSpy = jest.spyOn(coreModule, 'hydrate')
 
@@ -185,7 +184,7 @@ describe('React hydration', () => {
 
   test('should not hydrate queries if state is undefined', async () => {
     const queryCache = new QueryCache()
-    const queryClient = new QueryClient({ queryCache })
+    const queryClient = createQueryClient({ queryCache })
 
     const hydrateSpy = jest.spyOn(coreModule, 'hydrate')
 
diff --git a/src/reactjs/tests/QueryClientProvider.test.tsx b/src/reactjs/tests/QueryClientProvider.test.tsx
index deeb7c8ea..e451cd97c 100644
--- a/src/reactjs/tests/QueryClientProvider.test.tsx
+++ b/src/reactjs/tests/QueryClientProvider.test.tsx
@@ -2,7 +2,7 @@ import React from 'react'
 import { render, waitFor } from '@testing-library/react'
 import { renderToString } from 'react-dom/server'
 
-import { sleep, queryKey } from './utils'
+import { sleep, queryKey, createQueryClient } from './utils'
 import {
   QueryClient,
   QueryClientProvider,
@@ -16,7 +16,7 @@ describe('QueryClientProvider', () => {
     const key = queryKey()
 
     const queryCache = new QueryCache()
-    const queryClient = new QueryClient({ queryCache })
+    const queryClient = createQueryClient({ queryCache })
 
     function Page() {
       const { data } = useQuery(key, async () => {
@@ -49,8 +49,8 @@ describe('QueryClientProvider', () => {
     const queryCache1 = new QueryCache()
     const queryCache2 = new QueryCache()
 
-    const queryClient1 = new QueryClient({ queryCache: queryCache1 })
-    const queryClient2 = new QueryClient({ queryCache: queryCache2 })
+    const queryClient1 = createQueryClient({ queryCache: queryCache1 })
+    const queryClient2 = createQueryClient({ queryCache: queryCache2 })
 
     function Page1() {
       const { data } = useQuery(key1, async () => {
@@ -101,7 +101,7 @@ describe('QueryClientProvider', () => {
     const key = queryKey()
 
     const queryCache = new QueryCache()
-    const queryClient = new QueryClient({
+    const queryClient = createQueryClient({
       queryCache,
       defaultOptions: {
         queries: {
@@ -155,7 +155,7 @@ describe('QueryClientProvider', () => {
 
     test('should use window to get the context when contextSharing is true', () => {
       const queryCache = new QueryCache()
-      const queryClient = new QueryClient({ queryCache })
+      const queryClient = createQueryClient({ queryCache })
 
       let queryClientFromHook: QueryClient | undefined
       let queryClientFromWindow: QueryClient | undefined
@@ -182,7 +182,7 @@ describe('QueryClientProvider', () => {
 
     test('should not use window to get the context when contextSharing is true and window does not exist', () => {
       const queryCache = new QueryCache()
-      const queryClient = new QueryClient({ queryCache })
+      const queryClient = createQueryClient({ queryCache })
 
       // Mock a non web browser environment
       const windowSpy = jest
diff --git a/src/reactjs/tests/QueryResetErrorBoundary.test.tsx b/src/reactjs/tests/QueryResetErrorBoundary.test.tsx
index 65d0c1029..90f40a8f1 100644
--- a/src/reactjs/tests/QueryResetErrorBoundary.test.tsx
+++ b/src/reactjs/tests/QueryResetErrorBoundary.test.tsx
@@ -2,23 +2,17 @@ import { waitFor, fireEvent } from '@testing-library/react'
 import { ErrorBoundary } from 'react-error-boundary'
 import React from 'react'
 
-import { sleep, queryKey, mockConsoleError, renderWithClient } from './utils'
-import {
-  useQuery,
-  QueryClient,
-  QueryCache,
-  QueryErrorResetBoundary,
-} from '../..'
+import { sleep, queryKey, renderWithClient, createQueryClient } from './utils'
+import { useQuery, QueryCache, QueryErrorResetBoundary } from '../..'
 
 describe('QueryErrorResetBoundary', () => {
   const queryCache = new QueryCache()
-  const queryClient = new QueryClient({ queryCache })
+  const queryClient = createQueryClient({ queryCache })
 
   it('should retry fetch if the reset error boundary has been reset', async () => {
     const key = queryKey()
 
     let succeed = false
-    const consoleMock = mockConsoleError()
 
     function Page() {
       const { data } = useQuery(
@@ -69,15 +63,12 @@ describe('QueryErrorResetBoundary', () => {
     succeed = true
     fireEvent.click(rendered.getByText('retry'))
     await waitFor(() => rendered.getByText('data'))
-
-    consoleMock.mockRestore()
   })
 
   it('should not throw error if query is disabled', async () => {
     const key = queryKey()
 
     let succeed = false
-    const consoleMock = mockConsoleError()
 
     function Page() {
       const { data, status } = useQuery(
@@ -134,15 +125,12 @@ describe('QueryErrorResetBoundary', () => {
     succeed = true
     fireEvent.click(rendered.getByText('retry'))
     await waitFor(() => rendered.getByText('status: error'))
-
-    consoleMock.mockRestore()
   })
 
   it('should not throw error if query is disabled, and refetch if query becomes enabled again', async () => {
     const key = queryKey()
 
     let succeed = false
-    const consoleMock = mockConsoleError()
 
     function Page() {
       const [enabled, setEnabled] = React.useState(false)
@@ -200,15 +188,11 @@ describe('QueryErrorResetBoundary', () => {
     succeed = true
     fireEvent.click(rendered.getByText('retry'))
     await waitFor(() => rendered.getByText('data'))
-
-    consoleMock.mockRestore()
   })
 
   it('should throw error if query is disabled and manually refetched', async () => {
     const key = queryKey()
 
-    const consoleMock = mockConsoleError()
-
     function Page() {
       const { data, refetch, status, fetchStatus } = useQuery(
         key,
@@ -263,15 +247,12 @@ describe('QueryErrorResetBoundary', () => {
     )
     rendered.getByRole('button', { name: /refetch/i }).click()
     await waitFor(() => rendered.getByText('error boundary'))
-
-    consoleMock.mockRestore()
   })
 
   it('should not retry fetch if the reset error boundary has not been reset', async () => {
     const key = queryKey()
 
     let succeed = false
-    const consoleMock = mockConsoleError()
 
     function Page() {
       const { data } = useQuery(
@@ -321,15 +302,12 @@ describe('QueryErrorResetBoundary', () => {
     succeed = true
     fireEvent.click(rendered.getByText('retry'))
     await waitFor(() => rendered.getByText('error boundary'))
-
-    consoleMock.mockRestore()
   })
 
   it('should retry fetch if the reset error boundary has been reset and the query contains data from a previous fetch', async () => {
     const key = queryKey()
 
     let succeed = false
-    const consoleMock = mockConsoleError()
 
     function Page() {
       const { data } = useQuery(
@@ -381,8 +359,6 @@ describe('QueryErrorResetBoundary', () => {
     succeed = true
     fireEvent.click(rendered.getByText('retry'))
     await waitFor(() => rendered.getByText('data'))
-
-    consoleMock.mockRestore()
   })
 
   it('should not retry fetch if the reset error boundary has not been reset after a previous reset', async () => {
@@ -390,7 +366,6 @@ describe('QueryErrorResetBoundary', () => {
 
     let succeed = false
     let shouldReset = true
-    const consoleMock = mockConsoleError()
 
     function Page() {
       const { data } = useQuery(
@@ -449,13 +424,10 @@ describe('QueryErrorResetBoundary', () => {
     shouldReset = false
     fireEvent.click(rendered.getByText('retry'))
     await waitFor(() => rendered.getByText('error boundary'))
-
-    consoleMock.mockRestore()
   })
 
   it('should throw again on error after the reset error boundary has been reset', async () => {
     const key = queryKey()
-    const consoleMock = mockConsoleError()
     let fetchCount = 0
 
     function Page() {
@@ -507,13 +479,10 @@ describe('QueryErrorResetBoundary', () => {
     fireEvent.click(rendered.getByText('retry'))
     await waitFor(() => rendered.getByText('error boundary'))
     expect(fetchCount).toBe(3)
-
-    consoleMock.mockRestore()
   })
 
   it('should never render the component while the query is in error state', async () => {
     const key = queryKey()
-    const consoleMock = mockConsoleError()
     let fetchCount = 0
     let renders = 0
 
@@ -574,8 +543,6 @@ describe('QueryErrorResetBoundary', () => {
     await waitFor(() => rendered.getByText('data'))
     expect(fetchCount).toBe(3)
     expect(renders).toBe(1)
-
-    consoleMock.mockRestore()
   })
 
   it('should render children', async () => {
@@ -601,7 +568,6 @@ describe('QueryErrorResetBoundary', () => {
     const key = queryKey()
 
     let succeed = false
-    const consoleMock = mockConsoleError()
 
     function Page() {
       const { data } = useQuery(
@@ -652,7 +618,5 @@ describe('QueryErrorResetBoundary', () => {
     succeed = true
     fireEvent.click(rendered.getByText('retry'))
     await waitFor(() => rendered.getByText('data'))
-
-    consoleMock.mockRestore()
   })
 })
diff --git a/src/reactjs/tests/logger.native.test.tsx b/src/reactjs/tests/logger.native.test.tsx
deleted file mode 100644
index 799f73aab..000000000
--- a/src/reactjs/tests/logger.native.test.tsx
+++ /dev/null
@@ -1,9 +0,0 @@
-import { logger } from '../logger.native'
-
-describe('logger native', () => {
-  it('should expose logger properties', () => {
-    expect(logger).toHaveProperty('log')
-    expect(logger).toHaveProperty('error')
-    expect(logger).toHaveProperty('warn')
-  })
-})
diff --git a/src/reactjs/tests/ssr-hydration.test.tsx b/src/reactjs/tests/ssr-hydration.test.tsx
index d165543f2..3ea1111a2 100644
--- a/src/reactjs/tests/ssr-hydration.test.tsx
+++ b/src/reactjs/tests/ssr-hydration.test.tsx
@@ -4,14 +4,13 @@ import ReactDOMServer from 'react-dom/server'
 
 import {
   useQuery,
-  QueryClient,
   QueryClientProvider,
   QueryCache,
   dehydrate,
   hydrate,
 } from '../..'
 import * as utils from '../../core/utils'
-import { mockConsoleError, sleep } from './utils'
+import { createQueryClient, mockLogger, sleep } from './utils'
 
 // This monkey-patches the isServer-value from utils,
 // so that we can pretend to be in a server environment
@@ -31,7 +30,6 @@ function PrintStateComponent({ componentName, result }: any): any {
 
 describe('Server side rendering with de/rehydration', () => {
   it('should not mismatch on success', async () => {
-    const consoleMock = mockConsoleError()
     const fetchDataSuccess = jest.fn(fetchData)
 
     // -- Shared part --
@@ -46,13 +44,17 @@ describe('Server side rendering with de/rehydration', () => {
     setIsServer(true)
 
     const prefetchCache = new QueryCache()
-    const prefetchClient = new QueryClient({ queryCache: prefetchCache })
+    const prefetchClient = createQueryClient({
+      queryCache: prefetchCache,
+    })
     await prefetchClient.prefetchQuery(['success'], () =>
       fetchDataSuccess('success')
     )
     const dehydratedStateServer = dehydrate(prefetchClient)
     const renderCache = new QueryCache()
-    const renderClient = new QueryClient({ queryCache: renderCache })
+    const renderClient = createQueryClient({
+      queryCache: renderCache,
+    })
     hydrate(renderClient, dehydratedStateServer)
     const markup = ReactDOMServer.renderToString(
       <QueryClientProvider client={renderClient}>
@@ -73,7 +75,7 @@ describe('Server side rendering with de/rehydration', () => {
     el.innerHTML = markup
 
     const queryCache = new QueryCache()
-    const queryClient = new QueryClient({ queryCache })
+    const queryClient = createQueryClient({ queryCache })
     hydrate(queryClient, JSON.parse(stringifiedState))
 
     ReactDOM.hydrate(
@@ -84,17 +86,15 @@ describe('Server side rendering with de/rehydration', () => {
     )
 
     // Check that we have no React hydration mismatches
-    expect(consoleMock).not.toHaveBeenCalled()
+    expect(mockLogger.error).not.toHaveBeenCalled()
     expect(fetchDataSuccess).toHaveBeenCalledTimes(1)
     expect(el.innerHTML).toBe(expectedMarkup)
 
     ReactDOM.unmountComponentAtNode(el)
-    consoleMock.mockRestore()
     queryClient.clear()
   })
 
   it('should not mismatch on error', async () => {
-    const consoleMock = mockConsoleError()
     const fetchDataError = jest.fn(() => {
       throw new Error('fetchDataError')
     })
@@ -112,11 +112,15 @@ describe('Server side rendering with de/rehydration', () => {
     // -- Server part --
     setIsServer(true)
     const prefetchCache = new QueryCache()
-    const prefetchClient = new QueryClient({ queryCache: prefetchCache })
+    const prefetchClient = createQueryClient({
+      queryCache: prefetchCache,
+    })
     await prefetchClient.prefetchQuery(['error'], () => fetchDataError())
     const dehydratedStateServer = dehydrate(prefetchClient)
     const renderCache = new QueryCache()
-    const renderClient = new QueryClient({ queryCache: renderCache })
+    const renderClient = createQueryClient({
+      queryCache: renderCache,
+    })
     hydrate(renderClient, dehydratedStateServer)
     const markup = ReactDOMServer.renderToString(
       <QueryClientProvider client={renderClient}>
@@ -137,7 +141,7 @@ describe('Server side rendering with de/rehydration', () => {
     el.innerHTML = markup
 
     const queryCache = new QueryCache()
-    const queryClient = new QueryClient({ queryCache })
+    const queryClient = createQueryClient({ queryCache })
     hydrate(queryClient, JSON.parse(stringifiedState))
 
     ReactDOM.hydrate(
@@ -148,7 +152,7 @@ describe('Server side rendering with de/rehydration', () => {
     )
 
     // We expect exactly one console.error here, which is from the
-    expect(consoleMock).toHaveBeenCalledTimes(1)
+    expect(mockLogger.error).toHaveBeenCalledTimes(1)
     expect(fetchDataError).toHaveBeenCalledTimes(1)
     expect(el.innerHTML).toBe(expectedMarkup)
     await sleep(50)
@@ -158,12 +162,10 @@ describe('Server side rendering with de/rehydration', () => {
     )
 
     ReactDOM.unmountComponentAtNode(el)
-    consoleMock.mockRestore()
     queryClient.clear()
   })
 
   it('should not mismatch on queries that were not prefetched', async () => {
-    const consoleMock = mockConsoleError()
     const fetchDataSuccess = jest.fn(fetchData)
 
     // -- Shared part --
@@ -178,10 +180,14 @@ describe('Server side rendering with de/rehydration', () => {
     setIsServer(true)
 
     const prefetchCache = new QueryCache()
-    const prefetchClient = new QueryClient({ queryCache: prefetchCache })
+    const prefetchClient = createQueryClient({
+      queryCache: prefetchCache,
+    })
     const dehydratedStateServer = dehydrate(prefetchClient)
     const renderCache = new QueryCache()
-    const renderClient = new QueryClient({ queryCache: renderCache })
+    const renderClient = createQueryClient({
+      queryCache: renderCache,
+    })
     hydrate(renderClient, dehydratedStateServer)
     const markup = ReactDOMServer.renderToString(
       <QueryClientProvider client={renderClient}>
@@ -202,7 +208,7 @@ describe('Server side rendering with de/rehydration', () => {
     el.innerHTML = markup
 
     const queryCache = new QueryCache()
-    const queryClient = new QueryClient({ queryCache })
+    const queryClient = createQueryClient({ queryCache })
     hydrate(queryClient, JSON.parse(stringifiedState))
 
     ReactDOM.hydrate(
@@ -213,7 +219,7 @@ describe('Server side rendering with de/rehydration', () => {
     )
 
     // Check that we have no React hydration mismatches
-    expect(consoleMock).not.toHaveBeenCalled()
+    expect(mockLogger.error).not.toHaveBeenCalled()
     expect(fetchDataSuccess).toHaveBeenCalledTimes(0)
     expect(el.innerHTML).toBe(expectedMarkup)
     await sleep(50)
@@ -223,7 +229,6 @@ describe('Server side rendering with de/rehydration', () => {
     )
 
     ReactDOM.unmountComponentAtNode(el)
-    consoleMock.mockRestore()
     queryClient.clear()
   })
 })
diff --git a/src/reactjs/tests/ssr.test.tsx b/src/reactjs/tests/ssr.test.tsx
index c136e42f7..7259a3f96 100644
--- a/src/reactjs/tests/ssr.test.tsx
+++ b/src/reactjs/tests/ssr.test.tsx
@@ -6,10 +6,9 @@ import React from 'react'
 // @ts-ignore
 import { renderToString } from 'react-dom/server'
 
-import { sleep, queryKey } from './utils'
+import { sleep, queryKey, createQueryClient } from './utils'
 import {
   useQuery,
-  QueryClient,
   QueryClientProvider,
   QueryCache,
   useInfiniteQuery,
@@ -18,7 +17,7 @@ import {
 describe('Server Side Rendering', () => {
   it('should not trigger fetch', () => {
     const queryCache = new QueryCache()
-    const queryClient = new QueryClient({ queryCache })
+    const queryClient = createQueryClient({ queryCache })
     const key = queryKey()
     const queryFn = jest.fn().mockReturnValue('data')
 
@@ -47,7 +46,7 @@ describe('Server Side Rendering', () => {
 
   it('should add prefetched data to cache', async () => {
     const queryCache = new QueryCache()
-    const queryClient = new QueryClient({ queryCache })
+    const queryClient = createQueryClient({ queryCache })
     const key = queryKey()
     const fetchFn = () => Promise.resolve('data')
     const data = await queryClient.fetchQuery(key, fetchFn)
@@ -58,7 +57,7 @@ describe('Server Side Rendering', () => {
 
   it('should return existing data from the cache', async () => {
     const queryCache = new QueryCache()
-    const queryClient = new QueryClient({ queryCache })
+    const queryClient = createQueryClient({ queryCache })
     const key = queryKey()
     const queryFn = jest.fn(() => {
       sleep(10)
@@ -94,7 +93,7 @@ describe('Server Side Rendering', () => {
     const key = queryKey()
 
     const queryCache = new QueryCache()
-    const queryClient = new QueryClient({ queryCache })
+    const queryClient = createQueryClient({ queryCache })
 
     function Page() {
       const [page, setPage] = React.useState(1)
@@ -124,7 +123,7 @@ describe('Server Side Rendering', () => {
 
   it('useInfiniteQuery should return the correct state', async () => {
     const queryCache = new QueryCache()
-    const queryClient = new QueryClient({ queryCache })
+    const queryClient = createQueryClient({ queryCache })
     const key = queryKey()
     const queryFn = jest.fn(async () => {
       await sleep(5)
diff --git a/src/reactjs/tests/suspense.test.tsx b/src/reactjs/tests/suspense.test.tsx
index 6bf53b56e..c913bef0a 100644
--- a/src/reactjs/tests/suspense.test.tsx
+++ b/src/reactjs/tests/suspense.test.tsx
@@ -2,10 +2,9 @@ import { waitFor, fireEvent } from '@testing-library/react'
 import { ErrorBoundary } from 'react-error-boundary'
 import React from 'react'
 
-import { sleep, queryKey, mockConsoleError, renderWithClient } from './utils'
+import { sleep, queryKey, renderWithClient, createQueryClient } from './utils'
 import {
   useQuery,
-  QueryClient,
   QueryCache,
   QueryErrorResetBoundary,
   useQueryErrorResetBoundary,
@@ -16,7 +15,7 @@ import {
 
 describe("useQuery's in Suspense mode", () => {
   const queryCache = new QueryCache()
-  const queryClient = new QueryClient({ queryCache })
+  const queryClient = createQueryClient({ queryCache })
 
   it('should render the correct amount of times in Suspense mode', async () => {
     const key = queryKey()
@@ -265,7 +264,6 @@ describe("useQuery's in Suspense mode", () => {
     const key = queryKey()
 
     let succeed = false
-    const consoleMock = mockConsoleError()
 
     function Page() {
       useQuery(
@@ -325,15 +323,12 @@ describe("useQuery's in Suspense mode", () => {
     fireEvent.click(rendered.getByText('retry'))
 
     await waitFor(() => rendered.getByText('rendered'))
-
-    consoleMock.mockRestore()
   })
 
   it('should retry fetch if the reset error boundary has been reset', async () => {
     const key = queryKey()
 
     let succeed = false
-    const consoleMock = mockConsoleError()
 
     function Page() {
       useQuery(
@@ -390,8 +385,6 @@ describe("useQuery's in Suspense mode", () => {
     succeed = true
     fireEvent.click(rendered.getByText('retry'))
     await waitFor(() => rendered.getByText('rendered'))
-
-    consoleMock.mockRestore()
   })
 
   it('should refetch when re-mounting', async () => {
@@ -506,7 +499,6 @@ describe("useQuery's in Suspense mode", () => {
     const key = queryKey()
 
     let succeed = false
-    const consoleMock = mockConsoleError()
 
     function Page() {
       useQuery(
@@ -563,15 +555,11 @@ describe("useQuery's in Suspense mode", () => {
     succeed = true
     fireEvent.click(rendered.getByText('retry'))
     await waitFor(() => rendered.getByText('rendered'))
-
-    consoleMock.mockRestore()
   })
 
   it('should throw errors to the error boundary by default', async () => {
     const key = queryKey()
 
-    const consoleMock = mockConsoleError()
-
     function Page() {
       useQuery(
         key,
@@ -607,15 +595,11 @@ describe("useQuery's in Suspense mode", () => {
 
     await waitFor(() => rendered.getByText('Loading...'))
     await waitFor(() => rendered.getByText('error boundary'))
-
-    consoleMock.mockRestore()
   })
 
   it('should not throw errors to the error boundary when useErrorBoundary: false', async () => {
     const key = queryKey()
 
-    const consoleMock = mockConsoleError()
-
     function Page() {
       useQuery(
         key,
@@ -652,15 +636,11 @@ describe("useQuery's in Suspense mode", () => {
 
     await waitFor(() => rendered.getByText('Loading...'))
     await waitFor(() => rendered.getByText('rendered'))
-
-    consoleMock.mockRestore()
   })
 
   it('should not throw errors to the error boundary when a useErrorBoundary function returns true', async () => {
     const key = queryKey()
 
-    const consoleMock = mockConsoleError()
-
     function Page() {
       useQuery(
         key,
@@ -697,15 +677,11 @@ describe("useQuery's in Suspense mode", () => {
 
     await waitFor(() => rendered.getByText('Loading...'))
     await waitFor(() => rendered.getByText('error boundary'))
-
-    consoleMock.mockRestore()
   })
 
   it('should not throw errors to the error boundary when a useErrorBoundary function returns false', async () => {
     const key = queryKey()
 
-    const consoleMock = mockConsoleError()
-
     function Page() {
       useQuery(
         key,
@@ -742,8 +718,6 @@ describe("useQuery's in Suspense mode", () => {
 
     await waitFor(() => rendered.getByText('Loading...'))
     await waitFor(() => rendered.getByText('rendered'))
-
-    consoleMock.mockRestore()
   })
 
   it('should not call the queryFn when not enabled', async () => {
@@ -777,8 +751,6 @@ describe("useQuery's in Suspense mode", () => {
   it('should error catched in error boundary without infinite loop', async () => {
     const key = queryKey()
 
-    const consoleMock = mockConsoleError()
-
     let succeed = true
 
     function Page() {
@@ -841,12 +813,9 @@ describe("useQuery's in Suspense mode", () => {
     fireEvent.click(rendered.getByLabelText('fail'))
     // render error boundary fallback (error boundary)
     await waitFor(() => rendered.getByText('error boundary'))
-    consoleMock.mockRestore()
   })
 
   it('should error catched in error boundary without infinite loop when query keys changed', async () => {
-    const consoleMock = mockConsoleError()
-
     let succeed = true
 
     function Page() {
@@ -905,12 +874,9 @@ describe("useQuery's in Suspense mode", () => {
     fireEvent.click(rendered.getByLabelText('fail'))
     // render error boundary fallback (error boundary)
     await waitFor(() => rendered.getByText('error boundary'))
-    consoleMock.mockRestore()
   })
 
   it('should error catched in error boundary without infinite loop when enabled changed', async () => {
-    const consoleMock = mockConsoleError()
-
     const succeed = false
 
     function Page() {
@@ -975,7 +941,6 @@ describe("useQuery's in Suspense mode", () => {
 
     // render error boundary fallback (error boundary)
     await waitFor(() => rendered.getByText('error boundary'))
-    consoleMock.mockRestore()
   })
 
   it('should render the correct amount of times in Suspense mode when cacheTime is set to 0', async () => {
diff --git a/src/reactjs/tests/useInfiniteQuery.test.tsx b/src/reactjs/tests/useInfiniteQuery.test.tsx
index a9480415a..411f6af72 100644
--- a/src/reactjs/tests/useInfiniteQuery.test.tsx
+++ b/src/reactjs/tests/useInfiniteQuery.test.tsx
@@ -4,15 +4,14 @@ import React from 'react'
 import {
   queryKey,
   sleep,
-  mockConsoleError,
   renderWithClient,
   setActTimeout,
   Blink,
+  createQueryClient,
 } from './utils'
 import {
   useInfiniteQuery,
   UseInfiniteQueryResult,
-  QueryClient,
   QueryCache,
   QueryFunctionContext,
   InfiniteData,
@@ -44,7 +43,7 @@ const fetchItems = async (
 
 describe('useInfiniteQuery', () => {
   const queryCache = new QueryCache()
-  const queryClient = new QueryClient({ queryCache })
+  const queryClient = createQueryClient({ queryCache })
 
   it('should return the correct states for a successful query', async () => {
     const key = queryKey()
@@ -131,7 +130,6 @@ describe('useInfiniteQuery', () => {
   })
 
   it('should not throw when fetchNextPage returns an error', async () => {
-    const consoleMock = mockConsoleError()
     const key = queryKey()
     let noThrow: boolean
 
@@ -170,7 +168,6 @@ describe('useInfiniteQuery', () => {
     renderWithClient(queryClient, <Page />)
 
     await waitFor(() => expect(noThrow).toBe(true))
-    consoleMock.mockRestore()
   })
 
   it('should keep the previous data when keepPreviousData is set', async () => {
diff --git a/src/reactjs/tests/useIsFetching.test.tsx b/src/reactjs/tests/useIsFetching.test.tsx
index 085242cbf..9c91ca25b 100644
--- a/src/reactjs/tests/useIsFetching.test.tsx
+++ b/src/reactjs/tests/useIsFetching.test.tsx
@@ -2,19 +2,20 @@ import { fireEvent, waitFor } from '@testing-library/react'
 import React from 'react'
 
 import {
-  mockConsoleError,
+  createQueryClient,
+  mockLogger,
   queryKey,
   renderWithClient,
   setActTimeout,
   sleep,
 } from './utils'
-import { useQuery, useIsFetching, QueryClient, QueryCache } from '../..'
+import { useQuery, useIsFetching, QueryCache } from '../..'
 
 describe('useIsFetching', () => {
   // See https://github.com/tannerlinsley/react-query/issues/105
   it('should update as queries start and stop fetching', async () => {
     const queryCache = new QueryCache()
-    const queryClient = new QueryClient({ queryCache })
+    const queryClient = createQueryClient({ queryCache })
     const key = queryKey()
 
     function Page() {
@@ -50,9 +51,8 @@ describe('useIsFetching', () => {
   })
 
   it('should not update state while rendering', async () => {
-    const consoleMock = mockConsoleError()
     const queryCache = new QueryCache()
-    const queryClient = new QueryClient({ queryCache })
+    const queryClient = createQueryClient({ queryCache })
 
     const key1 = queryKey()
     const key2 = queryKey()
@@ -101,15 +101,12 @@ describe('useIsFetching', () => {
 
     renderWithClient(queryClient, <Page />)
     await waitFor(() => expect(isFetchings).toEqual([0, 0, 1, 2, 1, 0]))
-    expect(consoleMock).not.toHaveBeenCalled()
-    expect(consoleMock.mock.calls[0]?.[0] ?? '').not.toMatch('setState')
-
-    consoleMock.mockRestore()
+    expect(mockLogger.error).not.toHaveBeenCalled()
   })
 
   it('should be able to filter', async () => {
     const queryCache = new QueryCache()
-    const queryClient = new QueryClient({ queryCache })
+    const queryClient = createQueryClient({ queryCache })
     const key1 = queryKey()
     const key2 = queryKey()
 
diff --git a/src/reactjs/tests/useIsMutating.test.tsx b/src/reactjs/tests/useIsMutating.test.tsx
index 33af90259..19212513d 100644
--- a/src/reactjs/tests/useIsMutating.test.tsx
+++ b/src/reactjs/tests/useIsMutating.test.tsx
@@ -1,15 +1,19 @@
 import { waitFor, fireEvent } from '@testing-library/react'
 import React from 'react'
-import { QueryClient } from '../../core'
 import { useIsMutating } from '../useIsMutating'
 import { useMutation } from '../useMutation'
-import { renderWithClient, setActTimeout, sleep } from './utils'
+import {
+  createQueryClient,
+  renderWithClient,
+  setActTimeout,
+  sleep,
+} from './utils'
 import * as MutationCacheModule from '../../core/mutationCache'
 
 describe('useIsMutating', () => {
   it('should return the number of fetching mutations', async () => {
     const isMutatings: number[] = []
-    const queryClient = new QueryClient()
+    const queryClient = createQueryClient()
 
     function IsMutating() {
       const isMutating = useIsMutating()
@@ -43,7 +47,7 @@ describe('useIsMutating', () => {
 
   it('should filter correctly by mutationKey', async () => {
     const isMutatings: number[] = []
-    const queryClient = new QueryClient()
+    const queryClient = createQueryClient()
 
     function IsMutating() {
       const isMutating = useIsMutating(['mutation1'])
@@ -75,7 +79,7 @@ describe('useIsMutating', () => {
 
   it('should filter correctly by predicate', async () => {
     const isMutatings: number[] = []
-    const queryClient = new QueryClient()
+    const queryClient = createQueryClient()
 
     function IsMutating() {
       const isMutating = useIsMutating({
@@ -124,7 +128,7 @@ describe('useIsMutating', () => {
         return new MutationCacheMock(fn)
       })
 
-    const queryClient = new QueryClient()
+    const queryClient = createQueryClient()
 
     function IsMutating() {
       useIsMutating()
diff --git a/src/reactjs/tests/useMutation.test.tsx b/src/reactjs/tests/useMutation.test.tsx
index 0cab2ec28..8bf5e06b3 100644
--- a/src/reactjs/tests/useMutation.test.tsx
+++ b/src/reactjs/tests/useMutation.test.tsx
@@ -3,10 +3,10 @@ import '@testing-library/jest-dom'
 import React from 'react'
 import { ErrorBoundary } from 'react-error-boundary'
 
-import { useMutation, QueryClient, QueryCache, MutationCache } from '../..'
+import { useMutation, QueryCache, MutationCache } from '../..'
 import { UseMutationResult } from '../types'
 import {
-  mockConsoleError,
+  createQueryClient,
   mockNavigatorOnLine,
   queryKey,
   renderWithClient,
@@ -17,7 +17,7 @@ import {
 describe('useMutation', () => {
   const queryCache = new QueryCache()
   const mutationCache = new MutationCache()
-  const queryClient = new QueryClient({ queryCache, mutationCache })
+  const queryClient = createQueryClient({ queryCache, mutationCache })
 
   it('should be able to reset `data`', async () => {
     function Page() {
@@ -52,8 +52,6 @@ describe('useMutation', () => {
   })
 
   it('should be able to reset `error`', async () => {
-    const consoleMock = mockConsoleError()
-
     function Page() {
       const { mutate, error, reset } = useMutation<string, Error>(() => {
         const err = new Error('Expected mock error. All is well!')
@@ -88,8 +86,6 @@ describe('useMutation', () => {
     fireEvent.click(getByText('reset'))
 
     await waitFor(() => expect(queryByTestId('error')).toBeNull())
-
-    consoleMock.mockRestore()
   })
 
   it('should be able to call `onSuccess` and `onSettled` after each successful mutate', async () => {
@@ -142,8 +138,6 @@ describe('useMutation', () => {
   })
 
   it('should be able to call `onError` and `onSettled` after each failed mutate', async () => {
-    const consoleMock = mockConsoleError()
-
     const onErrorMock = jest.fn()
     const onSettledMock = jest.fn()
     let count = 0
@@ -208,8 +202,6 @@ describe('useMutation', () => {
     )
 
     expect(getByTestId('title').textContent).toBe('3')
-
-    consoleMock.mockRestore()
   })
 
   it('should be able to override the useMutation success callbacks', async () => {
@@ -258,8 +250,6 @@ describe('useMutation', () => {
   })
 
   it('should be able to override the error callbacks when using mutateAsync', async () => {
-    const consoleMock = mockConsoleError()
-
     const callbacks: string[] = []
 
     function Page() {
@@ -306,8 +296,6 @@ describe('useMutation', () => {
       'mutateAsync.onSettled',
       'mutateAsync.error:oops',
     ])
-
-    consoleMock.mockRestore()
   })
 
   it('should be able to use mutation defaults', async () => {
@@ -346,8 +334,6 @@ describe('useMutation', () => {
   })
 
   it('should be able to retry a failed mutation', async () => {
-    const consoleMock = mockConsoleError()
-
     let count = 0
 
     function Page() {
@@ -376,12 +362,9 @@ describe('useMutation', () => {
     await sleep(100)
 
     expect(count).toBe(2)
-
-    consoleMock.mockRestore()
   })
 
   it('should not retry mutations while offline', async () => {
-    const consoleMock = mockConsoleError()
     const onlineMock = mockNavigatorOnLine(false)
 
     let count = 0
@@ -441,7 +424,6 @@ describe('useMutation', () => {
 
     expect(count).toBe(2)
 
-    consoleMock.mockRestore()
     onlineMock.mockRestore()
   })
 
@@ -541,7 +523,6 @@ describe('useMutation', () => {
   })
 
   it('should be able to retry a mutation when online', async () => {
-    const consoleMock = mockConsoleError()
     const onlineMock = mockNavigatorOnLine(false)
 
     let count = 0
@@ -617,7 +598,6 @@ describe('useMutation', () => {
       data: 'data',
     })
 
-    consoleMock.mockRestore()
     onlineMock.mockRestore()
   })
 
@@ -642,8 +622,6 @@ describe('useMutation', () => {
   })
 
   it('should be able to throw an error when useErrorBoundary is set to true', async () => {
-    const consoleMock = mockConsoleError()
-
     function Page() {
       const { mutate } = useMutation<string, Error>(
         () => {
@@ -679,17 +657,13 @@ describe('useMutation', () => {
     await waitFor(() => {
       expect(queryByText('error')).not.toBeNull()
     })
-
-    consoleMock.mockRestore()
   })
 
   it('should pass meta to mutation', async () => {
-    const consoleMock = mockConsoleError()
-
     const errorMock = jest.fn()
     const successMock = jest.fn()
 
-    const queryClientMutationMeta = new QueryClient({
+    const queryClientMutationMeta = createQueryClient({
       mutationCache: new MutationCache({
         onSuccess: (_, __, ___, mutation) => {
           successMock(mutation.meta?.metaSuccessMessage)
@@ -743,8 +717,6 @@ describe('useMutation', () => {
     expect(successMock).toHaveBeenCalledWith(metaSuccessMessage)
     expect(errorMock).toHaveBeenCalledTimes(1)
     expect(errorMock).toHaveBeenCalledWith(metaErrorMessage)
-
-    consoleMock.mockRestore()
   })
 
   it('should call cache callbacks when unmounted', async () => {
diff --git a/src/reactjs/tests/useQueries.test.tsx b/src/reactjs/tests/useQueries.test.tsx
index 4a0bb9d28..3ac5e1955 100644
--- a/src/reactjs/tests/useQueries.test.tsx
+++ b/src/reactjs/tests/useQueries.test.tsx
@@ -4,6 +4,7 @@ import React from 'react'
 import * as QueriesObserverModule from '../../core/queriesObserver'
 
 import {
+  createQueryClient,
   expectType,
   expectTypeNotAny,
   queryKey,
@@ -13,7 +14,6 @@ import {
 } from './utils'
 import {
   useQueries,
-  QueryClient,
   UseQueryResult,
   QueryCache,
   QueryObserverResult,
@@ -26,7 +26,7 @@ import { QueryFunctionContext } from '../../core'
 
 describe('useQueries', () => {
   const queryCache = new QueryCache()
-  const queryClient = new QueryClient({ queryCache })
+  const queryClient = createQueryClient({ queryCache })
 
   it('should return the correct states', async () => {
     const key1 = queryKey()
diff --git a/src/reactjs/tests/useQuery.test.tsx b/src/reactjs/tests/useQuery.test.tsx
index 5e99ae409..6951a81e0 100644
--- a/src/reactjs/tests/useQuery.test.tsx
+++ b/src/reactjs/tests/useQuery.test.tsx
@@ -6,16 +6,16 @@ import {
   expectType,
   queryKey,
   mockVisibilityState,
-  mockConsoleError,
   sleep,
   renderWithClient,
   setActTimeout,
   Blink,
   mockNavigatorOnLine,
+  mockLogger,
+  createQueryClient,
 } from './utils'
 import {
   useQuery,
-  QueryClient,
   UseQueryResult,
   QueryCache,
   QueryFunction,
@@ -25,7 +25,7 @@ import { ErrorBoundary } from 'react-error-boundary'
 
 describe('useQuery', () => {
   const queryCache = new QueryCache()
-  const queryClient = new QueryClient({ queryCache })
+  const queryClient = createQueryClient({ queryCache })
 
   it('should return the correct types', () => {
     const key = queryKey()
@@ -215,7 +215,6 @@ describe('useQuery', () => {
 
   it('should return the correct states for an unsuccessful query', async () => {
     const key = queryKey()
-    const consoleMock = mockConsoleError()
 
     const states: UseQueryResult<undefined, string>[] = []
 
@@ -317,8 +316,6 @@ describe('useQuery', () => {
       status: 'error',
       fetchStatus: 'idle',
     })
-
-    consoleMock.mockRestore()
   })
 
   it('should set isFetchedAfterMount to true after a query has been fetched', async () => {
@@ -467,7 +464,6 @@ describe('useQuery', () => {
     const key = queryKey()
     const states: UseQueryResult<unknown>[] = []
     const onError = jest.fn()
-    const consoleMock = mockConsoleError()
 
     function Page() {
       const state = useQuery<unknown>(key, () => Promise.reject('error'), {
@@ -484,13 +480,11 @@ describe('useQuery', () => {
     expect(states.length).toBe(2)
     expect(onError).toHaveBeenCalledTimes(1)
     expect(onError).toHaveBeenCalledWith('error')
-    consoleMock.mockRestore()
   })
 
   it('should not call onError when receiving a CancelledError', async () => {
     const key = queryKey()
     const onError = jest.fn()
-    const consoleMock = mockConsoleError()
 
     function Page() {
       useQuery<unknown>(
@@ -511,7 +505,6 @@ describe('useQuery', () => {
     await sleep(5)
     await queryClient.cancelQueries(key)
     expect(onError).not.toHaveBeenCalled()
-    consoleMock.mockRestore()
   })
 
   it('should call onSettled after a query has been fetched', async () => {
@@ -537,7 +530,6 @@ describe('useQuery', () => {
     const key = queryKey()
     const states: UseQueryResult<string>[] = []
     const onSettled = jest.fn()
-    const consoleMock = mockConsoleError()
 
     function Page() {
       const state = useQuery(key, () => Promise.reject('error'), {
@@ -554,7 +546,6 @@ describe('useQuery', () => {
     expect(states.length).toBe(2)
     expect(onSettled).toHaveBeenCalledTimes(1)
     expect(onSettled).toHaveBeenCalledWith(undefined, 'error')
-    consoleMock.mockRestore()
   })
 
   it('should not cancel an ongoing fetch when refetch is called with cancelRefetch=false if we have data already', async () => {
@@ -924,7 +915,6 @@ describe('useQuery', () => {
   })
 
   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')
@@ -943,13 +933,11 @@ describe('useQuery', () => {
 
     await sleep(10)
 
-    expect(consoleMock).toHaveBeenCalledWith(error)
+    expect(mockLogger.error).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 track properties and only re-render when a tracked property changes', async () => {
@@ -1461,7 +1449,6 @@ describe('useQuery', () => {
 
   it('should transition to error state when keepPreviousData is set', async () => {
     const key = queryKey()
-    const consoleMock = mockConsoleError()
     const states: UseQueryResult<number>[] = []
 
     function Page({ count }: { count: number }) {
@@ -1562,8 +1549,6 @@ describe('useQuery', () => {
       isPreviousData: false,
     })
     expect(states[7]?.error).toHaveProperty('message', 'Error test')
-
-    consoleMock.mockRestore()
   })
 
   it('should not show initial data from next query if keepPreviousData is set', async () => {
@@ -2598,7 +2583,6 @@ describe('useQuery', () => {
 
   it('should set status to error if queryFn throws', async () => {
     const key = queryKey()
-    const consoleMock = mockConsoleError()
 
     function Page() {
       const { status, error } = useQuery<undefined, string>(
@@ -2621,13 +2605,10 @@ describe('useQuery', () => {
 
     await waitFor(() => rendered.getByText('error'))
     await waitFor(() => rendered.getByText('Error test jaylen'))
-
-    consoleMock.mockRestore()
   })
 
   it('should throw error if queryFn throws and useErrorBoundary is in use', async () => {
     const key = queryKey()
-    const consoleMock = mockConsoleError()
 
     function Page() {
       const { status, error } = useQuery<undefined, string>(
@@ -2652,8 +2633,6 @@ describe('useQuery', () => {
     )
 
     await waitFor(() => rendered.getByText('error boundary'))
-
-    consoleMock.mockRestore()
   })
 
   it('should update with data if we observe no properties and useErrorBoundary', async () => {
@@ -2682,7 +2661,6 @@ describe('useQuery', () => {
 
   it('should set status to error instead of throwing when error should not be thrown', async () => {
     const key = queryKey()
-    const consoleMock = mockConsoleError()
 
     function Page() {
       const { status, error } = useQuery<undefined, string>(
@@ -2711,13 +2689,10 @@ describe('useQuery', () => {
 
     await waitFor(() => rendered.getByText('error'))
     await waitFor(() => rendered.getByText('Local Error'))
-
-    consoleMock.mockRestore()
   })
 
   it('should throw error instead of setting status when error should be thrown', async () => {
     const key = queryKey()
-    const consoleMock = mockConsoleError()
 
     function Page() {
       const { status, error } = useQuery<undefined, string>(
@@ -2753,13 +2728,10 @@ describe('useQuery', () => {
 
     await waitFor(() => rendered.getByText('error boundary'))
     await waitFor(() => rendered.getByText('Remote Error'))
-
-    consoleMock.mockRestore()
   })
 
   it('should continue retries when observers unmount and remount while waiting for a retry (#3031)', async () => {
     const key = queryKey()
-    const consoleMock = mockConsoleError()
     let count = 0
 
     function Page() {
@@ -2803,13 +2775,10 @@ describe('useQuery', () => {
     await waitFor(() => rendered.getByText('error: some error'))
 
     expect(count).toBe(3)
-
-    consoleMock.mockRestore()
   })
 
   it('should restart when observers unmount and remount while waiting for a retry when query was cancelled in between (#3031)', async () => {
     const key = queryKey()
-    const consoleMock = mockConsoleError()
     let count = 0
 
     function Page() {
@@ -2858,8 +2827,6 @@ describe('useQuery', () => {
 
     // initial fetch (1), which will be cancelled, followed by new mount(2) + 2 retries = 4
     expect(count).toBe(4)
-
-    consoleMock.mockRestore()
   })
 
   it('should always fetch if refetchOnMount is set to always', async () => {
@@ -3061,7 +3028,6 @@ describe('useQuery', () => {
 
   it('should retry specified number of times', async () => {
     const key = queryKey()
-    const consoleMock = mockConsoleError()
 
     const queryFn = jest.fn()
     queryFn.mockImplementation(() => {
@@ -3091,14 +3057,11 @@ describe('useQuery', () => {
     await waitFor(() => rendered.getByText('Failed 2 times'))
 
     expect(queryFn).toHaveBeenCalledTimes(2)
-    consoleMock.mockRestore()
   })
 
   it('should not retry if retry function `false`', async () => {
     const key = queryKey()
 
-    const consoleMock = mockConsoleError()
-
     const queryFn = jest.fn()
 
     queryFn.mockImplementationOnce(() => {
@@ -3137,12 +3100,10 @@ describe('useQuery', () => {
     await waitFor(() => rendered.getByText('NoRetry'))
 
     expect(queryFn).toHaveBeenCalledTimes(2)
-    consoleMock.mockRestore()
   })
 
   it('should extract retryDelay from error', async () => {
     const key = queryKey()
-    const consoleMock = mockConsoleError()
 
     type DelayError = { delay: number }
 
@@ -3174,15 +3135,12 @@ describe('useQuery', () => {
     await waitFor(() => rendered.getByText('Failed 2 times'))
 
     expect(queryFn).toHaveBeenCalledTimes(2)
-    consoleMock.mockRestore()
   })
 
   // See https://github.com/tannerlinsley/react-query/issues/160
   it('should continue retry after focus regain', async () => {
     const key = queryKey()
 
-    const consoleMock = mockConsoleError()
-
     // make page unfocused
     const visibilityMock = mockVisibilityState('hidden')
 
@@ -3237,9 +3195,7 @@ describe('useQuery', () => {
     await waitFor(() => rendered.getByText('failureCount 4'))
 
     // Check if the error has been logged in the console
-    expect(consoleMock).toHaveBeenCalledWith('fetching error 4')
-
-    consoleMock.mockRestore()
+    expect(mockLogger.error).toHaveBeenCalledWith('fetching error 4')
   })
 
   it('should fetch on mount when a query was already created with setQueryData', async () => {
@@ -3276,7 +3232,6 @@ describe('useQuery', () => {
   it('should refetch after focus regain', async () => {
     const key = queryKey()
     const states: UseQueryResult<string>[] = []
-    const consoleMock = mockConsoleError()
 
     // make page unfocused
     const visibilityMock = mockVisibilityState('hidden')
@@ -3327,8 +3282,6 @@ describe('useQuery', () => {
         isStale: true,
       },
     ])
-
-    consoleMock.mockRestore()
   })
 
   // See https://github.com/tannerlinsley/react-query/issues/195
@@ -3399,8 +3352,6 @@ describe('useQuery', () => {
   it('should reset failureCount on successful fetch', async () => {
     const key = queryKey()
 
-    const consoleMock = mockConsoleError()
-
     function Page() {
       let counter = 0
 
@@ -3428,8 +3379,6 @@ describe('useQuery', () => {
 
     await waitFor(() => rendered.getByText('failureCount 2'))
     await waitFor(() => rendered.getByText('failureCount 0'))
-
-    consoleMock.mockRestore()
   })
 
   // See https://github.com/tannerlinsley/react-query/issues/199
@@ -4503,8 +4452,6 @@ describe('useQuery', () => {
   })
 
   it('should refetch when changed enabled to true in error state', async () => {
-    const consoleMock = mockConsoleError()
-
     const queryFn = jest.fn()
     queryFn.mockImplementation(async () => {
       await sleep(10)
@@ -4559,13 +4506,9 @@ describe('useQuery', () => {
     // // change to enabled to true
     fireEvent.click(rendered.getByLabelText('retry'))
     expect(queryFn).toBeCalledTimes(2)
-
-    consoleMock.mockRestore()
   })
 
   it('should refetch when query key changed when previous status is error', async () => {
-    const consoleMock = mockConsoleError()
-
     function Page({ id }: { id: number }) {
       const { error, isLoading } = useQuery(
         [id],
@@ -4622,13 +4565,9 @@ describe('useQuery', () => {
     // change to mount new query
     fireEvent.click(rendered.getByLabelText('change'))
     await waitFor(() => rendered.getByText('error'))
-
-    consoleMock.mockRestore()
   })
 
   it('should refetch when query key changed when switching between erroneous queries', async () => {
-    const consoleMock = mockConsoleError()
-
     function Page({ id }: { id: boolean }) {
       const { error, isFetching } = useQuery(
         [id],
@@ -4683,12 +4622,9 @@ describe('useQuery', () => {
     fireEvent.click(rendered.getByLabelText('change'))
     await waitFor(() => rendered.getByText('status: fetching'))
     await waitFor(() => rendered.getByText('error'))
-
-    consoleMock.mockRestore()
   })
 
   it('should have no error in loading state when refetching after error occurred', async () => {
-    const consoleMock = mockConsoleError()
     const key = queryKey()
     const states: UseQueryResult<number>[] = []
     const error = new Error('oops')
@@ -4759,8 +4695,6 @@ describe('useQuery', () => {
       data: 5,
       error: null,
     })
-
-    consoleMock.mockRestore()
   })
 
   describe('networkMode online', () => {
@@ -5103,7 +5037,6 @@ describe('useQuery', () => {
 
     it('online queries should pause retries if you are offline', async () => {
       const key = queryKey()
-      const consoleMock = mockConsoleError()
       let count = 0
 
       function Page() {
@@ -5158,7 +5091,6 @@ describe('useQuery', () => {
       expect(count).toBe(3)
 
       onlineMock.mockRestore()
-      consoleMock.mockRestore()
     })
 
     it('online queries should fetch if paused and we go online even if already unmounted (because not cancelled)', async () => {
@@ -5395,7 +5327,6 @@ describe('useQuery', () => {
 
     it('always queries should not pause retries', async () => {
       const onlineMock = mockNavigatorOnLine(false)
-      const consoleMock = mockConsoleError()
 
       const key = queryKey()
       let count = 0
@@ -5435,14 +5366,12 @@ describe('useQuery', () => {
 
       expect(count).toBe(2)
 
-      consoleMock.mockRestore()
       onlineMock.mockRestore()
     })
   })
 
   describe('networkMode offlineFirst', () => {
     it('offlineFirst queries should start fetching if you are offline, but pause retries', async () => {
-      const consoleMock = mockConsoleError()
       const onlineMock = mockNavigatorOnLine(false)
 
       const key = queryKey()
@@ -5491,7 +5420,6 @@ describe('useQuery', () => {
       expect(count).toBe(3)
 
       onlineMock.mockRestore()
-      consoleMock.mockRestore()
     })
   })
 })
diff --git a/src/reactjs/tests/utils.tsx b/src/reactjs/tests/utils.tsx
index 39d41cb36..e0add20f3 100644
--- a/src/reactjs/tests/utils.tsx
+++ b/src/reactjs/tests/utils.tsx
@@ -1,7 +1,16 @@
 import { act, render } from '@testing-library/react'
 import React from 'react'
 
-import { MutationOptions, QueryClient, QueryClientProvider } from '../..'
+import {
+  MutationOptions,
+  QueryClient,
+  QueryClientConfig,
+  QueryClientProvider,
+} from '../..'
+
+export function createQueryClient(config?: QueryClientConfig): QueryClient {
+  return new QueryClient({ logger: mockLogger, ...config })
+}
 
 export function renderWithClient(client: QueryClient, ui: React.ReactElement) {
   const { rerender, ...result } = render(
@@ -24,10 +33,10 @@ export function mockNavigatorOnLine(value: boolean) {
   return jest.spyOn(navigator, 'onLine', 'get').mockReturnValue(value)
 }
 
-export function mockConsoleError() {
-  const consoleMock = jest.spyOn(console, 'error')
-  consoleMock.mockImplementation(() => undefined)
-  return consoleMock
+export const mockLogger = {
+  log: jest.fn(),
+  warn: jest.fn(),
+  error: jest.fn(),
 }
 
 let queryKeyCount = 0