Vuex state changing with mutation - apollo graphql query - vue.js

I have a form where the user edits the object - in my case, metadata about a story - after it is loaded from GraphQL. However, when I use my vue-router guard to check if the story has been changed, the story state is always the modified value.
Vuex story.js
...
getters: {
...formSubmit.getters,
getStory: (state) => {
return state.story},
getEditedStory: (state) => state.editedStory,
getStoryDescription: (state) => {
return state.story.description
}
},
mutations: {
...formSubmit.mutations,
setStory(state, payload) {
state.story = payload
},
setEditedStory(state, payload) {
state.editedStory = payload
}
},
...
Form component
export default {
...
apollo: {
story: {
query: gql`query GetStory($id: ID!) {
story(id: $id) {
name
summary
description
}
}`,
variables() {
return {
id: this.id
}
},
result({ data}) {
this.setText(data.story.description)
this.setStory(data.story)
this.setEditedStory(data.story)
},
}
},
...
In my form I have the values mapped with v-model:
<v-text-field
v-model="story.name"
class="mx-4"
label="Add your title..."
single-line
:counter="TITLE_TEXT_MAX_LENGTH"
outlined
solo
:disabled="updateOrLoadInProgress"
/>
However, for some reason whenever I call this.getStory its value is modified accordingly to the v-model. Why?

Although I still don't quite understand why, it seems like the changes to the apollo variable story affect the store.story values with using the mutations to set them.
I've modified the initial setters to be like this:
this.setText(data.story.description)
let loadedStory = {
name: data.story.name,
description: data.story.description,
summary: data.story.summary,
}
this.setStory(loadedStory)
this.setEditedStory(data.story)
},
which seems to prevent the state.story from following the changes to the apollo created story variable. This seems like unexpected behaviour, and I don't quite understand what part of javascript object assignment makes this work this way.

Related

Sendbird - Nuxt -Vuex - Do not mutate vuex store state outside mutation handlers [duplicate]

