Vue: Why is this computed property not reactive? - vue.js

Here is a computed getter and setter from a vue component:
paidStartHours : {
get() {
return moment(this.position.paid_start, global.DB_DATETIME).format('HH');
},
set(value) {
this.$store.commit({
type : 'updatePaidStartHours',
newValue : value,
position : this.position
});
}
}
On get, it returns the hours (HH) from position.paid_start. On set, it commits a store mutation, which essentially recreates the time string for position.paid_start.
In is bound (two-way) to an input as follows:
<input v-model="paidStartHours" type="text">
On initial load, computed property paidStartHours is correct (per Vue Tools).
When I update the input however, the value of paidStartHours does not change. I know that the commit is successful, because the props that are handed to the component are correct. It makes no sense to me.
EDIT:
The code in the updatePaidStartHours mutation has changed a number of times. For example, I tried this:
updatePaidStartHours(state, payload) {
payload.position.paid_start = 999;
}
The value of the passed prop changed to 999, however the value of the computed prop remained the same.
EDIT II:
I don't think it is worth trying to resolve this issue because I think my whole Vue / Webpack / Node installation is very sick. For example, this morning I delightedly followed this answer, Vuejs and Webpack: Why is store undefined in child components, to import an instantiated store into my App. It all seemed fine but after about 8 hours of stuffing about I see that NO store properties are reactive. I reverted to importing the config only, and now most of my store properties are reactive, but sadly not the ones above. I think I need to abandon the approach until I get time to revisit blow away my Vue / Webpack / Node install and start again.

Vuex mutations should be called from Vuex actions only - so in your code you should dispatch an action, not a mutation. Also, your mutation function is expected to mutate the store argument - not the payload one. Your getter is also wrong - it should use this.$store.getters and not your local component data.
paidStartHours : {
get() {
return moment(this.$store.getters.position.paid_start, global.DB_DATETIME).format('HH');
},
set(value) {
this.$store.dispatch('updatePaidStartHours',value);
}
}
Vuex module:
// initial state
const state = {
position:
{
paid_start: null
}
};
// getters
const getters = {
position: (state) => state.position
}
// actions
const actions = {
updatePaidStartHours ({commit}, payload)
{
commit('SET_START_HOURS', payload);
}
}
// mutations
const mutations = {
SET_START_HOURS (state, payload)
{
state.position.paid_start = payload;
}
}

Related

Vue, looking for a way to watch values/changes from a different component/js file

So I have a plain js file which contains some stuff like functions, one of these is a function that gets a value from 1. localstorage if not present 2. vuex if not present use a default value. These values can be updated thru the whole app in several components which means that components that also are using this value neer te update this value. For now I cant seem to make this part reactive.
helper.js
export const helperFunc = () => {
let value
//dummy
if(checkforlocalstorage){
value = localstorage_value
} else {
value = other_value
}
return value
}
ComponentOne.vue
<template>
<div>{{dynamicValue}}</div>
</template>
<script>
import {helperFunc} from './plugins/helpers.js'
export default {
data () {
return {
}
},
computed: {
dynamicValue : function () {
return helperFunc()
}
},
}
<script>
ComponentTwo.vue
ComponentThree.vue
Update the values here
local or session storage is not a reactive data and vue can't watch them. so you better set the variable in a vuex store, this way the data is reactive and all the components can access it from everywhere in the app.
but there's a catch that you might run into and that is if you refresh the page, all the data in the store get lost and you get the initial values.
but there is one thing you can do, and that is:
save the variable in the localStorage to have it even after the page refresh
initialize the store state value to have the data reactive and accessible through out the app
update the state and localStorage every-time data changes so you can have the reactivity and also latest value in case of a page reload.
Here I show you the basic of this idea:
first you need to setup the store file with the proper state, mutation and action:
export default {
state() {
return {
myVar: 'test',
}
},
mutations: {
UPDATE_MY_VAR(state, value) {
state.myVar = value;
}
},
actions: {
updateMyVar({ commit }, value) {
localStorage.setItem('myVar', value);
commit('UPDATE_MY_VAR', value);
},
initializeMyVar({ commit }) {
const value = localStorage.getItem('myVar');
commit('UPDATE_MY_VAR', value);
}
}
}
then in the beforeCreate or created hook of the root component of your app you'll have:
created() {
this.$store.dispatch('initializeMyVar');
}
this action read the data from a localStorage and initialize the myVar state and you can access that form everywhere like $store.state.myVar and this is reactive and can be watched. also if there is no localStorage and you need a fallback you can write the proper logic for this.
then whenever the data needs to be changed you can use the second action like $store.dispatch('updateMyVar', newUpdatedValue) which updates both the localStorage and the state.
now even with a page reload you get the latest value from the localStorage and the process repeats.

Updating getter value Vuex store when state changes

I'm trying to figure out how to properly update a getter value when some other variable from VueX changes/updates.
Currently I'm using this way in a component to update:
watch: {
dates () {
this.$set(this.linedata[0].chartOptions.xAxis,"categories",this.dates)
}
}
So my getter linedata should be updated with dates value whenever dates changes. dates is state variable from VueX store.
The thing is with this method the value won't be properly updated when I changed route/go to different components. So I think it's better to do this kind of thing using the VueX store.
dates is updated with an API call, so I use an action to update it.
So the question is how can I do such an update from the VueX store?
EDIT:
I tried moving this to VueX:
async loadData({ commit }) {
let response = await Api().get("/cpu");
commit("SET_DATA", {
this.linedata[0].chartOptions.xAxis,"categories": response.data.dates1,
this.linedata[1].chartOptions.xAxis,"categories": response.data.dates2
});
}
SET_DATA(state, payload) {
state = Object.assign(state, payload);
}
But the above does not work, as I cannot set nested object in action this way...
Getters are generally for getting, not setting. They are like computed for Vuex, which return calculated data. They update automatically when reactive contents change. So it's probably best to rethink the design so that only state needs to be updated. Either way, Vuex should be updated only with actions/mutations
Given your example and the info from all your comments, using linedata as state, your action and mutation would look something like this:
actions: {
async loadData({ commit }) {
let response = await Api().get("/cpu");
commit('SET_DATA', response.data.dates);
}
}
mutations: {
SET_DATA(state, dates) {
Vue.set(state.linedata[0].chartOptions.xAxis, 'categories', dates[0]);
Vue.set(state.linedata[1].chartOptions.xAxis, 'categories', dates[1]);
}
}
Which you could call, in the component for example, like:
this.$store.dispatch('loadData');
Using Vue.set is necessary for change detection in this case and requires the following import:
import Vue from 'vue';
Theoretically, there should be a better way to design your backend API so that you can just set state.linedata = payload in the mutation, but this will work with what you have.
Here is a simple example of a Vuex store for an user.
export const state = () => ({
user: {}
})
export const mutations = {
set(state, user) {
state.user = user
},
unset(state) {
state.user = {}
},
patch(state, user) {
state.user = Object.assign({}, state.user, user)
}
}
export const actions = {
async set({ commit }) {
// TODO: Get user...
commit('set', user)
},
unset({ commit }) {
commit('unset')
},
patch({ commit }, user) {
commit('patch', user)
}
}
export const getters = {
get(state) {
return state.user
}
}
If you want to set the user data, you can call await this.$store.dispatch('user/set') in any Vue instance. For patching the data you could call this.$store.dispatch('user/patch', newUserData).
The getter is then reactively updated in any Vue instance where it is mapped. You should use the function mapGetters from Vuex in the computed properties. Here is an example.
...
computed: {
...mapGetters({
user: 'user/get'
})
}
...
The three dots ... before the function call is destructuring assignment, which will map all the properties that will the function return in an object to computed properties. Those will then be reactively updated whenever you call dispatch on the user store.
Take a look at Vuex documentation for a more in depth explanation.

