171

I recently started migrating things from jQ to a more structured framework being VueJS, and I love it!

Conceptually, Vuex has been a bit of a paradigm shift for me, but I'm confident I know what its all about now, and totally get it! But there exist a few little grey areas, mostly from an implementation standpoint.

This one I feel is good by design, but don't know if it contradicts the Vuex cycle of uni-directional data flow.

Basically, is it considered good practice to return a promise(-like) object from an action? I treat these as async wrappers, with states of failure and the like, so seems like a good fit to return a promise. Contrarily mutators just change things, and are the pure structures within a store/module.

Daniel Park
  • 3,903
  • 4
  • 23
  • 38

5 Answers5

313

actions in Vuex are asynchronous. The only way to let the calling function (initiator of action) to know that an action is complete - is by returning a Promise and resolving it later.

Here is an example: myAction returns a Promise, makes a http call and resolves or rejects the Promise later - all asynchronously

actions: {
    myAction(context, data) {
        return new Promise((resolve, reject) => {
            // Do something here... lets say, a http call using vue-resource
            this.$http("/api/something").then(response => {
                // http success, call the mutator and change something in state
                resolve(response);  // Let the calling function know that http is done. You may send some data back
            }, error => {
                // http failed, let the calling function know that action did not work out
                reject(error);
            })
        })
    }
}

Now, when your Vue component initiates myAction, it will get this Promise object and can know whether it succeeded or not. Here is some sample code for the Vue component:

export default {
    mounted: function() {
        // This component just got created. Lets fetch some data here using an action
        this.$store.dispatch("myAction").then(response => {
            console.log("Got some data, now lets show something in this component")
        }, error => {
            console.error("Got nothing from server. Prompt user to check internet connection and try again")
        })
    }
}

As you can see above, it is highly beneficial for actions to return a Promise. Otherwise there is no way for the action initiator to know what is happening and when things are stable enough to show something on the user interface.

And a last note regarding mutators - as you rightly pointed out, they are synchronous. They change stuff in the state, and are usually called from actions. There is no need to mix Promises with mutators, as the actions handle that part.

Edit: My views on the Vuex cycle of uni-directional data flow:

If you access data like this.$store.state["your data key"] in your components, then the data flow is uni-directional.

The promise from action is only to let the component know that action is complete.

The component may either take data from promise resolve function in the above example (not uni-directional, therefore not recommended), or directly from $store.state["your data key"] which is unidirectional and follows the vuex data lifecycle.

The above paragraph assumes your mutator uses Vue.set(state, "your data key", http_data), once the http call is completed in your action.

