VueJS component rerenders when props is unchanged - vue.js

I have a list in the vuex store with tables[t1,t2], when t1 updates i have to mutate the tables array in order to have the t1 updates.
For each table i use a <Table v-for="(item, index) in tables" :key="index" :data="item"> where it uses a Table.vue. In Table.Vue i have:
props: {
data: {
type: Object,
default: null
}
},
But when t1 changes it also forces an update to t2, which is understandable because the whole "tables" variable in store gets mutated. But I only want the component to update when there is an actual change in props, so that the t2 doesn't get updated unnecessarily. I've tried to look into memoization, which i used previously in React-projects to fix this problem. I didn't really find anything easy to use and searching "memoization vuejs" didn't really give me alot to go on.
What i do in the mutation in vuex store in order to mutate a table:
my_mutation(state, data) {
let newList = [...state.tables];
let index = newList.findIndex(i => i.tableName === data.tableName);
if (index !== -1) {
newList[index] = data.value;
}
Vue.set(state, 'currentViews', newList);
}
as you can see this code only mutate 1 element at the time in the state.tables, but sets the whole tables variable. This causes the update() function to trigger in the components (notice not created() as this only happens initally when i fetch and populate the tables variable) and rerenders the component.
Since the created() is not triggered when the mutation happens, this tells me that the component is still there and doesn't need to be recreated. The goal here is not to trigger updated() when theres no changes to the props and prevent rerendering of that particular component.

You should probably add some code because what you say you see seems strange...
Look at the following example (updated example to use Vuex...just to be sure)
first button mutate the 1st table's data at index 0. Result = only 1st component re-renders
second button completely replaces data for 2nd table. Result = only 2nd component re-renders
3rd button replaces whole tables array by a new array. Result = both component's re-render (as expected)
4th button for curious people ;) ...completely new tables array composed from existing instances. Result = no re-render at all
5th button (added after Q update)
same way of updating as in question
Result - only 2nd table rerenders! Clear contradiction to what you say...
note that if you have currentViews property in the state from the beggining, you don't need to use Vue.set - simple assignment will suffice
Update: Added updated hook with console.log into mytable component to check if logs correlate with time changes in tables headers - confirmed
Vue.use(Vuex)
const store = new Vuex.Store({
state: {
tables: [
['item 1', 'item 2'],
['item 1', 'item 2']
]
},
mutations: {
mutate1st(state) {
Vue.set(state.tables[0], 0, state.tables[0][0]+'+1')
},
mutateTables(state) {
Vue.set(state.tables, 1, ['item 3', 'item 4'])
},
replaceTables(state) {
state.tables = [
['item 1', 'item 2'],
['item 1', 'item 2']
]
},
newArrayCreatedFromPreviousOnes(state) {
state.tables = [...state.tables] // we are creating new array composed from existing instances
},
sameWayAsInQuestion(state) {
let newList = [...state.tables];
newList[1] = ['item 5', 'item 6'];
Vue.set(state, 'tables', newList);
}
}
})
const mytable = Vue.component('mytable', {
props: ['title', 'data'],
template: `
<div>
<div> {{ title }} ({{ now() }}) </div>
<div v-for="(item, index) in data" :key="index">{{ item }}</div>
<hr>
</div>
`,
methods: {
now() {
const date = new Date();
return date.toLocaleTimeString();
}
},
updated() {
console.log(`${this.title} updated at ${this.now()}`)
}
})
const app = new Vue({
store,
components: { mytable },
template: `
<div>
<button #click="mutate1st">Mutate 1st table data</button>
<button #click="mutateTables">Mutate tables array</button>
<button #click="replaceTables">Replace tables array</button>
<button #click="newArrayCreatedFromPreviousOnes">Just for curiosity...</button>
<button #click="sameWayAsInQuestion">Same way as in question</button>
<hr>
<mytable v-for="(table, index) in $store.state.tables" :key="index" :title="'Table ' + index" :data="table" />
</div>
`,
methods: {
...Vuex.mapMutations(['mutate1st', 'mutateTables', 'replaceTables', 'newArrayCreatedFromPreviousOnes', 'sameWayAsInQuestion'])
}
})
app.$mount("#app")
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.12/vue.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vuex/3.5.1/vuex.min.js"></script>
<div id="app"></div>

Update: I have another component inside Table.vue which was dependent on a store variable which was mutated when changing t1 causing the rest of the tables to change. Tip of the day: check if all the nested component isnt dependent on some store variable effected by the change

Related

How to change data in row without refreshing

