Repository: tannerlinsley/react-query
Pull request #2556 (2021-08-10 10:23:00)

fix(core): change `shouldFetchOptionally` condition
Related issue: https://github.com/tannerlinsley/react-query/issues/2187

### Description

Change `shouldFetchOptionally` condition to not change the `status` of result to `loading` when `state.status` is `error`.

1. `result.isLoading` is `true` in [this line](https://github.com/tannerlinsley/react-query/blob/master/src/react/useBaseQuery.ts#L110) when `result.error != null`
2. So the error did not propagate to the error boundary. (next condition)
3. Check the `createResult` function and `shouldFetchOptionally` function

Fixes tannerlinsley/react-query#2187

> Special thanks to @flybayer 

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

Latest deployment of this branch, based on commit b5686c2c55287d0f86ba772461a323f79098cbeb:

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


Patch:
diff --git a/src/core/queryObserver.ts b/src/core/queryObserver.ts
index fd5bcafbc..7e0c9924b 100644
--- a/src/core/queryObserver.ts
+++ b/src/core/queryObserver.ts
@@ -763,6 +763,7 @@ function shouldFetchOptionally(
   return (
     options.enabled !== false &&
     (query !== prevQuery || prevOptions.enabled === false) &&
+    (prevQuery.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 13077a4c3..80173e18c 100644
--- a/src/react/tests/suspense.test.tsx
+++ b/src/react/tests/suspense.test.tsx
@@ -577,4 +577,74 @@ describe("useQuery's in Suspense mode", () => {
     expect(queryFn).toHaveBeenCalledTimes(1)
     await waitFor(() => rendered.getByLabelText('fire'))
   })
+
+  it('should error catched in error boundary without infinite loop', async () => {
+    const key = queryKey()
+
+    const consoleMock = mockConsoleError()
+
+    let succeed = true
+
+    function Page() {
+      const [nonce] = React.useState(0)
+      const queryKeys = `${key}-${succeed}`
+      const result = useQuery(
+        queryKeys,
+        async () => {
+          await sleep(10)
+          if (!succeed) {
+            throw new Error('Suspense Error Bingo')
+          } else {
+            return nonce
+          }
+        },
+        {
+          retry: false,
+          suspense: true,
+        }
+      )
+      return (
+        <div>
+          <span>rendered</span> <span>{result.data}</span>
+          <button
+            aria-label="fail"
+            onClick={async () => {
+              await queryClient.resetQueries()
+            }}
+          >
+            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 query key
+    succeed = false
+    // reset query -> and throw error
+    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 12b6cd69a..5357ecc76 100644
--- a/src/react/tests/useQuery.test.tsx
+++ b/src/react/tests/useQuery.test.tsx
@@ -3883,4 +3883,65 @@ describe('useQuery', () => {
     expect(renders).toBe(2)
     expect(hashes).toBe(2)
   })
+
+  it('should refetch when changed enabled to true in error state', async () => {
+    const consoleMock = mockConsoleError()
+
+    const queryFn = jest.fn()
+    queryFn.mockImplementation(async () => {
+      await sleep(10)
+      return Promise.reject(new Error('Suspense Error Bingo'))
+    })
+
+    function Page({ enabled }: { enabled: boolean }) {
+      const { error, isLoading } = useQuery(['key'], queryFn, {
+        enabled,
+        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 [enabled, toggle] = React.useReducer(x => !x, true)
+
+      return (
+        <div>
+          <Page enabled={enabled} />
+          <button aria-label="retry" onClick={toggle}>
+            retry {enabled}
+          </button>
+        </div>
+      )
+    }
+
+    const rendered = renderWithClient(queryClient, <App />)
+
+    // initial state check
+    rendered.getByText('status: loading')
+
+    // // render error state component
+    await waitFor(() => rendered.getByText('error'))
+    expect(queryFn).toBeCalledTimes(1)
+
+    // change to enabled to false
+    fireEvent.click(rendered.getByLabelText('retry'))
+    await waitFor(() => rendered.getByText('error'))
+    expect(queryFn).toBeCalledTimes(1)
+
+    // // change to enabled to true
+    fireEvent.click(rendered.getByLabelText('retry'))
+    expect(queryFn).toBeCalledTimes(2)
+
+    consoleMock.mockRestore()
+  })
 })