query: `isFetching` returns wrong value when component is mounted with previously fetched key

Describe the bug

We are using useQuery (with refetchOnMount: false) and the same query key is used for both parent and child components. The parent has logic where only when isFetching: true the child is rendered. First render (with key “a”) and first key change (to key “b”) are working as expected, however when we change the key back to previous value (to “a”): for a brief moment the parent gets isFetching: true while child get isFetching: false

Your minimal, reproducible example

https://codesandbox.io/s/is-fetching-bug-hqv1v?file=/src/App.js

Steps to reproduce

On first render the console log looks this way:

parent isFetching=true 
child isFetching=true 
Starting fetch 
parent isFetching=false 

First time clicking “Change key” button we get:

parent isFetching=true 
child isFetching=true 
Starting fetch 
parent isFetching=false 

Second time clicking “Change key” shows unexpected behavior:

parent isFetching=true 
child isFetching=false 
Starting fetch 
parent isFetching=true 
child isFetching=true 
parent isFetching=false 

Expected behavior

When clicking the “Change key” for the second time, we expect the same result as the first time it was clicked:

parent isFetching=true 
child isFetching=true 
Starting fetch 
parent isFetching=false 

However for the parent and child get different values for isFetching

How often does this bug happen?

Every time

Screenshots or Videos

No response

Platform

  • Chrome: Version 95

react-query version

v3.34.12

TypeScript version

No response

Additional context

No response

About this issue

  • Original URL
  • State: closed
  • Created 2 years ago
  • Reactions: 1
  • Comments: 21

Most upvoted comments

so I don’t really see any way forward with this. It looks like its an edge case that we kinda have to accept for now due to how we set the value of isFetching optimistically.

One workaround I found to make this work consistently is to determine isFetching with useIsFetching instead:

const isFetching = useIsFetching(key) > 0;

here is a sandbox reproduction of your issue, yielding consistent results: https://codesandbox.io/s/is-fetching-bug-forked-ocunx0?file=/src/App.js

note that there are some more re-renders, but this gets better on react 18, which has automatic batching. This sandbox, using react 18 and our v4 beta shows the following output, consistently:

parent isFetching=false 
Starting fetch 
parent isFetching=true 
child isFetching=true 
parent isFetching=false 

https://codesandbox.io/s/is-fetching-bug-forked-unm5cy?file=/src/App.js

As you can see, there is an additional parent isFetching=false output at the beginning, which is to be expected, because useIsFetching does not set the fetching state to true optimistically - which is what would cause the mentioned problem.

I hope this is an acceptable workaround. Closing for now, but feel free to keep the discussion going if you have some new input.

I can reproduce it, even on later versions and with react18, where batching has improved. So it’s not that. Also you can turn refetchOnMount back on and the issue persists.

It’s seems to be a weird case that has to do with the conditional mounting + having cached data available already. I don’t have time to look into this right now, so any help is appreciated

That makes sense. I’ll investigate that.

I was able to reproduce it with such test case:

  it('should set isFetching to true when refetchOnMount is false and data has been fetched already and component was mounted', async () => {
    const key = queryKey()
    const states: UseQueryResult<string>[] = []
    const subPageStates: UseQueryResult<string>[] = []

    queryClient.setQueryData(key, 'prefetched')

    function SubPage() {
      const state = useQuery(key, () => 'test', {
        refetchOnMount: false,
      })
      subPageStates.push(state)
      return null
    }

    function Page() {
      const state = useQuery(key, () => 'test')
      states.push(state)
      return state.isFetching ? <SubPage /> : null
    }

    renderWithClient(queryClient, <Page />)

    await sleep(10)

    expect(states[0]?.isFetching).toBe(true)
    expect(subPageStates[0]?.isFetching).toBe(true)
  })

Now the hard part, to find a bug.