Why do I get this error:
Error [vuex] Do not mutate vuex store state outside mutation handlers.
What does it mean?
It happens when I try to type in the edit input file.
pages/todos/index.vue
<template>
<ul>
<li v-for="todo in todos">
<input type="checkbox" :checked="todo.done" v-on:change="toggle(todo)">
<span :class="{ done: todo.done }">{{ todo.text }}</span>
<button class="destroy" v-on:click="remove(todo)">delete</button>
<input class="edit" type="text" v-model="todo.text" v-todo-focus="todo == editedTodo" #blur="doneEdit(todo)" #keyup.enter="doneEdit(todo)" #keyup.esc="cancelEdit(todo)">
</li>
<li><input placeholder="What needs to be done?" autofocus v-model="todo" v-on:keyup.enter="add"></li>
</ul>
</template>
<script>
import { mapMutations } from 'vuex'
export default {
data () {
return {
todo: '',
editedTodo: null
}
},
head () {
return {
title: this.$route.params.slug || 'all',
titleTemplate: 'Nuxt TodoMVC : %s todos'
}
},
fetch ({ store }) {
store.commit('todos/add', 'Hello World')
},
computed: {
todos () {
// console.log(this)
return this.$store.state.todos.list
}
},
methods: {
add (e) {
var value = this.todo && this.todo.trim()
if (value) {
this.$store.commit('todos/add', value)
this.todo = ''
}
},
toggle (todo) {
this.$store.commit('todos/toggle', todo)
},
remove (todo) {
this.$store.commit('todos/remove', todo)
},
doneEdit (todo) {
this.editedTodo = null
todo.text = todo.text.trim()
if (!todo.text) {
this.$store.commit('todos/remove', todo)
}
},
cancelEdit (todo) {
this.editedTodo = null
todo.text = this.beforeEditCache
},
},
directives: {
'todo-focus' (el, binding) {
if (binding.value) {
el.focus()
}
}
},
}
</script>
<style>
.done {
text-decoration: line-through;
}
</style>
stores/todos.js
export const state = () => ({
list: []
})
export const mutations = {
add (state, text) {
state.list.push({
text: text,
done: false
})
},
remove (state, todo) {
state.list.splice(state.list.indexOf(todo), 1)
},
toggle (state, todo) {
todo.done = !todo.done
}
}
Any ideas how I can fix this?
It could be a bit tricky to use v-model on a piece of state that belongs to Vuex.
and you have used v-model on todo.text here:
<input class="edit" type="text" v-model="todo.text" v-todo-focus="todo == editedTodo" #blur="doneEdit(todo)" #keyup.enter="doneEdit(todo)" #keyup.esc="cancelEdit(todo)">
use :value to read value and v-on:input or v-on:change to execute a method that perform the mutation inside an explicit Vuex mutation handler
This issue is handled here: https://vuex.vuejs.org/en/forms.html
Hello I have get the same problem and solve it with clone my object using one of the following:
{ ...obj} //spread syntax
Object.assign({}, obj)
JSON.parse(JSON.stringify(obj))
For your code I think you need to replace this part
computed: {
todos () {
// console.log(this)
return this.$store.state.todos.list
}
}
With this
computed: {
todos () {
// console.log(this)
return {...this.$store.state.todos.list}
}
}
I don't make sure if this is the best way but hope this helpful for other people that have the same issue.
This error may come from the fact you shallow cloned an object.
Meaning that you've tried to copy an object but an object is not a primitive type (like String or Number), hence it's passed by reference and not value.
Here you think that you cloned one object into the other, while you are still referencing the older one. Since you're mutating the older one, you got this nice warning.
Here is a GIF from Vue3's documentation (still relevant in our case).
On the left, it's showing an object (mug) being not properly cloned >> passed by reference.
On the right, it's properly cloned >> passed by value. Mutating this one does not mutate the original
The proper way to manage this error is to use lodash, this is how to load it efficiently in Nuxt:
Install lodash-es, eg: yarn add lodash-es, this is an optimized tree-shakable lodash ES module
you may also need to transpile it in your nuxt.config.js with the following
build: {
transpile: ['lodash-es'],
}
load it into your .vue components like this
<script>
import { cloneDeep } from 'lodash-es'
...
const properlyClonedObject = cloneDeep(myDeeplyNestedObject)
...
</script>
Few keys points:
lodash is recommended over JSON.parse(JSON.stringify(object)) because it does handle some edge-cases
we only load small functions from lodash and not the whole library thanks to this setup, so there is no penalty in terms of performance
lodash has a lot of well battle-tested useful functions, which is heavily lacking in JS (no core library)
UPDATE: structuredClone is also native and quite performant if you're looking for a solution for a deep copy, bypassing the need for Lodash at all.
There is no headache if you can use lodash
computed: {
...mapState({
todo: (state) => _.cloneDeep(state.todo)
})
}
Just in case someone's still being troubled by this,
I got my code working by making a duplicate/clone of the store state.
In your case, try something like this...
computed: {
todos () {
return [ ...this.$store.state.todos.list ]
}
}
It's basically a spread operator which results in making a clone of the todos.list array. With that, you're not directly changing the values of your state, just don't forget commit so your mutations will be saved in the store.
export default new Vuex.Store({
...
strict: true
})
try to comment "strict"
If you are using Vuex Modules, you might bump into this error if your module's data property is an object, instead of a function that returns an object, and you are sharing this Module between more than one Store.
So instead of:
// In stores/YourModule.js
export default {
state: { name: 'Foo' },
}
Change it to:
// In stores/YourModule.js
export default {
state: () => {
return { name: 'Foo' };
},
}
This is actually documented here:
Sometimes we may need to create multiple instances of a module, for
example:
Creating multiple stores that use the same module (e.g. To avoid
stateful singletons in the SSR (opens new window)when the
runInNewContext option is false or 'once'); Register the same module
multiple times in the same store. If we use a plain object to declare
the state of the module, then that state object will be shared by
reference and cause cross store/module state pollution when it's
mutated.
This is actually the exact same problem with data inside Vue
components. So the solution is also the same - use a function for
declaring module state (supported in 2.3.0+):
If your data is an array with objects inside. Below snippet is the solution
const toyData = await this.$store.dispatch(
`user/fetchCoinToys`,
payload
)
const msgList = toyData.msglist.map((data) => {
return { ...data }
})
I had to add mutation and call it instead of setting directly.
wrong:
someAction({state, rootState}) {
state.someValue = true;
}
right:
mutations: {
...
setSomeValue(state, val) {
state.someValue = val;
},
...
}
...
someAction({state, commit, rootState}) {
commit('setSomeValue', true);
}
It is not your case but if someone is using typescript and is having the same problem, adding this: any as the first param in your method or somewhere else should fix the problem

