query: too many re-renders when changing keys

Describe the bug

When changing the query key, react-query re-renders the component two times with the exact same states

Your minimal, reproducible example

v4: https://codesandbox.io/s/react-query-re-renders-t2z4u8, v3: https://codesandbox.io/s/react-query-re-renders-v3-cfo8y4

Steps to reproduce

  • Open the sandbox and inspect the console
  • on page load, you’ll see two renders (loading and success): 🆗
  • click the next button
  • you’ll see 3 additional renders
    • loading: 🆗
    • loading: 🚫
    • success: 🆗

The second “loading” render has the exact same state as the previous one and is unnecessary.

This happens on v17 and v18 of react, and in v3 and v4 of react-query. It also has nothing to do with extra renders in dev-mode and strict effects (the example doesn’t use strict mode), and it can also be reproduced when the logging is moved to an effect. The component really renders twice.

Expected behavior

we should see two renders instead of three:

  • loading: 🆗
  • success: 🆗

How often does this bug happen?

Every time

Screenshots or Videos

Screenshot 2022-07-02 at 08 06 22

Platform

independent (I’m on macos & brave)

react-query version

v3 and v4

TypeScript version

No response

Additional context

first reported on stackoverflow: https://stackoverflow.com/questions/72834988/react-query-makes-component-to-rerender-multiple-times

my best guess is that setState triggers one render, and then useQuery re-renders again. But those should be deduped…

here’s a failing test-case to reproduce:

  it('should not render too often when query key changes', async () => {
    const key = queryKey()
    const states: UseQueryResult<{ count: number }>[] = []

    function Page() {
      const [count, setCount] = React.useState(0)
      const state = useQuery([key, count], async () => {
        await sleep(10)
        return { count }
      })
      states.push(state)

      return (
        <div>
          <h1>data: {state.data?.count}</h1>
          <button
            onClick={() => {
              setCount(prev => prev + 1);
            }}
          >
            Next
          </button>
        </div>
      );
    }

    const rendered = renderWithClient(queryClient, <Page />)

    await waitFor(() => rendered.getByText('data: 0'))

    // Initial
    expect(states[0]).toMatchObject({ status: 'loading', data: undefined })
    // loaded
    expect(states[1]).toMatchObject({ status: 'success', data: { count: 0 } })

    expect(states.length).toBe(2)

    fireEvent.click(rendered.getByRole('button', { name: /next/i }))

    await waitFor(() => rendered.getByText('data: 1'))

    // fetching with new key
    expect(states[2]).toMatchObject({ status: 'loading', data: undefined })
    // loaded
    expect(states[3]).toMatchObject({ status: 'success', data: { count: 0 } })

    expect(states.length).toBe(4)
  })

About this issue

  • Original URL
  • State: closed
  • Created 2 years ago
  • Reactions: 12
  • Comments: 23 (1 by maintainers)

Most upvoted comments

I think I have solved the problem. It was with the dependency array in the useEffect hook. Try giving an empty dependency array.

solution by having an empty dependency array

use effect with empty dependency array

problem by not having an empty dependency array

useEffect with no dependency array

it should be fixed when the bugs in react are fixed 🤷

There is 3 re-renders instead of 2

remove React.StrictMode from the example and the render count goes down to 2. This is not related.

Is this issue fixed? I’m facing this issue currently. The loading and success state causing re-render twice

image

amazing @incepter 🙌

I think the same issue is addressed in https://github.com/TanStack/query/issues/5538; and this PR may solve it.

Here is the same sandbox with the built version from the PR that shows the resolve actually: https://codesandbox.io/s/react-query-re-renders-forked-66s9td?file=/src/App.tsx

@karol-janik I’m not sure we can prevent/memo the change.

Might be easier to not fire the “fetch” event from react-query observer when the key changes since this is an extra event that causes the extra “loading” state to appear.

Note: this used to work in 3.5.11, but no longer in 3.5.12

see: