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
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.
Conclusion
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
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...
Related
I'm trying to understand usage of the Ref in React. I saw an example in the Ant Design documentation. https://3x.ant.design/components/tag/#components-tag-demo-control
There is a one line code that I couldn't get how it works.
saveInputRef = input => (this.input = input);
And usage as follows:
<Input ref={this.saveInputRef} ...
But in the React documentation, it is said that you create a ref using React.createRef() method.
https://reactjs.org/docs/refs-and-the-dom.html#adding-a-ref-to-a-dom-element
Is it an alternative way of using it? Why there is no React.createRef() method?
As far as I know there are 2 different kind of Refs, but they are commonly used together.
A. There is "Ref" created by "createRef" (or "useRef" when using hooks);
This stores a value in the "current" property of the Ref. This value won't cause rerenders and is kept after rerenders.
B. And there is ref as a property of build-in components. This property is used to access the domnode.
Usually ref (B) is stored in a Ref (A)
You can, however, store whatever you'd like in Ref (A).
And you don't need necessarily need to store a node gotten from ref (B) in a Ref (A), you can access it directly as well, that what this piece of code does:
<div ref={node => doSomething(node)}/>
Or simply
<div ref={doSomething}/>
This is called a "callback Ref":
Callback Refs
React also supports another way to set refs called “callback refs”, which gives more fine-grain control over when refs are set and unset.
Instead of passing a ref attribute created by createRef(), you pass a function. The function receives the React component instance or HTML DOM element as its argument, which can be stored and accessed elsewhere.
https://reactjs.org/docs/refs-and-the-dom.html#callback-refs
EDIT:
more about Ref (A) when using hooks.
Essentially, useRef is like a “box” that can hold a mutable value in its .current property.
You might be familiar with refs primarily as a way to access the DOM. If you pass a ref object to React with , React will set its .current property to the corresponding DOM node whenever that node changes.
However, useRef() is useful for more than the ref attribute. It’s handy for keeping any mutable value around similar to how you’d use instance fields in classes.
This works because useRef() creates a plain JavaScript object. The only difference between useRef() and creating a {current: ...} object yourself is that useRef will give you the same ref object on every render.
Keep in mind that useRef doesn’t notify you when its content changes. Mutating the .current property doesn’t cause a re-render. If you want to run some code when React attaches or detaches a ref to a DOM node, you may want to use a callback ref instead.
https://reactjs.org/docs/hooks-reference.html#useref
I noticed something strange with prop mutation (Vue 2.6).
Mutating props directly in a child component should be avoided and will trigger the following famous warning
"Avoid mutating a prop directly since the value will be
overwritten...."
So if in my parent component I have a data like
exemple: "Hello World"
that I passed down as a prop to a child component. If in that child component I do something like
this.exemple = "Hello World!"
I get a warning. Fair enough. Now I noticed that if the data in the parent is an object like
exemple : {message : "Hello World"}
and that in the child I do something like that
this.exemple.message = "Hello World!"
This does not trigger any warning and moreover the data in the parent component get updated
I'm wondering why. Why the prop mutation propagate to the parent in one case but not in the other ? Does it have maybe something to do with how javascript store these variable ? Is it good practice to use that trick ?
Why does the prop mutation propagate to the parent in one case but not in the other ? Does it have maybe something to do with how javascript stores these variable ?
Yes. JavaScript reference variables such as objects and arrays don't change their references when you mutate their properties/values. This means the parent can access the changed properties too, because it also has access to the reference. And Vue only gives a prop mutation warning if the reference changes.
Warning:
this.prop = {}
No warning:
this.prop.id = 5
Is it good practice to use that trick ?
No. It's back to violating One way data flow in which data is passed down through props (to child) and up through events (to parent).
Ok, but why doesn't Vue warn about those changes?
Consider an object with 1000 properties or an array with 1000 items. Vue would have to deep check all 1000 for changes, and there is no quick way to do it because each item has to be individually compared to the old value. This would have performance implications and probably make many apps unusable.
Here is a comment on the subject from Vue team member Bolerodan:
Generally you shouldnt do that. This can / could cause unexpected bugs in your code. Generally you should make a copy of your object, modify that, then emit upwards to the parent (as you have mentioned in your last paragraph I believe). This is the method I take and is the general consensus of top down, emit up approach.
A very simple answer is that You are mutating a reference value and Vue doesn't track the whole reference instead it only checks for the address where the reference is. So if you re-assign that value then it will warn you.
A good example to understand the behaviour is understanding through variable declaration const
const makes your variable immutable but again same applies to it if the value is primitive in nature you can't change it, but if it's a reference you can't change it but you can definitely update a value located at the reference
const a = 'Foo' // Primitive value
a = 'bar' // voilation
const b = { // Reference value
name: 'Foo'
}
b.name = 'Bar' // It's good
console.log(b)
b = {} // re-assign is voilation
Because the mutation prevention only catches direct assignments to the prop - it does not catch manipulations of its properties. The idea is that if you assign new values to the prop - you are assigning to the prop and not to the variable that is sitting in the parent Vue component which feeds the prop.
As soon as something in the parent component updates the feeding variable - the prop will be updated with this new value and the value that you have assigned to the prop in your child component will be overwritten.
If you feed the prop with an object and then mutate a property inside the prop in your child component - it will affect the same object inside the parent component. But if you try to assign a new object to the prop in your child component - you should get the same warning about mutating props.
Expected to achieve:
Most correct way to modify specific record stored in the Vuex state with one big (batch) write
Context:
Vuex state contains list of records with default values for each record.
Logic
Component initialises and uses getter to get one of the records.
It's needed to add new properties to the record and overwrite existing values.
Question
Is it acceptable to modify the object returned by the Vuex getter and later commit the whole result into the state? And if yes, what would be the best approach considering it will have to overwrite existing record in Vuex.
P.S: I also wonder if it can result in breaking behaviour of other components that are "getting" the same record that will be overwritten, and will appreciate a lot your thoughts on this topic :-)
Don't modify Vuex data except via Vuex mutation
1) First, clone the getter record. Make sure you deep clone if your record has anything but primitive properties, i.e it has properties that are objects or functions. Here are some cloning options, notice that many don't correctly deep clone.
2) Once you're done mutating the clone, call a Vuex action and have that action call a mutation. It's good practice to only call mutations from actions.
3) In that mutation, use Vue.set (or splice) to replace the state record.
4) Yes, if you mutated the getter directly, it would be mutated anywhere else it was used.
See Change detection caveats in the VueJS reactivity guide.
Sometimes you may want to assign a number of properties to an existing object, for example using Object.assign() or _.extend(). However, new properties added to the object will not trigger changes. In such cases, create a fresh object with properties from both the original object and the mixin object:
// instead of `Object.assign(this.someObject, { a: 1, b: 2 })`
this.someObject = Object.assign({}, this.someObject, { a: 1, b: 2 })
Why does the object returned by the useRef hook stores whatever value it's supposed to hold in current property? Why can't we assign something directly into the ref object as shown below:
const sampleRef = useRef([]);
/** why can't we do this... */
sampleRef.push('1');
/** ...instead of this? Why an extra `current` object? */
sampleRef.current.pus('1');
What is the purpose of useRef returning the argument wrapped inside another object with current property?
The answer to this question is not specific to React and its hooks system.
Mutating an object is just a solution to how you share values between different closures / scopes.
When you call useRef() for your component a "ref object" is created in React internals, tied to that particular instance of your component.
Each time useRef() is called across multiple renders, the same "ref object" is returned. Setting current on it is how your store a value to re-access it on next render.
By doing something like
let value = useRef();
value = 1234;
you're throwing away the ref object, replacing it with a new value in your local scope. There's no way React can track that action and update the ref object that is stored in its internals. (In fact React does not track any action to "ref objects" anyway, it just gives them to you. You mutate them, you access them).
But with the current API, you do
const ref = useRef(); // ref is an object that is stored somewhere in React internals
ref.current = 1234; // you update the property of that object
The next time your component renders, React gives you the same ref object, so that you can use the value that you set previously.
As I understood, they made it because they needed to create an object in order to seal the DOM element object(in development mode) and memoize it. As you know if we are going to memoize something, we need to convert it to an object or array.
Reference:
function mountRef<T>(initialValue: T): {current: T} {
const hook = mountWorkInProgressHook();
const ref = {current: initialValue};
if (__DEV__) {
Object.seal(ref);
}
hook.memoizedState = ref;
return ref;
}
https://github.com/facebook/react/blob/master/packages/react-reconciler/src/ReactFiberHooks.js#L916
The react hooks system works with immutable values. Whenever the component is rendered, the hooks are called (for example the useState hook), and they produce a value or two (state and setter function). If this values are changed from the previous values, other hooks might be called (useEffect when the setter function is initialised).
However, sometimes we don't want to react to this changes. We don't really care what is the value as long as it's there, and we don't care if something changes it. For this cases we've got the ref:
The “ref” object is a generic container whose current property is
mutable and can hold any value, similar to an instance property on a
class.
Whenever you need to store a value, that will be used, but won't cause a re-render, nor cause useMemo, useCallback, useEffect, etc... to recompute, you can set that value via a ref. Since the ref itself will be used as part of hooks dependencies (useMemo(() => {}, [ref]), you can't update it. To enable immutability, a property inside that ref object, can be changed ref.current, without causing the dependant to recompute, since it's the same ref.
I'm trying to answer the root cause of your question, which I interpret to be something like: "It seems that we should not need an intermediate object with a .current property." You're correct. We don't. Whatever the actual reason regarding useRef, I have noticed it is possible to instead do something like the following, which has the kind of syntax you were asking for in your question (the elimination of .current):
//note foo is in array brackets by itself
const [foo /*no setFoo here*/]= useState({bar:"baz"});
...
foo.bar="hello"
or
//note foo in brackets by itself
const [foo /*no setFoo here*/]= useState([]);
...
foo.push(1);
This lets us directly mutate foo's properties without using .current. As long as we never call setFoo, mutating foo's properties will not cause a rerender on its own. The value of foo itself is never changed, since it always points to the same object or array.
However, it is possible, just as with any other variables including useRef ones, to cause a useEffect hook to rerun after a rerender if a changed property like foo.bar appears in the 2nd argument array of the useEffect.
I haven't tried let [foo]= useState("whatever") yet. In that case, we'd be altering the actual value of foo, and depending on React to give that altered value back to us on subsequent rerenders, even though we never notified React of the change. Seems sketchy.
As I understand correctly in Javascript we have two different passing: by value and by reference. By value is when we pass the strings or numbers. By reference when we pass the objects or arrays.
Last time in some project I noticed antipattern -> someone assigned new value to this.props.arrayOfNames = newArrayOfNames;
I haven't had the chance to test it, but... will it change somehow the parent's object? By reference?
this.props.arrayOfNames = newArrayOfNames is not an antipattern but a mistake. props object is immutable, this is ensured by the use of Object.freeze in development mode. The assignment of arrayOfNames property value will result in error.
If props should differ from received ones, a correct way is to use a state that derives from props with getDerivedStateFromProps hook.