Vue and Vuex: computed property isn't called when changing state - javascript

I'm quite new with Vue and Vuex so please bear with me.
I want to make the computed function versions() get called when I change state.template, but I'm failing to do so. More specifically, when state.template.versions changes.
This is part of the component that I want to re-render when state.template.versions changes. You can also see the computed property versions() which I want to be called:
<el-dropdown-menu class="el-dropdown-menu--wide"
slot="dropdown">
<div v-for="version in versions"
:key="version.id">
...
</div>
</el-dropdown-menu>
...
computed: {
...mapState('documents', ['template', 'activeVersion']),
...mapGetters('documents', ['documentVersions', 'documentVersionById', 'documentFirstVersion']),
versions () {
return this.documentVersions.map(function (version) {
const v = {
id: version.id,
name: 'Draft Version',
effectiveDate: '',
status: 'Draft version',
}
return v
})
},
This is the getter:
documentVersions (state) {
return state.template ? state.template.versions : []
},
This is the action:
createProductionVersion (context, data) {
return new Promise((resolve, reject) => {
documentsService.createProductionVersion(data).then(result => {
context.state.template.versions.push(data) // <-- Here I'm changing state.template. I would expect versions() to be called
context.commit('template', context.state.template)
resolve(result)
})
This is the mutation:
template (state, template) {
state.template = template
},
I've read that there are some cases in which Vue doesn't detect chanegs made to an array, but .push() seems to be detected. Source: https://v2.vuejs.org/v2/guide/list.html#Caveats
Any idea on why the computed property is not being called when I update context.state.template.versions?

The issue may come from state.template = template. You guessed correctly that it was a reactivity issue, but not from the Array reactivity, but the template object.
Vue cannot detect property addition or deletion. This includes affecting a complex object to a property. For that, you need to use Vue.set.
So your mutation should be :
template (state, template) {
Vue.set(state, "template", template)
},
https://v2.vuejs.org/v2/guide/reactivity.html#Change-Detection-Caveats

Your function won't get called because this is wrong:
context.state.template.versions.push(data)
context.commit('template', context.state.template)
the context.state object just points to your current state nothing more.
My suggested solution will be:
First You need to declare your store state correctly
state: {
template: {
versions: []
}
}
You need to update your getter to look like this with no unnecessary
conditioning:
documentVersions: state => return state.template.versions,
add a new mutation
ADD_VERSION: (state, version) => {
state.template = {
...state.template,
versions: [...state.template.versions, version]
};
}
your action should like this now:
createProductionVersion({commit}, data) {
return new Promise((resolve, reject) => {
documentsService.createProductionVersion(data).then(result => {
commit('ADD_VERSION', data);
resolve(result);
});
});
}
In your component I suggest to change your computed property from a function
to an object that contains a get and set methods (set is optional)
versions: {
get() {
return this.documentVersions.map(function (version) {
const v = {
id: version.id,
name: 'Draft Version',
effectiveDate: '',
status: 'Draft version',
}
return v
})
}
},

I think this error occurred because you did not declare your store state correctly. Make sure you have the versions property in your template object.
state: {
template: {
versions: []
}
}
This way, any changes in the versions property will be detected by vue.

Related

Vuex Mutation running, but component not updating until manual commit in vue dev tools

I have a vue component that I can't get to update from a computed property that is populated from a service call.
Feed.vue
<template>
<div class="animated fadeIn">
<h1 v-if="!loading">Stats for {{ feed.name}}</h1>
<h2 v-if="loading">loading {{ feedID }}</h2>
</div>
</template>
<script>
export default {
data: () => {
return {
feedID: false
}
},
computed: {
feed(){
return this.$store.state.feed.currentFeed
},
loading(){
return this.$store.state.feed.status.loading;
}
},
created: function(){
this.feedID = this.$route.params.id;
var fid = this.$route.params.id;
const { dispatch } = this.$store;
dispatch('feed/getFeed', {fid});
}
}
</script>
That dispatches 'feed/getFeed' from the feed module...
feed.module.js
import { feedStatsService } from '../_services';
import { router } from '../_helpers';
export const feed = {
namespaced: true,
actions: {
getFeed({ dispatch, commit }, { fid }) {
commit('FeedRequest', {fid});
feedStatsService.getFeed(fid)
.then(
feed => {
commit('FeedSuccess', feed);
},
error => {
commit('FeedFailure', error);
dispatch('alert/error', error, { root: true });
}
)
}
},
mutations: {
FeedRequest(state, feed) {
state.status = {loading: true};
state.currentFeed = feed;
},
FeedSuccess(state, feed) {
state.currentFeed = feed;
state.status = {loading: false};
},
FeedFailure(state) {
state.status = {};
state.feed = null;
}
}
}
The feedStatsService.getFeed calls the service, which just runs a fetch and returns the results. Then commit('FeedSuccess', feed) gets called, which runs the mutation, which sets state.currentFeed=feed, and sets state.status.loading to false.
I can tell that it's stored, because the object shows up in the Vue dev tools. state.feed.currentFeed is the result from the service. But, my component doesn't change to reflect that. And there is a payload under mutations in the dev tool as well. When manually commit feed/feedSuccess in the dev tools, my component updates.
What am I missing here?
In the same way that component data properties need to be initialised, so too does your store's state. Vue cannot react to changes if it does not know about the initial data.
You appear to be missing something like...
state: {
status: { loading: true },
currentFeed: {}
}
Another option is to use Vue.set. See https://vuex.vuejs.org/guide/mutations.html#mutations-follow-vue-s-reactivity-rules...
Since a Vuex store's state is made reactive by Vue, when we mutate the state, Vue components observing the state will update automatically. This also means Vuex mutations are subject to the same reactivity caveats when working with plain Vue
Hey for all the people coming to this and not being able to find a solution. The following was what worked for me:
Declaring base state:
state: {
mainNavData: [],
}
Then I had my action which is calling the now fixed mutation:
actions : {
async fetchMainNavData({ commit }) {
var response = await axios.get();
commit('setMainNavData', response));
},
};
Now my mutation is calling this updateState() function which is key to it all
mutations = {
setMainNavData(state, navData) {
updateState(state, 'mainNavData', navData);
},
};
This is what the updateState function is doing which solved my issues.
const updateState = (state, key, value) => {
const newState = state;
newState[key] = value;
};
After adding updateState() my data reactively showed up in the frontend and I didn't have to manually commit the data in Vue tools anymore.
please note my store is in a different file, so its a little bit different.
Hope this helps others!
Sometimes updating property that are not directly in the state is the problem
{
directprop: "noProblem",
indirectParent: {
"test": 5 // this one has a problem but works if we clone the whole object indirectParent
}
}
but it is a temporary solution, it should help you to force update the state and discover what is the real problem.

Vuex Mapping Getter with Argument - Cached?

Here is an example of a Vuex Store with a parameterized getter which I need to map onto the Vue instance to use within the template.
const store = new Vuex.Store({
state: {
lower: 5,
higher: 10,
unrelated: 3
},
getters: {
inRange: state => value => {
console.log('inRange run')
return (state.lower <= value) && (state.higher >= value)
}
},
mutations: {
reduceLower: state => state.lower--,
incrementUnrelated: state => state.unrelated++
}
})
new Vue({
el: '#app',
template: "<div>{{ inRange(4) }}, {{ unrelated }}</div>",
store,
computed: Object.assign(
Vuex.mapGetters(['inRange']),
Vuex.mapState(['unrelated'])
),
})
setTimeout(function() {
console.log('reduceLower')
store.commit('reduceLower')
setTimeout(function() {
console.log('incrementUnrelated')
store.commit('incrementUnrelated')
}, 3000);
}, 3000);
<script src="https://unpkg.com/vue/dist/vue.js"></script>
<script src="https://unpkg.com/vuex/dist/vuex.js"></script>
<div id="app"></div>
Firstly, this does appear to be valid, working code. However considering computed is meant to be a cached set of computed properties, I'm curious about the behavior in this scenario, is there caching going on? If there isn't, is there a performance concern to consider? Even though the function does not cause any state change, should it be a method?
Is this an anti-pattern? The example isn't a real one, but I do want to centralize logic in the store.
UPDATE
I have updated the example to illustrate that modifications to the underlying lower/higher value upon which the inRange getter is based, are indeed reactive for the Vue instance (despite not being mapped as state). I have also included an unrelated value which is not part of the calculation, if the mapped getter were cached, modifying the unrelated value should not trigger the getter to be called again, however it does.
My conclusion is that there is no caching, thus this has poorer performance than a conventional computed property, however it is still functionally correct.
The question remains open as to whether there is any flaw in this pattern, or one available which performs better.
In my opinion this is an anti-pattern. It's a strange way to funnel a method. Also, no, there isn't caching here since inRange immediately return a value (the final function) without using any members in state - so Vue detects 0 reactive dependencies.
Getters can't be parameterized in this way, they can only derive things that are based in state. So if the range could be stored in state, that would work (and would be cached).
Similar question here: vuexjs getter with argument
Since you want to centralize this behavior - I think you should just do this in a separate module, perhaps as a mixin. This won't be cached, either, so you would have to wrap it (and the input) in a component's computed or use some other memoization
Something like this:
import { inRange } from './state/methods';
import { mapGetters } from 'vuex';
const Example = Vue.extend({
data: {
rangeInput: 10
},
computed: {
...mapGetters(['lower', 'higher']),
inRange() {
return inRange(this.rangeInput, this.lower, this.higher);
}
}
});
Just to illustrate why I accepted Matt's answer, here is a working snippet, the key point to notice is instead of:
Vuex.mapGetters(['inRange'])
There is a true computed property:
inRange4: function() {
return this.$store.getters.inRange(4);
}
This, as can be seen from running the snippet, caused the value to be cached correctly. As I stated, this pattern isn't one I can use as I would end up with too many computed properties (inRange1, inRange2, inRange3 etc), however it does answer the question with the example in question.
I have chosen to continue using the code from the question, unchanged.
Note: Matt's answer doesn't match this code exactly, and I believe his intent was that the state from the store would be mapped to the Vue instance, which I see as unnecessary.
const store = new Vuex.Store({
state: {
lower: 5,
higher: 10,
unrelated: 3
},
getters: {
inRange: state => value => {
console.log('inRange run')
return (state.lower <= value) && (state.higher >= value)
}
},
mutations: {
reduceLower: state => state.lower--,
incrementUnrelated: state => state.unrelated++
}
})
new Vue({
el: '#app',
template: "<div>{{ inRange4 }}, {{ unrelated }}</div>",
store,
computed: Object.assign(
{
inRange4: function() {
return this.$store.getters.inRange(4);
}
},
Vuex.mapState(['unrelated'])
),
})
setTimeout(function() {
console.log('reduceLower')
store.commit('reduceLower')
setTimeout(function() {
console.log('incrementUnrelated')
store.commit('incrementUnrelated')
}, 3000);
}, 3000);
<script src="https://unpkg.com/vue/dist/vue.js"></script>
<script src="https://unpkg.com/vuex/dist/vuex.js"></script>
<div id="app"></div>
There seems to be a way around this, create a map with the calculated values and access it as
inrange[4];
I frequently use it to initialize accessors of different kinds, I get an array from my backend and needs to access it by some field (e.g. ID).
For the above example it seems reasonable since the range is small:
const store = new Vuex.Store({
state: {
lower: 5,
higher: 10,
unrelated: 3
},
getters: {
inRange: state => {
console.log('inRange run')
var result = {};
for( var i = state.lower; i < state.higher; i++) {
result[i] = true;
}
return result;
}
},
mutations: {
reduceLower: state => state.lower--,
incrementUnrelated: state => state.unrelated++
}
})
new Vue({
el: '#app',
template: "<div>{{ inRange[4] }}, {{ unrelated }}</div>",
store,
computed: Object.assign(
{
inRange: function() {
return this.$store.getters.inRange;
}
},
Vuex.mapState(['unrelated'])
),
})
setTimeout(function() {
console.log('reduceLower')
store.commit('reduceLower')
setTimeout(function() {
console.log('incrementUnrelated')
store.commit('incrementUnrelated')
}, 3000);
}, 3000);
<script src="https://unpkg.com/vue/dist/vue.js"></script>
<script src="https://unpkg.com/vuex/dist/vuex.js"></script>
<div id="app"></div>

Updating data from Vuex get a infinite loop in watcher

I have a "dumb" component that just get props from a parent. The user can change a selector which fires an action (using Vuex) to get new data. When this new data has been received I want to pass it to the child and re-render that component with the new data. Unfortutalely I keep getting this warning in my watcher. Please help :slight_smile:
export default {
name: 'bubbles',
props: {
awesomeData: {
type: Array,
required: true
}
},
data () {
return {
title: 'Best component ever'
}
},
watch: {
awesomeData (newData) {
console.log('hello world')
this.refreshSomethingAwesome(newData)
}
},
methods: {
refreshSomethingAwesome (newData) {}
}
}
s
101 hello world
[Vue warn]: You may have an infinite update loop in watcher with expression "awesomeData"
I'm trying a solution pretty much exactly the same as this: Vuex Examples
But can't seem to get it working... hmmm
Found out the reason I was getting the loop was because I was actually trying to sort mutable data from the property (thought it was immutable)
let options = {
children: newData.sort((a, b) => a.value - b.value)
}
I changed it to something like:
const sortedNewData = [...newData].sort((a, b) => a.value - b.value)
let options = {
children: sortedNewData
}
Note: To prevent this across my app, I might wrap my initial state in a Map from immutable.js
import { Map, fromJS } from 'immutable'
const initialState = Map({
awesomeData: fromJS([])
})
Helpful article: alligator.io

operating on props in vue.js components

Im pretty newm to vue and i'm trying to migrate the frontend of my laravel project to vue but i'm having an issue with it, i'm trying to loop through an array of provided objects called rooms and create divs for each of the in my component as well as setting the default room_id as the first id of the room. The problem is when is access the provided prop array called 'room' in the dom (html) it works flawlessly, but in my vue code for the component file it always seems to be undefined or empty
Here is my components vue code:
export default {
created() {
//this.loadMessages(this.room_id)
console.log(this.first_room) //undefined;
console.log(this.rooms) //empty array;
},
props: ['rooms','first_room'],
computes:{
myrooms: function(){
return this.first_room;
}
},
data()
{
return{
messages: [],
newMessage: '',
room_id: 1 //for test purposes, this works,
}
},
methods: {
loadMessages(id)
{
axios.get('/messages/'+id).then(response => {
this.messages = response.data;
console.log(response.data);
});
}
}
}
the important part of the component html
<div v-for="room in rooms">
<div class="chat-user room">
<div v-for="other in room.others">
<img class="chat-avatar img-circle" :src="other.image" alt="image" >
<div class="chat-user-name">
<a :href="'/user/' + other.id">{{ other.name}}</a>
</div>
</div>
</div>
</div>
//this all works, just for reference
method where i set the values passed to the prop in my main vue instance
EDIT: THE PARENT INSTANCE CODE
Oh and i cant seem too access the rooms array being passed as it is always empty IN code but it loops in the html
window.App.app= new Vue({
el: '#wrapper',
data: {
messages: [],
rooms: [],
test: 'yes',
first: ''
},
created() {
this.fetchRooms();
this.first = 1;
},
methods: {
fetchMessages(id) {
console.log(id);
},
fetchRooms()
{
axios.get('/rooms').then(response => {
this.rooms = response.data;
});
}
}
});
finally where i call my component
<chat-messages :rooms="rooms" :first_room="1"></chat-messages>
//variables referenced from main vue instance
I have literally torn most of my hair on this, please any help is appreciated
In the child component to which the props are passed on.
export default {
created() {
console.log(this.first_room) //undefined;
},
props: ['rooms','first_room'],
computed :{
myrooms: function(){
return this.first_room;
}
},
data () {
return {
messages: [],
newMessage: '',
room_id: 1 //for test purposes, this works,
}
},
watch: {
rooms (n, o) {
console.log(n, o) // n is the new value, o is the old value.
}
},
methods: {
loadMessages (id) {
axios.get('/messages/'+id).then(response => {
this.messages = response.data;
console.log(response.data);
});
}
}
}
You can add a watch on data properties or computed to see the change in their values.
In the question, (as what it appears to be the case), you have consoled the value of the props in the created lifecycle, the props' value gets changed by an API call in the parent component, after the creation of the child component. That explains why your template shows the data but not in the console in the created lifecycle hook.

What's the correct way to pass props as initial data in Vue.js 2?

So I want to pass props to an Vue component, but I expect these props to change in future from inside that component e.g. when I update that Vue component from inside using AJAX. So they are only for initialization of component.
My cars-list Vue component element where I pass props with initial properties to single-car:
// cars-list.vue
<script>
export default {
data: function() {
return {
cars: [
{
color: 'red',
maxSpeed: 200,
},
{
color: 'blue',
maxSpeed: 195,
},
]
}
},
}
</script>
<template>
<div>
<template v-for="car in cars">
<single-car :initial-properties="car"></single-car>
</template>
</div>
</template>
The way I do it right now it that inside my single-car component I'm assigning this.initialProperties to my this.data.properties on created() initialization hook. And it works and is reactive.
// single-car.vue
<script>
export default {
data: function() {
return {
properties: {},
}
},
created: function(){
this.data.properties = this.initialProperties;
},
}
</script>
<template>
<div>Car is in {{properties.color}} and has a max speed of {{properties.maxSpeed}}</div>
</template>
But my problem with that is that I don't know if that's a correct way to do it? Won't it cause me some troubles along the road? Or is there a better way to do it?
Thanks to this https://github.com/vuejs/vuejs.org/pull/567 I know the answer now.
Method 1
Pass initial prop directly to the data. Like the example in updated docs:
props: ['initialCounter'],
data: function () {
return {
counter: this.initialCounter
}
}
But have in mind if the passed prop is an object or array that is used in the parent component state any modification to that prop will result in the change in that parent component state.
Warning: this method is not recommended. It will make your components unpredictable. If you need to set parent data from child components either use state management like Vuex or use "v-model". https://v2.vuejs.org/v2/guide/components.html#Using-v-model-on-Components
Method 2
If your initial prop is an object or array and if you don't want changes in children state propagate to parent state then just use e.g. Vue.util.extend [1] to make a copy of the props instead pointing it directly to children data, like this:
props: ['initialCounter'],
data: function () {
return {
counter: Vue.util.extend({}, this.initialCounter)
}
}
Method 3
You can also use spread operator to clone the props. More details in the Igor answer: https://stackoverflow.com/a/51911118/3143704
But have in mind that spread operators are not supported in older browsers and for better compatibility you'll need to transpile the code e.g. using babel.
Footnotes
[1] Have in mind this is an internal Vue utility and it may change with new versions. You might want to use other methods to copy that prop, see How do I correctly clone a JavaScript object?.
My fiddle where I was testing it:
https://jsfiddle.net/sm4kx7p9/3/
In companion to #dominik-serafin's answer:
In case you are passing an object, you can easily clone it using spread operator(ES6 Syntax):
props: {
record: {
type: Object,
required: true
}
},
data () { // opt. 1
return {
recordLocal: {...this.record}
}
},
computed: { // opt. 2
recordLocal () {
return {...this.record}
}
},
But the most important is to remember to use opt. 2 in case you are passing a computed value, or more than that an asynchronous value. Otherwise the local value will not update.
Demo:
Vue.component('card', {
template: '#app2',
props: {
test1: null,
test2: null
},
data () { // opt. 1
return {
test1AsData: {...this.test1}
}
},
computed: { // opt. 2
test2AsComputed () {
return {...this.test2}
}
}
})
new Vue({
el: "#app1",
data () {
return {
test1: {1: 'will not update'},
test2: {2: 'will update after 1 second'}
}
},
mounted () {
setTimeout(() => {
this.test1 = {1: 'updated!'}
this.test2 = {2: 'updated!'}
}, 1000)
}
})
<script src="https://cdn.jsdelivr.net/npm/vue#2.5.17/dist/vue.js"></script>
<div id="app1">
<card :test1="test1" :test2="test2"></card>
</div>
<template id="app2">
<div>
test1 as data: {{test1AsData}}
<hr />
test2 as computed: {{test2AsComputed}}
</div>
</template>
https://jsfiddle.net/nomikos3/eywraw8t/281070/
I believe you are doing it right because it is what's stated in the docs.
Define a local data property that uses the prop’s initial value as its initial value
https://vuejs.org/guide/components.html#One-Way-Data-Flow
Second or third time I run into that problem coming back to an old vue project.
Not sure why it is so complicated in vue, but it can we done via watch:
export default {
props: ["username"],
data () {
return {
usernameForLabel: "",
}
},
watch: {
username: {
immediate: true,
handler (newVal, oldVal) {
this.usernameForLabel = newVal;
}
},
},
Just as another approach, I did it through watchers in the child component.
This way is useful, specially when you're passing an asynchronous value, and in your child component you want to bind the passed value to v-model.
Also, to make it reactive, I emit the local value to the parent in another watcher.
Example:
data() {
return {
properties: {},
};
},
props: {
initial-properties: {
type: Object,
default: {},
},
},
watch: {
initial-properties: function(newVal) {
this.properties = {...newVal};
},
properties: function(newVal) {
this.$emit('propertiesUpdated', newVal);
},
},
This way I have more control and also less unexpected behaviour. For example, when props that passed by the parent is asynchronous, it may not be available at the time of created or mounted lifecycle. So you can use computed property as #Igor-Parra mentioned, or watch the prop and then emit it.
Following up on Cindy's comment on another answer:
Be carful. The spread operator only shallow clones, so for objects
that contain objects or arrays you will still copy pointers instead of
getting a new copy.
Indeed this is the case. Changes within objects inside arrays will still propagate to your components even when a spread operator is employed.
Here was my solution (using Composition API):
setup() {
properties = ref([])
onMounted(() => {
properties.value = props.initialProperties.map((obj) => ({ ...obj }));
})
}
This worked to set the values and prevent them from getting changed, even if the data was changed in the parent component.

Categories

Resources