Vue does not update items in v-for from Vuex with dynamic component - vue.js

We have a dynamic component for tab body, which defined as
<component :is="currentTab.itemType" :itemId="currentTab.itemId"></component>
Template has a span, which reflects itemId - it changes every time when the currentTab changed in tabs host component.
Each component of tab.itemType has Vuex module, belongs to it specific type.
For example, there is store module product with described state:
{
products: { [itemId: string]: IProduct }
}
When component created or itemId changed, it tries to run load action and put loaded product to products of vuex state.
So, there is Vue computed property, looks like
#State(productNamespace)
state: IProductState;
get currentProduct() {
return this.state.products[this.itemId];
}
or even
#Getter(GetterNames.GET_PRODUCT_BY_ID, bindingOptions)
getProductById: (itemId: string) => IProduct;
get currentProduct() {
return this.getProductById(this.itemId);
}
Each product has an attributes list, which is iterated by v-for with :key.
<v-list :key="itemId"><!-- itemId has no effect there -->
<v-list-item v-for="attribute in currentProduct.attributes" :key="attribute.id">
...
</v-list-item>
</v-list>
The problem is:
when we change itemId, the attributes list displays all attributes from last added product and does not refresh it when switching to previous "tabs" with another itemId but the same itemType.
I've tried to set :key of parent div as itemId but with no effect.
When I set :key to <component>, vuex state becomes broken.
Vue version is 2.6.10
UPDATE:
It does not work with simple property of product too:
{{ currentProduct.name }}
Summary:
There is the itemId property in. And computed property which depends on it. So computed property does not reflect changes when itemId prop changed while Vuex collection does not changed.
Confirmed:
Computed property renews only when state.products collection changed. I've emulate this by run createProduct action for each tab switching. Collection in vuex state accepts unwatched product stub and reflect changes to legal currentProduct with given itemId
UPDATE 2: component with watcher. Still no way...
#Component
export default class Product extends Vue {
#Prop({ type: Object, required: true })
readonly tabItem: ITabItem;
#State(productNamespace)
state: IProductState;
itemId: string;
created() {
//...
this.initCurrentProduct();
}
// No changes until state.products was changed.
get currentProduct(): IProduct | {} {
if (!this.state) return {};
return this.state.products[this.itemId];
}
#Watch('tabItem')
onTabItemChanged()
{
DEBUG && console.log('Tab changed: keep moving!');
this.initCurrentProduct();
}
private async initCurrentProduct() {
const { isNew, itemId } = this.tabItem;
if (itemId === this.itemId)
return;
DEBUG && console.log('ItemId changed.');
this.itemId = itemId;
// ...
}
// ...
}

Okay so the property you're passing to the dynamic component is currentTab.itemId which means itemId is actually an element in the currentTab object not the root Vue data object?
Vue does not track nested objects by default, it will only trigger redraw when the entire object is changed (for example if you do something like currentTab = {...}). You can either:
Use a watcher on currentTab with deep: true attribute: https://v2.vuejs.org/v2/api/#watch, and then trigger redraw with this.$forceUpdate whenever it is called.
Move itemId to the root of data and just update it from there

in your vuex mutation
let items = [...state.items]; // create a new copy
// mutate it
items.map(item => item.selected = true);
// return the new copy
state.items = items;

Related

View not updating on Set changes in Vue.js

In Vue.js, I have this pieces of code (with Typescript Vue.js 2 and classed components):
toggle(id: string): void {
if (this.selectedIds.has(id)) {
this.selectedIds.delete(id);
} else {
this.selectedIds.add(id);
}
}
and
get handledUsers() {
return this.users.map((user) => ({
...user,
selected: this.selectedIds.has(user._id),
}));
}
where selectedIds is a Set<string>.
The problem is that in Vue.js, Set is not modified as Array, so it seems that when I update the Set, Vue.js does not detects it as if I did a .splice() of an array. How can I make the view update?
You can add a key to the element or component that you want to be updated, and then change the key value. Changing the key value of an element or a component would make theme rerender.
Here is an example:
<div :key="refreshKey" >
some content...
</div>
Changing the value of refreshKey would cause this div to rerender and update it's content. So you should define a refreshKey in your components data, and then change it's value in your toggle method, for example from true to false.

Nuxt / Vue - Do not mutate vuex store state outside mutation handlers

