Search code examples
javascriptvue.jslocal-storagevuex

Mutations not registering using computed properties in Vuex


I have a system that uses a lot of form components to live update a view based on the inputted information. When I first built the app I was mostly using v-model, the local component data, and vue save state to keep the data in local storage so it persisted when they reloaded the page.

However when I moved over to vuex as the program expanded, this was not so straightforward so I decided to use v-model with computed properties using getters and setters so I wouldn't have to write on change functions for over 50 different inputs. I also realized that you can create one computed variable for an object and still use v-model to access and update properties of that object, like so:

  <div v-for="prof in info.profs">
  <textarea v-model="prof.name" class="code-input uk-input" rows="1" cols="20"></textarea>
  <textarea v-model="prof.email" class="code-input uk-input" rows="1" cols="25"></textarea>
  <textarea v-model="prof.office" class="code-input uk-input" rows="1" cols="50"></textarea> <br>
  </div>

info: {
  get () {
    return this.$store.getters.getInfo
  },
  set (payload) {
    this.$store.commit('updateInfo', payload)
  }
},

This works perfectly, with the store updating the data for each property without having to create their own separate computed variables, but for some reason it doesn't show up as a committed mutation "updateInfo" on the vue chrome dev tools and when I use a local storage plugin for vuex like vuex-persistedstate or vuex-persist it doesnt change the localstorage data until I commit another mutation that is structured normally. Right now my workaround is to create a local copy of the property in the component and then watch that property for changes and commit to the store, which lets me use the component level localstorage mixin again, but I feel like there has to be a better way to do this that does not involve writing a change function or a computed variable for each property in info as that would be quite verbose in this application.

data () {
return {
  info: this.$store.getters.getInfo
 }
},
watch: {
info: function(payload){
  this.$store.commit('updateInfo', payload)
 }
},

Solution

  • Actually, both ways are wrong. Add strict: true to your store and you'll see errors being thrown in both cases.

    In both alternatives the prof's name, email and office properties are being modified directly (which is against the Vuex principles that dictates every change should happen through mutations).

    Similarly, not that computed setter (first case), nor that watcher (second case) are being triggered because you aren't modifying item, but deeply nested properties of it (e.g. name).


    The simplest solution, which would allow you to still use a mixin, is to ditch v-model and use the :value and @input bindings. Example:

    <textarea :value="prof.name" @input="updateProf(prof, 'name', $event)" >
    

    Notice that it uses a updateProf method, which commits the mutation (see below) and goes in the mixin.

    This way all modification are done within mutations. One final note, if you vind the use of :value and @input verbose, you can create a custom directive to handle it.

    JSFiddle link or demo (same code) below.

    const store = new Vuex.Store({
      strict: true,
      state: {
        info: {
            profs: [
            {name: "Alice", email: "alice@example.com", office: "NY"},
            {name: "Bob", email: "bob@example.com", office: "CA"}
          ]
        }
      },
      mutations: {
        updateProf(state, {prof, prop, value}) {
            prof[prop] = value;
        }
      },
      getters: {
        getInfo: state => {
          return state.info
        }
      }
    });
    const mixin = {
      computed: {
        info() {
          return this.$store.getters.getInfo
        }
      },
      methods: {
        updateProf(prof, prop, e) {
            this.$store.commit('updateProf', {prof, prop, value: e.target.value})
        }
        }
    }
    new Vue({
      store,
      mixins: [mixin],
      el: '#app'
    })
    <script src="https://unpkg.com/vue"></script>
    <script src="https://unpkg.com/vuex"></script>
    
    <div id="app">
      {{ info }}
      <div v-for="prof in info.profs">
        <hr>
        name: <textarea :value="prof.name" @input="updateProf(prof, 'name', $event)" class="code-input uk-input" rows="1" cols="20"></textarea> <br>
        email: <textarea :value="prof.email" @input="updateProf(prof, 'email', $event)" class="code-input uk-input" rows="1" cols="25"></textarea> <br>
        office: <textarea :value="prof.office" @input="updateProf(prof, 'office', $event)" class="code-input uk-input" rows="1" cols="50"></textarea>
      </div>
    </div>


    Keeping v-model

    Just so nobody tells I didn't say it was possible, here's one way you could do to keep using v-model. The key point in this alternative is the functions that do deep cloning and deep equals. I have provided two simple/naive implementations, YMMV:

    JSFiddle link. Demo (same code as fiddle) below:

    const store = new Vuex.Store({
      strict: true,
      state: {
        info: {
            profs: [
            {name: "Alice", email: "alice@example.com", office: "NY"},
            {name: "Bob", email: "bob@example.com", office: "CA"}
          ]
        }
      },
      mutations: {
        updateInfo(state, data) {
            state.info = data
        }
      },
      getters: {
        getInfo: state => {
          return state.info
        }
      }
    });
    
    // these two functions are key here
    // consider using other implementations if you have more complicated property types, like Dates
    function deepClone(o) { return JSON.parse(JSON.stringify(o)); }
    function deepEquals(o1, o2) { return JSON.stringify(o1) === JSON.stringify(o2) }
    
    const mixin = {
        data() {
        return {
          info: deepClone(this.$store.getters.getInfo),
        }
      },
      computed: {
        getInfo() {
          return this.$store.getters.getInfo;
        }
      },
      watch: {
        getInfo: {
            deep: true,
            handler(newInfo) {
            if (!deepEquals(newInfo, this.info)) { // condition to prevent infinite loops
                    this.info = deepClone(newInfo);
            }
            }
        },
        info: {
            deep: true,
            handler(newInfo) {
                this.$store.commit('updateInfo', deepClone(newInfo))
            }
        }
        }
    }
    new Vue({
      store,
      mixins: [mixin],
      el: '#app'
    })
    <script src="https://unpkg.com/vue"></script>
    <script src="https://unpkg.com/vuex"></script>
    
    <div id="app">
      {{ info }}
      <div v-for="prof in info.profs">
        <hr>
        name: <textarea v-model="prof.name"  class="code-input uk-input" rows="1" cols="20"></textarea> <br>
        email: <textarea v-model="prof.email"  class="code-input uk-input" rows="1" cols="25"></textarea> <br>
        office: <textarea v-model="prof.office"  class="code-input uk-input" rows="1" cols="50"></textarea>
      </div>
    </div>