DEV Community

loading...

Declarative Feedback on Vuex Actions through Vuex

Muhammad Talha Akbar
I provide solutions that matter. With my strong background in Internet and Web, I've worked on challenging platforms and have created web applications that are used on a daily basis.
・3 min read

As a frontend developer, we come across many a times the scenario where we dispatch actions and in our components, have status "flags" that track whether that action is being processed aka. loading, succeeded or failed. And, then show an appropriate feedback to the user based on these flags. Take this Vue component with Vuex store as an example:

<template>
  <div>
    <message v-if="error">Could not do something.</message>
    <message v-if="success">Did something successfully.</message>
    <button @click="doSomething()" :disabled="loading">
      <spinner v-if="loading"></spinner> Do Something
    </button>
  </div>
</template>

<script>
export default {
  data() {
    return {
      error: false,
      success: false,
      loading: false
    }
  },
  methods: {
    async doSomething() {
      this.loading = true;

      try {
        await this.$store.dispatch('someAction');
        this.success = true;
      } catch(e) {
        this.error = true;
      }
    }
  }
}
</script>
Enter fullscreen mode Exit fullscreen mode

Above, you can see, we have a simple, effective feedback state. However, it's repetitive and only available inside the component itself. What if we could make this tracking of action's state declarative, available globally and with almost no boilerplate? What if it was something like this:

<template>
  <action-status :actionName="someAction">
    <div slot-scope="{ status }">
      <message v-if="status.error">Could not do something.</message>
      <message v-if="status.success">Did something successfully.</message>
      <button @click="doSomething()" :disabled="status.loading">
        <spinner v-if="status.loading"></spinner> Do Something
      </button>
    </div>
  </action-status>
</template>

<script>
export default {
  methods: {
    async doSomething() {
      await this.$store.dispatch('someAction');
    }
  }
}
</script>
Enter fullscreen mode Exit fullscreen mode

Do you like the new way: a single neat component that takes the name of the Vuex action you want to observe and provides you with its status? If yes, here's how you can develop the action-status component:

The first step is to hook into our Vuex store and get updated about every action that is ever dispatched. To do that, you have store.subscribeAction method available. It takes an object with 3 callbacks i.e. before, after and error. So, register all 3 callbacks with something like:

store.subscribeAction({
  before(action) { },
  after(action) { },
  error(action) { }
});
Enter fullscreen mode Exit fullscreen mode

The second step is develop a store module named actionStatus which will store the status of every action that is being dispatched. Here's how actionStatus module will look like:

export default {
  namespaced: true,
  state: {
    actions: [],
    statusAction: {}
  },
  getters: {
    status: state => actionName => {
      return state.statusAction[actionName] || {
        error: false,
        success: false,
        loading: false
      }
    }
  },
  mutations: {
    NEW: (state, action) => {
      if(!state.statusAction[action.type]) {
        state.actions = [...state.actions, action.type];
      }
      state.statusAction = {
        ...state.statusAction,
        [action.type]: {
          loading: true,
          error: false,
          success: false
        }
      }
    },
    SUCCESS: (state, action) => {
      state.statusAction = {
        ...state.statusAction,
        [action.type]: {
          loading: false,
          error: false,
          success: true
        }
      }
    },
    ERROR: (state, action) => {
      state.statusAction = {
        ...state.statusAction,
        [action.type]: {
          loading: false,
          error: true,
          success: false
        }
      }
    },
  },
  actions: {
    trackNew: ({ commit }, action) => {
      commit('NEW', action);
    },
    trackSuccess: ({ commit }, action) => {
      commit('SUCCESS', action);
    },
    trackError: ({ commit }, action) => {
      commit('ERROR', action);
    }
  },
}
Enter fullscreen mode Exit fullscreen mode

The third step will be to dispatch actionStatus actions inside our store.subscribeAction hook:

function isActionStatusAction(action) {
  return action.type.indexOf('actionStatus) > -1;
}
store.subscribeAction({
  before(action) {
    if(!isActionStatusAction(action)) {
      store.dispatch('actionStatus/trackNew', action);
    }
  },
  after(action) {
    if(!isActionStatusAction(action)) {
      store.dispatch('actionStatus/trackSuccess', action);
    }
  },
  error(action, status, error) {
    // `error` is available too
    if(!isActionStatusAction(action)) {
      store.dispatch('actionStatus/trackError', action);
    }
  }
});
Enter fullscreen mode Exit fullscreen mode

The fourth step is create the action-status component that gets the status data about Vuex actions from the actionStatus module and makes it available to be used in any Vue component:

export default {
  props: {
    actionName: {
      type: String,
      required: true
    }
  },
  render() {
    return this.$scopedSlots.default({
      status: this.$store.getters['actionStatus/status'](this.actionName)
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

And, with these 4 steps, we can now say goodbye status flags inside of our components and track any Vuex action from any Vue component. You can modify the above code to make the actual error object available in the slot-scope, or modify it to accept multiple action names and report the status of each Vuex action inside your component. The possibilities are there.

Finally, do you think it's something you will end up using in your project? Look forward to hear your feedback about this.

Discussion (1)

Collapse
simonholdorf profile image
Simon Holdorf

Cool, thanks!