Repository: tannerlinsley/react-query
Pull request #2565 (2021-08-12 07:33:30)

fix(core): change `prevQuery` to `query` in `shouldFetchOptionally` function
> related pull request: #2556 

### Description

Change `prevQuery` to `query` in `shouldFetchOptionally` because of checking query key changed
situation.

- This PR(#2556) fixes a bug which an error was not caught when the query was reset.
- When query key changed, infinity re-request is reproduced.
  - prevQuery.state.status === 'success'
  - query.state.status === 'error'

#### Checking

- [x] Is refetch when query key changed without suspense? -> **YES**
- [x] Catch error in `ErrorBoundary` when query key changed and thrown error with error boundary? -> **YES**

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/tannerlinsley/react-query/9qCuFj8uGL2DJmyBinUmUP9umejV](https://vercel.com/tannerlinsley/react-query/9qCuFj8uGL2DJmyBinUmUP9umejV)  
✅ 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/2565/builds/159006) or the icon next to each commit SHA.

Latest deployment of this branch, based on commit ab3724af324b9ef819f47fa2b9b53b379a44a667:

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


Patch:
diff --git a/src/core/queryObserver.ts b/src/core/queryObserver.ts
index 7e0c9924b..cfff92f7a 100644
--- a/src/core/queryObserver.ts
+++ b/src/core/queryObserver.ts
@@ -763,7 +763,7 @@ function shouldFetchOptionally(
   return (
     options.enabled !== false &&
     (query !== prevQuery || prevOptions.enabled === false) &&
-    (prevQuery.state.status !== 'error' || prevOptions.enabled === false) &&
+    (query.state.status !== 'error' || prevOptions.enabled === false) &&
     isStale(query, options)
   )
 }


Test patch:
diff --git a/src/react/tests/suspense.test.tsx b/src/react/tests/suspense.test.tsx
index 80173e18c..7946cf3d6 100644
--- a/src/react/tests/suspense.test.tsx
+++ b/src/react/tests/suspense.test.tsx
@@ -647,4 +647,68 @@ describe("useQuery's in Suspense mode", () => {
     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() {
+      const [key, rerender] = React.useReducer(x => x + 1, 0)
+      const queryKeys = [key, succeed]
+
+      const result = useQuery(
+        queryKeys,
+        async () => {
+          await sleep(10)
+          if (!succeed) {
+            throw new Error('Suspense Error Bingo')
+          } else {
+            return 'data'
+          }
+        },
+        {
+          retry: false,
+          suspense: true,
+        }
+      )
+      return (
+        <div>
+          <span>rendered</span> <span>{result.data}</span>
+          <button aria-label="fail" onClick={rerender}>
+            fail
+          </button>
+        </div>
+      )
+    }
+
+    function App() {
+      const { reset } = useQueryErrorResetBoundary()
+      return (
+        <ErrorBoundary
+          onReset={reset}
+          fallbackRender={() => <div>error boundary</div>}
+        >
+          <React.Suspense fallback="Loading...">
+            <Page />
+          </React.Suspense>
+        </ErrorBoundary>
+      )
+    }
+
+    const rendered = renderWithClient(queryClient, <App />)
+
+    // render suspense fallback (Loading...)
+    await waitFor(() => rendered.getByText('Loading...'))
+    // resolve promise -> render Page (rendered)
+    await waitFor(() => rendered.getByText('rendered'))
+
+    // change promise result to error
+    succeed = false
+    // change query key
+    fireEvent.click(rendered.getByLabelText('fail'))
+    // render error boundary fallback (error boundary)
+    await waitFor(() => rendered.getByText('error boundary'))
+    consoleMock.mockRestore()
+  })
 })
diff --git a/src/react/tests/useQuery.test.tsx b/src/react/tests/useQuery.test.tsx
index 5357ecc76..b10ffd896 100644
--- a/src/react/tests/useQuery.test.tsx
+++ b/src/react/tests/useQuery.test.tsx
@@ -3944,4 +3944,65 @@ describe('useQuery', () => {
 
     consoleMock.mockRestore()
   })
+
+  it('should refetch when query key changed when previous status is error', async () => {
+    const consoleMock = mockConsoleError()
+    const queryFn = jest.fn()
+
+    function Page({ id }: { id: number }) {
+      queryFn.mockImplementation(async () => {
+        await sleep(10)
+        if (id % 2 === 1) {
+          return Promise.reject(new Error('Suspense Error Bingo'))
+        } else {
+          return 'data'
+        }
+      })
+      const { error, isLoading } = useQuery([id], queryFn, {
+        retry: false,
+        retryOnMount: false,
+        refetchOnMount: false,
+        refetchOnWindowFocus: false,
+      })
+
+      if (isLoading) {
+        return <div>status: loading</div>
+      }
+      if (error instanceof Error) {
+        return <div>error</div>
+      }
+      return <div>rendered</div>
+    }
+
+    function App() {
+      const [id, changeId] = React.useReducer(x => x + 1, 1)
+
+      return (
+        <div>
+          <Page id={id} />
+          <button aria-label="change" onClick={changeId}>
+            change {id}
+          </button>
+        </div>
+      )
+    }
+
+    const rendered = renderWithClient(queryClient, <App />)
+
+    // initial state check
+    rendered.getByText('status: loading')
+
+    // render error state component
+    await waitFor(() => rendered.getByText('error'))
+
+    // change to enabled to false
+    fireEvent.click(rendered.getByLabelText('change'))
+    await waitFor(() => rendered.getByText('rendered'))
+
+    // // change to enabled to true
+    fireEvent.click(rendered.getByLabelText('change'))
+    await waitFor(() => rendered.getByText('error'))
+
+    consoleMock.mockRestore()
+  })
 })