query: useInfiniteQuery stale data when updated with setQueryData while another page is fetching

Describe the bug

I found a bug with useInfiniteQuery. If an update is performed to an old page (using setQueryData) while a newer page is being fetched, the update is lost.

This is a basic reproduction of the bug

Context (if anyone’s wondering why I need this! xD): I’m writing a chat application, and noticed that new messages which arrive (via websocket) while an old batch of messages is being fetched are lost as soon as the old batch finishes fetching.

Your minimal, reproducible example

https://codesandbox.io/s/useinfinitequery-bug-rux7pr?file=/src/App.js

Steps to reproduce

  1. Go to the MWE
  2. Click “fetch next page” a couple times (each “request” takes 3 seconds)
  3. Click “update first page” a few times, observe how the result for the first page is updated.
  4. Click “fetch next page”. Note that, after the request finishes, nothing changes in the first result (as expected).
  5. Now comes the weird part. Click “fetch next page”, and then click “update first page” as many times as you want. However: a. As soon as you click “update first page”, the “loading” indicator disappears (i.e., isFetching is no longer true). This is not the bug I’m interested in, but I thought it’s worth mentioning it. b. As soon as the “request” finishes, the data for the first page reverts back to what it was before clicking “fetch next page”. This is the important one 🐛🐛🐛

Expected behavior

As a user, I expected the updates to one page of the infinite query performed while another page was fetching to persist.

How often does this bug happen?

Every time

Screenshots or Videos

https://user-images.githubusercontent.com/4582440/166328686-cf98c9d3-c441-4569-be1f-79bafd84a614.mov

Platform

  • OS: macos
  • browser: chrome
  • version: 100

react-query version

3.38.1 (also happens with 4.0.0-beta.7)

TypeScript version

No response

Additional context

No response

About this issue

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

Most upvoted comments

I’d like to share my workaround, in case anyone else ever runs into this issue:

const queryKey = ['my-infinite-query'];
const isSomePageFetching = queryClient.getQueryState(queryKey)?.isFetching;
if (isSomePageFetching) {
  await new Promise<void>((resolve) => {
    const observer = new QueryObserver(queryClient, { queryKey });
    const unsubscribe = observer.subscribe(({ isFetching }) => {
      // this is going to run when there's any update to the query
      if (!isFetching) {
        unsubscribe();
        resolve();
      }
    });
  });
}

// we can now safely read the cache (queryClient.getQueryData(queryKey)) 
// and update it (queryClient.setQueryData(queryKey, updatedData))

I’m gonna close this issue since there’s no traction for quite some time, and there is a workaround to address this in userland.

Thanks for creating the separate issue 🙏

Regarding your scenario:

Let’s assume I have 3 pages, each has 5 items of strings:

- page 0: [A, B, C, D, E]
- page 1: [F, G, H, I, J]
- page 2: [K, L, M, N, O]

keep in mind that this is one cache entry. Now, a refetch starts due to some invalidation, and we trigger the refetches. Each item has a number appended now. After we fetch page0, we update page2:

- page 0: [A1, B1, C1, D1, E1]

page 0 is updated and page 1 is currently refetching, when we do:

queryClient.setQueryData(prev => ({ ...prev, pages: prev.pages.map((page, index) => index === 2 ? ['FOO'] : page))

this is the code to update the last page to the the Array ['FOO']. However, because we have only one cache entry, we have to “set” the whole cache entry. After that, our cache entry looks like:

- page 0: [A, B, C, D, E]
- page 1: [F, G, H, I, J]
- page 2: [FOO]

the background refetches are continuing in the meantime, so we’ll eventually get this from the backend:

- page 0: [A1, B1, C1, D1, E1]
- page 1: [F1, G1, H1, I1, J1]
- page 2: [K1, L1, M1, N1, O1]

and then, after all the background refetches are finished, that data is put into the one cache entry again, thus overriding your manual update.


Please correct me if that example is wrong, but where do you see the bug here, and how would you solve it differently? I don’t think that we can just merge the two concurrent updates together.

I think you do need to call queryClient.cancelQueries() if you do a manual update. That’s also what the docs on optimistic updates suggest.

fetching one page will always set the whole cached data for all pages, because there is only one cache entry. It’s a classic “concurrent modification” scenario - two actors write to the same cache location at the same time, one overwriting the other.

I think we could fix this by reading the latest state again after we’ve finished fetching a page, but I fear this will only work in limited scenarios. I would just not allow updating page 0 while page 4 is fetching.

Aaah yes that makes sense. I understand now. Let me see if we can fix that. If you want to help out , I’d usually start by making a failing test 😃