Repository: tannerlinsley/react-query (ts)

Issue:
NotifyManager breaks when a exception occurs in onSuccess callback
### Describe the bug

When a exception occurs in `onSuccess` callback
```ts
mutation.mutate(
  {},
  {
    onSuccess: () => {
      throw new Error();
    },
  },
);
```
whitch notify by `NotifyManager.batch`
```ts
batch<T>(callback: () => T): T {
  this.transactions++
  const result = callback() // exception raise
  this.transactions-- // not executed
  if (!this.transactions) { // never be 0
    this.flush()
  }
  return result
}
```
since `NotifyManager.batch` breaks, all newly created query's result would stuck in loading state even fetch succuessful

### Your minimal, reproducible example
https://codesandbox.io/s/gracious-danny-zfq8u?file=/src/App.js
1. click mutate
2. click query

### Steps to reproduce

```ts
mutation.mutate(
  {},
  {
    onSuccess: () => {
      throw new Error();
    },
  },
);
```

### Expected behavior

QueryObervable should notify onSuccess defensively

### How often does this bug happen?

_No response_

### Screenshots or Videos

_No response_

### Platform

irrelevant

### react-query version

3.34.7

### TypeScript version

_No response_

### Additional context

_No response_

Pull request:
fix(core): make sure batching finishes if exception is thrown
incrementing and decrementing transactions need to happen in an atomic operation, no matter what happens in between; if we increment, but never decrement, we will get to a state where transactions can never become zero - we're basically "leaking" an open transaction. This leads to no observers being informed ever again because we think we still have an open transaction

this can be fixed by using try/finally to always decrement the transactions

closes #3215 

