mkubilayk
mkubilayk

Reputation: 2595

"Simulate" mutations in vuex

import { remoteSettings } from 'somewhere';

const store = {
    state: {
        view: {
            foo: true
        }
    },
    mutations: {
        toggleFoo(state) {
            state.view.foo = !state.view.foo;
        }
    },
    actions: {
        async toggleFoo({ state, commit }) {
            commit('toggleFoo');
            await remoteSettings.save(state);
        }
    }
};

Say I have a simple store like this. toggleFoo action applies the mutation, then saves the new state by making an async call. However, if remoteSettings.save() call fails, local setting I have in the store and remote settings are out of sync. What I really want to achieve in this action is something like this:

async toggleFoo({ state, commit }) {
    const newState = simulateCommit('toggleFoo');
    await remoteSettings.save(newState);
    commit('toggleFoo');
}

I'd like to get the new state without actually committing it. If remote call succeeds, then I'll actually update the store. If not, it's going to stay as it is.

What's the best way to achieve this (without actually duplicating the logic in the mutation function)? Maybe "undo"? I'm not sure.

Upvotes: 2

Views: 304

Answers (2)

Vamsi Krishna
Vamsi Krishna

Reputation: 31352

One way of doing this would be: (credit to @Bert for correcting mistakes)

  1. Store the old state using const oldState = state; before committing the mutation.

  2. Wrap the async call in a try-catch block.

  3. If the remoteSettings fails it will pass the execution to catch block.

  4. In the catch block commit a mutation to reset the state.

Example:

const store = {
  state: {
    view: {
      foo: true
    }
  },
  mutations: {
    toggleFoo(state) {
      state.view.foo = !state.view.foo;
    },
    resetState(state, oldState){
      //state = oldState; do not do this

       //use store's instance method replaceState method to replace rootState
        //see :   https://vuex.vuejs.org/en/api.html
      this.replaceState(oldState)
    }
  },
  actions: {
    async toggleFoo({ state, commit }) {
      const oldState =  JSON.parse(JSON.stringify(state));  //making a deep copy of the state object
      commit('toggleFoo');
      try {
        await remoteSettings.save(newState);
        //commit('toggleFoo'); no need to call this since mutation already commited
      } catch(err) {
        //remoteSettings failed
        commit('resetState', oldState)
      }
    }

  }
};

Upvotes: 4

Bert
Bert

Reputation: 82499

Borrowing code from @VamsiKrishna (thank you), I suggest an alternative. In my opinion, you want to send the changes to the server, and update the local state on success. Here is a working example.

To prevent duplicating logic, abstract the change into a function.

console.clear()

const remoteSettings = {
  save(state){
    return new Promise((resolve, reject) => setTimeout(() => reject("Server rejected the update!"), 1000))
  }
}

function updateFoo(state){
  state.view.foo = !state.view.foo
}

const store = new Vuex.Store({
  state: {
    view: {
      foo: true
    }
  },
  mutations: {
    toggleFoo(state) {
      updateFoo(state)
    },
  },
  actions: {
    async toggleFoo({ state, commit }) {
      // Make a copy of the state. This simply uses JSON stringify/parse
      // but any technique/library for deep copy will do. Honestly, I don't
      // think you would be sending the *entire* state, but rather only
      // what you want to change
      const oldState = JSON.parse(JSON.stringify(state))
      // update the copy
      updateFoo(oldState)
      try {
        // Attempt to save
        await remoteSettings.save(oldState);
        // Only commit locally if the server OKs the change
        commit('toggleFoo');
      } catch(err) {
        // Otherwise, notify the user the change wasn't allowed
        console.log("Notify the user in some way that the update failed", err)
      }
    }
  }
})

new Vue({
  el: "#app",
  store,
  computed:{
    foo(){
      return this.$store.state.view.foo
    }
  },
  mounted(){
    setTimeout(() => this.$store.dispatch("toggleFoo"), 1000)
  }
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vuex/3.0.1/vuex.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.9/vue.js"></script>
<div id="app">
  <h4>This value never changes, because the server rejects the change</h4>
  {{foo}}
</div>

Upvotes: 2

Related Questions