Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

setQueryData not re-rendering the React Component #1535

Closed
DaniAkash opened this issue Dec 28, 2020 · 26 comments · Fixed by #1608
Closed

setQueryData not re-rendering the React Component #1535

DaniAkash opened this issue Dec 28, 2020 · 26 comments · Fixed by #1608

Comments

@DaniAkash
Copy link

Issue:

I'm running setQueryData to update query data while also updating the internal state of the component. This causes the component to not re-render after the change in query data

const updater = (nextData) => {
    /**
     * Appending new task to the top of the array
     * but react component is not re-rendering...
     */
    queryClient.setQueryData("todos", (oldData) => {
      return [...nextData, ...oldData];
    });
};

Inside React component

<button onClick={() => {
          updater([
            {
              id: 0,
              title: newTaskText
            }
          ]);
          setNewTaskText("new task");
        }}
      >
        Add
</button>

To Reproduce

Refer the codesandbox

  • Clicking on Add Task will add the new task to query data
  • But you need to click add task twice to see it re-render in the UI
@TkDodo
Copy link
Collaborator

TkDodo commented Dec 28, 2020

The issue seems to be that useState doesn’t trigger a re-render if the value in the state is the same as the current value in the state. It works just fine if you enter a different text and press the Button.

@DaniAkash
Copy link
Author

@TkDodo the queryClient.setQueryData should trigger the update regardless of useState right?

@TkDodo
Copy link
Collaborator

TkDodo commented Dec 29, 2020

@TkDodo the queryClient.setQueryData should trigger the update regardless of useState right?

yes, it should and it does. if you remove setNewTaskText from your example completely, it updates every time you click Add.

I'm not really sure why the interaction of local state + setQueryData causes this behaviour 🤔 . Here would be an even more minimal reproduction of the issue: https://codesandbox.io/s/queryclientsetquerydata-no-re-render-forked-0gx3k?file=/src/App.tsx

  • click the button once: in the devtools, you will see that data is now [], but on the screen, it's not.
  • if you remove the local setState, or set it to a different value, it works

So it seems to only occur if you have local state that is also set to exactly the value that we currently have in the state...

@DaniAkash
Copy link
Author

Thanks @TkDodo I'm aware that removing setNewTaskText or calling it with new data will fix the issue. But this seems to be an edge case if it gets called with the same value. I opened the issue for that scenario only

@boschni
Copy link
Collaborator

boschni commented Dec 30, 2020

After executing the onClick function the component is re-rendered and useQuery is returning the new data ([]). But it seems like React is discarding this render because the state did not change. After that a notification from the cache comes in, telling useQuery to re-render because of the new data, but because useQuery thinks it already rendered this data, it does not trigger an additional re-render. This was an optimization to prevent unnecessary renders, but if certain renders are discarded, then we cannot rely on it.

@DaniAkash
Copy link
Author

Thanks @boschni. I now have a clearer picture of the issue.

@utkarshgupta137
Copy link

utkarshgupta137 commented Jan 6, 2021

I hope this issue gets fixed soon as it took me quite a bit of time to debug why the components weren't re-rendering.
You can work around this by completely reconstructing the return object.
So this will work:

queryClient.setQueryData(queryKey, (oldData) => {
  return {
    ...oldData,
    pages: oldData.pages.map((page) => {
      return {
        ...page,
        playlistItemsList: page.playlistItemsList
          .filter((playlistItem) => {
            return !variables.includes(playlistItem.id);
          }),
      };
    }),
  };
});

But this doesn't:

queryClient.setQueryData(queryKey, (oldData) => {
  oldData.pages.forEach((page) => {
    page.playlistItemsList = page.playlistItemsList
      .filter((playlistItem) => {
        return !variables.includes(playlistItem.id);
      });
  });
  return oldData;
});

@TkDodo
Copy link
Collaborator

TkDodo commented Jan 6, 2021

@utkarshgupta137 your example has nothing to do with the issue i believe. Please don’t mutate existing cache values but return a new object / array instead. You can also use immer if you don’t like the spread syntax.

@mleister97
Copy link

mleister97 commented Dec 10, 2022

For anyone who is facing issues with setQueryData not re-rendering the React Component
What fixed the problem for me was the following:

Make sure you really update the data inside query client immutalbe!

My example:
Note: You do not really need to understand the whole example, just my comment is important for you

queryClient.setQueryData([QUERY_KEYS.CUSTOMERS], (old) => ({
   ...(old as Record<string, unknown>),
   data: removeDigitalCheckFromCustomers(...),
}))