Patch:
diff --git a/docs/src/pages/guides/ssr.md b/docs/src/pages/guides/ssr.md
index 623b60161..f5f1165f6 100644
--- a/docs/src/pages/guides/ssr.md
+++ b/docs/src/pages/guides/ssr.md
@@ -107,6 +107,12 @@ function Posts() {
 
 As demonstrated, it's fine to prefetch some queries and let others fetch on the queryClient. This means you can control what content server renders or not by adding or removing `prefetchQuery` for a specific query.
 
+### Caveat for Next.js rewrites
+
+There's a catch if you're using [Next.js' rewrites feature](https://nextjs.org/docs/api-reference/next.config.js/rewrites) together with [Automatic Static Optimization](https://nextjs.org/docs/advanced-features/automatic-static-optimization) or `getStaticProps`: It will cause a second hydration by React Query. That's because [Next.js needs to ensure that they parse the rewrites](https://nextjs.org/docs/api-reference/next.config.js/rewrites#rewrite-parameters) on the client and collect any params after hydration so that they can be provided in `router.query`.
+
+The result is missing referential equality for all the hydration data, which for example triggers whereever your data is used as props of components or in the dependency array of `useEffect`s/`useMemo`s.
+
 ## Using Other Frameworks or Custom SSR Frameworks
 
 This guide is at-best, a high level overview of how SSR with React Query should work. Your mileage may vary since there are many different possible setups for SSR.
diff --git a/src/core/notifyManager.ts b/src/core/notifyManager.ts
index b584f7bf8..75124f61b 100644
--- a/src/core/notifyManager.ts
+++ b/src/core/notifyManager.ts
@@ -30,11 +30,15 @@ export class NotifyManager {
   }
 
   batch<T>(callback: () => T): T {
+    let result
     this.transactions++
-    const result = callback()
-    this.transactions--
-    if (!this.transactions) {
-      this.flush()
+    try {
+      result = callback()
+    } finally {
+      this.transactions--
+      if (!this.transactions) {
+        this.flush()
+      }
     }
     return result
   }
diff --git a/src/core/queryClient.ts b/src/core/queryClient.ts
index cb9d6ba18..4d85b11dc 100644
--- a/src/core/queryClient.ts
+++ b/src/core/queryClient.ts
@@ -10,6 +10,7 @@ import {
   MutationFilters,
 } from './utils'
 import type {
+  QueryClientConfig,
   DefaultOptions,
   FetchInfiniteQueryOptions,
   FetchQueryOptions,
@@ -40,12 +41,6 @@ import { CancelOptions } from './types'
 
 // TYPES
 
-interface QueryClientConfig {
-  queryCache?: QueryCache
-  mutationCache?: MutationCache
-  defaultOptions?: DefaultOptions
-}
-
 interface QueryDefaults {
   queryKey: QueryKey
   defaultOptions: QueryOptions<any, any, any>
diff --git a/src/core/types.ts b/src/core/types.ts
index 583b34a15..d3de8c0bf 100644
--- a/src/core/types.ts
+++ b/src/core/types.ts
@@ -2,6 +2,8 @@ import type { MutationState } from './mutation'
 import type { QueryBehavior, Query } from './query'
 import type { RetryValue, RetryDelayValue } from './retryer'
 import type { QueryFilters } from './utils'
+import type { QueryCache } from './queryCache'
+import type { MutationCache } from './mutationCache'
 
 export type QueryKey = string | readonly unknown[]
 export type EnsuredQueryKey<T extends QueryKey> = T extends string
@@ -669,6 +671,12 @@ export type MutationObserverResult<
   | MutationObserverErrorResult<TData, TError, TVariables, TContext>
   | MutationObserverSuccessResult<TData, TError, TVariables, TContext>
 
+export interface QueryClientConfig {
+  queryCache?: QueryCache
+  mutationCache?: MutationCache
+  defaultOptions?: DefaultOptions
+}
+
 export interface DefaultOptions<TError = unknown> {
   queries?: QueryObserverOptions<unknown, TError>
   mutations?: MutationObserverOptions<unknown, TError, unknown, unknown>
diff --git a/src/react/useQueries.ts b/src/react/useQueries.ts
index cd2d5d0ad..482be7c0b 100644
--- a/src/react/useQueries.ts
+++ b/src/react/useQueries.ts
@@ -30,12 +30,12 @@ type GetOptions<T extends any> =
     ? UseQueryOptions<TQueryFnData>
     : // Part 3: responsible for inferring and enforcing type if no explicit parameter was provided
     T extends {
-        queryFn?: QueryFunction<infer TQueryFnData>
+        queryFn?: QueryFunction<infer TQueryFnData, infer TQueryKey>
         select: (data: any) => infer TData
       }
-    ? UseQueryOptions<TQueryFnData, unknown, TData>
-    : T extends { queryFn?: QueryFunction<infer TQueryFnData> }
-    ? UseQueryOptions<TQueryFnData>
+    ? UseQueryOptions<TQueryFnData, unknown, TData, TQueryKey>
+    : T extends { queryFn?: QueryFunction<infer TQueryFnData, infer TQueryKey> }
+    ? UseQueryOptions<TQueryFnData, unknown, TQueryFnData, TQueryKey>
     : // Fallback
       UseQueryOptions
 
@@ -56,11 +56,11 @@ type GetResults<T> =
     ? UseQueryResult<TQueryFnData>
     : // Part 3: responsible for mapping inferred type to results, if no explicit parameter was provided
     T extends {
-        queryFn?: QueryFunction<any>
+        queryFn?: QueryFunction<any, any>
         select: (data: any) => infer TData
       }
     ? UseQueryResult<TData>
-    : T extends { queryFn?: QueryFunction<infer TQueryFnData> }
+    : T extends { queryFn?: QueryFunction<infer TQueryFnData, any> }
     ? UseQueryResult<TQueryFnData>
     : // Fallback
       UseQueryResult


Test patch:
diff --git a/src/core/tests/notifyManager.test.tsx b/src/core/tests/notifyManager.test.tsx
index 53816aa76..4ee03fc65 100644
--- a/src/core/tests/notifyManager.test.tsx
+++ b/src/core/tests/notifyManager.test.tsx
@@ -29,4 +29,19 @@ describe('notifyManager', () => {
     expect(callbackBatchLevel2Spy).toHaveBeenCalledTimes(1)
     expect(callbackScheduleSpy).toHaveBeenCalledTimes(1)
   })
+
+  it('should flush if error is thrown', () => {
+    const notifyManagerTest = new NotifyManager()
+    const flushSpy = jest.fn()
+
+    notifyManagerTest.flush = flushSpy
+
+    try {
+      notifyManagerTest.batch(() => {
+        throw new Error('Foo')
+      })
+    } catch {}
+
+    expect(flushSpy).toHaveBeenCalledTimes(1)
+  })
 })
diff --git a/src/react/tests/useQueries.test.tsx b/src/react/tests/useQueries.test.tsx
index 305e7af5c..c36e4d592 100644
--- a/src/react/tests/useQueries.test.tsx
+++ b/src/react/tests/useQueries.test.tsx
@@ -18,6 +18,7 @@ import {
   QueryCache,
   QueryObserverResult,
   QueriesObserver,
+  QueryFunction,
 } from '../..'
 
 describe('useQueries', () => {
@@ -899,6 +900,62 @@ describe('useQueries', () => {
     }
   })
 
+  it('handles types for QueryFunction factory with strongly typed QueryKey', () => {
+    type QueryKeyA = ['queryA']
+    const getQueryKeyA = (): QueryKeyA => ['queryA']
+    type GetQueryFunctionA = () => QueryFunction<number, QueryKeyA>
+    const getQueryFunctionA: GetQueryFunctionA = () => async () => {
+      return 1
+    }
+    type SelectorA = (data: number) => [number, string]
+    const getSelectorA = (): SelectorA => data => [data, data.toString()]
+
+    type QueryKeyB = ['queryB', string]
+    const getQueryKeyB = (id: string): QueryKeyB => ['queryB', id]
+    type GetQueryFunctionB = () => QueryFunction<string, QueryKeyB>
+    const getQueryFunctionB: GetQueryFunctionB = () => async () => {
+      return '1'
+    }
+    type SelectorB = (data: string) => [string, number]
+    const getSelectorB = (): SelectorB => data => [data, +data]
+
+    // @ts-expect-error (Page component is not rendered)
+    // eslint-disable-next-line
+    function Page() {
+      const result = useQueries([
+        {
+          queryKey: getQueryKeyA(),
+          queryFn: getQueryFunctionA(),
+        },
+        {
+          queryKey: getQueryKeyB('id'),
+          queryFn: getQueryFunctionB(),
+        },
+      ])
+      expectType<QueryObserverResult<number, unknown>>(result[0])
+      expectType<QueryObserverResult<string, unknown>>(result[1])
+
+      const withSelector = useQueries([
+        {
+          queryKey: getQueryKeyA(),
+          queryFn: getQueryFunctionA(),
+          select: getSelectorA(),
+        },
+        {
+          queryKey: getQueryKeyB('id'),
+          queryFn: getQueryFunctionB(),
+          select: getSelectorB(),
+        },
+      ])
+      expectType<QueryObserverResult<[number, string], unknown>>(
+        withSelector[0]
+      )
+      expectType<QueryObserverResult<[string, number], unknown>>(
+        withSelector[1]
+      )
+    }
+  })
+
   it('should not change state if unmounted', async () => {
     const key1 = queryKey()