2
votes

I have typical scenario where I call REST API in vuex actions to fetch some data and then I commit that to mutation. I use async/await syntax and try/catch/finally blocks. My vuex module looks something like this:

const state = {
  users: null,
  isProcessing: false,
  operationError: null
}

const mutations = {
  setOperationError (state, value) {
    state.operationError = value
  },
  setIsProcessing (state, value) {
    state.isProcessing = value
    if (value) {
      state.operationError = ''
    }
  },
  setUsers(state, value) {
    state.users= value
  }
}

const actions = {
  async fetchUsers ({ commit }) {
    try {
      commit('setIsProcessing', true)

      const response = await api.fetchUsers()
      commit('setUsers', response.result)
    } catch (err) {
      commit('setUsers', null)
      commit('setOperationError', err.message)
    } finally {
      commit('setIsProcessing', false)
    }
  }
}

export default {
  namespaced: true,
  state,
  mutations,
  actions
}

Notice that I handle catch(err) { } in vuex action and don’t rethrow that error. I just save error message in the state and then bind it in vue component to show it if operationError is truthy. This way I want to keep vue component clean from error handling code, like try/catch.

I am wondering is this right pattern to use? Is there a better way to handle this common scenario? Should I rethrow error in vuex action and let it propagate to the component?

1

1 Answers

0
votes

What I usually do, is have a wrapper around the data being posted, that handles the api requests and stores errors. This way your users object can have the errors recorded on itself and you can use them in the components if any of them are present.

For example:

import { fetchUsers } from '@\Common\api'
import Form from '@\Utils\Form'

const state = {
  isProcessing: false,
  form: new Form({
    users: null
  })
}

const mutations = {
  setIsProcessing(state, value) {
    state.isProcessing = value
  },
  updateForm(state, [field, value]) {
   state.form[field] = value
  }
}

const actions = {
  async fetchUsers ({ state: { form }, commit }) {
    let users = null
    commit('setIsProcessing', true)

    try {
      users = await form.get(fetchUsers);
    } catch (err) {
      // - handle error
    }

    commit('updateForm', ['users', users])
    commit('setIsProcessing', false)
  }
}

export default {
  namespaced: true,
  state,
  mutations,
  actions
}

Then in the component you can use the errors object on the wrapper like so:

<template>
  <div>
    <div class="error" v-if="form.erros.has('users')">
      {{ form.errors.get('users') }}
    </div>
    <ul v-if="users">
      <li v-for="user in users" :key="user.id">{{ user.username }}</li>
    </ul>
  </div>
</template>

<script>
import { mapState } from 'vuex'
export default {
  computed: {
    ...mapState('module' ['form']),
    users () {
      return this.form.users
    }
}
</script>

This is just my personal approach that I find very handy and it served me well up to now. Don't know if there are any standard patterns or if there is an explicit "correct way" to do this.

I like the wrapper approach, because then your errors become automatically reactive when a response from api returns an error.

You can re-use it outside vuex or even take it further and inject the errors into pre-defined error boundaries which act as wrapper components and use the provide/inject methods to propagate error data down the component tree and display them where ever you need them to show up.

Here's an example of error boundary component:

<template>
  <div>
    <slot></slot>
  </div>
</template>

<script>
export default {
  props: {
    module: {
      type: String,
      required: true,
      validator: function (value) {
        return ['module1', 'module2'].indexOf(value) !== -1
      }
    },
    form: {
      type: String,
      default: 'form'
    }
  },
  provide () {
    return {
      errors: this.$store.state[this.module][this.form].errors
    }
  }
}
</script>

Wrap some part of the application that should receive the errors:

<template>
  <div id="app">
    <error-boundary :module="module1">
      <router-view/>
    </error-boundary>
  </div>
</template>

Then you can use the errors from the users wrapper in child components like so:

If you have a global error like no response from api and want to display it in the i.e.: sidebar

<template>
  <div id="sidebar">
    <div v-if="errors.has('global')" class="error">
      {{ errors.get('global').first() }}
    </div>
    ...
  </div>
</template>
<script>
export default {
  inject: [
    'errors'
  ],
  ...
}
</script>

And the same error object re-used somewhere inside a widget for an error on the users object validation:

<template>
  <div id="user-list">
    <div v-if="errors.has('users')" class="error">
      {{ errors.get('users').first() }}
    </div>
    ...
  </div>
</template>
<script>
export default {
  inject: [
    'errors'
  ],
  ...
}
</script>

Jeffrey Way did a series on Vue2 a while ago and he proposed something similar. Here's a suggestion on the Form and Error objects that you can build upon: https://github.com/laracasts/Vue-Forms/blob/master/public/js/app.js