I have a simple cart store, to add / remove items.
When you add a product to the cart, I show a flash message with the possibility to update the quantity. Since I don't know which product is concerned in my flash message component, I use the store.
I have a simple bootstrap spinner :
<b-form-spinbutton
:value="value"
min="1"
max="26"
#change="setItems"
/>
When I change this, I want to call the setItems function of my store to update the last item added.
So I used :
data () {
return {
value: 1
}
},
methods: {
setItems (value) {
const items = [...this.$store.getters['cart/items']]
// update quantity of the last item - we suppose it's the current item
items.slice(-1)[0].quantity = value
this.$store.commit('cart/setItems', items)
}
}
I read about this error here : Vuex - Do not mutate vuex store state outside mutation handlers, so I changed v-model to value approach.
My store cart.js is :
export const state = () => ({
items: []
})
export const mutations = {
setItems (state, items) {
state.items = items
},
// ....
I can't figure how to handle this problem ?
The line were the store state is mutated outside mutation handler is:
items.slice(-1)[0].quantity = value
By using spread operator you create a shallow copy of store items. each item property still references the vuex state. you can create a deep copy, with:
const items = JSON.parse(JSON.stringify(this.$store.getters['cart/items']))
Or, if you have lodash in your project:
const items = _.cloneDeep(this.$store.getters['cart/items'])

How to avoid rerendering all child components which are created by v-for directive

There is a list of child component
<question-list-item
v-for="(item, index) in questionListParsed"
:key="item.id"
:both-question="item"
:class-id="classId"
:subject-id="subjectId"
:index="index+1"
/>
and the questionListParsed is a getter in vuex.
/**************************************************************************
* getters
**************************************************************************/
get questionListParsed(): QuestionListItemRes[] {
const { questionList, showingOriginalQuestion } = this
const questionListParsed = questionList.map((e) => {
const recommendQuestion = e.recommendedQuestions[0]
const recommendQuestionIds = showingOriginalQuestion[e.questionNumber]
let arr = []
if (recommendQuestionIds) {
arr = recommendQuestionIds.filter((item) => {
return !this.removedRecommendQuestionIds.includes(item)
})
}
return {
recommendQuestion: {
...recommendQuestion,
stem: recommendQuestion.question,
knowledges: splitMultiKnowledge(recommendQuestion.knowledge),
questionSourceList: recommendQuestion.sources,
categoryId: recommendQuestion.categoryId,
},
originalQuestion: {
...e,
id: e.questionNumber,
stem: e.question,
difficulty: e.complexity,
knowledges: splitMultiKnowledge(e.knowledge),
},
id: recommendQuestion.id,
questionSimilarId: e.questionNumber,
mistakeAnswerId: e.id,
targetExerciseId: e.targetExerciseId,
status: recommendQuestion.status,
}
})
return questionListParsed
}
and the questionListParsed is mainly depends on the state questionList whitch is the originnal data from server side. Now i change questionList by the following way
#Mutation
updateQuestionListByIndex(data: UpdateParams): void {
if (data.value) {
const temp = [...this.questionList]
temp[data.index] = data.value
this.questionList = temp
}
}
and commit the mutation inside an Action like these
this.context.commit('updateQuestionListByIndex', {
index: targetIndex,
value: originQuestion[0],
})
I just want to change one item in the array questionList and then questionListParsed changed.
The expectation is that only one component updated but all of the child component updated(use console.log('updated') in its updated hocks).
How to do that?
The reason why all components are updated is because you use computed property (Vuex getters are Vue computed properties).
Whenever anything in questionList is changed, questionListParsed is recomputed and because you are using map and generating new objects, the result is a new array with completely new objects --> every child in list is updated
I would not consider it a problem because in reality only the DOM elements of the changed item are updated (that is the beauty of virtual DOM). If you do see some performance problem, the way around it is to stop using computed/getters and instead do the transformation only once when data is loaded and continue to work only with questionListParsed
You don't need to prevent the child components from rerendering, Vue does that for you. By providing a unique key to each list element :key="item.id" you give Vue a hint about the item, so Vue can identify and reuse the already rendered parts.
See https://v2.vuejs.org/v2/api/#key for more information.

Tracking a child state change in Vue.js

I have a component whose purpose is to display a list of items and let the user select one or more of the items.
This component is populated from a backend API and fed by a parent component with props.
However, since the data passed from the prop doesn't have the format I want, I need to transform it and provide a viewmodel with a computed property.
I'm able to render the list and handle selections by using v-on:click, but when I set selected=true the list is not updated to reflect the change in state of the child.
I assume this is because children property changes are not tracked by Vue.js and I probably need to use a watcher or something, but this doesn't seem right. It seems too cumbersome for a trivial operation so I must assume I'm missing something.
Here's the full repro: https://codesandbox.io/s/1q17yo446q
By clicking on Plan 1 or Plan 2 you will see it being selected in the console, but it won't reflect in the rendered list.
Any suggestions?
In your example, vm is a computed property.
If you want it to be reactive, you you have to declare it upfront, empty.
Read more here: reactivity in depth.
Here's your example working.
Alternatively, if your member is coming from parent component, through propsData (i.e.: :member="member"), you want to move the mapper from beforeMount in a watch on member. For example:
propsData: {
member: {
type: Object,
default: null
}
},
data: () => ({ vm: {}}),
watch: {
member: {
handler(m) {
if (!m) { this.vm = {}; } else {
this.vm = {
memberName: m.name,
subscriptions: m.subscriptions.map(s => ({ ...s }))
};
}
},
immediate: true
}
}

Can't copy props to model data and render it in Vue 2

I'm having this problem that looks a lot like a bug to me and I can't figure out how to solve it.
I created a generic list component and I tell it what child component it should insert in each item and what are the data it should pass to the child component. I'm passing everything as props along with the list (array) itself.
The problem is that I can't mutate the list props. So I try to copy it to model attribute. Otherwise I get this error:
Avoid mutating a prop directly since the value will be overwritten
whenever the parent component re-renders.....
And I can't just make it work in any of the lifecycle events. When I save the file and the hot-reloading reloads the page, the list is there, rendered, full of items. When I press F5 to manually reload the page, it is no more. Everything seems to be alright with code though
So in the parent component I'm doing this:
<List ref="link_list"
:list="this.foo.links" //this is array
:child="'LinkFormItem'" //this is the name of the child component
:section_name="'Links'"
:defaults="{content: '', type: 'facebook'}" />
In the List component I get this:
Template
<li class="" v-for="item in datalist">
<component :is="child" :item="item" ></component>
<button v-on:click='remove(index++)' type="button" name="button" class='red button postfix small'>Remove</button>
</li>
Script
<script>
import Child1 from './Child1'
import Child2 from './Child2'
export default {
name: 'search',
props: ['child', 'list', 'defaults','section_name'], //it is received as 'list'
components: {
Child1, Child2
},
data () {
return {
index: 0,
datalist: [] //i'm trying to copy 'list' to 'datalist'
}
},
beforeMount: function () {
// i'm copying it
for(var k in this.list){
this.datalist.push(this.list[k])
}
},
methods: {
//and here I should change it the way I want
add: function () {
this.datalist.push(this.defaults)
},
getList () {
return this.datalist;
},
remove(index){
var datalist = [];
for(var k in this.datalist){
if(k != index) datalist.push(this.datalist[k]);
}
this.datalist = datalist;
}
}
}
</script>
I don't see any problems with my Script. What is going on??
#edit
Ok, some console.log later I found out what the problem seems to be. The HTTP Request is really taking much longer than the mounting of the component to happen. But when it happens, it is not triggering the update in the list component. Nothing is re-rendered and the list is empty.
Workaround
well I realised the problem was related to propagation. I made a few changes in the code to asure the parent component was updating and changing the model value. but the child component (the list component) was not receiving it.
then I gave up trying to understand why and did the following:
1- used the ref in the child component to force an update in the child component with $forceUpdate and then I was assigning the props to the model in the beforeUpdate event. It was causing an error: an re-rendering loop. The update caused a new update and so on. We could just use a flag to stop it.
2- Instead I just called a child method directly:
this.$refs.link_list.updateList(data.links);
I hate this approach because I think it's way too explicit. But it did the job. Then in the child component a new method:
updateList(list){
this.datalist = list;
}
3- The other possibility that passed through my mind was emitting an event. But I didn't try, too complicated
You can simply do like as follows
data () {
return {
index: 0,
datalist: this.list // to copy props to internal component data
}
},
Once you done about you need to apply data manipulation opertions on new this.datalist , not on this.list
If you don't want to mutate the original list array you can do this:
data () {
return {
index: 0,
datalist: Object.assign({}, this.list)
}
}
I think this will help you