Vuex mutate object best practices - vue.js

I'm developing a module that enables users to place remarks on an object. I use a Vuex store and a component to visualize the remarks. I've pasted the code below.
Thing is I bind the text area that displays the remark body to a v-model that is the property body of the remark object. Since I return the array of remarks from the store with the getremarks getter any changes to the remark object properties are directly changed in the store as well since it is the same object that gets passed around. I thereby effectively bypass the entire action/mutation pattern Vuex wants me to implement.
I can always pass the mutated remark to an action and mutation in the store but that means I'm mutating an object that is allready mutated. I was wondering if there are any best practices around?
Store
import Vue from 'vue'
const model = {
state: {
remarks: [],
},
mutations: {
SET_REMARKS(state, remarks) {
state.remarks = remarks;
}
},
getters: {
getRemarks(state) {
return state.remarks;
},
getRemarkById: (state) => (id) => {
return state.remarks.find(remark => remark.model_remark_id === id);
}
},
actions: {
}
}
export default model;
Component
<template>
<v-list two-line expand>
<v-list-group v-for="remark in remarks" :key="remark.model_remark_id">
<template v-slot:activator>
<v-list-item-icon>
<v-icon v-text="icon" :color="remark.priority === 1 ? 'red' : remark.priority === 2 ? 'orange' : '#1478c7'" />
</v-list-item-icon>
<v-list-item-content>
<v-list-item-title><b>{{ remark.created_by_user.name }}</b></v-list-item-title>
<v-list-item-subtitle>Created at {{ moment(remark.created_at).format('DD-MM-YYYY HH:ss:mm') }}</v-list-item-subtitle>
</v-list-item-content>
</template>
<v-list-item>
<v-list-item-icon>
</v-list-item-icon>
<v-list-item-content>
<v-row>
<v-col>
<v-textarea
class="mx-2"
v-model="remark.body"
auto-grow
/>
</v-col>
</v-row>
<v-row>
<v-col align="right">
<v-btn
color="primary"
elevation="2"
#click="remarkDone(remark)"
>Done</v-btn>
<v-btn
color="primary"
elevation="2"
#click="remarkUpdate(remark)"
>Update</v-btn>
</v-col>
</v-row>
</v-list-item-content>
</v-list-item>
</v-list-group>
</v-list>
</template>
<script>
import { mapGetters, mapActions } from "vuex";
export default {
data: () => ({
icon: "mdi-comment"
}),
computed: {
remarks: function () {
return this.getRemarks();
},
},
methods: {
...mapGetters([
"getRemarks",
"getRemarkById"
]),
remarkUpdate(remark) {
console.log(remark);
const x = this.getRemarkById()(remark.model_remark_id);
console.log(x);
}
},
};
</script>

I think the key principle to keep in mind here is separation of concerns. Let Vuex handle all mutations of the state and let your Vue component simply fetch the remarks from the state. The state is reactive, so if you call a mutation from your component to change your remarks, you want the component to simply react to that change and render the updated markup.
Also, you should use mapGetters to map your getters to computed properties, not methods. I'm also not sure if you need getRemarkById getter in updateRemark() since you already have the remark as an argument.
export default {
computed: {
...mapGetters({ remarks: 'getRemarks' })
},
methods: {
updateRemark(remark) {
// here you should commit a mutation to update your remark
this.$store.commit('updateRemark', remark)
}
}
}

Related

can not initialize data from prop

here is my child component:
export default {
name: "SnackBar",
props: ["show", 'msg', 'progress'],
data: () => ({
show2: this.show,
}),
}
and its template:
<template>
<div class="text-center">
<v-snackbar
v-model="this.show2"
:multi-line="true"
timeout="-1"
>
{{ msg }}
<template v-slot:action="{ attrs }">
<v-progress-circular
v-if="progress"
style="float: right"
indeterminate
color="red"/>
<v-btn
color="red"
text
v-bind="attrs"
#click="show2 = false">
Close
</v-btn>
</template>
</v-snackbar>
</div>
</template>
and I use it in the parent :
<SnackBar :show="snackbar.show" :msg="snackbar.msg" :progress="snackbar.progress"/>
the problem is that the show2 data is not defined:, here is the console log:
Property or method "show2" is not defined on the instance but referenced during render.
Make sure that this property is reactive, either in the data option, or for class-based components, by initializing the property.
from official docs:
The prop is used to pass in an initial value; the child component
wants to use it as a local data property afterwards. In this case,
it’s best to define a local data property that uses the prop as its
initial value:
props: ['initialCounter'],
data: function () {
return {
counter: this.initialCounter
}
}
I am doing exactly the same thing, what is the problem?
Using the arrow function for the data property loses the vue component context, try to use a simple function :
export default {
name: "SnackBar",
props: ["show", 'msg', 'progress'],
data(){
return {show2: this.show,}
},
}
or try out this :
export default {
name: "SnackBar",
props: ["show", 'msg', 'progress'],
data: (vm) => ({
show2: vm.show,
}),
}