Mani
  • 23,635
  • 6
  • 67
  • 54
  • 11
    "As you can see above, it is highly beneficial for actions to return a Promise. Otherwise there is no way for the action initiator to know what is happening and when things are stable enough to show something on the user interface." IMO, this is missing the point of Vuex. The action initiator shouldn't *need* to know what's happening. The action should mutate the state when data comes back from the asynchronous event, and the component should respond to that stage change based on the Vuex store's state, not a Promise. – ceejayoz Oct 21 '16 at 03:00
  • 1
    @ceejayoz Agreed, the state should be the single source of truth for all data objects. But the Promise is the only way to communicate back to the action initiator. For example, if you want to show a "Try Again" button after http failure, that information cannot go into state, but can be communicated back only through a `Promise.reject()`. – Mani Oct 21 '16 at 03:24
  • 1
    That can easily be handled within the Vuex store. The action itself can fire a `failed` mutator that sets `state.foo.failed = true`, which the component can handle. No need for the promise to be passed to the component for that, and as a bonus, anything *else* that wants to react to the same failure can do so from the store too. – ceejayoz Oct 21 '16 at 13:55
  • 8
    @ceejayoz Check out **Composing Actions** (last section) in the docs - http://vuex.vuejs.org/en/actions.html - actions are asynchronous and therefore returning a Promise is a good idea, as stated in those docs. Maybe not in $http case above, but in some other case we may need to know when an action is completed. – Mani Oct 21 '16 at 17:12
  • Again, you can let the component know when the action is completed by mutating the state within the action when appropriate. Doing it via a mutation means *other* parts of the application can act on the same change. – ceejayoz Oct 21 '16 at 17:32
  • 6
    @ceejayoz is correct that your store can certainly be used to provide feedback on when actions have completed, and sometimes this is what you should do. i.e. fire an action and commit mutations on complete (pass or fail). Vuex is reactive so states can be watched very easily. However Mani's idea is also valid as it provides the ability to both chain Promises which allows for a far clearer workflow, and also commit mutations prior to completing as well. Therefore on completion of the Promise you know the state is correct as you have already called the synchronous mutations. – GuyC Oct 25 '16 at 07:46
  • 1
    Thanks for the open discussion all; from what I gather, its more of a "depends" scenario; Promises are available, and should be leveraged as long as it does not defer the single truth source that is the store. My main beef has been that my state is becoming quite cluttered with intermediary scenarios largely bound to single UI elements. – Daniel Park Oct 30 '16 at 21:42
  • 10
    @DanielPark Yes, "it depends" on the scenario and individual developer preferences. In my case, I wanted to avoid intermediate values like `{isLoading:true}` in my state, and therefore resorted to Promises. Your preferences may vary. At the end of the day, our objective is to write clutter-free and maintainable code. Whether promise achieves that objective, or vuex state - is left to individual developers and teams to decide. – Mani Oct 31 '16 at 13:45
  • @Mani I agree with you here. I find that when i always return a promise from actions it is very clear what will happen when i call them. That gives me the freedom to do what i need for a scenario. Also, I don't think putting flags like isFetching/isReRetching in the store is helpful when they are only used in context of a single component. – Austio Mar 03 '17 at 14:49
  • How do you go about catching that rejection? If I throw a catch catch when I call the action, I still see the uncaught rejection error in the console. – Amrit Kahlon Nov 15 '17 at 01:31
  • @AmritKahlon If you define `Promise.then(success_handler, error_handler)`, the reject will lead to invoking that `error_handler()`. `Promise.catch` also internally calls the same error handler, as per the [api specs](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/catch). In your case, it might be because of some other action or call that hasn't specified an error handler. If you are using Vue dev build, you should get details on the exact location of uncaught error in console (component name and line number details). – Mani Nov 15 '17 at 03:23
  • @Mani I know where it is coming from as I am explicitly calling reject('message') in an action, but it doesn't seem to let me catch it when the action is called – Amrit Kahlon Nov 15 '17 at 03:31
  • @AmritKahlon I can assure you that it works perfectly in my app, with similar code as in my answer above. It is not possible to debug in comments section here. Can you create a new question with a jsFiddle example so that the community can take a look at it and point out the error, if any? – Mani Nov 15 '17 at 12:33
  • 3
    @Mani good lord you were right, figured it out while making the fiddle. Many thanks! – Amrit Kahlon Nov 15 '17 at 21:21
  • 2
    @AmritKahlon Thats great to hear! I think it is a perfect example for [Rubber Duck Debugging](https://en.wikipedia.org/wiki/Rubber_duck_debugging) working through jsFiddle and stackoverflow. Do read about it if you get some time :-) – Mani Nov 16 '17 at 03:33
  • wrapping http with a promise is an anti pattern – gyc Oct 26 '18 at 06:26
  • 1
    @Mani surely the store is there to manage the state of the application so why not use a flag in the store to track the loading and use a getter in the component to follow it. Actions are fire and forget so returning promises just adds unnecessary complexity IMHO! :-) – Barnaby May 10 '19 at 08:20
  • 1
    @Barnaby fully agree. This was a development pattern I used 2 years ago when I was new to Vue.js and Vue store. Nowadays I just use a flag precisely like you said. Sometimes if only one component is interested in loading some API data, I keep the flag within the component itself and not even on the store :-) – Mani May 11 '19 at 09:56
  • I'm fairly new to vue but have just treid both approaches. IMO wrapping a fetch in a promise feels like a new version of callback hell. The other approach is much simpler and easier to manage. – Luke Oct 03 '20 at 06:47
  • What is unclear from @ceejayoz endorsement above is what's the best way to make the component respond to say store.state.fetchStatus. Say the component is a form that should go to a new route/component if the form's fetch is successful. Should one put a watch in the component that looks for store.state.fetchStatus? And then in the watch do something like: store.state.fetchStatus == "success" ? go to new route : stay on page. Or is there a better way w/o a watch? – Luke Oct 03 '20 at 07:03
