DEV Community

Cover image for How I designed Vuex Store for better maintenance and code reusability
Lakh Bawa
Lakh Bawa

Posted on

How I designed Vuex Store for better maintenance and code reusability

Designing the actions in Vuex modules may be pretty messy and there might be a lot of code duplication.

JavaScript does not provide many options for code reusability, but there are certainly some features of JavaScript which are different from other languages and may prove helpful.

In my vuex store design, I used the feature of JavaScript which creates a lot of trouble for developers especially me, which is, In JavaScript Objects are always passed by reference. I used it for my advantage.

I created a VuexUtil Module.

Utils/VuexUtils.js

export default {
  getRequestWrapper(config) {
    return this.requestWrapper(config, 'get')
  },

  postRequestWrapper(config) {
    return this.requestWrapper(config, 'post')
  },

  postRequestWithMediaWrapper(config) {
    // making this separate function can be useful in making progress bar and other things
    return this.requestWrapper(config, 'post_with_media')
  },

  requestWrapper(config, type) {
    /*

  This is a special wrapper to abstract the requests inside vuex actions
  This wrapper takes all the necessary resources within the config variable and makes the action on them,
  This can take mutation names and corresponding response parameter and commit them,
  /
     */

    if (config.fetcher == null || config.url == null) {
      throw new Error('Some Parameters were Missing')
    }

    if (!config.context.state.loading) {
      return new Promise((resolve, reject) => {
        // using return is essential here to use that method in asyncData function
        if (!config.context.state.loading) {
          config.context.commit('SET_LOADING', true, { root: true })
          let request
          if (type === 'get') {
            request = config.fetcher.get(config.url, {
              params: config.params,
            })
          } else if (type === 'post_with_media') {
            request = config.fetcher.post(config.url, config.params, {
              headers: {
                'Content-Type': 'multipart/form-data',
              },
            })
          } else if (type === 'post') {
            request = config.fetcher.post(config.url, config.params)
          } else {
            throw new Error('Request type unknown')
          }
          // console.log(config.commitableMutations)

          request
            .then((response) => {
              if (!response) {
                reject(new Error('Response is Undefined, Not sure why'))
              }
              if (!response.data.success) {
                reject(response.data.message)
              }
              if (config.commitableMutations.length) {
                for (const mutation of config.commitableMutations) {
                  for (const property in mutation) {
                    // iterating over object properties
                    // just to satify IDE
                    if (
                      Object.prototype.hasOwnProperty.call(mutation, property)
                    ) {
                      // console.log(`${property}: ${mutation[property]}`)

                      // eslint-disable-next-line no-eval
                      if (eval(mutation[property]) != null) {
                        let commitValueString
                        if (mutation[property] == null) {
                          commitValueString = 'response.data.result'
                        } else {
                          commitValueString = mutation[property]
                        }
                        // eslint-disable-next-line no-eval
                        config.context.commit(
                          'modules/' + config.module + '/' + property,
                          // eslint-disable-next-line no-eval
                          eval(commitValueString),
                          { root: true }
                        )
                        if (config.callback) {
                          config.callback()
                        }
                      } else {
                        config.context.commit(
                          'modules/' + config.module + '/' + property,
                          // eslint-disable-next-line no-eval
                          [],
                          { root: true }
                        )
                      }
                    }
                  }
                }
              }

              resolve(response.data)
            })
            .catch((error) => {
              reject(error)
            })
            .finally(() => {
              config.context.commit('SET_LOADING', false, { root: true })
            })
        }
      })
    }
  },

  configAdapter({ config, getters, includeQuery }) {
    // includeQuery defines if we need to include query params such as page and sort etc
    // used in vuex actions
    if (typeof config === 'undefined') {
      config = {}
      let essentialParams
      if (typeof includeQuery !== 'undefined' && includeQuery === false) {
      } else if (typeof getters.getQuery !== 'undefined') {
        essentialParams = getters.getQuery
      }

      config.allParams = { ...essentialParams }
    } else if (
      typeof config.formData !== 'undefined' &&
      config.formData instanceof FormData
    ) {
      // backward compatiabiltiy
      if (typeof includeQuery !== 'undefined' && includeQuery === false) {
      } else if (typeof getters.getQuery !== 'undefined') {
        Object.entries(getters.getQuery).map((item) => {
          // eslint-disable-next-line no-eval
          config.formData.append(item[0], item[1])
        })
      }

      config.queryParams = config.formData
      config.allParams = config.queryParams
    } else if (typeof config === 'string') {
      const urlAsConfig = config
      config = {}
      config.url = urlAsConfig
    } else {
      config.allParams = config.queryParams
      let essentialParams
      if (typeof includeQuery !== 'undefined' && includeQuery === false) {
      } else if (typeof getters.getQuery !== 'undefined') {
        essentialParams = getters.getQuery
      }

      config.allParams = { ...essentialParams, ...config.queryParams }
    }
    return config
  },
}



Enter fullscreen mode Exit fullscreen mode

the main purpose of this module is to provide the functions which are responsible for making the ajax requests, committing the mutations.

store/index.js


// state mutations and actions defined here will get added to every module file
// store gets automaticly created form modules
// https://nuxtjs.org/guide/vuex-store#modules-mode
export const state = () => ({
  loading: false,
})

export const mutations = {
  SET_LOADING: (state, value) => (state.loading = value),
}
export const actions = {

}
export const getters = {
  getLoadingState: (state) => state.loading,
}


Enter fullscreen mode Exit fullscreen mode

Here is how I am using these utility functions.

store/modules/experiences.js

import VuexUtils from '~/utils/VuexUtils'

const state = () => ({
  // Pre-fill one row with an
  // empty `Contact` model.
  experience: {},
  query: {
    page: 1,
  },
})
const getters = {
  getExperience: (state) => state.experience,
}
const mutations = {
  SET_EXPERIENCE: (state, payload) => {
    state.experience = payload
  },
}
const actions = {
  submitExperienceAction(context, config) {
    config = VuexUtils.configAdapter({
      config,
      getters: context.getters,
      includeQuery: false,
    })
    const defaultValues = {
      url: '/experiencess',
      queryParams: {},
      allParams: {}, // also includes additional params such as page no. sort etc
    }
    config = Object.assign(defaultValues, config)
    return VuexUtils.postRequestWithMediaWrapper({
      fetcher: this.$axios,
      url: config.url,
      module: 'experience',
      context,
      params: config.allParams,
      commitableMutations: [{ SET_EXPERIENCE: 'response.data.result' }],
    })
  },
}
export default {
  actions,
  mutations,
  state,
  getters,
}


Enter fullscreen mode Exit fullscreen mode

You will notice that I am returning the promise from vuex actions, Purpose of returning promise here is, I can also chain the response from action dispatch like below


this.$store
        .dispatch('modules/auth/switchAccount', {
          queryParams: {
            new_account_type: newAccountType,
          },
        })
        .then((response) => {
          if (response.success) {
            this.$buefy.toast.open({
              message: response.message,
              type: 'is-success',
            })
            // alert('status changed success fully')
          }
        })
        .catch((error) => {
        })
        .finally(function () {
        })


Enter fullscreen mode Exit fullscreen mode

I also created a gist for it, https://gist.github.com/bawa93/64752137a6d6ab2e6c411c9629d44a66

Top comments (0)