Vuex variable properly updated but not refreshed in template

When I change the value of my v-text-field, I can see from the Vue tab of the developer tools that the value of lastName is updated in the user store with my code below. But, for some reason, the values of my 3 tests in my template below are not updated (or synchronized). Why ? What's wrong in my code ?
pages/home.vue:
<template>
<v-container>
<v-container>
<v-row>
<v-col>
<v-text-field v-model="lastName" />
</v-col>
</v-row>
</v-container>
TEST 1:
{{ $store.state.user.last_name }}
TEST 2:
{{ lastName }}
TEST 3:
<v-text-field v-model="lastName" />
</v-container>
</template>
<script>
export default {
computed: {
lastName: {
get() {
return this.$store.state.user.last_name
},
set(value) {
this.$store.commit('user/UPDATE_PROP', ['last_name', value])
}
}
}
}
</script>
store/user.js
export const mutations = {
UPDATE_PROP(state, payload) {
state[payload[0]] = payload[1]
}
}
I think the problem is in the way you set up your store. According to the Nuxt documentation,
you create a store directory and
every .js file inside the store directory is transformed as a namespaced module (index being the root module).
Also, according to the documentation,
... your state value should always be a function to avoid unwanted shared state on the server side.
Your state should look something like this
store/user.js
export const state = () => ({
last_name: ''
})
export const mutations = {
UPDATE_PROP(state, payload) {
state[payload[0]] = payload[1]
}
}

UI not reactive / doesn't rerender after state update

I built the following simple UI.
On clicking the trash icon, the bookmark should be deleted and the UI updated, because of the state change. The API call is made, and I can see in the dev tools, that the action takes place. However, I have to either merge the action or navigate away from the page or do a hard reload for the deleted bookmark not to show up. I expected this to work through the usage of vuex's mapState helper.
Below are the relevant parts.
view (sorry, this is a little messy) - this is actually the unabridged version:
<template>
<div>
<v-card class="mx-auto" max-width="700">
<v-list two-line subheader>
<v-subheader>Bookmarks</v-subheader>
<v-list-item
v-for="obj in Object.entries(bookmarks).sort((a, b) => {
return a[1].paragraph - b[1].paragraph;
})"
:key="obj[0]"
>
<v-list-item-avatar>
<v-icon #click="goTo(obj)">mdi-bookmark</v-icon>
</v-list-item-avatar>
<v-list-item-content #click="goTo(obj)">
<v-list-item-title>
{{ obj[0].split('/')[1] + ' by ' + obj[0].split('/')[0] }}
</v-list-item-title>
<v-list-item-subtitle>
Part {{ obj[1].part + 1 }}, paragraph {{ obj[1].paragraph + 1 }}
</v-list-item-subtitle>
</v-list-item-content>
<v-list-item-action>
<v-btn icon>
<v-icon #click="deleteBookmark(obj[0])" title="Remove bookmark"
>mdi-delete</v-icon
>
</v-btn>
</v-list-item-action>
</v-list-item>
</v-list>
</v-card>
</div>
</template>
<script>
import { mapState, mapActions } from 'vuex';
export default {
computed: {
...mapState(['bookmarks'])
},
methods: {
...mapActions(['deleteBookmark']),
goTo(obj) {
const [authorName, title] = obj[0].split('/');
this.$router.push({
name: 'showText',
params: {
authorName,
title
},
query: { part: obj[1].part, paragraph: obj[1].paragraph }
});
}
}
};
</script>
store:
import Vue from 'vue';
import Vuex from 'vuex';
Vue.use(Vuex);
import apiService from '#/services/ApiService';
const store = new Vuex.Store({
state: {
bookmarks: {}
},
mutations: {
SET_BOOKMARKS(state, bookmarks) {
state.bookmarks = bookmarks;
}
},
actions: {
async deleteBookmark({ commit, state }, key) {
let { bookmarks } = state;
const response = await apiService.deleteBookmark(key);
delete bookmarks[key];
commit('SET_BOOKMARKS', bookmarks);
return response;
}
}
});
export default store;
apiService:
import axios from 'axios';
const apiClient = axios.create({
baseURL: process.env.VUE_APP_API_URL,
withCredentials: true,
headers: {
Accept: 'application/json',
'Content-Type': 'application/json'
},
responseType: 'json'
});
export default {
deleteBookmark(key) {
return apiClient.delete(`/api/bookmarks/${key}`);
}
};
Red flag right here:
delete bookmarks[key];
Please read Change Detection Caveats.
Use Vue.delete instead:
Vue.delete(bookmarks, key);
Doing commit('SET_BOOKMARKS', bookmarks); immediately after doesn't result in any change happening because you're just assigning the same object instance. It might be best to write a REMOVE_BOOKMARK mutation to handle this so you're not changing the Vuex state outside of a mutation.

