Repository: tannerlinsley/react-query (ts)

Issue:
useQueries doesn't work with 'keepPreviousData' properly
**Describe the bug**
```
  const [count, setCount] = useState(0);
  const result = useQueries(
    [1, 2].map((item) => {
      return {
        queryKey: [item, count],
        queryFn: queryFn,
        keepPreviousData: true
      };
    })
  );
```
The `result[x].data` should be "previousData" when `queryKey` change, but it is `undefined`.

**To Reproduce**
Steps to reproduce the behavior:
1. Go to https://codesandbox.io/s/usequeries-with-keeppreviousdata-c9cim?file=/src/index.js:559-781
2. Click '++count'
3. See `result[x].data`

**Expected behavior**
`result[x].data` should always be `{}` when query key changed

**Screenshots**
![image](https://user-images.githubusercontent.com/24486385/115869870-14abdf80-a471-11eb-9cd8-a5ef61a958cf.png)


**Desktop (please complete the following information):**
 - OS: Mac
 - Browser: chrome
 - Version: v90



Pull request:
fix(useQueries): make sure keepPreviousData is respected
fixes #2181

Patch:
diff --git a/src/core/queriesObserver.ts b/src/core/queriesObserver.ts
index e936ee7e8..99ddf7e77 100644
--- a/src/core/queriesObserver.ts
+++ b/src/core/queriesObserver.ts
@@ -64,20 +64,24 @@ export class QueriesObserver extends Subscribable<QueriesObserverListener> {
   }
 
   getOptimisticResult(queries: QueryObserverOptions[]): QueryObserverResult[] {
-    return queries.map(options => {
+    return queries.map((options, index) => {
       const defaultedOptions = this.client.defaultQueryObserverOptions(options)
-      return this.getObserver(defaultedOptions).getOptimisticResult(
+      return this.getObserver(defaultedOptions, index).getOptimisticResult(
         defaultedOptions
       )
     })
   }
 
-  private getObserver(options: QueryObserverOptions): QueryObserver {
+  private getObserver(
+    options: QueryObserverOptions,
+    index: number
+  ): QueryObserver {
     const defaultedOptions = this.client.defaultQueryObserverOptions(options)
-    return (
-      this.observersMap[defaultedOptions.queryHash!] ||
-      new QueryObserver(this.client, defaultedOptions)
-    )
+    let currentObserver = this.observersMap[defaultedOptions.queryHash!]
+    if (!currentObserver && defaultedOptions.keepPreviousData) {
+      currentObserver = this.observers[index]
+    }
+    return currentObserver ?? new QueryObserver(this.client, defaultedOptions)
   }
 
   private updateObservers(notifyOptions?: NotifyOptions): void {
@@ -96,9 +100,9 @@ export class QueriesObserver extends Subscribable<QueriesObserverListener> {
           options
         )
         const queryHash = defaultedOptions.queryHash!
-        const observer = this.getObserver(defaultedOptions)
+        const observer = this.getObserver(defaultedOptions, i)
 
-        if (prevObserversMap[queryHash]) {
+        if (prevObserversMap[queryHash] || defaultedOptions.keepPreviousData) {
           observer.setOptions(defaultedOptions, notifyOptions)
         }
 


Test patch:
diff --git a/src/react/tests/useQueries.test.tsx b/src/react/tests/useQueries.test.tsx
index c66470a85..0263eb473 100644
--- a/src/react/tests/useQueries.test.tsx
+++ b/src/react/tests/useQueries.test.tsx
@@ -1,6 +1,7 @@
+import { waitFor } from '@testing-library/react'
 import React from 'react'
 
-import { queryKey, renderWithClient, sleep } from './utils'
+import { queryKey, renderWithClient, setActTimeout, sleep } from './utils'
 import { useQueries, QueryClient, UseQueryResult, QueryCache } from '../..'
 
 describe('useQueries', () => {
@@ -42,4 +43,196 @@ describe('useQueries', () => {
     expect(results[1]).toMatchObject([{ data: 1 }, { data: undefined }])
     expect(results[2]).toMatchObject([{ data: 1 }, { data: 2 }])
   })
+
+  it('should keep previous data if amount of queries is the same', async () => {
+    const key1 = queryKey()
+    const key2 = queryKey()
+    const states: UseQueryResult[][] = []
+
+    function Page() {
+      const [count, setCount] = React.useState(1)
+      const result = useQueries([
+        {
+          queryKey: [key1, count],
+          keepPreviousData: true,
+          queryFn: async () => {
+            await sleep(5)
+            return count * 2
+          },
+        },
+        {
+          queryKey: [key2, count],
+          keepPreviousData: true,
+          queryFn: async () => {
+            await sleep(10)
+            return count * 5
+          },
+        },
+      ])
+      states.push(result)
+
+      React.useEffect(() => {
+        setActTimeout(() => {
+          setCount(prev => prev + 1)
+        }, 20)
+      }, [])
+
+      return null
+    }
+
+    renderWithClient(queryClient, <Page />)
+
+    await waitFor(() => expect(states.length).toBe(7))
+
+    expect(states[0]).toMatchObject([
+      {
+        status: 'loading',
+        data: undefined,
+        isPreviousData: false,
+        isFetching: true,
+      },
+      {
+        status: 'loading',
+        data: undefined,
+        isPreviousData: false,
+        isFetching: true,
+      },
+    ])
+    expect(states[1]).toMatchObject([
+      { status: 'success', data: 2, isPreviousData: false, isFetching: false },
+      {
+        status: 'loading',
+        data: undefined,
+        isPreviousData: false,
+        isFetching: true,
+      },
+    ])
+    expect(states[2]).toMatchObject([
+      { status: 'success', data: 2, isPreviousData: false, isFetching: false },
+      { status: 'success', data: 5, isPreviousData: false, isFetching: false },
+    ])
+    expect(states[3]).toMatchObject([
+      { status: 'success', data: 2, isPreviousData: true, isFetching: true },
+      { status: 'success', data: 5, isPreviousData: true, isFetching: true },
+    ])
+    expect(states[4]).toMatchObject([
+      { status: 'success', data: 2, isPreviousData: true, isFetching: true },
+      { status: 'success', data: 5, isPreviousData: true, isFetching: true },
+    ])
+    expect(states[5]).toMatchObject([
+      { status: 'success', data: 4, isPreviousData: false, isFetching: false },
+      { status: 'success', data: 5, isPreviousData: true, isFetching: true },
+    ])
+    expect(states[6]).toMatchObject([
+      { status: 'success', data: 4, isPreviousData: false, isFetching: false },
+      { status: 'success', data: 10, isPreviousData: false, isFetching: false },
+    ])
+  })
+
+  it('should keep previous data for variable amounts of useQueries', async () => {
+    const key = queryKey()
+    const states: UseQueryResult[][] = []
+
+    function Page() {
+      const [count, setCount] = React.useState(2)
+      const result = useQueries(
+        Array.from({ length: count }, (_, i) => ({
+          queryKey: [key, count, i + 1],
+          keepPreviousData: true,
+          queryFn: async () => {
+            await sleep(5 * (i + 1))
+            return (i + 1) * count * 2
+          },
+        }))
+      )
+
+      states.push(result)
+
+      React.useEffect(() => {
+        setActTimeout(() => {
+          setCount(prev => prev + 1)
+        }, 20)
+      }, [])
+
+      return null
+    }
+
+    renderWithClient(queryClient, <Page />)
+
+    await waitFor(() => expect(states.length).toBe(8))
+
+    expect(states[0]).toMatchObject([
+      {
+        status: 'loading',
+        data: undefined,
+        isPreviousData: false,
+        isFetching: true,
+      },
+      {
+        status: 'loading',
+        data: undefined,
+        isPreviousData: false,
+        isFetching: true,
+      },
+    ])
+    expect(states[1]).toMatchObject([
+      { status: 'success', data: 4, isPreviousData: false, isFetching: false },
+      {
+        status: 'loading',
+        data: undefined,
+        isPreviousData: false,
+        isFetching: true,
+      },
+    ])
+    expect(states[2]).toMatchObject([
+      { status: 'success', data: 4, isPreviousData: false, isFetching: false },
+      { status: 'success', data: 8, isPreviousData: false, isFetching: false },
+    ])
+
+    expect(states[3]).toMatchObject([
+      { status: 'success', data: 4, isPreviousData: true, isFetching: true },
+      { status: 'success', data: 8, isPreviousData: true, isFetching: true },
+      {
+        status: 'loading',
+        data: undefined,
+        isPreviousData: false,
+        isFetching: true,
+      },
+    ])
+    expect(states[4]).toMatchObject([
+      { status: 'success', data: 4, isPreviousData: true, isFetching: true },
+      { status: 'success', data: 8, isPreviousData: true, isFetching: true },
+      {
+        status: 'loading',
+        data: undefined,
+        isPreviousData: false,
+        isFetching: true,
+      },
+    ])
+    expect(states[5]).toMatchObject([
+      { status: 'success', data: 6, isPreviousData: false, isFetching: false },
+      { status: 'success', data: 8, isPreviousData: true, isFetching: true },
+      {
+        status: 'loading',
+        data: undefined,
+        isPreviousData: false,
+        isFetching: true,
+      },
+    ])
+    expect(states[6]).toMatchObject([
+      { status: 'success', data: 6, isPreviousData: false, isFetching: false },
+      { status: 'success', data: 12, isPreviousData: false, isFetching: false },
+      {
+        status: 'loading',
+        data: undefined,
+        isPreviousData: false,
+        isFetching: true,
+      },
+    ])
+    expect(states[7]).toMatchObject([
+      { status: 'success', data: 6, isPreviousData: false, isFetching: false },
+      { status: 'success', data: 12, isPreviousData: false, isFetching: false },
+      { status: 'success', data: 18, isPreviousData: false, isFetching: false },
+    ])
+  })
 })