Search code examples
vue.jsvuexvuetify.js

UI not reactive / doesn't rerender after state update


I built the following simple UI.

screenshot of 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}`);
  }
};

Solution

  • 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.