Vue: Data is not changing with Vuex getter?

I have a modal component that I was triggered when different components mutate the triggerModalState field. I have a getter in my Vuex store called getFulfillmentModalState. The modal is driven off of the local data field called dialog, which I have set to be the value of the getter this.$store.getters.getFulfillmentModalState. I am checking the value of this.$store.getters.getFulfillmentModalState, and it is true after I trigger the modal, but the dialog data is still false. What am I doing wrong here?
<template>
<div class="fulfillment-modal-component">
<v-row justify="center">
<v-dialog v-model="dialog" persistent max-width="290">
<v-card>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="rgba(53, 87, 151, 0.85)" text #click="changeModalState(true)">Cancel</v-btn>
<v-btn color="rgba(53, 87, 151, 0.85)" text #click="changeModalState(false)">Continue</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</v-row>
</div>
</template>
<script>
import {mapState} from 'vuex';
import store from './../../store/store';
export default {
name: 'fulfillment-modal',
mounted() {
},
data() {
return {
dialog: this.$store.getters.getFulfillmentModalState
}
},
}
</script>
dialog isn't reactive in this case because it's declared in data(). Move dialog to computed to make it reactive:
//...
export default {
data() {
return {
//dialog: /*..*/ // move to computed
}
},
computed: {
dialog() {
return this.$store.getters.getFulfillmentModalState
}
}
}
demo

Computed property "dialog" was assigned to but it has no setter [duplicate]

This question already has answers here:
Vuex - Computed property "name" was assigned to but it has no setter
(5 answers)
Closed 4 years ago.
I am reproducing this code (Codepen):
<div id="app">
<v-app id="inspire">
<div class="text-xs-center">
<v-dialog
v-model="dialog"
width="500"
>
<v-btn
slot="activator"
color="red lighten-2"
dark
>
Click Me
</v-btn>
<v-card>
<v-card-title
class="headline grey lighten-2"
primary-title
>
Privacy Policy
</v-card-title>
<v-card-text>
Hello there Fisplay
</v-card-text>
<v-divider></v-divider>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn
color="primary"
flat
#click="dialog = false"
>
I accept
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</div>
</v-app>
</div>
The only difference between my real code and this one, is that I defined dialog in store/index.js (this in Nuxt.js) where I declared dialog an element of the state:
return new Vuex.Store({
state: {
dialog: false,
And then, in my current component I import that $store.state.dialog flag:
<script>
import { mapState } from 'vuex';
export default {
computed: {
...mapState([
'dialog'
]),
}
</script>
Whenever I click on the button, I get this error message:
[Vue warn]: Computed property "dialog" was assigned to but it has no
setter.
How to fix this? Any alternative?
You need to use the Vuex mutation to update the state.
https://vuex.vuejs.org/guide/mutations.html
In your example,
Your click event should be handled in the methods #click="handleClick"
methods: {
handleClick() {
this.$store.commit('openDialog')
}
In your store.js
mutations: {
openDialog(state) {
state.dialog = true
}
}
mapState will only create getters. You should define a mutation in your vuex store, which will be able to change the state.
Just add this to your store:
...
mutations: {
SET_DIALOG_FLAG_FALSE (state) {
state.dialog = false;
},
//optional if you do not want to call the mutation directly
//must important different is, that a mutation have to be synchronous while
//a action can be asynchronous
actions: {
setDialogFalse (context) {
context.commit('SET_DIALOG_FLAG_FALSE');
}
}
If you want to work with mapMutations/mapAction in your component:
import { mapMutation, mapActions } from vuex;
...
//they are methods and not computed properties
methods: {
...mapMutations([
'SET_DIALOG_FLAG_FALSE'
]),
...mapActions([
'setDialogFalse'
])
}
Now in your v-btn you can call the action or mutation.
<v-btn
color="primary"
flat
#click="this.setDialogFalse"> I accept </v-btn>