I have been working on a Vue 2 project for a while, and upon upgrading our linting requirements I discovered that we had prop mutation errors in many of our child components. In our project, we pass a singleton object as a prop to many components and were originally updating the object directly from the child components. Vue seems to suggest using the v-bind.sync feature for updating props from child components (or using the equivalent v-bind and v-on). This, however, doesn't solve the issue of prop modification from nested components in an array.
Take this (pseudo)code for example that uses prop mutation:
Note: Assume const sharedObject: { arrayElements: Array<{ isSelected: boolean }> } = ...
Page.vue
<template>
...
<Component1 :input1="sharedObject" />
...
</template>
Component1.vue
<template>
...
<template v-for="elem in sharedObject.arrayElements">
<Component2 :input2="elem" />
</template>
...
</template>
Component2.vue
<template>
...
<q-btn #click="input2.isSelected = !input2.isSelected"></q-btn>
...
</template>
What is the proper way of updating a property like input2.isSelected from nested components in Vue 2? All of the approaches I've thought of are flawed.
Flawed Approaches
I believe that we would like to bubble up that input2.isSelected has been modified in Component2 to Page.vue, however, this seems to either lead to messy code or an uneasy feeling that we are just suppressing linting errors in a roundabout way.
To demonstrate the "messy code" approach, first note that Page.vue does not know the index of of elem in sharedObject.arrayElements. Therefore, we would need to emit an object to Page.vue from Component1 which contains the state of input2.isSelected as well of the index of elem in sharedObject.arrayElements. This gets messy quickly. What about the example where we have:
Component1.vue
<template>
...
<template v-for="elem in sharedObject.arrayElements">
<template v-for="elem2 in elem.arrayElements">
<Component2 :input2="elem2" />
</template>
</template>
...
</template>
in this case, then we could need to pass up 2 indices! It doesn't seem like a sustainable solution to me.
The alternative that I thought of is a callback function (passed as a prop through the component hierarchy) that takes as input the element we want to update and an object that contains the properties we want to update (using Object.assign).
This makes me very uneasy since I don't know the real reason why we can't update a pass-by-reference prop from a child component. To me, it seems like it's just a roundabout way of updating the passed-in from Component2 without the linter noticing. If there is some magic modification that happens to props when they're passed to child components, then surely passing in the object that I received in Component2 to the callback function and modifying it in the parent component would basically just be updating the prop in the child component, but more complicated.
What is the proper way of approaching this problem in Vue 2?
Very good question and analysis of the current state of this long-standing issue in Vue ecosystem.
Yes, modifying "value type" props from the child is a problem as it creates runtime issues (parent overwriting the changes when re-rendered) and thus Vue generates a runtime error when this happens...
Modifying a property of object passed as prop is OK from the "code works fine" POV. Unfortunately there are some influential people in the community with opinion (and many who blindly follow them) that this is an anti-pattern. I do not agree with that and raised my arguments many times (for example here). You described the reasons very well - it just creates unnecessary complexity/boilerplate code...
So what you are dealing with is really just a linting rule (vue/no-mutating-props). There is an ongoing issue/discussion that proposes the configuration option that should allow to ease the strictness of the rule with many good arguments but it gets very little attention from the maintainers (feel free to raise your voice there too)
For now what you can do is:
Disable the rule (far from perfect but luckily thanks to a Vue runtime error you can catch the real incorrect cases during development just fine)
Accept the reality and use workarounds
Workaround - use global state (store like Vuex or Pinia)
Note: Pinia is preferred as next version of Vuex will have same API
General idea is to place the sharedObject in the store and use props only to navigate the child components to the right object - in your case the Component2 will receive an index by prop and retrieve the right element from the store using it.
Stores are great for sharing the global state but using is just to overcome the linting rule is bad. Also as a result, components are coupled to the store hence both reusability suffers and testing is harder
Workaround - events
Yes it is possible to create mess and lot of boilerplate code using only events (especially if you nest components more than 2 levels) but there are ways to make things cleaner.
For example in your case Component2 does not need to know the index as you can handle the event like this
// Component1
<template>
...
<template v-for="elem in sharedObject.arrayElements">
<template v-for="(elem2, index) in elem.arrayElements">
<Component2 :input2="elem2" #update="updateElement($event, index)" />
</template>
</template>
...
</template>
In your case, the Component2 handles only change of single boolean property so $event can be simple boolean. If there are more than one property to be changed inside Component2, $event can be an object and you can use object spread syntax to "simplify" the Component2 (using one event instead of multiple - one for each property)
// Component2
<template>
...
<input v-model="someText" type="text">
<q-btn #click="updateInput('isSelected', !input2.isSelected)"></q-btn>
...
</template>
<script>
export default {
props: ['input2'],
computed: {
someText: {
get() { return this.input2.someText },
set(newVal) { updateInput('someText', newVal) }
}
},
methods: {
updateInput(propName, newValue) {
const updated = { ...this.input2 } // make a copy of input2 object
updated[propName] = newValue // change selected property
this.$emit('update', updated) // send updated object to parent
}
}
}
</script>
Well...I prefer just to disable the rule and set some clear naming conventions to indicate that the component is responsible for changing it's input...
Note that there are other workarounds like using this.$parent, inject\provide or event bus but those are really bad
Related
I am trying to get my head wrap around how props are passed to child components and how they are updated from there.
// parent.vue
<template>
<div>
<div v-for="elem in getCurrentArray">
<child-component :elem="elem"></child-component>
</div>
<button #click.prevent="currentIdx--">Prev</button>
<button #click.prevent="currentIdx++">Next</button>
</div>
</template>
<script>
export default {
components : {
ChildComponent
},
data(){
return {
arr : [ ["A", "B", "C" ], [ "D", "E" ]], // this would be populated from vue store
currentIdx : 0
}
},
computed : {
getCurrentArray(){
return this.arr[this.currentIdx]
}
},
}
</script>
// child.vue
<template>
<div>Prop: {{elem}} <input type="text" v-model="myinput" #blur="save" /></div>
</template>
<script>
export default {
props : [ "elem" ],
data(){
return {
myinput : this.elem
}
},
methods : {
save(){
// this.$store.dispatch("saveChildElement", { myinput }
}
},
mounted(){ console.log( this.elem + " being rendered" ) }
}
</script>
In this example, I have two sets of arrays ['A','B','C'] and ['D','E']. On page load, the first set is rendered thru child components.
Looks good. However, when I click next to go to the second set, I get this:
So, while the props are being passed correctly, the textbox input values are not updated. When I checked the console.log, it's clear that vue does not re-render child components for "D" and "E". Instead, it simply uses the existing components for "A" and "B".
Is there a way to force vue to re-render the components? If not, how can I make sure that the textbox input gets the latest prop values? Keep in mind that I want to be able to save changes to the input values via vue store.
Add a key as cool-man says, it will fix the issue.
However, your check for component re-rendering is flawed, you used the child mounted life cycle, and because you see it running only once you think the component isn't re-rendering. But this is wrong. "mounted" happens only once in a component life cycle, and because you're using the same component for ['A','B','C'] and then for ['D','E'] vue knows not to re-create the components and simply re-renders them with the new props.
Try to add the :Key prop in the v-for loop
<div v-for="elem in getCurrentArray" :key="elem">
To give Vue a hint so that it can track each node’s identity, and thus reuse and reorder existing elements, you need to provide a unique key attribute for each item:
... List Rendering - Maintaining State
Keys must be unique, if you have two or more same value in your array, for example ['A',' B', 'A'] it will create a conflict, the best way to overcome this is to add a generated unique 'ID' for each instance.
Or In alternative (a short term solution)
You can use the loop index in combination with the array value, this should give you a more or less unique key.
<div v-for="(elem, index) in getCurrentArray" :key="elem + index">
You can ask your component to re-render. (The other answers of updating the key is correct).
However, if you do want to use Vuex to manage state, I suggest you do that first as there is (essentially) a different mechanism that will do that re-render for you, and feel much easier. Namely the “computed properties” will likely solve that for you.
I remember getting stuck on this when I was starting out, and once I implemented Vuex I wished I had just done it that way, which kind of just solved the problem for me as part of its workflow. (It will likely mean you don’t need to update key, but you have that in case you have a complex situation when you still need to force a re-render).
Edit:
As #JonyB rightly pointed out, you are trying to re-render the whole component. If you think about it, what you really want to do here is not re-render the whole component, but merely update the state. Therefore, once you implement Vuex, it will likely solve this for you as it allows you to deal with state separately.
All you need to add the key to the v-for loop and it will work.
Replace
<div v-for="elem in getCurrentArray">
with
<div v-for="elem in getCurrentArray" :key="elem">
Ref: https://v2.vuejs.org/v2/guide/list.html#v-for-with-a-Component
From React documentation.
Conceptually, components are like JavaScript functions. They accept
arbitrary inputs (called “props”) and return React elements describing
what should appear on the screen.
Considering:
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}
or
class Welcome extends React.Component {
render() {
return <h1>Hello, {this.props.name}</h1>;
}
}
Will give us the ability to do this:
<Welcome name="Luke" />;
<Welcome name="Leia" />;
to use as we wish in the DOM,
Hello, Luke
Hello, Leia
Now when people prescribe props shouldn't be changed, it would make sense the reason is in my thinking would be like the same as changing the values of attributes of an image tag?
HTML:
<img id="Executor" alt="Picture of Executor" src="/somepath/vaders-star-destroyer-executor.jpg"/>
JS:
Meanwhile in a Javascript file a long time ago in a galaxy far, far away...
var imageOfVadersStarDestroyer = document.getElementById('Executor');
imageOfVadersStarDestroyer.src = "/somepath/vaders-star-destroyer-avenger.jpg"
Because if we keeping changing an elements attribute values this can cause confusion and slower renderings?
So is the reason why the prescription is to never change props in React is because is the library is trying to make elements as predictable as possible?
Setting props outside of React is dangerous and should be avoided. Why? The main reason is that it doesn't trigger re-renders. Hence bugs and unexpected behaviour.
Re-rendering
Most of the time, props are data that is store as state in the parent component, which is manipulated by calling setState() (or the second function returned by React.useState()). Once setState() is called, React re-renders and computes what has changed under the hood, with the latest props and state. Manually assigning values to props, therefore won't notify React that the data has changed and something has to be re-rendered.
The good practice
Making props read-only allows React components to be as pure as possible, which is obviously a good practice anyway even when writing plain JS. Data won't be changed unexpectedly and can only be done so by calling setState() (You might have heard of the single source of truth, which is what React is trying to leverage).
Imagine you notice something went wrong in the app and the data shown to the end user is completely different from the source, it would be a pain trying to find out where the data has been manipulated wouldn't it? :)
never change props in React
means that you should never do this.props.name = "userName" because of React's one way data binding, props are read only, to update a component's props, you should pass a function from the parent that will do that ( in the parent ) , or dispatch an action if you're using redux, a change in the props will trigger a re-render
props is a constant in this case. You will always need it in your components.
But there is a cleaner way to write it or even omit it.
Regular way with Function Expression (same as your exemple)
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}
ES6 Object Destructing - explicit
function Welcome(props) {
const {name} = pros
return <h1>Hello, {name}</h1>;
}
ES6 Object Destructing - inplicit, cleaner way
function Welcome({name}) {
return <h1>Hello, {name}</h1>;
}
And of course, you can use the class way which requires the usage of this.props.yourAttr
However, in the new version 3 of create-react-app, changed class components to functional components. You can see this exact modification on Github here.
You can need to learn more about destructing assignment in the old and good MDN linked here or an in-depth approach both array and object destructuring here.
I have a component who initialized like this
<custom :opts="{map: false}"></custom>
and there is HTML similar to this
<template id="custom">
<div v-if="opts.map">
I'm awesome
</div>
<button v-on:click="show"></button>
</template>
where
function show(){
this.opts = {map:true} // (1) <-- This is working and I could see hidden div
this.opts.map = true // (2) <-- For some reason not working
Vue.set(this.opts, 'map', true) // (3) <-- Still not working
}
So my question is why variant 2 doesn't work and what should I change to make my control react to value reset on a button click. Or a proper explanation why (1) is working, but (2) isn't - also will be accepted as an answer.
The real problem with the code (all 3 versions) is changing a component's property from within a component. In idiomatic Vue, only the parent should change properties. If a component needs to effect a change, it should emit an event to the parent and let the parent make the necessary changes. Otherwise, there is ambiguity in which component "owns" the property.
One Way Data Flow
All props form a one-way-down binding between the child property and the parent one: when the parent property updates, it will flow down to the child, but not the other way around.
Sending Messages to Parents with Events
Can be off base here but I believe this happens because in vue component props are not reactive, so their objects are not being observed in depth. Or rather they are "a little bit reactive", reassigning the root prop does cause the DOM update but is not expected to be done manually and you'll see a warning when doing such on development build:
[Vue warn]: Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders. Instead, use a data or computed property based on the prop's value. Prop being mutated: "..."
And for as why props are not completely reactive in the first place: https://v2.vuejs.org/v2/guide/components-props.html#One-Way-Data-Flow
To work around the whole issue you must pass any necessary props to the component data and if those props were passed as nested objects you might also want to completely avoid mutating them from within the component since it will propagate to the parent which, unless clearly mentioned, can be a source of bad news.
Let's say I have a button component that is imported in several other components. I want the child component to not be coupled to any one type of logic that happens when the button is clicked. So I want to hold that logic in the various components that leverage this button component.
I think there are at least 2 ways of going about this.
Have the child emit an event to the parents, and then let the parents define the handler.
Define the handlers in the parents and pass it down as props to the button component.
I'm used to doing the latter in React. Is there a best practice in vue for this situation?
The Vue philosophy is props down, events up. The first option follows that closer as the event itself is emitted (up) to the parent and then handled.
Also within a Vue SFC you have the added benefit of prefixing the bound attribute with a v-on (or #) which describes its intent as an event traveling up and not a v-bind (or :) which implies it's a prop even though its really a callback to an event.
Vue.js events are callbacks, they are not DOM events. You can verify this, since you add a custom name to the event listener and not a DOM event name (click, focus...), and there is no event object passed to the function, unless you specify an $event argument in the $emit call.
Events
Pros
For libraries: keeps it lighter and clients have more flexibility on methods usage
Helpful Vue devtools event logging
Allow global listener (this.$root.on), although this can be better enhanced by Vuex.js.
Differentiated syntax: : for props and # for events/methods
Cons
Less explicit, harder to debug (fail silently if there are no listeners or the event name is misspelled)
Props
Pros
More explicit, are declarative, can be defaulted, required, validated, what turns them easier to debug (runtime errors or compilation errors in TypeScript)
Cons
Have to include props validation so you don't have to check if a function() prop exists before calling it (but using props validation is a good practice anyway...)
Conclusion
Looks like the approaches are more convention and personal preference over anything else, although I think that if it wasn't for the Vue.js documentation giving preference to the events approach, everybody would be gladly using props only, which in my opinion is better (clearer).
Props can do everything events do, except for a few cases (like $root event listening pattern - noting Vuex.js replaces this feature and is preferred for scalability), with the advantage they are more explicit, debuggable and check-prone.
Summarized from: https://forum.vuejs.org/t/events-vs-callback-props/11451
As a newbie perspective migrated from React, I don't know why #event even exists (or like the answers above - being the standard). I can't declare which events a component would $emit?, but I can easily see which props are passed down. And by a good naming, I will be able to know which one is actually a callback event.
Best Practice
Best practice would be option number 1. You can see this practice being used in the official documentation: https://v2.vuejs.org/v2/guide/components.html#Sending-Messages-to-Parents-with-Events
Performance
As long as you pass a reference to a function to be executed when using the event bus or passing down as a prop, you should see almost no performance difference.
Example using option number 1
You can use this.$emit('eventName', dataToSend, ...) to send the data to the parent component that would then listen on the component like this <my-component #eventName="yourHandler" />. You would then be able to use different logic for each button.
I have created a fiddle for a multi-select component that implements this: https://jsfiddle.net/wkdL0xbc/
// HTML
<div id="app">
<multi-choice :items="myItems" #selected="alert($event)"></multi-choice>
<multi-choice :items="myItems" #selected="sayIsCool"></multi-choice>
</div>
// JavaScript
const multiChoice = {
template: '<div class="multi-choice"><span v-for="item in items" #click="select(item)">{{ item }}</span></div>',
props: ['items'],
methods: {
select(item) {
this.$emit('selected', item);
}
}
};
new Vue({
el: "#app",
data() {
return {
myItems: [
'Homer',
'Marge',
'Bart'
],
}
},
components: {
multiChoice: multiChoice
},
methods: {
sayIsCool(item) {
alert(item + ' is cool!')
}
}
})
You’re looking for “Transparent Wrappers”
Vue's customs event works different from a native DOM event. So you need to attach .native property to the event
But if you want the event to happen on the child, then you define a computed property that will return and an object of listeners. And now you won't
By default, attributes not defined as props will be added to the root element of the view
So you can set inheritAttrs: false and then bind the $attrs to the child and it then becomes the target for those attributes
Now you don't have to think about what the root component is.
Chris Fritz does a great job explaining how they work in his 7 secret patterns talk. Starts around 21:44 https://youtu.be/7lpemgMhi0k?t=21m44s
I think this depends if we don't give a better context.
Consider props vs event is like pull vs push, quite similar to any pub-sub system.
When passing props, we inject (push) the dependencies of parent context to child context, and then child context can be polluted by parent context, not just holding the weak ref to the parent, any effect from a parent is now also executed within child context. This is also coupled between parent-child.
Consider event pulling, which parent is listening event from child, now every event data is preferably a copy value instead of ref, we don't have coupling issue between parent-child. In case we have event, we have also control by queue or custom modifier so that the usage from parent is easier to maintain (like we don't have to maintain debounce, throttle on parent context, but expect by event modifier, it should be done within child context, in this case is the Button component).
Imagine I have a <progress-bar> UI component and I want to create an <app-progress-bar> component that is the same as the <progress-bar> but with some style tweaks.
AppProgressBar.vue:
<template>
<progress-bar class="app-progress-bar"></progress-bar>
</template>
This will break because <progress-bar> requires a percent prop. So I can pass it in:
<progress-bar class="app-progress-bar" percent="percent"></progress-bar>
This seems fine, but is brittle when <progress-bar> has many props. Can I simply pass through all props? Theoretical syntax:
<progress-bar class="app-progress-bar" {...props}></progress-bar>
Which is similar to React/JSX.
Like already answered, extending the component would be a nice way to create your components in this scenario. Similar to class inheritance in other languages.
However, you can also pass an object as prop. If you want to keep things clean, and don't want to extend your components, you could pass a prop like this:
//object with many values that you need to pass. for ex.
myObjectWithSuff: { percent: 10%, someOtherStuff: value, ... }
<progress-bar class="app-progress-bar" myProp="myObjectWithStuff"></progress-bar>
Inside the progress-bar component, declare the prop myProp. Then you can access any properties on that object. For example: this.myProp.percent.
This is just quick and simple, but depending on your architecture, extending components may be the way to go.
After researching this, I don't think it's possible. Instead, you can extend a component and put an extraClass key on data or computed:
import ProgressBar from './ProgressBar'
export default {
extends: ProgressBar,
data() {
return { extraClass: 'app-progress-bar' }
}
}
ProgressBar.vue:
<template>
<div class="progress-bar" :class="extraClass">...</div>
</template>
This isn't as clean or flexible as React, but it works.
Abstract
This can be done by binding $attrs to the child. So in your case it would be:
<template>
<progress-bar v-bind="$attrs" class="app-progress-bar"></progress-bar>
</template>
Link to Docs
vm.$attrs: Contains parent-scope attribute bindings (except for class and style)...and can be passed down to an inner component
Source: Vue $attrs documentation
Example
Let's say that a component called <inner> has a boolean prop called dark and a string prop called color.
Now let's define an outer component called <outer> which wraps <inner> in its definition.
<template>
<inner></inner>
</template>
.
.
.
name: 'outer'
The issue here is if we use the <outer> component, we can't transfer the dark and color props through to the <inner> component which knows what to do with them. For example <inner dark color="blue"></inner> works, but <outer dark color="blue"></outer> doesn't.
If you change the definition of <outer> to include the $attrs binding, it will transfer all the props for you (this doesn't include style and class). Here is the new definition:
<template>
<inner v-bind="$attrs"></inner>
</template>
.
.
.
name: 'outer'
Now you can use <outer dark color="blue"></outer>.
Additional note
You can also use the inheritAttrs prop to prevent this behaviour by default. You can take a look at the official Vue docs here and I also found this JSFiddle which gives an example of inheritAttrs being used.