vercel / swr

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

`optimisticData(old)` in `useSWRMutation` is not invoked with latest data from cache #2701

Open westmark opened 1 year ago

westmark commented 1 year ago

Bug report

Description / Observed Behavior

When calling the trigger function returned from useSWRMutation in rapid succession(i.e. before the fetcher has returned) and using the optimisticData callback, the callback does not receive the latest data from the cache, or perhaps more importantly: it does not receive the data that was returned from its last invokation if there have been such and the fetcher is still unresolved.

Expected Behavior

I expect the data sent to optimisticData to always be the latest (optimistic) data

Repro Steps / Code Example

https://codesandbox.io/s/aged-shadow-l4wrhj?file=/src/App.js

Notice how the counter changes immediately to 2 but does not progress to 3 until the clicking stops and the fetcher is allowed to return

Additional Context

SWR version 2.1.1 (also in 2.2 in my project)

Not sure if you consider this a bug or a feature, considering the following text in the docs

Also, this hook doesn’t share states with other useSWRMutation hooks.

However, does this count as an "other hook"?

henriqemalheiros commented 1 year ago

Possibly related to https://github.com/vercel/swr/issues/2186? I believe it's the same reasoning and both issues require proper enqueuing of mutate calls.

JamieS1211 commented 1 year ago

Just worked around this myself by using a "boundMutate" and using the data form "useSWR" hook so I the data doesn't come from the function call on updating optimistic data.

PieterDePauw commented 11 months ago

Just worked around this myself by using a "boundMutate" and using the data form "useSWR" hook so I the data doesn't come from the function call on updating optimistic data.

@JamieS1211 Can you provide an example of how you solved this problem ?

alexnsorensen commented 10 months ago

Is there an update on getting this fixed as it's a bit of a deal breaker for me using this in my project.

alexnsorensen commented 10 months ago

Just worked around this myself by using a "boundMutate" and using the data form "useSWR" hook so I the data doesn't come from the function call on updating optimistic data.

@JamieS1211 I tried this, and it's a good work around for what I'm after, thanks.

JamieS1211 commented 10 months ago

Just worked around this myself by using a "boundMutate" and using the data form "useSWR" hook so I the data doesn't come from the function call on updating optimistic data.

@JamieS1211 Can you provide an example of how you solved this problem ?

Sorry missed this - the documentation on bound mutation can be found here https://swr.vercel.app/docs/mutation.en-US#bound-mutate. Essentially use that, then when an optimistic update happens, the data variable from the hook updates and causes a re-render. Then you can do a mutation with the optimistic data present. In the example they show the mutation as: mutate({ ...data, name: newName }) however you can do as needed here.

PieterDePauw commented 10 months ago

Just worked around this myself by using a "boundMutate" and using the data form "useSWR" hook so I the data doesn't come from the function call on updating optimistic data.

@JamieS1211 Can you provide an example of how you solved this problem ?

Sorry missed this - the documentation on bound mutation can be found here swr.vercel.app/docs/mutation.en-US#bound-mutate. Essentially use that, then when an optimistic update happens, the data variable from the hook updates and causes a re-render. Then you can do a mutation with the optimistic data present. In the example they show the mutation as: mutate({ ...data, name: newName }) however you can do as needed here.

So, if I am not mistaken, you essentially trigger a revalidation in between consecutive mutations?

alexnsorensen commented 10 months ago

Just worked around this myself by using a "boundMutate" and using the data form "useSWR" hook so I the data doesn't come from the function call on updating optimistic data.

@JamieS1211 Can you provide an example of how you solved this problem ?

Sorry missed this - the documentation on bound mutation can be found here swr.vercel.app/docs/mutation.en-US#bound-mutate. Essentially use that, then when an optimistic update happens, the data variable from the hook updates and causes a re-render. Then you can do a mutation with the optimistic data present. In the example they show the mutation as: mutate({ ...data, name: newName }) however you can do as needed here.

So, if I am not mistaken, you essentially trigger a revalidation in between consecutive mutations?

If I'm not mistaken this is what @JamieS1211 is doing as this is what worked for me:

const { data, mutate } = useSWR(['keyHere']);

function saveData(newData) {
    mutate(/* Post here to server */,
    {
        populateCache: false,
        optimisticData: () => {
            // Notice using the data from the useSWR hook instead
            // of the oldData that comes as an argument/parameter for this function
            return [...data, newData];
        }
    );
}
JamieS1211 commented 10 months ago

Yep thats it, not sure if there is some drawback of this approach so maybe a maintainer could chip in on that front

hitsthings commented 10 months ago

For anyone else seeing old data passed to your optimisticData function, I had a slightly different issue. I was using a filter function for the keys (e.g. key => key.startsWith('blah')). When I instead did my filtering before starting the process, and called mutate for each key individually that seemed to fix it. So old was:

const { mutate } = useSWRConfig();

useEffect(() => {
    setTimeout(onCompletelySeparateEventLoop, 1000);

    function onCompletelySeparateEventLoop() {
        const p = doAThing();
        mutate(key => key.startsWith('blah'), doAThing, {
              populateCache: false,
              revalidate: false,
              optimisticData: (cached) => transform(cached)
        }
    } 
}, [])

New:

const { mutate, cache } = useSWRConfig();

useEffect(() => {
    setTimeout(onCompletelySeparateEventLoop, 1000);
    const keys = Array.from(cache.keys()).filter(key => key.startsWith('blah'));
    function onCompletelySeparateEventLoop() {
        const p = doAThing();
        for (const key of keys) {
            mutate(key, doAThing, {
                  populateCache: false,
                  revalidate: false,
                  optimisticData: (cached) => transform(cached)
            }
        }
    } 
}, [])

This still doesn't get the latest data if you have multiple optimisticData calls in a row - but cache.get(key) will have the latest in that case.