56

Just for an information on a closed topic: you don’t have to create a promise, axios returns one itself:

Ref: https://forum.vuejs.org/t/how-to-resolve-a-promise-object-in-a-vuex-action-and-redirect-to-another-route/18254/4

Example:

    export const loginForm = ({ commit }, data) => {
      return axios
        .post('http://localhost:8000/api/login', data)
        .then((response) => {
          commit('logUserIn', response.data);
        })
        .catch((error) => {
          commit('unAuthorisedUser', { error:error.response.data });
        })
    }

Another example:

    addEmployee({ commit, state }) {       
      return insertEmployee(state.employee)
        .then(result => {
          commit('setEmployee', result.data);
          return result.data; // resolve 
        })
        .catch(err => {           
          throw err.response.data; // reject
        })
    }

Another example with async-await

    async getUser({ commit }) {
        try {
            const currentUser = await axios.get('/user/current')
            commit('setUser', currentUser)
            return currentUser
        } catch (err) {
            commit('setUser', null)
            throw 'Unable to fetch current user'
        }
    },
Anoop Thiruonam
  • 2,567
  • 2
  • 28
  • 48
9

Actions

ADD_PRODUCT : (context,product) => {
  return Axios.post(uri, product).then((response) => {
    if (response.status === 'success') {  
      context.commit('SET_PRODUCT',response.data.data)
    }
    return response.data
  });
});

Component

this.$store.dispatch('ADD_PRODUCT',data).then((res) => {
  if (res.status === 'success') {
    // write your success actions here....
  } else {
     // write your error actions here...
  }
})
Bhaskararao Gummidi
  • 2,513
  • 1
  • 12
  • 15
1

TL:DR; return promises from you actions only when necessary, but DRY chaining the same actions.

For a long time I also though that returning actions contradicts the Vuex cycle of uni-directional data flow.

But, there are EDGE CASES where returning a promise from your actions might be "necessary".

Imagine a situation where an action can be triggered from 2 different components, and each handles the failure case differently. In that case, one would need to pass the caller component as a parameter to set different flags in the store.

Dumb example

Page where the user can edit the username in navbar and in /profile page (which contains the navbar). Both trigger an action "change username", which is asynchronous. If the promise fails, the page should only display an error in the component the user was trying to change the username from.

Of course it is a dumb example, but I don't see a way to solve this issue without duplicating code and making the same call in 2 different actions.

srmico
  • 151
  • 1
  • 4
-2

actions.js

const axios = require('axios');
const types = require('./types');

export const actions = {
  GET_CONTENT({commit}){
    axios.get(`${URL}`)
      .then(doc =>{
        const content = doc.data;
        commit(types.SET_CONTENT , content);
        setTimeout(() =>{
          commit(types.IS_LOADING , false);
        } , 1000);
      }).catch(err =>{
        console.log(err);
    });
  },
}

home.vue

<script>
  import {value , onCreated} from "vue-function-api";
  import {useState, useStore} from "@u3u/vue-hooks";

  export default {
    name: 'home',

    setup(){
      const store = useStore();
      const state = {
        ...useState(["content" , "isLoading"])
      };
      onCreated(() =>{
        store.value.dispatch("GET_CONTENT" );
      });

      return{
        ...state,
      }
    }
  };
</script>
Chris Michael
  • 1,557
  • 3
  • 15
  • 23