Reputation: 342
I'm very enthausiast about vuejs but the composition API made me get lost slightly on reactivity:
In a .vue child component a prop is passed (by another parent component), movies:
export default {
props:{
movies: Array
},
setup(props){}
.
Placing in the setup function body of this child component:
A) console.log("props: ",props) // Proxy {}
[above EXPECTED] above tells me that the props object is a reactive proxy object.
B) console.log("props.movies: ",props.movies); // Proxy {}
[above UNEXPECTED] above tells me that the props key 'movies' is a reactive proxy object as well, why? (thus why is not only the main props object reactive? Maybe I should just take this for granted (and e.g. think of it as the result of a props = reactive(props) call) --> ANSWER: props is a **deep ** reactive object (thanks Michal Levý!)
C) let moviesLocal = props.movies ; // ERROR-> getting value from props in root scope of setup will cause the value to loose reactivity
[above UNEXPECTED] As props.movies is a reactive object, I'd assume that I can assign this to a local variable, and this local variable then also would become reactive. Why is this not the case (I get the error shown above)
D) let moviesLocal = ref(props.movies);
moviesLocal.value[0].Title="CHANGED in child component";
[above UNEXPECTED] I made a local copy in the child component (moviesLocal). This variable I made reactive (ref). Changing the value of the reactive moviesLocal also causes the 'movies' reactive object in the parent object to change, why is that ? This even holds if I change D: let moviesLocal = ref(props.movies); to let moviesLocal = ref({...props.movies});
Thanks a lot! Looking forward to understand this behaviour
Upvotes: 7
Views: 21595
Reputation: 37753
Important part of understanding Vue reactivity is understanding JavaScript - specifically difference between Value and Reference. If you are not sure what I'm talking about, read the article carefully...
In following examples I will be using props
object as it was created following way (in reality it is not created exactly like this but runtime behavior is very similar):
const props = reactive({ movies: [], price: 100 })
B) console.log("props.movies: ",props.movies); // Proxy {}
above tells me that the props key 'movies' is a reactive proxy object as well, why?
Because when Vue creates new reactive object (by wrapping existing object into a Proxy), that conversion is deep - it affects all nested properties (this behavior can be changed by using for example markRaw
or shallowRef
)
In terms of Value vs Reference the props
is a variable holding a reference to a reactive proxy object. That object has a property movies
which holds the reference to a reactive proxy of array (arrays are also technically Objects)
C) let moviesLocal = props.movies ;
// ERROR-> getting value from props in root scope of setup will cause the value to loose reactivity
As props.movies is a reactive object, I'd assume that I can assign this to a local variable, and this local variable then also would become reactive. Why is this not the case
Variables are not reactive. Only Objects can be reactive. In this case moviesLocal
variable is assigned with reference to same reactive object as props.movies
(moviesLocal === props.movies
returns true
). This object is still reactive.
Let's use our moviesLocal
variable in the template to render some kind of list...
If for example parent mutates the array referenced by props.movies
(adding new item with push()
, assigning different value to props.movies[0]
etc.) child will be notified about the change and its template will re-render.
So why the error? Where is the "loose reactivity" ? Problem is what happens when parent replaces the value of props.movies
with different\new array. Our moviesLocal
variable will still hold the reference to previous reactive array. Our child component will still be reactive to changes (mutations) of that original array but lost the ability to react to changes of props.movies
property.
This is demonstrated in this demo
The funny thing is that in the core, this behavior has nothing to do with Vue reactivity. It is just plain JS. Check this:
const obj = {
movies: ['Matrix']
}
const local = obj.movies
obj.movies = ['Avatar']
What is the value of local
at this point ? Of course it is ['Matrix']
! const local = obj.movies
is just value assignment. It does not somehow magically "link" the local
variable with the value of obj.movies
object property.
D) let moviesLocal = ref(props.movies);
moviesLocal.value[0].Title="CHANGED in child component";
I made a local copy in the child component (moviesLocal). This variable I made reactive (ref).
Again, variables are not reactive. Objects they point to (reference) can be reactive. Array referenced by props.movies
was already reactive proxy. You just created a ref
which holds that same object
Changing the value of the reactive moviesLocal also causes the 'movies' reactive object in the parent object to change, why is that ?
Because both point to (reference) same array (wrapped by Vue proxy) in the memory. Assign a new array to one of them and the this "link" will break...
Upvotes: 13
Reputation: 29071
Use toRef
to maintain reactivity.
const moviesLocalRef = toRef(props, 'movies')
See https://v3.vuejs.org/api/refs-api.html#toref
The rule is that a reactive
object child property must always be accessed from the root of the object. a.b.c.d
. You cannot just break off a piece and modify it
, because the act of resolving from the root is what allows Vue to track changes.
In your case, the internal property is the same, but Vue has lost the ability to track changes.
As a side note, you can also create a computed reference.
const moviesLocalRef = computed(()=>props.movies)
Whether you use a computed ref
or toRef
, you will need to access your property using theRef.value
outside of templates. As I state in conclusion, this is what allows Vue to maintain the reactivity.
Here is how I have chosen to think about Vue3 reactivity.
The act of dereferencing a property
from an object is what triggers the magic.
For a reactive object
, you need to start with the reactive object
and then drill down
to your property of interest. For a ref
, you need to unbox the value
from the ref
. Either way, there is always a get
operation that triggers the internal mechanism and notifies Vue who is watching what.
This drilling down
happens automatically in templates and watchers that are aware they received a reference, but not anywhere else.
See https://v3.vuejs.org/guide/reactivity.html#how-vue-tracks-these-changes
Upvotes: 9