-
Notifications
You must be signed in to change notification settings - Fork 4.7k
Common problems with reactivity #849
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
Comments
Full example: https://jsfiddle.net/skirtle/wcdfe1ub/ Key section: async created () {
const list = await loadList()
this.items = list
const list2 = await loadList2()
list.push(...list2)
} This uses the options API with no explicit mention of In Vue 2 this would have worked fine. A similar example inside a Vuex action would be even more fiddly, with |
There's a common question that gets asked: What is the difference between Usually the question is in the context of objects rather than primitive values. One way of explaining it is with analogy to It's also potentially worth explaining that Here's an example based on a question on the forum. There are various problems with it but I think it's interesting to consider why it has been written this way and how we should address that in the docs: setup () {
const data = reactive({ text: 'Loading' })
return { data }
},
async mounted () {
const resp = await this.$http.get(...)
this.data = resp.data
} |
Reassigning a local variable does nothing from a reactivity perspective: setup () {
let myValue = ref(false)
const update () {
myValue = ref(true)
// or even:
// myValue = true
}
return {
myValue,
update
}
} While the example above uses The misunderstanding seems to come from believing that the The confusion can sometimes be tracked back to the automatic wrapping/unwrapping, which can make <button @click="myValue = true">Update</button> While this event listener appears to be doing the same thing as the earlier Beyond the problem that reassigning a local variable can't be tracked, there is the further problem that the value returned by |
While it is already documented, destructuring is a common source of problems. This isn't a problem with destructuring itself, it's just the most common way to read a property outside of an effect: setup({ text }) {
// text is not reactive
} Or without destructuring: setup(props) {
const text = props.text
// text is not reactive
} Arguably, the underlying problem here is that Something similar can happen with data () {
return {
text: this.propValue
}
} While this is perfectly valid for defining an initial value for |
Update: This is already documented but In particular, the props object in <template>
<div>{{ content || 'Loading' }}</div>
</template>
<script>
import { toRef } from 'vue'
export default {
props: ['text'],
setup (props) {
// `content` will be undefined if the `text` prop is missing.
// The initial render will be fine but it won't be reactive.
const content = toRefs(props).text
return {
content
}
}
}
</script> The example above is sufficiently simple that it can be easily fixed by using the prop directly in the template. It is purely intended to illustrate how |
Working with third-party libraries that aren't designed to be compatible with Vue can be difficult and has changed from Vue 2 to Vue 3. Similar problems can occur when users attempt to use their own objects if those objects aren't 'plain'. When an object is provided by a third-party library there often isn't any way to tell the library to make changes via the proxy. In Vue 2, the getter/setter approach still worked so long as the library exposed all the relevant properties rather than hiding them as local variables within closures. So, an object like this wouldn't be compatible with reactivity in Vue 2 or Vue 3: let value = 1
const obj = {
getValue () {
return value
},
setValue (newValue) {
value = newValue
}
} Whereas something like this could work in both: const obj = {
value: 1,
getValue () {
return this.value
},
setValue (newValue) {
this.value = newValue
}
} It isn't immediately obvious that this second example would be compatible with proxy-based reactivity, as it appears to be accessing The proxy-based reactivity can be a massive performance boost with some third-party libraries, so it isn't all bad news from that perspective. In Vue 2 there were cases where objects had to be held in non-reactive properties just to avoid the performance drag of rewriting all the nested properties. In practice, using 'plain' objects has always been recommended, so trying to graft reactivity onto a third-party library has always been a bit dubious. |
There are two key principles that are sometimes overlooked, or deliberately ignored because they aren't properly understood:
These can be applied to working with Vuex but I'll stick to components here. One reason why mutating objects in a child is problematic is because the object passed in via a prop may not be reactive. In that case, modifying it directly won't necessarily trigger the required updates. As for why such an object would be unreactive, one common reason would be that the object is created by a Similarly, props don't add any reactivity if their values are objects/arrays. Only the property itself is reactive. When these values are logged they will be plain values, rather than proxies. This can confuse some users who perceive this to be a sign that reactivity is broken. Worse, the confusion can be heightened because logging other values would show proxies, if those values happen to have been made reactive by some other upstream process. |
The documentation is already clear about the need to always go through the proxy. Further, it outlines the potential problems that can be caused by equality checking, because a proxy does not Some interesting edges cases occur with the array methods const obj = {}
const react = Vue.reactive(obj)
const arr = Vue.reactive([obj])
console.log(arr.indexOf(obj)) // Logs 0
console.log(arr.indexOf(react)) // Logs 0 What gives? If There is some magic at work here: |
Update: Things have changed since this comment was written. It is still potentially useful as a checklist of things to consider, but I wouldn't rely on it if you're trying to learn how unwrapping works. The automatic wrapping/unwrapping of a The biggest myth seems to be that the unwrapping is performed in the template. I can't find any evidence of that. The template relies on the same unwrapping mechanisms that you'd get if you accessed the same properties using I'm going to use the term A quick summary:
Add to that the following:
Once you've got all of that clear in your head, you might be able to figure out whether something is going to be automatically wrapped/unwrapped or not. It is also worth noting that assigning a const react = reactive({})
const obj = ref(7)
// The `count` property and `obj.value` will now be linked
react.count = obj
// This will update both `count` and `obj.value`
react.count = 8 This also applies to a Assigning another // Remove the link to the ref
delete react.count
// This will just assign the raw value without updating the previously linked ref
react.count = 8 |
Any effect created during Here is a concrete example. Everything it does seems reasonable but it runs into this problem: https://jsfiddle.net/skirtle/0cnek4sr/ In that specific case it is easily fixed, but only once you know what the problem is. |
Proxy-based reactivity supports far more operations than Vue 2's reactivity system but it still doesn't cover everything. The following all work:
However, |
Saving a reactive proxy to Snippet from the repo linked in that issue (in case it might get deleted) // this.errorinfo is a reactive proxy containing: {errors: []}
chrome.storage.local.set({error_info: this.error_info}, function(){
chrome.storage.local.get({error_info: {errors: []}}, function(obj){
console.log('saved result', obj);
// trying filter array of errors
// throws `TypeError: obj.error_info.errors.filter is not a function`
console.log(obj.error_info.errors.filter(function(el){
el.message !== undefined
}).length);
});
});
|
The statements above regarding this aren't always correct. They seem to be based on props. I have been doing some experiments with reactivity and I want to share a counter-example. I'm just going to provide the setup function.
Given that store has been imported and is reactive then the item returned from the computed function is a reactive object. I agree that the docs can still use some work. |
There are some interesting cases involving Following on from earlier, properties accessed outside a reactive effect aren't tracked. The watch expression is a common source of this type of problem: watch(a.b, ...) Assuming the intention was to watch for changes to the watch(() => a.b, ...) I see this pretty frequently and it often proves difficult to explain why the function is necessary. It is sometimes perceived as Vue being deliberately awkward. I suspect many people regard the first example as being a bit like watching the string When watching a const a = ref(7)
watch(() => a, ...) Without the wrapper function, watching a Another interesting aspect of watch(() => a.b, () => {
// ...
}, { deep: true }) Accessing Watching multiple values can provide some subtle edge cases too. Given the following object: const obj = reactive({ a: 1, b: 2 }) What is the difference between these two different ways of watching watch([() => obj.a, () => obj.b], v => {
console.log(v)
})
watch(() => [obj.a, obj.b], v => {
console.log(v)
}) In both cases, the same dependencies are tracked and the value of The difference occurs when the watched value is checked for changes. The first example yields a pair of values, and each will be checked to see whether it has changed. In the second example, a new array is being returned each time the function is called. Even if the values of Running example: https://jsfiddle.net/skirtle/dq9kxfea/ |
I've encountered a few cases where These two examples try to use This tends to happen in scenarios where some of the data can't be made reactive, either because of performance or because it comes from a third-party library that is incompatible with proxy-based reactivity. The idea is to try to manually trigger the reactivity system, but as shown in the examples above, it only works in a very limited way. |
thanks. well summarized. |
I am having the following problem. Among the same type of elements generated using v-for, only one is reactive. 3 page links in the nav should be displayed in 3 languages by user choice. In the initial page load it works fine. Then click one language change link then again it works fine. But when I click a language change link another time only the last link's language changes when all of them should change language. What is causing this problem and how to fix it? app.vue:
languages.vue:
languages.js:
I noticed that removing |
`I'm suck with vue reactivity. I think it's some kind of joke.
|
UPDATED: Added two potential patterns for making internal mutations in classes reactive. There's no mention of classes here that I could see. Classes are sometimes the right way to solve a problem and having to avoid them just to get Vue reactivity working is a pretty major downside. I needed to find a way to trigger active updates when a class instance updates itself internally. Without doing anything special, these mutations don't go through any reactivity proxies, so Vue components using instances of a regular class aren't able to update. I was able to find two plausible methods for getting this working. The first method uses a "decorator" that takes an existing non-reactive class and returns an extension of the class that uses class constructor return overriding to return a proxy that wraps the new instance. import { reactive } from "vue";
class NonReactiveFoo {
id = null;
doThing() {
this.id = 1234;
}
}
const ReactiveFoo = makeReactive(NonReactiveFoo);
const instance = new ReactiveFoo();
instance.doThing(); // should trigger reactive updates
function makeReactive(Class) {
return class extends Class {
constructor(...args) {
super(...args);
return reactive(this);
}
};
} The second method is more direct: if you have control over the class, you can return a reactive object yourself from the constructor. import { reactive } from "vue";
class ReactiveFoo {
id = null;
constructor() {
return reactive(this);
}
doThing() {
this.id = 1234;
}
}
const instance = new ReactiveFoo();
instance.doThing(); // should trigger reactive updates Having some official guidance on the right way to do this—whether similar to or different from what I've outlined here—without telling people to not use classes would be fantastic. |
hey @chriscalo , thanks for posting solution & example for classes! I need that too, for me there's no way to avoid classes, even if I wanted to... Just wanted to add, that |
Indeed there are gotchas with these approaches, which is why it would be helpful to have official guidance from the Vue team that's better than "don't use classes." I would love to see an in-depth article in the style of Reactivity in Depth† that:
That seems like it would be a really useful article. † It's puzzling that classes aren't mentioned anywhere in this article. |
Uh oh!
There was an error while loading. Please reload this page.
This is an umbrella issue, intended to gather examples of common problems, mistakes and misunderstandings when using the proxy-based reactivity system. The objective is to gather source material that might be used to add a new page to the documentation, or enhance the existing pages.
The idea is that it will be something similar to the Reactivity Caveats for Vue 2. However, I'm keen for the scope to be quite broad at this stage and we can decide what is actually worth documenting later. Anything related to reactivity that might catch out a beginner is in scope for now, even if it is already documented. That includes examples of things that do work but where it isn't necessarily obvious why they work. These can cause just as much confusion as scenarios that don't work correctly.
While most reactivity problems can be reduced down to 'you need to go through the proxy', I think there are plenty of cases where it isn't immediately obvious how that applies, especially for newcomers. My hope is that documenting some of these edge cases will save people from having to learn about them the hard way, while also giving them a deeper understanding of how reactivity works.
I've outlined various example below. If anyone has more to add, please do. Anything else that might help us to document the problems I've already described would also be welcome.
The text was updated successfully, but these errors were encountered: