Setting intial local data from Vuex store giving "do not mutate" error - vue.js

I thought I understood the correct way to load inital state data from Vuex into the local data of a component, but why is this giving me “[vuex] do not mutate vuex store state outside mutation handlers.” errors! I am using a mutation handler!
I want my component data to start empty, unless coming back from a certain page (then it should pull some values from Vuex).
The component is using v-model=“selected” on a bunch of checkboxes. Then I have the following:
// Template
<grid-leaders
v-if="selected.regions.length"
v-model="selected"
/>
// Script
export default {
data() {
return {
selectedProxy: {
regions: [],
parties: [],
},
}
},
computed: {
selected: {
get() {
return this.selectedProxy
},
set(newVal) {
this.selectedProxy = newVal
// If I remove this next line, it works fine.
this.$store.commit("SET_LEADER_REGIONS", newVal)
},
},
},
mounted() {
// Restore saved selections if coming back from a specific page
if (this.$store.state.referrer.name == "leaders-detail") {
this.selectedProxy = {...this.$store.state.leaderRegions }
}
}
}
// Store mutation
SET_LEADER_REGIONS(state, object) {
state.leaderRegions = object
}

OK I figured it out! The checkbox component (which I didn't write) was doing this:
updateRegion(region) {
const index = this.value.regions.indexOf(region)
if (index == -1) {
this.value.regions.push(region)
} else {
this.value.regions.splice(index, 1)
}
this.$emit("input", this.value)
},
The line this.value.regions.push(region) is the problem. You can't edit the this.value prop directly. I made it this:
updateRegion(region) {
const index = this.value.regions.indexOf(region)
let regions = [...this.value.regions]
if (index == -1) {
regions.push(region)
} else {
regions.splice(index, 1)
}
this.$emit("input", {
...this.value,
regions,
})
},
And then I needed this for my computed selected:
selected: {
get() {
return this.selectedProxy
},
set(newVal) {
// Important to spread here to avoid Vuex mutation errors
this.selectedProxy = { ...newVal }
this.$store.commit("SET_LEADER_REGIONS", { ...newVal })
},
},
And it works great now!
I think the issue is that you can't edit a v-model value directly, and also you also have to be aware of passing references to objects, and so the object spread operator is a real help.

Related

How to full state before going throw script in component vue

Mey be it is simple, but I'm new in frontend. I have a page component. And I need to fetch data before component calculated.
import {mapActions, mapGetters} from 'vuex'
export default {
name: "notFoundPage",
methods: {
...mapActions([
'GET_SUBCATEGORIES_FROM_CATEGORIES'
]),
},
computed: {
...mapGetters([
'SUBCATEGORIES'
]),
subCategories() {
// doing some calculations with already updated SUBCATEGORIES in store
}
return result;
}
},
created() {
this.GET_SUBCATEGORIES_FROM_CATEGORIES()
> **// from here we go to store**
},
mounted() {
this.GET_SUBCATEGORIES_FROM_CATEGORIES()
}
}
store:
let store = new Vuex.Store({
state: {
categories: [],
subcategories: []
},
mutations: {
SET_CATEGORIES_TO_STATE: (state, categories) => {
state.categories = categories;
},
SET_SUBCATEGORIES_TO_STATE: (state, subcategories) => {
state.subcategories = subcategories;
}
},
actions: {
GET_CATEGORIES_FROM_API({commit}) {
return axios('http://localhost:3000/categories',
{
method: "GET"
})
But here compiler returns to component. I do not have any idea, why it is not finishing this action. And after calculating the computed block in component it returns to this point. But I need 'SET_CATEGORIES_TO_STATE' already updated
.then((categories) => {
commit('SET_CATEGORIES_TO_STATE', categories.data)
return categories;
}).catch((error) => {
console.log(error);
return error;
})
},
GET_SUBCATEGORIES_FROM_CATEGORIES({commit}) {
this.dispatch('GET_CATEGORIES_FROM_API').then(categories => {
let subs = categories.data.map(function(category) {
return category.subcategories.map(function(subcategory) {
return subcategory.name
})
})
commit('SET_SUBCATEGORIES_TO_STATE', subs)
return subs
})
}
},
getters: {
CATEGORIES(state) {
return state.categories;
},
SUBCATEGORIES(state) {
return state.subcategories;
}
}
if you have difficulties with timings and async tasks, why don't you use async/await?
you want to wait in a async function (for example calling a backend for data) till the data is fetched. then you want to manipulate/delete/change/add, do what ever you want with that data and display the result on screen.
the point is, Vue is a reactive Framework, which means it rerenders (if the setup is correct made) the content by itself after what ever calculation is finished. so don't worry about something like that.
to be honest, the question is asked really weird. and your code is hard to read. sometimes moving two steps back and try a other way isn't false as well.

How to trigger watch AFTER I read array from db?

In my vue/cli 4/vuex opening page I need to fill select input with default value from
vuex store. To fill select input I need have selection items read from db and I have a problem that watch is triggered
BEFORE I read data from db in mount event.
I do as :
watch: {
defaultAdSavedFilters: {
handler: function (value) {
console.log('WATCH defaultAdSavedFilters value::')
console.log(value)
if (!this.isEmpty(value.title)) {
this.filter_title = value.title
}
if (!this.isEmpty(value.category_id)) {
this.categoriesLabels.map((nexCategoriesLabel) => { // this.categoriesLabels IS EMPTY
if (nexCategoriesLabel.code === value.category_id) {
this.selection_filter_category_id = {code: value.category_id, label: nexCategoriesLabel.label};
}
});
}
}
}, //
}, // watch: {
mounted() {
retrieveAppDictionaries('ads_list', ['ads_per_page', 'categoriesLabels']); // REQUEST TO DB
bus.$on('appDictionariesRetrieved', (response) => {
if (response.request_key === 'ads_list') { // this is triggered AFTER watch
this.ads_per_page = response.ads_per_page
this.categoriesLabels = response.categoriesLabels
// this.$forceUpdate() // IF UNCOMMENT THAT DOES NOT HELP
Vue.$forceUpdate() // THAT DOES NOT HELP
}
})
this.loadAds(true)
}, // mounted() {
I found this Can you force Vue.js to reload/re-render?
branch and tried some decisions, like
Vue.$forceUpdate()
but that does not work.
If there is a right way to trigger watch defaultAdSavedFilters AFTER I read array from db ?
Modified BLOCK :
I use Vuex actions/mutations when I need to read / keep /use /update data of the logged user, like
defaultAdSavedFilters, which is defined as :
computed: {
defaultAdSavedFilters: function () {
return this.$store.getters.defaultAdSavedFilters
},
Data ads_per_page(used for pagionaion), categoriesLabels(used for selection input items) has nothing to do with
logged user, that is why I do not use vuex for them, and I use retrieveAppDictionaries method to read them from the db
and bus to listen to them, which is defined as :
import {bus} from '#/main'
Sure I have data( block :
export default {
data() {
return {
...
ads_per_page: 20,
categoriesLabels: [],
...
}
},
"vue": "^2.6.10",
"vue-router": "^3.1.3",
"vuex": "^3.1.2"
Thanks!
Please add the data() method from you component. But I'm pretty sure it is NOT triggering because of the way you are assigning the result from the API call.
Try this:
mounted() {
retrieveAppDictionaries('ads_list', ['ads_per_page', 'categoriesLabels']); // REQUEST TO DB
bus.$on('appDictionariesRetrieved', (response) => {
if (response.request_key === 'ads_list') { // this is triggered AFTER watch
this.ads_per_page = [ ...response.ads_per_page ]
this.categoriesLabels = [ ...response.categoriesLabels ]
}
})
this.loadAds(true)
}
However, I don't understand what bus is doing for you and why you are NOT using Vuex actions/mutations

Vue computed property not responding to state change

I cannot figure out why the details computed property in the following component is not updating when the fetch() method is called:
<template>
<div>
{{ haveData }} //remains undefined
</div>
</template>
<script>
export default {
props: {
group: {
type: Object,
required: true
},
},
computed: {
currentGroup() {
return this.$store.getters['user/navbar_menu_app_current_group'](
this.group.id
)
/*-- which is the following function
navbar_menu_app_current_group: state => item => {
return state.menu.find(m => {
return m.id == item
})
}
*/
/*-- this function returns an object like so
{
id: 1,
label: 'kity cats',
}
***details --> IS NOT DEFINED. If I add it to the current group as null, my problem goes away. However, this is a previous API call that does not set the `details` parameter.
*/
},
details() {
let c = this.currentGroup.details
console.log(c) // returns undefined, which makes sense, but it should be updated after this.fetch() is called
return c
},
haveData() {
return this.details != null
}
},
methods: {
async fetch() {
await this.$store.dispatch(
'user/navbar_menu_app_details_get',
this.group.id
)
//This is setting the "details" part of the state on menu which is referred to in the computed properties above
//Previous to this there is no state "this.group.details"
//If I add a console log to the mutation the action calls, I get what is expected.
}
},
created() {
if (!this.haveData) {
this.fetch()
}
}
}
</script>
If I change the array items to include details, it works:
{
id: 1,
label: 'kity cats',
details: null // <-- added
}
The unfortunate part is that the array is created from a large API call, and adding the details seems unnecessary, as it may never be needed.
How can I get the computed properties to work without adding the details:null to the default state?
Attempt 1:
// Vuex mutation
navbar_menu_app_details_set(state, vals) {
let app = state.menu.find(item => {
return item.id == vals[0] //-> The group id passing in the dispatch function
})
//option 1 = doesn't work
app = { app, details: vals[1] } //-> vals[1] = the details fetched from the action (dispatch)
//option 2 = doesnt work
app.details = vals[1]
//option 3 = working but want to avoid using Vue.set()
import Vue from 'vue' //Done outside the actual function
Vue.set( app, 'details', vals[1])
},
Attempt 2:
// Vuex action
navbar_menu_app_details_get(context, id) {
return new Promise((resolve, reject) => {
setTimeout(() => {
context.commit('navbar_menu_app_details_set', [
context.getters.navbar_menu_app_current(id), //-> the same as find function in the mutation above
apps[id]
])
resolve()
}, 1000)
})
}
// --> mutation doesn't work
navbar_menu_app_details_set(state, vals) {
vals[0].details = vals[1]
},
The Vue instance is available from a Vuex mutation via this._vm, and you could use vm.$set() (equivalent to Vue.set()) to add details to the menu item:
navbar_menu_app_details_set(state, vals) {
let app = state.menu.find(item => {
return item.id == vals[0]
})
this._vm.$set(app, 'details', vals[1])
},
All Objects in Vue are reactive and are designed in a way such that only when the object is re-assigned, the change will be captured and change detection will happen.
Such that, in your case, following should do fine.
app = { ...app, details: vals[1] }

How to get value from state as my vuex getter is returning empty value in observer

Getter is returning empty value in observer. But the state is setting properly in the mutation.
Not able to check in Vuex dev tools in console as it says "No Store Detected". I've checked it by logging it in console
Vue File :
computed: {
...mapGetters('listings', ['listingContracts']),
},
methods: {
...mapActions('listings', [
'productBasedListings',
]),
},
onChange(product) {
this.productBasedListings( product.id );
console.log('LIST:', this.listingContracts); // Empty in observer
},
Store :
state: {
contracts: [],
},
getters: {
listingContracts(state) {
console.log('GETTER', state.contracts); // Empty in observer
return state.contracts;
},
},
mutations: {
setListing(state, { lists }) {
state.contracts = lists;
console.log('AFTER MUTATION:', state.contracts); // Setting the value properly
},
},
actions: {
async productBasedListings({ commit }, { id, state }) {
let listing = [];
try {
listing = await publicApi.listings(id);
console.log('ACTION:', listing);
commit({
lists: listing,
type: 'setListing',
});
} catch (e) {
console.error(`Failed to change #${id} state to #${state}:\t`, e);
throw e;
}
},
}
Here "Getter" does not have any values but "After Mutation" we have the values.
Because initially the store variable is empty.The values are itself set in the mutation.Hence showing up after mutation is called.
Well now to get data after mutation is fired use async await in your method as below:
async onChange(product) {
await this.productBasedListings( product.id ).then(() => {
console.log('LIST:', this.listingContracts);
})
},

VueJS - Dynamic State Management multiple instances

I am creating an app and I have a component "Message" which uses a store to get data back from a JSON file (this will be eventually a database) and the component is as follows:
export default {
props: ['message'],
mounted: function() {
this.$store.dispatch("FETCHMESSAGE", this.message);
},
computed: {
title: function() {
return this.$store.state.message;
}
}
}
I have the following mutation:
FETCHMESSAGE: function (context, type)
{
var data = json.type; // Get the data depending on the type passed in
// COMMIT THE DATA INTO THE STORE
}
And I use it as the following:
<MessageApp message="welcome"></MessageApp>
This works for the most part and the correct message is displayed. The issue is when I have multiple instances of MessageApp being called on the same page. They both show the same message (of the last message) being called. E.g.
<MessageApp message="welcome"></MessageApp>
<MessageApp message="goodbye"></MessageApp>
They will each show the goodbye message. I know why this is happening but is it possible to have multiple instances of the store so that this does not happen?
Vuex is "a centralized store for all the components in an application," as the docs say.
So imagine that you have a variable (or many) which you can use and change from all your components.
Also when you want to get properties from state, it is recommended to use getters.
I can't understand what you want to do, but if you want, you can have multiple states, getters, mutations and actions and use them as modules in the store (read more). See below example from Vuex docs:
const moduleA = {
state: { title: '' },
mutations: { changeTitle(state, payload) { state.title = payload } },
actions: { changeTitle({commit}, payload) { commit('changeTitle', payload) } },
getters: { getTitle(state) { return state.title } }
}
const moduleB = {
state: { title: '' },
mutations: { changeTitle(state, payload) { state.title = payload } },
actions: { changeTitle({commit}, payload) { commit('changeTitle', payload) } },
getters: { getTitle(state) { return state.title } }
}
const store = new Vuex.Store({
modules: {
a: moduleA,
b: moduleB
}
})
store.state.a // -> `moduleA`'s state
store.state.b // -> `moduleB`'s state