Has anyone encountered this? Here is the basic pattern:
const { mutate } = useSwr(...)
mutate( data=>{...data, newElem: 'foo'} )
I have a case where multiple async operations fire mutate in an async callback, but I noticed that the final mutate doesn't always re-render.
mutate( data=>{...data, newElem: 'foo'} )
That should be:
mutate(data=> ({...data, newElem: 'foo'}))
Note sure if that may cause it. If that doesn't fix it, try creating a codesandbox to replicate it.
@sergiodxa Here is a sandbox demonstrating the issue https://codesandbox.io/s/swr-mutate-bug-lhq8p

The sandbox does not work, but I think it should because it's returning a new object. This is true whether the data source is an array of objects ([{title: 'foo'}]) or a single object.
This does not work:
const { data, error, mutate } = useSWR("foo", () => {
console.log("Fetching data")
return { title: "Foo 1" } // [{ title: "Foo 1" }] also has same problem
})
const handleMutate = () => {
mutate(data => {
data.title = `${data.title} mutated`;
return { ...data }
}, false)
}
But this does work:
mutate(data => {
return { ...data, title: `${data.title} mutated` };
}, false);
But I don't understand, because both return a new object. Is swr doing a deep comparison and finding oldObject.title === newObject.title?
swr's mutate is definitely behaving differently than useState's mutator: https://codesandbox.io/s/swr-mutate-bug-kbr6g
This works every time:
const App = () => {
const [obj, setObj] = useState({ title: "foo" })
return (
<Fragment>
{obj.title}
<button
onClick={() =>
setObj(o => {
o.title = `${o.title} mutated`
return { ...o }
})
}>
mutate
</button>
</Fragment>
)
}

HI @sergiodxa I left a few sandbox samples above, just wanted to make sure you saw it because I've made lots of edits throughout the day :)
I think I've got the same issue. Created an other reproduction. Maybe it helps.
@ties-v You need to return {...list, items: [...list.items]}; from bm(), then you will have the same issue :)
Right now, you are doing return list which is not correct, you should always return a new object reference.
Ah yes, Thanks for the catch @benallfree! But indeed, when applying your fix it renders sometimes.
I have also encountered this problem. I haven't completely fleshed out the server side of my app yet so I'm not using revalidation.
In my case, mutate only causes a re-render the first time I call it. After that, it does not trigger any re-rendering.
<SWRConfig
value={{
refreshInterval: 0,
dedupingInterval: 0,
suspense: true,
fetcher: (...args) => fetch(...args).then(res => res.json())
}}
>
Any tips on where to debug? I'm happy to take a stab at figuring this out with some guidance. At the moment I'll probably work around this by forcing the component to re-render manually.
@benallfree looks like the issue is caused by cache. mutate API plays with the swr internal cache which implemented by Map. once you do
data.title = `${data.title} mutated`;
map['foo'] will be manipulated as well. then deep qual comparing cannot will return true.
@huozhi Sorry if I am misunderstanding, but I think we are saying opposite things. I have a code sandbox above that disproves your statement.
data.title = `${data.title} mutated`;
return { ...data }
should work, but it does not. That's the bug.
hi @benallfree yea, sorry for my unclear explanation here. I mean the current cache is implemented with Map. and when we do data.title = 'sth' it operated the map value in the cache. then even {...data} will return a difference reference with exact same key-value pairs, the deep equal comparing still returns "same" which leads to this bug.
I guess currently the workaround is to use {...data, title: nextTitle} as you mentioned, to replace the data.title = ... assignment expression.
I guess if swr wants to fix it, probably need to change the implementation of cache, to let it return the cache value with different reference from original one, but have same properties when retrieve.
then doing data.title = 'xxx' will be safe since it won't manipulate the value in cache.
@benallfree Like @huozhi said, you shouldn't mutate the data object reference, since it's stored in the cache (and used everywhere).
Instead, you should treat it as an immutable value (so the original data isn't changed):
mutate(data => ({ ...data, title: 'new' }))
To add more context, the mental model is the same as a reducer:
dispatch(state => ({ ...state, title: 'new' }))
If you return the same value from a Reducer Hook as the current state, React will bail out without rendering the children or firing effects.
https://reactjs.org/docs/hooks-reference.html#bailing-out-of-a-dispatch