swr icon indicating copy to clipboard operation
swr copied to clipboard

Can not mutate data from SWRInfinite with `revalidateFirstPage: false` when key is not mounted

Open tomaszczura opened this issue 2 years ago • 20 comments

Bug report

I can not manually refresh data using mutate when using SWRInfinite and revalidateFirstPage: false and key of that mutate is not bound to current page.

Description / Observed Behavior

I have a list of items, which I fetch using useSWRInfinite. Since I do not want to fetch the first page everytime user fetches next page, I use it with revalidateFirstPage: false option, and that works great. So, imagine I have a list of items: A, B, C, D, E. Each item have also its own details page. I can change item A to Aa and use mutate when I am on list page to refresh cache without calling request again, and that also works. The problem is when I go to page A and change that item's name to Aa - even though I call exactly the same methods, when I go back to list page, the item is still A, not Aa. I tested it a lot of times, and that scenario works when I remove revalidateFirstPage: false from useSWRInfinite. But without it I have to load first page every time user scrolls down, which slows down loading new data...

Expected Behavior

Cache is always updated when using mutate, but first page is not fetched everytime when I want to fetch n+1 page

Additional Context

SWR version 1.2.0

tomaszczura avatar Jan 04 '22 13:01 tomaszczura

@tomaszczura Could you create a reproduced case of this?

koba04 avatar Jan 12 '22 01:01 koba04

I prepared something here: https://codesandbox.io/s/funny-driscoll-qrqwf?file=/src/App.js

'Reset cache' - this button calls mutate for used swr infinite key 'Mutate cache' - this button calls mutate for swr infinite key with data and without refetching

