Repository: tannerlinsley/react-query (ts)

Issue:
Make undefined an illegal cache value (for successful queries)
Please see [this discussion](https://github.com/tannerlinsley/react-query/discussions/2823)

This is an ever-recurring issue that you can't bail out of updates with `setQueryData`. But because the updater function gives you a potentially undefined value (if there is no cache entry for the key yet), TypeScript is very unhappy because you're supposed to create a new cache entry, and there is no "bailing out of it" because react-query actually creates the cache entry _before_ the functional updater of setQueryData is called.

### Proposed solution

As stated [here](https://github.com/tannerlinsley/react-query/discussions/2823#discussioncomment-1562184):

- let's allow the setQueryData functional updater to return undefined
  - signature: T | ((prev: T | undefined) => T | undefined)
  - if `undefined` is returned, we do not create or update the cache entry
  - that means if we have a previous value and `undefined` is returned - `undefined` is _not_ written to the cache, and observers are not informed about that.
  - this is technically breaking if `undefined` is a legit cache entry value for someone

This would be in-ine with what the `initialData` function already does: if `undefined` is returned from that, it is _not_ persisted to the cache.

Also, if a fetch function returns `undefined`, we should:
- transform it to a failed promise (error)
- warn during development mode about it

Pull request:
Bail out if query data undefined
closes #2925 

@TkDodo here is the wildly simpler PR we have all been hoping for :-)

Patch:
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 4856dbbeb..6cb9773cf 100644
--- a/docs/src/pages/guides/migrating-to-react-query-4.md
+++ b/docs/src/pages/guides/migrating-to-react-query-4.md
@@ -245,6 +245,21 @@ Types now require using TypeScript v4.1 or greater
 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.
 
+### 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.
+
+Further, it is an easy bug to produce `Promise<void>` by adding logging in the queryFn:
+
+```js
+useQuery(
+  ['key'],
+  () => axios.get(url).then(result => console.log(result.data))
+)
+```
+
+This is now disallowed on type level; at runtime, `undefined` will be transformed to a _failed Promise_, which means you will get an `error`, which will also be logged to the console in development mode.
+
 ## New Features 🚀
 
 ### Proper offline support
@@ -265,3 +280,14 @@ Mutations can now also be garbage collected automatically, just like queries. Th
 ### Tracked Queries per default
 
 React Query defaults to "tracking" query properties, which should give you a nice boost in render optimization. The feature has existed since [v3.6.0](https://github.com/tannerlinsley/react-query/releases/tag/v3.6.0) and has now become the default behavior with v4.
+
+### Bailing out of updates with setQueryData
+
+When using the [functional updater form of setQueryData](../reference/QueryClient#queryclientsetquerydata), you can now bail out of the update by returning `undefined`. This is helpful if `undefined` is given to you as `previousValue`, which means that currently, no cached entry exists and you don't want to / cannot create one, like in the example of toggling a todo:
+
+```js
+ queryClient.setQueryData(
+   ['todo', id],
+   (previousTodo) => previousTodo ? { ...previousTodo, done: true } : undefined
+)
+ ```
diff --git a/docs/src/pages/reference/QueryClient.md b/docs/src/pages/reference/QueryClient.md
index 69e144832..2c4ab7046 100644
--- a/docs/src/pages/reference/QueryClient.md
+++ b/docs/src/pages/reference/QueryClient.md
@@ -214,7 +214,7 @@ queryClient.setQueryData(queryKey, updater)
 **Options**
 
 - `queryKey: QueryKey`: [Query Keys](../guides/query-keys)
-- `updater: TData | (oldData: TData | undefined) => TData`
+- `updater: TData | (oldData: TData | undefined) => TData | undefined`
   - If non-function is passed, the data will be updated to this value
   - If a function is passed, it will receive the old data value and be expected to return a new one.
 
@@ -224,6 +224,8 @@ queryClient.setQueryData(queryKey, updater)
 setQueryData(queryKey, newData)
 ```
 
+If the value is `undefined`, the query data is not updated.
+
 **Using an updater function**
 
 For convenience in syntax, you can also pass an updater function which receives the current data value and returns the new one:
@@ -232,6 +234,8 @@ For convenience in syntax, you can also pass an updater function which receives
 setQueryData(queryKey, oldData => newData)
 ```
 
+If the updater function returns `undefined`, the query data will not be updated. If the updater function receives `undefined` as input, you can return `undefined` to bail out of the update and thus _not_ create a new cache entry.
+
 ## `queryClient.getQueryState`
 
 `getQueryState` is a synchronous function that can be used to get an existing query's state. If the query does not exist, `undefined` will be returned.
diff --git a/docs/src/pages/reference/useQuery.md b/docs/src/pages/reference/useQuery.md
index 4f628bebb..e826bed04 100644
--- a/docs/src/pages/reference/useQuery.md
+++ b/docs/src/pages/reference/useQuery.md
@@ -68,7 +68,7 @@ const result = useQuery({
 
 **Options**
 
-- `queryKey:  unknown[]`
+- `queryKey: unknown[]`
   - **Required**
   - The query key to use for this query.
   - The query key will be hashed into a stable hash. See [Query Keys](../guides/query-keys) for more information.
@@ -77,7 +77,7 @@ const result = useQuery({
   - **Required, but only if no default query function has been defined** See [Default Query Function](../guides/default-query-function) for more information.
   - The function that the query will use to request data.
   - Receives a [QueryFunctionContext](../guides/query-functions#queryfunctioncontext)
-  - Must return a promise that will either resolve data or throw an error.
+  - Must return a promise that will either resolve data or throw an error. The data cannot be `undefined`.
 - `enabled: boolean`
   - Set this to `false` to disable this query from automatically running.
   - Can be used for [Dependent Queries](../guides/dependent-queries).
diff --git a/src/core/query.ts b/src/core/query.ts
index 06935088d..be087dbbe 100644
--- a/src/core/query.ts
+++ b/src/core/query.ts
@@ -1,7 +1,5 @@
 import {
   getAbortController,
-  Updater,
-  functionalUpdate,
   noop,
   replaceEqualDeep,
   timeUntilStale,
@@ -195,14 +193,11 @@ export class Query<
   }
 
   setData(
-    updater: Updater<TData | undefined, TData>,
+    data: TData,
     options?: SetDataOptions & { notifySuccess: boolean }
   ): TData {
     const prevData = this.state.data
 
-    // Get the new data
-    let data = functionalUpdate(updater, prevData)
-
     // Use prev data if an isDataEqual function is defined and returns `true`
     if (this.options.isDataEqual?.(prevData, data)) {
       data = prevData as TData
@@ -438,11 +433,41 @@ export class Query<
       this.dispatch({ type: 'fetch', meta: context.fetchOptions?.meta })
     }
 
+    const onError = (error: TError | { silent?: boolean }) => {
+      // Optimistically update state if needed
+      if (!(isCancelledError(error) && error.silent)) {
+        this.dispatch({
+          type: 'error',
+          error: error as TError,
+        })
+      }
+
+      if (!isCancelledError(error)) {
+        // Notify cache callback
+        this.cache.config.onError?.(error, this as Query<any, any, any, any>)
+
+        if (process.env.NODE_ENV !== 'production') {
+          getLogger().error(error)
+        }
+      }
+
+      if (!this.isFetchingOptimistic) {
+        // Schedule query gc after fetching
+        this.scheduleGc()
+      }
+      this.isFetchingOptimistic = false
+    }
+
     // Try to fetch the data
     this.retryer = createRetryer({
       fn: context.fetchFn as () => TData,
       abort: abortController?.abort?.bind(abortController),
       onSuccess: data => {
+        if (typeof data === 'undefined') {
+          onError(new Error('Query data cannot be undefined') as any)
+          return
+        }
+
         this.setData(data as TData)
 
         // Notify cache callback
@@ -454,30 +479,7 @@ export class Query<
         }
         this.isFetchingOptimistic = false
       },
-      onError: (error: TError | { silent?: boolean }) => {
-        // Optimistically update state if needed
-        if (!(isCancelledError(error) && error.silent)) {
-          this.dispatch({
-            type: 'error',
-            error: error as TError,
-          })
-        }
-
-        if (!isCancelledError(error)) {
-          // Notify cache callback
-          this.cache.config.onError?.(error, this as Query<any, any, any, any>)
-
-          if (process.env.NODE_ENV !== 'production') {
-            getLogger().error(error)
-          }
-        }
-
-        if (!this.isFetchingOptimistic) {
-          // Schedule query gc after fetching
-          this.scheduleGc()
-        }
-        this.isFetchingOptimistic = false
-      },
+      onError,
       onFail: () => {
         this.dispatch({ type: 'failed' })
       },
diff --git a/src/core/queryClient.ts b/src/core/queryClient.ts
index d09461ef8..589b72350 100644
--- a/src/core/queryClient.ts
+++ b/src/core/queryClient.ts
@@ -8,6 +8,7 @@ import {
   partialMatchKey,
   hashQueryKeyByOptions,
   MutationFilters,
+  functionalUpdate,
 } from './utils'
 import type {
   QueryClientConfig,
@@ -125,14 +126,22 @@ export class QueryClient {
 
   setQueryData<TData>(
     queryKey: QueryKey,
-    updater: Updater<TData | undefined, TData>,
+    updater: Updater<TData | undefined, TData> | undefined,
     options?: SetDataOptions
-  ): TData {
+  ): TData | undefined {
+    const query = this.queryCache.find<TData>(queryKey)
+    const prevData = query?.state.data
+    const data = functionalUpdate(updater, prevData)
+
+    if (typeof data === 'undefined') {
+      return undefined
+    }
+
     const parsedOptions = parseQueryArgs(queryKey)
     const defaultedOptions = this.defaultQueryOptions(parsedOptions)
     return this.queryCache
       .build(this, defaultedOptions)
-      .setData(updater, { ...options, notifySuccess: false })
+      .setData(data, { ...options, notifySuccess: false })
   }
 
   setQueriesData<TData>(
@@ -151,7 +160,7 @@ export class QueryClient {
     queryKeyOrFilters: QueryKey | QueryFilters,
     updater: Updater<TData | undefined, TData>,
     options?: SetDataOptions
-  ): [QueryKey, TData][] {
+  ): [QueryKey, TData | undefined][] {
     return notifyManager.batch(() =>
       this.getQueryCache()
         .findAll(queryKeyOrFilters)
diff --git a/src/core/types.ts b/src/core/types.ts
index f17703096..9df4e6a95 100644
--- a/src/core/types.ts
+++ b/src/core/types.ts
@@ -6,10 +6,14 @@ import type { QueryCache } from './queryCache'
 import type { MutationCache } from './mutationCache'
 
 export type QueryKey = readonly unknown[]
+export type QueryFunctionData<T> = T extends undefined ? never : T
+
 export type QueryFunction<
   T = unknown,
   TQueryKey extends QueryKey = QueryKey
-> = (context: QueryFunctionContext<TQueryKey>) => T | Promise<T>
+> = (
+  context: QueryFunctionContext<TQueryKey>
+) => QueryFunctionData<T> | Promise<QueryFunctionData<T>>
 
 export interface QueryFunctionContext<
   TQueryKey extends QueryKey = QueryKey,


Test patch:
diff --git a/src/core/tests/query.test.tsx b/src/core/tests/query.test.tsx
index cc120b933..b5aefa4b1 100644
--- a/src/core/tests/query.test.tsx
+++ b/src/core/tests/query.test.tsx
@@ -12,6 +12,7 @@ import {
   isError,
   onlineManager,
   QueryFunctionContext,
+  QueryObserverResult,
 } from '../..'
 import { waitFor } from '@testing-library/react'
 
@@ -787,6 +788,7 @@ describe('query', () => {
     let signalTest: any
     await queryClient.prefetchQuery(key, ({ signal }) => {
       signalTest = signal
+      return 'data'
     })
 
     expect(signalTest).toBeUndefined()
@@ -814,6 +816,31 @@ describe('query', () => {
     consoleMock.mockRestore()
   })
 
+  test('fetch should dispatch an error if the queryFn returns undefined', async () => {
+    const key = queryKey()
+
+    const observer = new QueryObserver(queryClient, {
+      queryKey: key,
+      queryFn: (() => undefined) as any,
+      retry: false,
+    })
+
+    let observerResult: QueryObserverResult<unknown, unknown> | undefined
+
+    const unsubscribe = observer.subscribe(result => {
+      observerResult = result
+    })
+
+    await sleep(10)
+
+    expect(observerResult).toMatchObject({
+      isError: true,
+      error: new Error('Query data cannot be undefined'),
+    })
+
+    unsubscribe()
+  })
+
   test('fetch should dispatch fetch if is fetching and current promise is undefined', async () => {
     const key = queryKey()
 
diff --git a/src/core/tests/queryClient.test.tsx b/src/core/tests/queryClient.test.tsx
index c32cf97cc..0d60ff420 100644
--- a/src/core/tests/queryClient.test.tsx
+++ b/src/core/tests/queryClient.test.tsx
@@ -53,7 +53,7 @@ describe('queryClient', () => {
         },
       })
 
-      const fetchData = () => Promise.resolve(undefined)
+      const fetchData = () => Promise.resolve('data')
       await testClient.prefetchQuery(key, fetchData)
       const newQuery = testClient.getQueryCache().find(key)
       expect(newQuery?.options.cacheTime).toBe(Infinity)
@@ -301,6 +301,34 @@ describe('queryClient', () => {
       expect(queryClient.getQueryData(key)).toBe('qux')
     })
 
+    test('should not create a new query if query was not found and data is undefined', () => {
+      const key = queryKey()
+      expect(queryClient.getQueryCache().find(key)).toBe(undefined)
+      queryClient.setQueryData(key, undefined)
+      expect(queryClient.getQueryCache().find(key)).toBe(undefined)
+    })
+
+    test('should not create a new query if query was not found and updater returns undefined', () => {
+      const key = queryKey()
+      expect(queryClient.getQueryCache().find(key)).toBe(undefined)
+      queryClient.setQueryData(key, () => undefined)
+      expect(queryClient.getQueryCache().find(key)).toBe(undefined)
+    })
+
+    test('should not update query data if data is undefined', () => {
+      const key = queryKey()
+      queryClient.setQueryData(key, 'qux')
+      queryClient.setQueryData(key, undefined)
+      expect(queryClient.getQueryData(key)).toBe('qux')
+    })
+
+    test('should not update query data if updater returns undefined', () => {
+      const key = queryKey()
+      queryClient.setQueryData(key, 'qux')
+      queryClient.setQueryData(key, () => undefined)
+      expect(queryClient.getQueryData(key)).toBe('qux')
+    })
+
     test('should accept an update function', () => {
       const key = queryKey()
 
@@ -871,8 +899,8 @@ describe('queryClient', () => {
     test('should refetch all queries when no arguments are given', async () => {
       const key1 = queryKey()
       const key2 = queryKey()
-      const queryFn1 = jest.fn()
-      const queryFn2 = jest.fn()
+      const queryFn1 = jest.fn().mockReturnValue('data1')
+      const queryFn2 = jest.fn().mockReturnValue('data2')
       await queryClient.fetchQuery(key1, queryFn1)
       await queryClient.fetchQuery(key2, queryFn2)
       const observer1 = new QueryObserver(queryClient, {
@@ -897,8 +925,8 @@ describe('queryClient', () => {
     test('should be able to refetch all fresh queries', async () => {
       const key1 = queryKey()
       const key2 = queryKey()
-      const queryFn1 = jest.fn()
-      const queryFn2 = jest.fn()
+      const queryFn1 = jest.fn().mockReturnValue('data1')
+      const queryFn2 = jest.fn().mockReturnValue('data2')
       await queryClient.fetchQuery(key1, queryFn1)
       await queryClient.fetchQuery(key2, queryFn2)
       const observer = new QueryObserver(queryClient, {
@@ -916,8 +944,8 @@ describe('queryClient', () => {
     test('should be able to refetch all stale queries', async () => {
       const key1 = queryKey()
       const key2 = queryKey()
-      const queryFn1 = jest.fn()
-      const queryFn2 = jest.fn()
+      const queryFn1 = jest.fn().mockReturnValue('data1')
+      const queryFn2 = jest.fn().mockReturnValue('data2')
       await queryClient.fetchQuery(key1, queryFn1)
       await queryClient.fetchQuery(key2, queryFn2)
       const observer = new QueryObserver(queryClient, {
@@ -936,8 +964,8 @@ describe('queryClient', () => {
     test('should be able to refetch all stale and active queries', async () => {
       const key1 = queryKey()
       const key2 = queryKey()
-      const queryFn1 = jest.fn()
-      const queryFn2 = jest.fn()
+      const queryFn1 = jest.fn().mockReturnValue('data1')
+      const queryFn2 = jest.fn().mockReturnValue('data2')
       await queryClient.fetchQuery(key1, queryFn1)
       await queryClient.fetchQuery(key2, queryFn2)
       queryClient.invalidateQueries(key1)
@@ -958,8 +986,8 @@ describe('queryClient', () => {
     test('should be able to refetch all active and inactive queries', async () => {
       const key1 = queryKey()
       const key2 = queryKey()
-      const queryFn1 = jest.fn()
-      const queryFn2 = jest.fn()
+      const queryFn1 = jest.fn().mockReturnValue('data1')
+      const queryFn2 = jest.fn().mockReturnValue('data2')
       await queryClient.fetchQuery(key1, queryFn1)
       await queryClient.fetchQuery(key2, queryFn2)
       const observer = new QueryObserver(queryClient, {
@@ -977,8 +1005,8 @@ describe('queryClient', () => {
     test('should be able to refetch all active and inactive queries', async () => {
       const key1 = queryKey()
       const key2 = queryKey()
-      const queryFn1 = jest.fn()
-      const queryFn2 = jest.fn()
+      const queryFn1 = jest.fn().mockReturnValue('data1')
+      const queryFn2 = jest.fn().mockReturnValue('data2')
       await queryClient.fetchQuery(key1, queryFn1)
       await queryClient.fetchQuery(key2, queryFn2)
       const observer = new QueryObserver(queryClient, {
@@ -996,8 +1024,8 @@ describe('queryClient', () => {
     test('should be able to refetch only active queries', async () => {
       const key1 = queryKey()
       const key2 = queryKey()
-      const queryFn1 = jest.fn()
-      const queryFn2 = jest.fn()
+      const queryFn1 = jest.fn().mockReturnValue('data1')
+      const queryFn2 = jest.fn().mockReturnValue('data2')
       await queryClient.fetchQuery(key1, queryFn1)
       await queryClient.fetchQuery(key2, queryFn2)
       const observer = new QueryObserver(queryClient, {
@@ -1015,8 +1043,8 @@ describe('queryClient', () => {
     test('should be able to refetch only inactive queries', async () => {
       const key1 = queryKey()
       const key2 = queryKey()
-      const queryFn1 = jest.fn()
-      const queryFn2 = jest.fn()
+      const queryFn1 = jest.fn().mockReturnValue('data1')
+      const queryFn2 = jest.fn().mockReturnValue('data2')
       await queryClient.fetchQuery(key1, queryFn1)
       await queryClient.fetchQuery(key2, queryFn2)
       const observer = new QueryObserver(queryClient, {
@@ -1060,8 +1088,8 @@ describe('queryClient', () => {
     test('should refetch active queries by default', async () => {
       const key1 = queryKey()
       const key2 = queryKey()
-      const queryFn1 = jest.fn()
-      const queryFn2 = jest.fn()
+      const queryFn1 = jest.fn().mockReturnValue('data1')
+      const queryFn2 = jest.fn().mockReturnValue('data2')
       await queryClient.fetchQuery(key1, queryFn1)
       await queryClient.fetchQuery(key2, queryFn2)
       const observer = new QueryObserver(queryClient, {
@@ -1079,8 +1107,8 @@ describe('queryClient', () => {
     test('should not refetch inactive queries by default', async () => {
       const key1 = queryKey()
       const key2 = queryKey()
-      const queryFn1 = jest.fn()
-      const queryFn2 = jest.fn()
+      const queryFn1 = jest.fn().mockReturnValue('data1')
+      const queryFn2 = jest.fn().mockReturnValue('data2')
       await queryClient.fetchQuery(key1, queryFn1)
       await queryClient.fetchQuery(key2, queryFn2)
       const observer = new QueryObserver(queryClient, {
@@ -1098,8 +1126,8 @@ describe('queryClient', () => {
     test('should not refetch active queries when "refetch" is "none"', async () => {
       const key1 = queryKey()
       const key2 = queryKey()
-      const queryFn1 = jest.fn()
-      const queryFn2 = jest.fn()
+      const queryFn1 = jest.fn().mockReturnValue('data1')
+      const queryFn2 = jest.fn().mockReturnValue('data2')
       await queryClient.fetchQuery(key1, queryFn1)
       await queryClient.fetchQuery(key2, queryFn2)
       const observer = new QueryObserver(queryClient, {
@@ -1119,8 +1147,8 @@ describe('queryClient', () => {
     test('should refetch inactive queries when "refetch" is "inactive"', async () => {
       const key1 = queryKey()
       const key2 = queryKey()
-      const queryFn1 = jest.fn()
-      const queryFn2 = jest.fn()
+      const queryFn1 = jest.fn().mockReturnValue('data1')
+      const queryFn2 = jest.fn().mockReturnValue('data2')
       await queryClient.fetchQuery(key1, queryFn1)
       await queryClient.fetchQuery(key2, queryFn2)
       const observer = new QueryObserver(queryClient, {
@@ -1142,8 +1170,8 @@ describe('queryClient', () => {
     test('should refetch active and inactive queries when "refetch" is "all"', async () => {
       const key1 = queryKey()
       const key2 = queryKey()
-      const queryFn1 = jest.fn()
-      const queryFn2 = jest.fn()
+      const queryFn1 = jest.fn().mockReturnValue('data1')
+      const queryFn2 = jest.fn().mockReturnValue('data2')
       await queryClient.fetchQuery(key1, queryFn1)
       await queryClient.fetchQuery(key2, queryFn2)
       const observer = new QueryObserver(queryClient, {
@@ -1269,8 +1297,8 @@ describe('queryClient', () => {
     test('should refetch all active queries', async () => {
       const key1 = queryKey()
       const key2 = queryKey()
-      const queryFn1 = jest.fn()
-      const queryFn2 = jest.fn()
+      const queryFn1 = jest.fn().mockReturnValue('data1')
+      const queryFn2 = jest.fn().mockReturnValue('data2')
       const observer1 = new QueryObserver(queryClient, {
         queryKey: key1,
         queryFn: queryFn1,
diff --git a/src/core/tests/queryObserver.test.tsx b/src/core/tests/queryObserver.test.tsx
index 9daf7d820..ed95023aa 100644
--- a/src/core/tests/queryObserver.test.tsx
+++ b/src/core/tests/queryObserver.test.tsx
@@ -331,7 +331,7 @@ describe('queryObserver', () => {
 
   test('should be able to watch a query without defining a query function', async () => {
     const key = queryKey()
-    const queryFn = jest.fn()
+    const queryFn = jest.fn().mockReturnValue('data')
     const callback = jest.fn()
     const observer = new QueryObserver(queryClient, {
       queryKey: key,
@@ -346,7 +346,7 @@ describe('queryObserver', () => {
 
   test('should accept unresolved query config in update function', async () => {
     const key = queryKey()
-    const queryFn = jest.fn()
+    const queryFn = jest.fn().mockReturnValue('data')
     const observer = new QueryObserver(queryClient, {
       queryKey: key,
       enabled: false,
diff --git a/src/reactjs/tests/ssr.test.tsx b/src/reactjs/tests/ssr.test.tsx
index 2363a160c..1a2c73833 100644
--- a/src/reactjs/tests/ssr.test.tsx
+++ b/src/reactjs/tests/ssr.test.tsx
@@ -60,7 +60,10 @@ describe('Server Side Rendering', () => {
     const queryCache = new QueryCache()
     const queryClient = new QueryClient({ queryCache })
     const key = queryKey()
-    const queryFn = jest.fn(() => sleep(10))
+    const queryFn = jest.fn(() => {
+      sleep(10)
+      return 'data'
+    })
 
     function Page() {
       const query = useQuery(key, queryFn)
diff --git a/src/reactjs/tests/suspense.test.tsx b/src/reactjs/tests/suspense.test.tsx
index a4b68d421..6bf53b56e 100644
--- a/src/reactjs/tests/suspense.test.tsx
+++ b/src/reactjs/tests/suspense.test.tsx
@@ -114,7 +114,10 @@ describe("useQuery's in Suspense mode", () => {
     const key = queryKey()
 
     const queryFn = jest.fn()
-    queryFn.mockImplementation(() => sleep(10))
+    queryFn.mockImplementation(() => {
+      sleep(10)
+      return 'data'
+    })
 
     function Page() {
       useQuery([key], queryFn, { suspense: true })
@@ -138,7 +141,14 @@ describe("useQuery's in Suspense mode", () => {
     const key = queryKey()
 
     function Page() {
-      useQuery(key, () => sleep(10), { suspense: true })
+      useQuery(
+        key,
+        () => {
+          sleep(10)
+          return 'data'
+        },
+        { suspense: true }
+      )
 
       return <>rendered</>
     }
@@ -212,10 +222,17 @@ describe("useQuery's in Suspense mode", () => {
     const successFn2 = jest.fn()
 
     function FirstComponent() {
-      useQuery(key, () => sleep(10), {
-        suspense: true,
-        onSuccess: successFn1,
-      })
+      useQuery(
+        key,
+        () => {
+          sleep(10)
+          return 'data'
+        },
+        {
+          suspense: true,
+          onSuccess: successFn1,
+        }
+      )
 
       return <span>first</span>
     }
diff --git a/src/reactjs/tests/useQueries.test.tsx b/src/reactjs/tests/useQueries.test.tsx
index 1ca4f48c3..4a0bb9d28 100644
--- a/src/reactjs/tests/useQueries.test.tsx
+++ b/src/reactjs/tests/useQueries.test.tsx
@@ -933,10 +933,12 @@ describe('useQueries', () => {
             return {
               queryKey: key,
               onError: err,
-              queryFn: (ctx: QueryFunctionContext<TQueryKey>) => {
-                expectType<TQueryKey>(ctx.queryKey)
-                return fn?.call({}, ctx)
-              },
+              queryFn: fn
+                ? (ctx: QueryFunctionContext<TQueryKey>) => {
+                    expectType<TQueryKey>(ctx.queryKey)
+                    return fn.call({}, ctx)
+                  }
+                : undefined,
             }
           }
         ),
diff --git a/src/reactjs/tests/useQuery.test.tsx b/src/reactjs/tests/useQuery.test.tsx
index 93e1ea3e5..6a68bd126 100644
--- a/src/reactjs/tests/useQuery.test.tsx
+++ b/src/reactjs/tests/useQuery.test.tsx
@@ -3447,7 +3447,7 @@ describe('useQuery', () => {
       const [enabled, setEnabled] = React.useState(false)
       const [isPrefetched, setPrefetched] = React.useState(false)
 
-      const query = useQuery(key, () => undefined, {
+      const query = useQuery(key, () => 'data', {
         enabled,
       })
 
@@ -3609,7 +3609,7 @@ describe('useQuery', () => {
     const key = queryKey()
 
     function Page() {
-      const query = useQuery(key, () => undefined, {
+      const query = useQuery(key, () => 'data', {
         enabled: false,
       })