UI not reactive / doesn't rerender after state update - vue.js

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.

Related

Vuex mutate object best practices

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)
}
}
}

Limiting the two way binding with vuejs / variable values change when modifying another variable

I have a 'client-display' component containing a list of clients that I get in my store via mapGetter. I use 'v-for' over the list to display all of them in vuetify 'v-expansion-panels', thus one client = one panel. In the header of those panels, I have a 'edit-delete' component with the client passed to as a prop. This 'edit-delete' basically just emits 'edit' or 'delete' events when clicked on the corresponding icon with the client for payload. When I click on the edit icon, the edit event is then catched in my 'client-display' so I can assign the client to a variable called 'client' (sorry I know it's confusing a bit). I pass this variable to my dialog as a prop and I use this dialog to edit the client.
So the probleme is : When I edit a client, it does edit properly, but if I click on 'cancel', I find no way to revert what happened in the UI. I tried keeping an object with the old values and reset it on a cancel event, but no matter what happens, even the reference values that I try to keep in the object change, and this is what is the most surprising to me. I tried many things for this, such as initiating a new object and assigning the values manually or using Object.assign(). I tried a lot of different ways to 'unbind' all of this, nothing worked out. I'd like to be able to wait for the changes to be commited in the store before it's visible in the UI, or to be able to have a reference object to reset the values on a 'cancel' event.
Here are the relevant parts of the code (I stripped a lot of stuff to try and make it easier to read, but I think everything needed is there):
Client module for my store
I think this part works fine because I get the clients properly, though maybe something is binded and it should not
const state = {
clients: null,
};
const getters = {
[types.CLIENTS] : state => {
return state.clients;
},
};
const mutations = {
[types.MUTATE_LOAD]: (state, clients) => {
state.clients = clients;
},
};
const actions = {
[types.FETCH]: ({commit}) => {
clientsCollection.get()
.then((querySnapshot) => {
let clients = querySnapshot.docs.map(doc => doc.data());
commit(types.MUTATE_LOAD, clients)
}).catch((e) => {
//...
});
},
}
export default {
state,
getters,
mutations,
...
}
ClientsDisplay component
<template>
<div>
<div>
<v-expansion-panels>
<v-expansion-panel
v-for="c in clientsDisplayed"
:key="c.name"
>
<v-expansion-panel-header>
<div>
<h2>{{ c.name }}</h2>
<edit-delete
:element="c"
#edit="handleEdit"
#delete="handleDelete"
/>
</div>
</v-expansion-panel-header>
<v-expansion-panel-content>
//the client holder displays the client's info
<client-holder
:client="c"
/>
</v-expansion-panel-content>
</v-expansion-panel>
</v-expansion-panels>
</div>
<client-add-dialog
v-model="clientPopup"
:client="client"
#cancelEdit="handleCancel"
/>
</div>
</template>
<script>
import { mapGetters, mapActions } from 'vuex';
import * as clientsTypes from '../../../../store/modules/clients/types';
import ClientDialog from './ClientDialog';
import EditDelete from '../../EditDelete';
import ClientHolder from './ClientHolder';
import icons from '../../../../constants/icons';
export default {
name: 'ClientsDisplay',
components: {
ClientHolder,
ClientAddDialog,
EditDelete,
},
data() {
return {
icons,
clientPopup: false,
selectedClient: null,
client: null,
vueInstance: this,
}
},
created() {
this.fetchClients();
},
methods: {
...mapGetters({
'stateClients': clientsTypes.CLIENTS,
}),
...mapActions({
//this loads my clients in my state for the first time if needed
'fetchClients': clientsTypes.FETCH,
}),
handleEdit(client) {
this.client = client;
this.clientPopup = true;
},
handleCancel(payload) {
//payload.uneditedClient, as defined in the dialog, has been applied the changes
},
},
computed: {
isMobile,
clientsDisplayed() {
return this.stateClients();
},
}
}
</script>
EditDelete component
<template>
<div>
<v-icon
#click.stop="$emit('edit', element)"
>edit</v-icon>
<v-icon
#click.stop="$emit('delete', element)"
>delete</v-icon>
</div>
</template>
<script>
export default {
name: 'EditDelete',
props: ['element']
}
</script>
ClientDialog component
Something to note here : the headerTitle stays the same, even though the client name changes.
<template>
<v-dialog
v-model="value"
>
<v-card>
<v-card-title
primary-title
>
{{ headerTitle }}
</v-card-title>
<v-form
ref="form"
>
<v-text-field
label="Client name"
v-model="clientName"
/>
<address-fields
v-model="clientAddress"
/>
</v-form>
<v-card-actions>
<v-btn
#click="handleCancel"
text
>Annuler</v-btn>
<v-btn
text
#click="submit"
>Save</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>
<script>
import AddressFields from '../../AddressFields';
export default {
name: 'ClientDialog',
props: ['value', 'client'],
components: {
AddressFields,
},
data() {
return {
colors,
clientName: '',
clientAddress: { province: 'QC', country: 'Canada' },
clientNote: '',
uneditedClient: {},
}
},
methods: {
closeDialog() {
this.$emit('input', false);
},
handleCancel() {
this.$emit('cancelEdit', { uneditedClient: this.uneditedClient, editedClient: this.client})
this.closeDialog();
},
},
computed: {
headerTitle() {
return this.client.name
}
},
watch: {
value: function(val) {
// I watch there so I can reset the client whenever I open de dialog
if(val) {
// Here I try to keep an object with the value of this.client before I edit it
// but it doesn't seem to work as I intend
Object.assign(this.uneditedClient, this.client);
this.clientName = this.client.name;
this.clientContacts = this.client.contacts;
this.clientAddress = this.client.address;
this.clientNote = '';
}
}
}
}
</script>
To keep an independent copy of the data, you'll want to perform a deep copy of the object using something like klona. Using Object.assign is a shallow copy and doesn't protect against reference value changes.

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]
}
}