const removeDigitalCheckFromCustomers = (...): Customer[] => {
    const customerIndex = customers.findIndex(c => c.id === id)
    if (customerIndex === -1) return customers
    const updatedCustomers = [...customers]
    // XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
    // THE NEXT LINE DOES NOT CREATE AN IMMUTABLE OBJECT !!!!!!!!!
    // XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
    updatedCustomers[customerIndex].digitalChecks = { ... }
    return updatedCustomers
}

Use instead something like this:

updatedCustomers[customerIndex] = {
   ...updatedCustomers[customerIndex],
   digitalChecks: { ... }
}

@TkDodo
Copy link
Collaborator

TkDodo commented Dec 10, 2022

@mleister97 please make a PR to the docs if you think we could highlight this more

@jackfiallos
Copy link

thanks guys, this series of posts helped me to fix an issue when deleting an item from a list of infinite items fetched using useInfiniteQuery.

Now I'm seeing a weird behaviour that not sure if its related or should be in a different issue and is, after updating the cache using queryClient.setQueryData when an item from the list is removed (re-render is triggered correctly), as soon as refetch is used, the deleted item is back .. is this possible, any clue what I should look for?

@TkDodo
Copy link
Collaborator

TkDodo commented Apr 30, 2023

as soon as refetch is used, the deleted item is back

this sounds like the item hasn't really been deleted in the backend if a refetch brings it right back ...

@jackfiallos
Copy link

I can confirm the backend is working as intented, actually noticed this is happening to other item updates as well, like changing values to properties from true to false or viceversa, that apparently its working, because the state is updated and the changes are reflected in the frontend, but same story, with the refetch, all changes prior to my update come back.

I haven't mentioned this is a RN project with no hydratation, but shouldn't change anything, right?

Seems I need to investigate this further using a new project that I can share.

@jackfiallos
Copy link

jackfiallos commented May 2, 2023

after running some local tests with web vs mobile (using the same base code), noticed the web version was working fine all the time whereas the mobile was having something odd.

for some reason, the api call from the mobile was always re-rendering deleted items (server and from query-cache), axios apparently was doing the requests but the server never recieved the request event, so in the end, decided to add a random value to the url and this did the trick, so now everytime the refetch is triggered, the requests is received by the server and data is refreshed according to last version.

so the question now is if there's any relation between these ends (axios, react-query) and this behaviour?

also, adding this random value at the end of the url could cause any other issue?

thanks @TkDodo

@BhavyaCodes
Copy link

BhavyaCodes commented Jun 27, 2023

For anyone who is facing issues with setQueryData not re-rendering the React Component I was just as desperate and in the same situation as you. What fixed the problem for me was the following:

Make sure you really update the data inside query client immutalbe!

My example: Note: You do not really need to understand the whole example, just my comment is important for you

queryClient.setQueryData([QUERY_KEYS.CUSTOMERS], (old) => ({
   ...(old as Record<string, unknown>),
   data: removeDigitalCheckFromCustomers(...),
}))

const removeDigitalCheckFromCustomers = (...): Customer[] => {
    const customerIndex = customers.findIndex(c => c.id === id)
    if (customerIndex === -1) return customers
    const updatedCustomers = [...customers]
    // THE NEXT LINE DOES NOT CREATE AN IMMUTABLE OBJECT !!!!!!!!!
    updatedCustomers[customerIndex].digitalChecks = { ... }
    return updatedCustomers
}

Use instead something like this:

updatedCustomers[customerIndex] = {
   ...updatedCustomers[customerIndex],
   digitalChecks: { ... }
}

Thanks mleister97 , I was doing the same mistake, wasted 3 hours till I found your reply 👍

@scottstern
Copy link

scottstern commented Sep 15, 2023

@TkDodo im having a similar issue very similar to #1535 (comment)

  1. We are fetching data from the backend and storing it in a react-query cache
  2. I am storing bulk select state in local state in a provider.

I make the call to setQueryData and then immediately make the call to update state and it breaks for the reasons outlined in this issue.

Whats also interesting if i split the functions like so.

const makeCallToReactQuery = async () => {
  queryClient.setQueryData(value);
 }
 
 // another module

await makeCallToReactQuery();
setLocalState()

It re-renders properly if i await the call to react query OR i wrap the setLocalState call in a setTimeout.

Which im not happy about both.

So im wondering if something is architected fundamentally wrong and we should fix that issue instead of forcing this to work.

This has nothing to do with JS object immutability

Thanks!

@TkDodo
Copy link
Collaborator

TkDodo commented Sep 15, 2023

@TkDodo im having a similar issue very similar to #1535 (comment)

