Repository: tannerlinsley/react-query (ts)

Issue:
Query displays old data when memoized select function is used
### Describe the bug

I'm using memoized select functions on my queries (following [TkDodo's Blog recommendations](https://tkdodo.eu/blog/react-query-data-transformations#3-using-the-select-option) on this topic).

After upgrading to react-query 3.34.6 from 3.34.0 I notice that the query recomputes the result each time the select function changes but it returns an old result if the component containing the query re-renders due to, for example, user interaction.

### Your Example Website or App

https://codesandbox.io/s/blazing-dawn-ejcb0?file=/src/index.js

### Steps to reproduce

1. Wait 2 seconds until the page shows `Data: selected 2`
2. Click `inc` button. Page changes and shows `Data: selected 3`
3. Click `refresh` button. Page changes back to `Data: selected 2`

You can repeat steps 2 and 3 as many times you want

### Expected behavior

Page should show `Data: selected 3`

### How often does this bug happen?

Every time

### Screenshots or Videos


https://user-images.githubusercontent.com/1459326/147570074-a6f6a1bf-2472-42c7-9bec-bdbcef350150.mp4



### Platform

- OS: Linux
- Browser: Chrome
- Version: 96.0.4664.110


### Additional context

I believe the issue was introduced by https://github.com/tannerlinsley/react-query/pull/3098/files but I don't have enough knowledge to be sure or propose a fix

Pull request:
fix(queryObserver): memoized select functions should not return outdated values
not all runs to createResult are saved on `this.currentResult` (optimistically created updates do not - we just create the result again later). With the fix from #3098, we made sure that memoized select does not run again in those cases by storing every run. However, that now means that we cannot rely on `prevResult.data` because it might still contain outdated data.

The solution is to store a pair of previous selectFn + data and return the exact data that was computed from the previous select fn if we get the exact same function passed

closes #3139 

Patch:
diff --git a/src/core/queryObserver.ts b/src/core/queryObserver.ts
index 7dd759727..652fb2a25 100644
--- a/src/core/queryObserver.ts
+++ b/src/core/queryObserver.ts
@@ -69,7 +69,10 @@ export class QueryObserver<
   >
   private previousQueryResult?: QueryObserverResult<TData, TError>
   private previousSelectError: Error | null
-  private previousSelectFn?: (data: TQueryData) => TData
+  private previousSelect?: {
+    fn: (data: TQueryData) => TData
+    result: TData
+  }
   private staleTimeoutId?: number
   private refetchIntervalId?: number
   private currentRefetchInterval?: number | false
@@ -497,14 +500,17 @@ export class QueryObserver<
       if (
         prevResult &&
         state.data === prevResultState?.data &&
-        options.select === this.previousSelectFn &&
+        options.select === this.previousSelect?.fn &&
         !this.previousSelectError
       ) {
-        data = prevResult.data
+        data = this.previousSelect.result
       } else {
         try {
-          this.previousSelectFn = options.select
           data = options.select(state.data)
+          this.previousSelect = {
+            fn: options.select,
+            result: data,
+          }
           if (options.structuralSharing !== false) {
             data = replaceEqualDeep(prevResult?.data, data)
           }


Test patch:
diff --git a/src/react/tests/useQuery.test.tsx b/src/react/tests/useQuery.test.tsx
index 5170c51c5..f93ac90ec 100644
--- a/src/react/tests/useQuery.test.tsx
+++ b/src/react/tests/useQuery.test.tsx
@@ -3998,6 +3998,56 @@ describe('useQuery', () => {
     expect(selectRun).toBe(3)
   })
 
+  it('select should always return the correct state', async () => {
+    const key1 = queryKey()
+
+    function Page() {
+      const [count, inc] = React.useReducer(prev => prev + 1, 2)
+      const [forceValue, forceUpdate] = React.useReducer(prev => prev + 1, 1)
+
+      const state = useQuery(
+        key1,
+        async () => {
+          await sleep(10)
+          return 0
+        },
+        {
+          select: React.useCallback(
+            (data: number) => {
+              return `selected ${data + count}`
+            },
+            [count]
+          ),
+          placeholderData: 99,
+        }
+      )
+
+      return (
+        <div>
+          <h2>Data: {state.data}</h2>
+          <h2>forceValue: {forceValue}</h2>
+          <button onClick={inc}>inc: {count}</button>
+          <button onClick={forceUpdate}>forceUpdate</button>
+        </div>
+      )
+    }
+
+    const rendered = renderWithClient(queryClient, <Page />)
+    await waitFor(() => rendered.getByText('Data: selected 101')) // 99 + 2
+
+    await waitFor(() => rendered.getByText('Data: selected 2')) // 0 + 2
+
+    rendered.getByRole('button', { name: /inc/i }).click()
+
+    await waitFor(() => rendered.getByText('Data: selected 3')) // 0 + 3
+
+    rendered.getByRole('button', { name: /forceUpdate/i }).click()
+
+    await waitFor(() => rendered.getByText('forceValue: 2'))
+    // data should still be 3 after an independent re-render
+    await waitFor(() => rendered.getByText('Data: selected 3'))
+  })
+
   it('should cancel the query function when there are no more subscriptions', async () => {
     const key = queryKey()
     let cancelFn: jest.Mock = jest.fn()