query icon indicating copy to clipboard operation
query copied to clipboard

`prefetchInRender` causes infinite render loop with deferred value if a promise rejects

Open ali-idrizi opened this issue 1 year ago • 1 comments

Describe the bug

When the query of a deferred component rejects, it falls into an infinite render loop. This is consistent and I have written a test case.

I was waiting to see if this was being caused by https://github.com/TanStack/query/issues/8219, but it appears not. Here's the test:

it.only('should throw error if query fails with deferred value', async () => {
  function MyComponent(props: { promise: Promise<string> }) {
    const data = React.use(props.promise)

    return <>{data}</>
  }

  const key = queryKey()
  let renderCount = 0

  function Page() {
    renderCount++

    const [_count, setCount] = React.useState(0)
    const count = React.useDeferredValue(_count)

    const query = useQuery({
      queryKey: [key, count],
      queryFn: async () => {
        await sleep(1)
        // succeed only on first query
        if (count === 0) {
          return 'test' + count
        }
        throw new Error('Error test')
      },
      retry: false,
    })

    return (
      <React.Suspense fallback="loading..">
        <button onClick={() => setCount((curr) => curr + 1)}>inc</button>
        <MyComponent promise={query.promise} />
      </React.Suspense>
    )
  }

  const rendered = renderWithClient(
    queryClient,
    <ErrorBoundary fallbackRender={() => <div>error boundary</div>}>
      <Page />
    </ErrorBoundary>,
  )

  await waitFor(() => rendered.getByText('loading..'))
  await waitFor(() => rendered.getByText('test0'))

  const consoleMock = vi
    .spyOn(console, 'error')
    .mockImplementation(() => undefined)

  fireEvent.click(rendered.getByText('inc'))

  await waitFor(() => rendered.getByText('error boundary'))

  consoleMock.mockRestore()

  // unsure what the exact render count should be, but it should be less than 10
  // it's 6 without `useDeferredValue`, when test passes
  expect(renderCount).toBeLessThan(10)
})

It currently fails waiting for the error boundary because of the infinite render. If you catch the waitFor throw, you will notice very large values for renderCount:

AssertionError: expected 5828 to be less than 10

The same issue occurs when removing useDeferredValue, and updating the count within a startTransition instead.

Your minimal, reproducible example

Failing test provided

Steps to reproduce

N/A

Expected behavior

N/A

How often does this bug happen?

Every time

Screenshots or Videos

No response

Platform

OS: Windows 11 Browser: Chrome Version: 129.0

Tanstack Query adapter

react-query

TanStack Query version

06e315c

TypeScript version

No response

Additional context

No response

ali-idrizi avatar Nov 04 '24 14:11 ali-idrizi

I ran this test with:

  • https://github.com/TanStack/query/pull/8434

and it seems to be fixed in that branch, so it’ll implicitly fix this issue as well I guess. @KATT FYI

TkDodo avatar Dec 31 '24 11:12 TkDodo