How to set a state (and persist after reload) in vuex using mutation?

I'm currently doing the login authentication part of a web app using Vue.js / Vuex and Laravel PHP.
After successful authentication, I want to call my userObject from the backend (attributes includes: email, role_id, etc.) and show specific attributes in the frontend (navbar component).
I have checked the localStorage and the userObject is stored there, but it seems it is not committed to my store (currentUser) (vue dev tools showed the currentUser{} is empty)
The following are my codes:
store.js
import Vue from 'vue';
import Vuex from 'vuex';
import Api from './_services/Api.js';
Vue.use(Vuex)
export default new Vuex.Store({
state: {
currentUser: {},
},
mutations: {
SET_CURRENT_USER(state, user) {
state.currentUser = user;
window.localStorage.currentUser = JSON.stringify(user);
},
LOGOUT_USER(state) {
state.currentUser = {}
window.localStorage.currentUser = JSON.stringify({});
}
},
actions: {
logoutUser({commit}) {
commit('LOGOUT_USER')
},
async loginUser({commit}, loginInfo) { //store.js
console.log(loginInfo)
try {
let response = await Api().post('/login', loginInfo);
console.log(response)
let user = response.data.user;
console.log(user)
commit('SET_CURRENT_USER', user);
console.log(JSON.parse(window.localStorage.currentUser));
return user;
} catch {
return {error: "Email / Password combination was incorrect or unrecognized by the system. Please try again."}
}
}
}
})
navbar.vue
<template>
<nav>
<v-toolbar app :fixed="toolbar.fixed" :clipped-left="toolbar.clippedLeft" color="black">
<v-toolbar-side-icon class= "white--text" #click.stop="toggleMiniDrawer"></v-toolbar-side-icon>
<v-toolbar-title class="text-uppercase white--text" >
<v-btn flat to="/dashboard">
<h1 class="font-weight-black display-1 white--text" >Activities</h1>
</v-btn>
</v-toolbar-title>
<v-spacer></v-spacer>
<h1 class="font-weight-regular title white--text">{{currentUser.name + ', ' + currentUser.email}}</h1>
</v-toolbar>
<v-list-tile #click="logoutUser">
<v-list-tile-action>
<v-icon class="black--text">exit_to_app</v-icon>
</v-list-tile-action>
<v-list-tile-content >
<v-list-tile-title class="black--text">Logout</v-list-tile-title>
</v-list-tile-content>
</v-list-tile>
</v-flex>
</v-list>
</v-navigation-drawer>
</nav>
</template>
<script>
import axios from 'axios'
import { mapState } from 'vuex';
export default {
data(){
return{
}
},
methods: {
logoutUser () {
this.$store.dispatch("logoutUser");
}
},
computed: {
...mapState(['currentUser'])
}
};
</script>
I tried explicitly putting data in the currentUser{} in store.js
currentUser{name: 'Test', email: 'test#mail.com'}
and it successfully displayed in the navbar.
This brought me to the conclusion that I weren't able to successfully put the userObject from 'user' to 'currentUser{}', or rather, it doesn't persist after reloads.
Thank you!

vuex module mode in nuxtjs

I'm trying to implement a todo list using modules mode in the vuex store in nuxtjs but get the error this.$store.todo is undefined and cant find much about this relating to nuxt
Can anyone assist please I have
store index.js
export const state = () => ({
})
export const mutations = {
}
store todo.js
export const state = () => ({
todos: [],
})
export const mutations = {
mutations ...
}
export const actions = {
actions ...
}
export const getters = {
getters ...
}
index.vue page
<template>
<div>
<h2>Todos:</h2>
<p> Count: {{ doneTodosCount }} </p>
<ul v-if="todos.length > 0">
<li v-for="(todo, i) in todos" :key="i">
...
</li>
</ul>
<p v-else>Done!</p>
<div class="add-todo">
<input type="text" v-model="newTodoText">
<button #click="add">Add todo</button>
</div>
</div>
</template>
<script>
import { mapState, mapMutations, mapActions, mapGetters } from 'vuex'
export default {
name: 'app',
data () {
return {
newTodoText: ""
}
},
created () {
this.$store.todo.dispatch('loadData')
},
computed: {
...mapState(['todos', ]),
...mapGetters([ 'doneTodosCount', 'doneTodos'])
},
methods: {
toggle (todo) {
this.$store.todo.dispatch('toggleTodo', todo)
},
}
}
</script>
From what i read I thought this should work but doesn't
I should add it all works fine if i don't use modules mode and just have a single index.js setup
Many Thanks
You need to call it differently
this.$store.dispatch('todo/toggleTodo', todo)
Also better to call it in fetch method, not created