Bootstrap vue table does not detect when there is a change. It shows when I refresh. How to change data in row without refreshing? After I update the data, I want the data to be updated without making a get request again.
My Data:
items = [
{ id: 1, name: 'Veli' },
{ id: 2, name: 'Berkay' },
{ id: 0, name: 'Mehmet' },
]
<b-table
:items="person"
:fields="fields"
/>
<template>
<form
v-for="(person, index) in persons"
#submit.prevent="submit"
>
<input v-model="person.name">
</form>
</template>
<template>
<div v-for="(person, index) in persons">
<span :key="index">{{person.name}}</span>
</div>
</template>
<script>
import axios from 'axios'
export default {
data() {
return {
persons: []
}
},
beforeCreate() {
this.$store.dispatch("loadPerson").then((response) => {
Vue.set(this, "persons", response);
})
},
methods: {
submit() {
axios.put('/person', this.person)
}
},
}
</script>
I think like this, we normally re-render the DOM with state. So changes in state cause the DOM to be updated.
If you don't have a lot of data, you can only update the changed data with the map method on the variable you hold your data, so I think the DOM will be updated as well.
However, if you receive a lot of data (eg thousands, millions), I think it would be better to send a GET request to the database again.
Because it would be unreasonable to use array methods on millions of data.

Vue passing data back multiple levels

i'm having issues passing a value through multiple components, I think I have got it wrong, can someone help me please:
Vue.component('layout', {
props: [ 'current' ],
template: `<deformer :current="current" #current="$emit('update:current', $event)" />`
});
Vue.component('deformer', {
props: [ 'current' ],
template: `<button #click="$emit('update:current', current + 1)">Increase {{ current }}</button>`
});
new Vue({
data: {
current: 1
},
watch: {
current: function(value) {
// Do something with value
console.log(value);
}
}
}).$mount('#app')
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
{{ current }}
<layout :current="current" />
</div>
So that button should increase the number within the main app, but if this is changed from somewhere else this change should also reflect on the button.
current should be a prop, not data. The emit message will notify the parent component to change the value, and it will trickle back down through the prop.
This design ensures only one component owns the data.

Vue stored valued through props not being reactive

So I pass value using [props] and stored it in child component's data. However, when passing [props] value changes from parent, it's not updating in child component's data. Is there a fix for this..?
Here is the link to w3 test (I tried to clarify the problem as much as possible here)
<div id='app'>
<div id='parent'>
<button #click='current_value()'>Click to see parent value</button>
<br><br>
<button #click='change_value($event)'>{{ txt }}</button>
<br><br>
<child-comp :test-prop='passing_data'></child-comp>
</div>
<br><br>
<center><code>As you can see, this methods is <b>NOT</b> reactive!</code></center>
</div>
<script>
new Vue({
el: "#parent",
data: {
passing_data: 'Value',
txt: 'Click to change value'
},
methods: {
current_value(){
alert(this.passing_data);
},
change_value(e){
this.passing_data = 'New Vaule!!';
this.txt = 'Now click above button again to see new value';
e.target.style.backgroundColor = 'red';
e.target.style.color = 'white';
}
},
components: {
"child-comp": {
template: `
<button #click='test()'>Click here to see child (stored) value</button>
`,
props: ['test-prop'],
data(){
return {
stored_data: this.testProp
}
},
methods: {
test(){
alert(this.stored_data);
}
},
watch: {
stored_data(){
this.stored_data = this.testProp;
}
}
}
}
});
Props have one way data flow, that's why it doesn't react when you update it from the parent component. Define a clone of your prop at data to make it reactive, and then you can change the value within the child component.
Short answer: you don't need stored_data. Use alert(this.testProp) directly.
Long answer: when child component is created, stored_data get it's value from this.testProp. But data is local, it won't change automatically. That's why you need to watch testProp and set it again. But is not working because of a simple mistake, your watch should be:
watch: {
testProp(){ // here was the mistake
this.stored_data = this.testProp;
}
}

Vue.js emit, pass array to child,map the array in child and emit event to parent, developer tool shows array is updated but the page not updated