You can test it with the following steps:

  1. Make sure revalidateFirstPage is false
  2. Click 'Reset cache' and note the date displayed - assume it is Mon Jan 31 2022 16:04:01 GMT+0100
  3. Click 'Show details' and then 'Hide details' - displayed date is still the same
  4. Click 'Mutate cache' - you should see 'TEST'
  5. Click 'Show details' and 'Hide details' - instead of 'TEST' you will see Mon Jan 31 2022 16:04:01 GMT+0100 (same date as in step 2 Now repeat the step with revalidateFirstPage: true - you will see current date everytime - and that's my desired behavior. I do want to update the first page when I call mutate - but not when I call setSize

tomaszczura avatar Jan 31 '22 15:01 tomaszczura

@koba04 any ideas? This bug blocks us from upgrading to 1.x version

tomaszczura avatar Feb 07 '22 09:02 tomaszczura

@tomaszczura I've tried your test case.

Click 'Show details' and then 'Hide details' - displayed date is still the same

At the step, the data has been updated because of revalidateOnMount: true rather than being the same. I might be wrong, but you should use revalidateIfStale: false instead of revalidateOnMount: true. Could you try your example updated with the option?

koba04 avatar Feb 07 '22 16:02 koba04

@koba04 I created codesandbox which is closer to our case and with version 1.2.1: https://codesandbox.io/s/distracted-chihiro-d682v?file=/src/App.js Steps to reproduce:

  1. Open console (logs will be very helpful here)
  2. Click 'Show react query' - this simulates click on item in our list
  3. Click 'Mutate SWR Description'. You can see Description updated to Updated description in logs. This simluates mutating item which appears on this page and the prebious one.
  4. Click 'Close react query'. This simulates going back to previous page. You can see two new lines in logs:
DATA Updated description 
DATA React Hooks for Data Fetching 

But without API Call between them. So, the cache was updated in step 3, but after step 4, it was reverted to state from step 1. Where does this update come from? Also, no api call was made (there is a log when API is called). I'd expect that when going to step 4, API call is made and cache value is the same as in step 3 until response from API comes. I think that this may be related to fact, that our component which uses useSWRInfinite is not unmounted and mounted again between steps 2/3/4 - we reuse the same component, just use another key

tomaszczura avatar Feb 07 '22 17:02 tomaszczura

I guess your useMatchMutate implementation updates cache data incorrectly, because SWR stores some metadata in addition to cache data, and useMatchMutate seems to update the metadata not only cache data. To avoid that, it's the best way to use mutate returned from useSWRInfinite if possible. SWR also provides unstable_serialize that returns a cache key that SWR uses internally. But as the name implies, it's an unstable API.

koba04 avatar Feb 08 '22 15:02 koba04

We can not use mutate returned from useSWRInfinite, since we need a way to mutate our cache from different components in our code, even those which do not use useSWRInfinite. Also, we can not use unstable_serialize, because we may not know the exact key - our real keys contain a lot of query params, and we want to modify all keys which contains some text (e.g. all with param type=image). We checked the cache, and key starting with $inf.... is always updated with our method, but this state is overwritten when going from point 3 to 4 (without any mutate or API call). What we discover is that if I have keyA for useSWRInfinite, you create two entries incache: one with keyA and one with $inf$keyA. Our useMatchMutate updates the second one, but does not touch the first. This approach worked in version 1.0.0-beta.5 (and earlier), and it still works when revalidateFirstPage is true.

If that is not a valid way of updating cache, do you have any adice for us how to update cache, when we do not know the exact key and without access to bound mutate?

tomaszczura avatar Feb 08 '22 16:02 tomaszczura

@koba04 also running into this issue and was hoping for some clarification.

Here's a codesandbox that mixes @tomaszczura's example with the useSWRInfinite example from the documentation. Its also using the same useMatchMutate hook as described in swr's docs here.

My use case is that I have multiple lists using useSWRInfinite and need a way to mutate data in them without needing to use the hook & its bound mutate because the components calling mutate don't need access to the data provided by the hook.

In the codesandbox you'll see I changed the example from the docs slightly to only use two keys for simplicity's sake. To reproduce the issue I'm running in to, you will:

  1. Load the app. This will fetch the first page of issues from the first repo.
  2. Click “Load Second Repo”. This will fetch the first page of issues from the second repo.
  3. Click “Mutate Second Issue First Repo”. This will mutate the second issue in the cache for the first repo, changing the title to "MUTATED ISSUE". In the logs, you'll see that the $inf$ cache corresponding to the first repo is updated correctly.
  4. Click “Load Initial Repo”. This will load the first repo data from the cache, including the mutation (you'll see in the logs that the very first data log after clicking the button includes the "MUTATED ISSUE" data. But, immediately after data is changed to match its state before the mutation, wiping out the "MUTATED ISSUE" data.

Now, my confusion stems from what happens in 4. When we change the hooks key to the mutated data's key, it first loads the mutated data, but then replaces it with the initial data without ever hitting the API. My expectation was that it would either:

  • Make a request to the API, compare the result to whats in the cache, and update the cache if the two aren't equal.

or

  • Not make a request to the API and keep whats in the cache.

Neither seems to be happening, so I'm curious if you have any thoughts on how to best implement my use case and get around this issue in swr 1.2.1.

Crockery avatar Feb 08 '22 17:02 Crockery

@koba04 I added one more thing to https://codesandbox.io/s/distracted-chihiro-d682v?file=/src/App.js. Now tere is another component a the bottow, which uses uswSWRInfinite with key for swr repo. So, if you click 'Mutate SWR description' it will changes. Try to click 'Show react query' and click 'Mutate SWR description' - you will see it changes to 'Updated description'. But if you click 'Show react query', it goes back to description from API even if there was no API call, and that component has not been touched

tomaszczura avatar Feb 09 '22 08:02 tomaszczura

@tomaszczura

you create two entries incache: one with keyA and one with $inf$keyA. Our useMatchMutate updates the second one, but does not touch the first. This approach worked in version 1.0.0-beta.5 (and earlier), and it still works when revalidateFirstPage is true.

I guess you have to update both data of the keys(keyA and $inf$keyA) because the stale data seems to be returned from the cache data of keyA. If you also want to revalidate only a page of keyA, you would have to update context cache data of useSWRInfinite. But I don't recommend those ways because these are implementation details and would be broken at any updates. Currently, useSWRInfinite doesn't provide an official way to update cache data of a specific page without the bounded mutate function. The best way to solve the problem is adding an API of mutate function for useSWRInfinite, so I think this issue is a feature request rather than a bug.

koba04 avatar Feb 10 '22 15:02 koba04

@koba04 Isn't really a way to change updates useSWRInfinite with revalidateFirstPage: false to work as it worked before version 1.0.1? I mean, to update it everytime but not refetch when setSize is used? For me it looks like a bug that I can modify cache and use it in two components, but when one of the components remounts, the cache is updated to the old one, so that both show incorrect state

tomaszczura avatar Feb 10 '22 16:02 tomaszczura

@koba04 any updates on this?

tomaszczura avatar Mar 17 '22 09:03 tomaszczura

We can not use mutate returned from useSWRInfinite, since we need a way to mutate our cache from different components in our code, even those which do not use useSWRInfinite. Also, we can not use unstable_serialize, because we may not know the exact key - our real keys contain a lot of query params, and we want to modify all keys which contains some text (e.g. all with param type=image). We checked the cache, and key starting with $inf.... is always updated with our method, but this state is overwritten when going from point 3 to 4 (without any mutate or API call). What we discover is that if I have keyA for useSWRInfinite, you create two entries incache: one with keyA and one with $inf$keyA. Our useMatchMutate updates the second one, but does not touch the first. This approach worked in version 1.0.0-beta.5 (and earlier), and it still works when revalidateFirstPage is true.

If that is not a valid way of updating cache, do you have any adice for us how to update cache, when we do not know the exact key and without access to bound mutate?

we have same use case on our end, any updates?

condinoaljoseph avatar Jul 09 '22 19:07 condinoaljoseph

@condinoaljoseph we migrated to react-query

tomaszczura avatar Jul 10 '22 09:07 tomaszczura

I have the same issue, if we put revalidateFirstPage: false we can't mutate the keys cached, this has to be a bug, because why i can't mutate all the pages from swrInfinite when that option is in false but i can o true

FernetB avatar Jan 09 '23 13:01 FernetB

@condinoaljoseph we migrated to react-query

Is there no issue like this in react query? We're considering mirgration to react query.

Donggggg avatar Nov 29 '23 09:11 Donggggg

We should consider it as a part of #2497

koba04 avatar Nov 29 '23 09:11 koba04

@condinoaljoseph we migrated to react-query

Is there no issue like this in react query? We're considering mirgration to react query.

we also migrated to react query and its working great for us

condinoaljoseph avatar Nov 29 '23 16:11 condinoaljoseph

nope, you can invalidate infinite cache as simply as "normal" cache

tomaszczura avatar Nov 29 '23 17:11 tomaszczura