How to watch for vuex state?

I need do fire a function within component when my vuex state data change, but it does not work , is there any wrong usage about watch hook for vuex?
const state = {
currentQueryParameter:[],
};
const mutations = {
currentQueryParameter(state,info){
state.currentQueryParameter[info.index]=info.value
Vue.set(info, info.index, info.value);
}
}
in component
watch: {
'$store.state.currentQueryParameter': function() {
console.log("changed")
this.getData()
}
},
What you are doing is technically correct and will work.
However several thing are still going wrong:
If you want your state to be reactive you need to fill Arrays with native array methods (.push(), .splice() etc). Vue.set() is only used to set Object properties.
You are watching currentQueryParameter, which is an Array. Its value does not change through your mutation - it stays the same Array. If you want to watch nested elements as well, you need to use the deep flag in your watcher, like so:
watch: {
'$store.state.currentQueryParameter': {
deep: true,
handler(newVal) {
console.log('queryParameter changed');
}
}
}
I don't know what you are trying to do with this one in your mutation:
Vue.set(info, info.index, info.value); but you should not mutate the properties you pass to a function.

How to get data from vuex state into local data for manipulation

I'm having trouble understanding how to interact with my local state from my vuex state. I have an array with multiple items inside of it that is stored in vuex state. I'm trying to get that data from my vuex state into my components local state. I have no problems fetching the data with a getter and computed property but I cannot get the same data from the computed property into local state to manipulate it. My end goal is to build pagination on this component.
I can get the data using a getters and computed properties. I feel like I should be using a lifecycle hook somewhere.
Retrieving Data
App.vue:
I'm attempting to pull the data before any components load. This seems to have no effect versus having a created lifecycle hook on the component itself.
export default {
name: "App",
components: {},
data() {
return {
//
};
},
mounted() {
this.$store.dispatch("retrieveSnippets");
}
};
State:
This is a module store/modules/snippets.js
const state = {
snippets: []
}
const mutations = {
SET_SNIPPETS(state, payload) {
state.snippets = payload;
},
}
const actions = {
retrieveSnippets(context) {
const userId = firebase.auth().currentUser.uid;
db.collection("projects")
.where("person", "==", userId)
.orderBy("title", "desc")
.onSnapshot(snap => {
let tempSnippets = [];
snap.forEach(doc => {
tempSnippets.push({
id: doc.id,
title: doc.data().title,
description: doc.data().description,
code: doc.data().code,
person: doc.data().person
});
});
context.commit("SET_SNIPPETS", tempSnippets);
});
}
}
const getters = {
getCurrentSnippet(state) {
return state.snippet;
},
Inside Component
data() {
return {
visibleSnippets: [],
}
}
computed: {
stateSnippets() {
return this.$store.getters.allSnippets;
}
}
HTML:
you can see that i'm looping through the array that is returned by stateSnippets in my html because the computed property is bound. If i remove this and try to loop through my local state, the computed property doesn't work anymore.
<v-flex xs4 md4 lg4>
<v-card v-for="snippet in stateSnippets" :key="snippet.id">
<v-card-title v-on:click="snippetDetail(snippet)">{{ snippet.title }}</v-card-title>
</v-card>
</v-flex>
My goal would be to get the array that is returned from stateSnippets into the local data property of visibleSnippets. This would allow me to build pagination and manipulate this potentially very long array into something shorter.
You can get the state into your template in many ways, and all will be reactive.
Directly In Template
<div>{{$store.state.myValue}}</div>
<div v-html='$store.state.myValue'></div>
Using computed
<div>{{myValue}}</div>
computed: {
myValue() { return this.$store.state.myValue }
}
Using the Vuex mapState helper
<div>{{myValue}}</div>
computed: {
...mapState(['myValue'])
}
You can also use getters instead of accessing the state directly.
The de-facto approach is to use mapGetters and mapState, and then access the Vuex data using the local component.
Using Composition API
<div>{{myValue}}</div>
setup() {
// You can also get state directly instead of relying on instance.
const currentInstance = getCurrentInstance()
const myValue = computed(()=>{
// Access state directly or use getter
return currentInstance.proxy.$store.state.myValue
})
// If not using Vue3 <script setup>
return {
myValue
}
}
I guess you are getting how Flux/Vuex works completely wrong. Flux and its implementation in Vuex is one way flow. So your component gets data from store via mapState or mapGetters. This is one way so then you dispatch actions form within the component that in the end commit. Commits are the only way of modifying the store state. After store state has changed, your component will immediately react to its changes with latest data in the state.
Note: if you only want the first 5 elements you just need to slice the data from the store. You can do it in 2 different ways:
1 - Create a getter.
getters: {
firstFiveSnipets: state => {
return state.snipets.slice(0, 5);
}
}
2 - Create a computed property from the mapState.
computed: {
...mapState(['allSnipets']),
firstFiveSnipets() {
return this.allSnipets.slice(0, 5);
}
}

Vuex changes not impacting modules

I have a UserDialog component which leverages a part of the Vuex state-tree to determine whether it should display itself or not:
import { Component, Prop, Vue } from 'vue-property-decorator';
import { State, Getter, Mutation, Action, namespace } from 'vuex-class';
import { fk } from 'firemodel';
import { User } from '#/models/User';
const Users = namespace('users');
#Component({})
export default class UserDialog extends Vue {
#Prop() public id!: fk;
#Users.State public show: fk;
#Users.Getter public selectedUser: User;
#Users.Mutation public HIDE_USER_PROFILE: () => void;
public get showDialog() {
return this.show === undefined ? false : true;
}
}
From the parent component I am calling Vuex's commit('SHOW_USER_PROFILE', id) and thereby setting this ID it should update the UserDialog's show property accordingly.
I can see very clearly that the Vuex store has received the call to SHOW_USER_PROFILE and that indeed has updated the state in the state tree (this is through the Vue Developer plugin in the browser). But then when I switch over to the UserProfile component I see that it still has not received the state update.
Note: if I reload the page (aka, CMD-R) after having set the UserID I want to highlight, it reloads the components and because I'm using veux-persist, the ID is still set in the state tree. At this point the component DOES receive the correct state but when relying on the normal reactivity system it just doesn't work.
Can anyone help?
for additional context, here are a few more modules:
Store Definition::
export default new Vuex.Store<IRootState>({
modules: {
packages,
users,
searchCriteria,
snackbar
},
plugins: [FireModelPlugin, localStorage.plugin]
});
Users Mutations:
const mutations: MutationTree<IUsers> = {
selectUser(state, id: fk) {
state.selected = id;
},
SHOW_USER_PROFILE(state, id: fk) {
state.show = id;
},
HIDE_USER_PROFILE(state) {
state.show = undefined;
}
};
I have added a computed property to the UserDialog component above:
public get userId() {
return this.$store.state.users.show;
}
There was a thought that maybe this would be reactive whereas the #Users.State decorated show property was not. Unfortunately, they both perform exactly the same.
#Derek and I talked last night and realized that the cause of this problem was due to the state transitions to "undefined" which the current Reactive system does not handle (it should be fine when we get to Vue-NEXT with Object Proxies). The remaining code works just fine when I switch out the state transition from: undefined → string → undefined to null → string → undefined.
Many thanks to #Derek for spending the time.
In the example above you're directly calling the Vuex state store. When you do this from your component this is a one time get deale. The state store is not reactive and will never tell your computed property that it changed.
The correct way to get the reactivity you're looking for is to implement Vuex getters:
const store = new Vuex.Store({
state: {/*...*/},
getters: {
show(state) {
return state.show;
}
}
})
Then in your component:
computed: {
show() {
return this.$store.getters.show;
}
}
Read more about Vuex getters here: https://vuex.vuejs.org/guide/getters.html