I am passing the array drinks as props to Drinks.vue and map elements inside of Drinks.vue
When I click the button, it is supposed to emit the event to change the element to 'Sold Out'.
However, when debugging using console.log or Vue developer tool, it shows the drinks array is updated, but the page itself doesn't re-render or update...
Fruits works well. Do you know why Drinks.vue could not update drinks array?
App.vue
<template>
<div>
<Header :shop_name="shop_name" v-on:change_shop_name_event="change_shop_name($event)" />
<Fruits :stocks="fruits" v-on:sell_fruit_event="update_fruit_status($event)" />
<Drinks :stocks="drinks" v-on:sell_drink_event="update_drink_status($event)" />
<Footer :shop_name="shop_name" />
</div>
</template>
<script>
import Header from "./components/Header.vue";
import Fruits from "./components/Fruits.vue";
import Drinks from "./components/Drinks.vue";
import Footer from "./components/Footer.vue";
export default {
name: "App",
components: {
Header,
Fruits,
Drinks,
Footer,
},
data: function () {
return {
shop_name: "Zowie Grocery",
fruits: {
Peach: 10,
Apple: 10,
Blueberry: 10,
Strawberry: 10,
Mongo: 10,
Watermelon: 10,
},
drinks: ["beer", "orange juice", "milk shake", "tea", "coke"],
};
},
methods: {
change_shop_name: function (payload) {
this.shop_name = payload;
},
update_fruit_status: function (payload) {
this.fruits[payload] -= 1;
},
update_drink_status: function (index) {
console.log(index);
this.drinks[index] = "Sold Out";
},
},
};
</script>
<style>
</style>
Drinks.vue
<template>
<div>
<h2>This is the drinks we have in stock</h2>
<div v-for="(value,index) in stocks" :key="index">
<p>{{value}}</p>
<button #click="sell(index)">Sell</button>
</div>
</div>
</template>
<script>
export default {
name: "Drinks",
props: ["stocks"],
methods: {
sell: function (index) {
this.$emit("sell_drink_event", index);
},
},
};
</script>
can you try this instead of this.drinks[index] = "Sold Out" :
this.drinks = this.drinks.map((k,idx)=> idx === index ? 'Sold Out' : k)
Arrays are not reactive the way objects are in Vue.js. While just assigning a new value to an objects attribute just like that
someObject.x = 'some value'
it's not the same for an array and its elements. Just to clarify - yes, you can modify any index of an array but this won't trigger a re-render in Vue.js because of its reactivity system. If you modify an array by its index you've to take care about re-rendering the component yourself.
However, there's still hope. If you want Vue.js's reactivity system to take care about re-renders (even for Arrays) you've to use Vue.$set as stated in the docs.
So for your example it should be like this.
this.$set(this.drinks, index, 'Sold Out');
This will not only update the array but also toggle your expected re-render.

How to pass editable data to component?

I'm working on an app that allows you to capture and edit soccer match results.
there is a Matches component that makes an AP call to get the data of multiple matches from a server (match_list) and then renders a bunch of Match components, passing the data as props to these sub-components to fill their initial values.
<component :is="Match" v-for="match in match_list"
v-bind:key="match.id"
v-bind="match"></component>
On the Match component, I accept all the values as props.
But I get a warning that props shouldn't be edited and these should be data elements instead. So I tried passing them to the component data.
export default {
name: "Match",
props: ['local_team', 'visitor_team', 'localScore', 'visitorScore', 'date', 'time', 'location', 'matchId'],
data(){
return{
id: this.id,
local_team: this.local_team,
visitor_team: this.visitor_team,
location: this.location,
date: this.date,
time: this.time,
localScore: this.localScore,
visitorScore: this.visitorScore
}
},
Now I get a warning that editable data shouldn't be based on props.
How can I make the data from the Match component editable so it safely propagates to the parent component?
You need to accept your match object on the component's props, and make a copy of it on data (to be used as a model for your inputs). When your model changes you should emit that change to the parent so that it can change its own data appropriately (which then gets passed and reflected correctly through the child's props):
In this example I watch for any changes to the model and then emit the event directly, you can of course replace that behavior by having a submit button that fires the event upon click or something.
Vue.component('match', {
template: `
<div>
<p>{{match.name}}</p>
<input v-model="matchModel.name" />
</div>
`,
props: ['match'],
data() {
return {
matchModel: Object.assign({}, this.match)
}
},
watch: {
matchModel: {
handler(val) {
this.$emit('match-change', val)
},
deep: true,
}
}
});
new Vue({
el: "#app",
data: {
matches: [{
id: 1,
name: 'first match'
},
{
id: 2,
name: 'second match'
}
]
},
methods: {
onMatchChange(id, newMatch) {
const match = this.matches.find((m) => m.id == id);
Object.assign(match, newMatch);
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.16/vue.min.js"></script>
<div id="app">
<match v-for="match in matches" :match="match" :key="match.id" #match-change="onMatchChange(match.id, $event)"></match>
</div>