vercel / swr

React Hooks for Data Fetching
https://swr.vercel.app
MIT License
30.52k stars 1.22k forks source link

mutate() and the deduping interval #1417

Open hazae41 opened 3 years ago

hazae41 commented 3 years ago

Bug report

Description / Observed Behavior

I prefetch and mutate some data when the user hovers a link using mutate(key, data, false) When the user clicks the link, the page is loaded and useSWR revalidates the data.

However, the hover and the click where made within deduping interval.

Expected Behavior

Do not revalidate if mutate(key, data, false) has been called within the deduping interval

Repro Steps / Code Example

export async function prefetch(){
  mutate("/api/data", () => fetcher("/api/data"), false)
}

export function DataPage(){
  const res = useSWR("/api/data", fetcher) // will revalidate even though mutate has been called withing the deduping interval

  return <>
    ...
  </>
}

Additional Context

swr@1.0.0

promer94 commented 3 years ago

could you try useSWR("/api/data", fetcher, { revalidateIfStale: false })

hazae41 commented 3 years ago

I tried and it doesn't revalidate, but I want it to revalidate if the stale data is older than the deduping interval

promer94 commented 3 years ago

dedupingInterval is the time between two revalidate not the time between mutate and revalidate.

------------dedupingInterval----------
------mutate---revalidate----revalidate--                              second one will be ignored
---mutate-----revalidate---                                will still revalidate 

if you want to send less requests, revalidateIfStale could save one request when hook mounted.

hazae41 commented 3 years ago

There should be a time between mutate a revalidate then

computerjazz commented 3 years ago

I have this same issue. Here's my use case:

I'm on React Native and am wrapping useSwr in a custom hook, let's call it useSomeData({ paused: boolean }). I want to be able to do a "soft" revalidation when various conditions are met: when I "unpause" useSomeData, when some user state changes, etc. However, I want these revalidations to be deduped, and I don't want each instance of my useSomeData() hook to make a separate request, as would be the case if I called mutate in an effect.

fwiw I was able to solve this using undocumented dedupe param in revalidate argument in the pre-1.0.0 release: https://github.com/vercel/swr/blob/0.5.6/src/use-swr.ts#L360

so this does what I want it to do:

function useSomeData({ paused }) {
  const readData = useSwr("some/key", myFetcher, { isPaused: () => paused })
  const { revalidate, mutate } = readData

  useEffect(() => {
      // @ts-ignore dedupe is a revalidation option but not included on the type: https://github.com/vercel/swr/blob/0.5.6/src/use-swr.ts#L360
    if (!paused) revalidate({ dedupe: true })

    // note that calling 'mutate' here would make an api request for _each instance_ consuming this hook. I don't want that.
    // mutate()
 }, [paused, revalidate])

}

However, since revalidate was removed in 1.0.0 I'm going to need to find a new path forward.

promer94 commented 3 years ago

Each instance of my useSomeData() hook to make a separate request, as would be the case if I called mutate in an effect

This doesn't sound correct to me.
If multiple hooks mounted, calling mutate will only trigger one revalidate. FYI: https://stackblitz.com/edit/nextjs-ybma55?file=pages%2Findex.js @computerjazz

shuding commented 3 years ago

BTW from your code snippet, I suppose the new hook in #1262 is what you need. @computerjazz

computerjazz commented 3 years ago

Thanks @promer94 and @shuding !

So, here's a more complete example of my use case: https://stackblitz.com/edit/nextjs-4xtgyg?file=pages%2Findex.js

Notice that if the fetch starts as paused: true, and then toggles to paused: false at some time later, a fetch does not occur by default, so we have to trigger it manually. Adding the useEffect and calling mutate results in each instance of the hook making its own separate fetch.

The new hook in #1262 looks like it may solve the issue as long as it deduplicates correctly, but I wonder if this specific case should be handled within the useSwr hook itself in an effect that calls revalidate({ dedupe: true }) when config.isPaused() state becomes false?

And apologies if I'm steering this issue too far away from the original report. I'd be happy to open a new issue and move discussion there.

swushi commented 2 years ago

I also feel that it makes sense to dedupe all mutate calls.

It feels like a pretty common pattern to ingest the same hook in multiple components, and allow SWR to handle the de-duping to avoid unnecessary re-renders. In React-Native, you have to handle revalidateOnFocus on your own, something like the following:

const useProfile = () => {
  const swr = useSwr('/users/me');
  useFocusEffect(() => swr.mutate());
  return swr;
}

const Profile = () => {
  const { data } = useProfile();
  return <ProfileHeader />
}

const ProfileHeader = () => {
  const { data } = useProfile();
  return <Foo />
}

Standard fetching is deduped correctly, and that's the biggest selling point of SWR for me. However, with this custom revalidation on focus for react-native, every call to mutate, which would be every component that uses useProfile, another request would be sent out.

In this example, two requests are being sent out, when I would expect them to be deduped. I don't really think it has anything to do with paused property.

hazae41 commented 2 years ago

For those who don't want to scratch their heads anymore, wondering why the fuck swr does this, I made my own library.

https://github.com/hazae41/xswr

It uses composition-based hooks and has a very easy learning curve, and 0% weird behaviour.

siawyoung commented 2 years ago

I thought I was going mad, but turns out this is actually a thing. Why aren't mutate calls deduped too?!

zongyz commented 1 year ago

I also have a similar problem, in the case of revalidateOnFocus=true, when using useSWR("/list") to provide list data for a page, if there is a refresh button on the page, click the button to use mutate( "/list") Refresh list data, when switching to another window, and then click the refresh button to return to the page, re-focus and button click will initiate two requests for "/list"

Lwdthe1 commented 1 year ago

I came here searching to make sure that mutate() is independent and will always fulfill the desire to trigger a unique request to the server regardless of dedupingInterval. I'm glad to see that's exactly how it works because if I (from my user's action) explicitly call mutate, I want it to do exactly that without exception.

Perhaps a middle ground is to have mutate() or useSWR() accept a configuration that allows for what the others are asking, so that they can make mutate() dependent on / respect dedupingInterval.

sannajammeh commented 1 year ago

I am also trying to make mutate actually dedupe the requests. Is it possible?

I have a revalidateOnFocus middleware for react-native which currently causes 60 requests as its used in a list of 60 components. The default behavor works as useSWR only is called once as the key is the same, but trying to mutate the hooks based on focus will not dedupe.

sannajammeh commented 1 year ago

I am also trying to make mutate actually dedupe the requests. Is it possible?

I have a revalidateOnFocus middleware for react-native which currently causes 60 requests as its used in a list of 60 components. The default behavor works as useSWR only is called once as the key is the same, but trying to mutate the hooks based on focus will not dedupe.

Honestly, pretty easy solution with swr middleware

const dedupeCache = new Map();

export const revalidateOnFocus: Middleware = (useSWRNext) => {
  return (key, fn, config) => {
    const swr = useSWRNext(key, fn, config);
    useFocusEffect(
      useCallback(() => {
        if (typeof swr.data === "undefined") return;

        const serializedKey = unstable_serialize(key);
        if (dedupeCache.has(serializedKey)) return;
        dedupeCache.set(serializedKey, true);
        swr.mutate().finally(() => {
          dedupeCache.delete(serializedKey);
        });
      }, [swr.data, swr.mutate]),
    );

    return swr;
  };
};

// Your query
const {data} = useSWR("/path", {
   use: [revalidateOnFocus]
})

// Or global
<SWRConfig value={{ use: [revalidateOnFocus] }}>
{children}
</SWRConfig>