Vuex: getter to dispatch action if no data is in the state

I use vuex for my state as well as fetching data and display it in my application.
But I wonder if I'm doing it right. At the moment I dispatch an fetchDataAsync action from the component mounted hook, and I have an getter to display my data. Below is a code example of how I do it currently.
I wonder if it's necessary. What I really want is a getter, that looks at the state, checks if the data is already there and if the data is not there it is able to dispatch an action to fetch the missing data.
The API of vuex does not allow it so I need to put more logic into my components. E.g. if the data is depended of a prop I need a watcher that looks at the prop and dispatches the fetchDataAsync action.
For me it just feels wrong and I wonder if there is a better way.
let store = new Vuex.Store({
state: {
posts: {}
},
mutations: {
addPost(state, post) {
Vue.set(state.posts, post.id, post);
}
},
actions: {
fetchPostAsync({ commit }, parameter) {
setTimeout(
() =>
commit("addPost", { id: parameter, message: "got loaded asynchronous" }),
1000
);
}
},
getters: {
// is it somehow possible to detect: ob boy, I don't have this id,
// I'd better dispatch an action trying to fetch it...?
getPostById: (state) => (id) => state.posts[id]
}
});
new Vue({
el: "#app",
store,
template : "<div>{{ postToDisplay ? postToDisplay.message : 'loading...' }} </div>",
data() {
return {
parameter: "a"
};
},
computed: {
...Vuex.mapGetters(["getPostById"]),
postToDisplay() {
return this.getPostById(this.parameter);
}
},
methods: {
...Vuex.mapActions(["fetchPostAsync"])
},
mounted() {
this.fetchPostAsync(this.parameter);
}
});
I also created a codepen
Personally I think the solution you suggested (adding a watcher that dispatches fetchPostAsync if the post is not found) is the best one. As another commenter stated, getters should not have side effects.

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

Vuex state change on object does not trigger rerender

I have a variable in the vuex store called permissions. And i want my component to trigger a rerender when the getPermissions changes. In the vue devtools i clearly see that the state has changed in the store, but the component stil get the old state from getPermissions. In order for me to see changes, I have to do a refresh. Has it something to do with the way i mutate it? or the fact that it is an object?
It looks like this when populated:
permissions: {
KS1KD933KD: true,
KD9L22F732: false
}
I use this method to do mutations on it and a getter to get it:
const getters = {
getPermissions: state => state.permissions
};
const mutations = {
set_recording_permissions(state, data) {
let newList = state.permissions;
newList[data.key] = data.bool;
Vue.set(state, 'permissions', newList);
}
};
And in the component i use mapGetters to get access to it
computed: {
...mapGetters('agentInfo',['getPermissions'])
}
In order to update the permissions value i use this action (it does require a succesfull api request before updating the value) :
const actions = {
async setRecordingPermissions({ commit }, data) {
let body = {
agentId: data.userName,
callId: data.callId,
allowUseOfRecording: data.allowUseOfRecording
};
try {
await AgentInfoAPI.editRecordingPermissions(body).then(() => {
commit('set_recording_permissions', { key: data.callId, bool: data.allowUseOfRecording });
commit('set_agent_info_message', {
type: 'success',
text: `Endret opptaksrettigheter`
});
});
} catch (error) {
console.log(error);
commit('set_agent_info_message', {
type: 'error',
text: `Request to ${error.response.data.path} failed with ${error.response.status} ${error.response.data.message}`
});
}
}
}
Since the getter only returns state variable you should use mapState, if you want to access it directly.
computed: mapState(['permissions'])
However, you can also use mapGetters, but then in your template, have to use getPermissions and not permissions.
Example template:
<ul id="permissions">
<li v-for="permission in getPermissions">
{{ permission }}
</li>
</ul>
If you have done this it is probably an issue with the object reference. You use Vue.set, but you set the same object reference. You have to create a new object or set the key you want to update directly.
new object
let newList = { ...state.permissions };
Vue.set
Vue.set(state.permission, data.key, data.value);
I don't know what the rest of you code looks like, but you will need to use actions to correctly mutate you store.
For example:
const actions = {
setName({ commit }, name) {
commit('setName', name);
},
}