The reproduction from that time is no longer "broken": It works fine in v4: https://codesandbox.io/s/queryclientsetquerydata-no-re-render-forked-0gx3k?file=/src/App.tsx

My guess is because we now useSyncExternalStore.

@scottstern
Copy link

@TkDodo im having a similar issue very similar to #1535 (comment)

The reproduction from that time is no longer "broken": It works fine in v4: https://codesandbox.io/s/queryclientsetquerydata-no-re-render-forked-0gx3k?file=/src/App.tsx

My guess is because we now useSyncExternalStore.

Thank you so much for the quick reply and response. Will bump if im still seeing issues.

@scottstern
Copy link

@TkDodo the upgrade didnt seem to fix my issue.

I have a virtualized table with items (fetched from react-query) and bulk update state (checkboxes when I store a list of ids in provider state)

When I select an item in the table and then delete that item

  1. The cache is updated
  2. Then I update the provider state to remove the item from the checked state

But it looks like the provider state update is winning every time, the component is being re-rendered and the item is undefined and breaks the ui.

The docs say setQueryData is synchronous which is why ive been so perplexed.

Like in my previous comment, awaiting on the function that calls setQueryData fixes the issue but feels "dirty".

I fixed it by just guarding in the component. Tried to build a sandbox and repro but couldnt in a test env.

Let me know if you have any thoughts or ideas.

Thanks

@codestacx
Copy link

codestacx commented Sep 19, 2023

I was facing the same issue in one of my production ready app where i was utilizing react query a lot. Whenever, I made update with setQueryData it update in react query cache but doesn't update the DOM.

It was a headache for me as I wasn't going to replace the whole logic with global state(redux/recoil/context) and not even calling API again.

What i did is created a state

const [key, setKey] = useState(+new Date());

and then use this as a key to the parent container from where I was passing down the data to children something like below

I update the key as soon i call setQueryData. Now, it make sense that the cache doesn't have connection with the UI but the state have. So that's the reason why setQueryData doesn't update the UI but since the key is binded with the state and as soon I update the key, it immediatly update that specific part of UI. Now you don't need to pass the key to top parent container but only the component itself that is using that particular data. Just updating key like

setKey(+new Date()) update the state and re-render UI

`

        <ChatUsers
          key={key}
          hasNextPage={Boolean(hasNextPage)}
          chats={chats}
          chatClick={handleClick}
          loadmore={fetchNextPage}
        />

`

key is actually updated when cache is updated and chats is what i am using from react query. This worked for me.

@scottstern
Copy link

@codestacx thats probably not ideal, youre forcing the key to change which is why the re-render is happening. You might have an immutability issue. Dont know if you wanna share the code in github.

@brannonvann
Copy link

@scottstern I believe I'm having the same issue outline above or at least similar symptoms. I haven't tried splitting setQueryData to another function that I await but if that were to work, I would also agree with your "dirty" comment.

In my case, I'm calling queryClient.setQueryData from inside an onMutate just like This Optimistic Update Example.

I'm returning a new Array by spreading the old one after modifying it but the new data is not triggering a state change in the component where I have the useQuery with the same query I'm setting in the setQueryData.

Even if I trigger a render of the component through other means it's failing to trigger a useEffect which contains the data from useQuery. It's as if the was simply modified and returned rather than what I'm actually doing which is returning a new array.

For the time being I'm using dataUpdatedAt from the same useQuery to trigger a useEffect but this is a hack that gives the appearance that it's working correctly until I can figure out another solution.

@TkDodo
Copy link
Collaborator

TkDodo commented Sep 20, 2023

I'm returning a new Array by spreading the old one after modifying it

don't modify it! It's not an immutable update if you modify the source. Just spreading the top level doesn't make it an immutable update ...

@brannonvann
Copy link

Thanks @TkDodo I was witting a new comment where I discovered that. I think that's where I was going wrong. I modified my code to prevent modifying the old (from the example) and it's working as expected, for my case.

@vigenere23
Copy link

Thanks a lot, comment #1535 (comment) really did it for me.

@bailout00
Copy link

I think there are two seperate issues here. I'm not manipulating the old data in any way, just completely replacing it entirely and I'm seeing the same behavior, whether I use put it in onSuccess or in the mutationFn.

`const { data: cartItems, dataUpdatedAt } = useGetCart()
const [cartCount, setCartCount] = useState(0)

useEffect(() => {
    setCartCount(cartItems?.items ? cartItems.items.length : 0)
}, [cartItems, dataUpdatedAt])`

cartItems alone will not trigger this, but with dataUpdatedAt, it works fine as mentioned above.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.