Vuex - Do not mutate vuex store state outside mutation handlers

Why do I get this error:
Error [vuex] Do not mutate vuex store state outside mutation handlers.
What does it mean?
It happens when I try to type in the edit input file.
pages/todos/index.vue
<template>
<ul>
<li v-for="todo in todos">
<input type="checkbox" :checked="todo.done" v-on:change="toggle(todo)">
<span :class="{ done: todo.done }">{{ todo.text }}</span>
<button class="destroy" v-on:click="remove(todo)">delete</button>
<input class="edit" type="text" v-model="todo.text" v-todo-focus="todo == editedTodo" #blur="doneEdit(todo)" #keyup.enter="doneEdit(todo)" #keyup.esc="cancelEdit(todo)">
</li>
<li><input placeholder="What needs to be done?" autofocus v-model="todo" v-on:keyup.enter="add"></li>
</ul>
</template>
<script>
import { mapMutations } from 'vuex'
export default {
data () {
return {
todo: '',
editedTodo: null
}
},
head () {
return {
title: this.$route.params.slug || 'all',
titleTemplate: 'Nuxt TodoMVC : %s todos'
}
},
fetch ({ store }) {
store.commit('todos/add', 'Hello World')
},
computed: {
todos () {
// console.log(this)
return this.$store.state.todos.list
}
},
methods: {
add (e) {
var value = this.todo && this.todo.trim()
if (value) {
this.$store.commit('todos/add', value)
this.todo = ''
}
},
toggle (todo) {
this.$store.commit('todos/toggle', todo)
},
remove (todo) {
this.$store.commit('todos/remove', todo)
},
doneEdit (todo) {
this.editedTodo = null
todo.text = todo.text.trim()
if (!todo.text) {
this.$store.commit('todos/remove', todo)
}
},
cancelEdit (todo) {
this.editedTodo = null
todo.text = this.beforeEditCache
},
},
directives: {
'todo-focus' (el, binding) {
if (binding.value) {
el.focus()
}
}
},
}
</script>
<style>
.done {
text-decoration: line-through;
}
</style>
stores/todos.js
export const state = () => ({
list: []
})
export const mutations = {
add (state, text) {
state.list.push({
text: text,
done: false
})
},
remove (state, todo) {
state.list.splice(state.list.indexOf(todo), 1)
},
toggle (state, todo) {
todo.done = !todo.done
}
}
Any ideas how I can fix this?
It could be a bit tricky to use v-model on a piece of state that belongs to Vuex.
and you have used v-model on todo.text here:
<input class="edit" type="text" v-model="todo.text" v-todo-focus="todo == editedTodo" #blur="doneEdit(todo)" #keyup.enter="doneEdit(todo)" #keyup.esc="cancelEdit(todo)">
use :value to read value and v-on:input or v-on:change to execute a method that perform the mutation inside an explicit Vuex mutation handler
This issue is handled here: https://vuex.vuejs.org/en/forms.html
Hello I have get the same problem and solve it with clone my object using one of the following:
{ ...obj} //spread syntax
Object.assign({}, obj)
JSON.parse(JSON.stringify(obj))
For your code I think you need to replace this part
computed: {
todos () {
// console.log(this)
return this.$store.state.todos.list
}
}
With this
computed: {
todos () {
// console.log(this)
return {...this.$store.state.todos.list}
}
}
I don't make sure if this is the best way but hope this helpful for other people that have the same issue.
This error may come from the fact you shallow cloned an object.
Meaning that you've tried to copy an object but an object is not a primitive type (like String or Number), hence it's passed by reference and not value.
Here you think that you cloned one object into the other, while you are still referencing the older one. Since you're mutating the older one, you got this nice warning.
Here is a GIF from Vue3's documentation (still relevant in our case).
On the left, it's showing an object (mug) being not properly cloned >> passed by reference.
On the right, it's properly cloned >> passed by value. Mutating this one does not mutate the original
The proper way to manage this error is to use lodash, this is how to load it efficiently in Nuxt:
Install lodash-es, eg: yarn add lodash-es, this is an optimized tree-shakable lodash ES module
you may also need to transpile it in your nuxt.config.js with the following
build: {
transpile: ['lodash-es'],
}
load it into your .vue components like this
<script>
import { cloneDeep } from 'lodash-es'
...
const properlyClonedObject = cloneDeep(myDeeplyNestedObject)
...
</script>
Few keys points:
lodash is recommended over JSON.parse(JSON.stringify(object)) because it does handle some edge-cases
we only load small functions from lodash and not the whole library thanks to this setup, so there is no penalty in terms of performance
lodash has a lot of well battle-tested useful functions, which is heavily lacking in JS (no core library)
UPDATE: structuredClone is also native and quite performant if you're looking for a solution for a deep copy, bypassing the need for Lodash at all.
There is no headache if you can use lodash
computed: {
...mapState({
todo: (state) => _.cloneDeep(state.todo)
})
}
Just in case someone's still being troubled by this,
I got my code working by making a duplicate/clone of the store state.
In your case, try something like this...
computed: {
todos () {
return [ ...this.$store.state.todos.list ]
}
}
It's basically a spread operator which results in making a clone of the todos.list array. With that, you're not directly changing the values of your state, just don't forget commit so your mutations will be saved in the store.
export default new Vuex.Store({
...
strict: true
})
try to comment "strict"
If you are using Vuex Modules, you might bump into this error if your module's data property is an object, instead of a function that returns an object, and you are sharing this Module between more than one Store.
So instead of:
// In stores/YourModule.js
export default {
state: { name: 'Foo' },
}
Change it to:
// In stores/YourModule.js
export default {
state: () => {
return { name: 'Foo' };
},
}
This is actually documented here:
Sometimes we may need to create multiple instances of a module, for
example:
Creating multiple stores that use the same module (e.g. To avoid
stateful singletons in the SSR (opens new window)when the
runInNewContext option is false or 'once'); Register the same module
multiple times in the same store. If we use a plain object to declare
the state of the module, then that state object will be shared by
reference and cause cross store/module state pollution when it's
mutated.
This is actually the exact same problem with data inside Vue
components. So the solution is also the same - use a function for
declaring module state (supported in 2.3.0+):
If your data is an array with objects inside. Below snippet is the solution
const toyData = await this.$store.dispatch(
`user/fetchCoinToys`,
payload
)
const msgList = toyData.msglist.map((data) => {
return { ...data }
})
I had to add mutation and call it instead of setting directly.
wrong:
someAction({state, rootState}) {
state.someValue = true;
}
right:
mutations: {
...
setSomeValue(state, val) {
state.someValue = val;
},
...
}
...
someAction({state, commit, rootState}) {
commit('setSomeValue', true);
}
It is not your case but if someone is using typescript and is having the same problem, adding this: any as the first param in your method or somewhere else should fix the problem