Reducing Vuex boilerplate for AJAX calls

Lachlan Miller

Anyone who has used the flux architecture in their apps, be it Vuex, Redux or another, knows a lot of boilerplate comes with it. Let’s see how we can extract away the boilerplate for asynchronous calls, and handle the call — pending — success — failure pattern.

This article builds on my previous one, but goes further and extracts the mutations into utility function, as well as improve the action utility function.

Demo here, and source here.

The current state

The workflow for a successful asynchronous request should look like:

$store.dispatch(‘getAsync’)

// dispatch the request $store.dispatch(‘getAsync’)state = {
getInfoAsyncPending: true // pending... show a spinner
getInfoAsyncStatusCode: null,
getInfoAsyncData: null
}
// some time later... the ajax request is successfulstate = {
getInfoAsyncPending: false, // no longer pending
getInfoAsyncStatusCode: 200, // success code 200
getInfoAsyncData: {...} // response data
}

Standard Vuex code would yield something like this:

state = {
getInfoAsyncPending: false
getInfoAsyncStatusCode: null,
getInfoAsyncData: null
}
mutations = {
SET_INFO_PENDING (state, payload) {
getInfoAsyncPending = payload.pending
},
SET_INFO_SUCCESS (state, payload) {
getInfoAsyncStatusCode = payload.statusCode
getInfoAsyncData = payload.data
getInfoAsyncPending = payload.pending
},
SET_INFO_FAILURE (state, payload) {
// ...
}
}
actions = {
getInfo (store) {
store.commit('SET_INFO_PENDING', { pending: true })
return axios('someApiService')
.then(response => {
store.commit('SET_INFO_SUCCESS', {
pending: false
statusCode: response.status,
data: response.data
})
})
.catch(error => {
store.commit('SET_INFO_FAILURE', {
pending: false,
statusCode: error.status
})
})
}
}

Roughly 40 lines of code. Let’s say we have 5 more requests for different resources. We now need to define 15 properties in the state — pending, data and statusCode for each request, and 5 mutations — SUCCESS, FAILURE, and PENDING, and 5 actions, which all do the same thing. Immediately the store bloats to several hundred lines. Let’s DRY it up.


The Mutations and State

Let’s say I want to retrieve a post from a REST API. We can use jsonplaceholder to test. For my action, getAsync, I want to generate the following object, which defines the mutations and state properties:

GET_INFO_ASYNC = {
BASE: 'GET_INFO_ASYNC',
SUCCESS: 'GET_INFO_ASYNC_SUCCESS',
PENDING: 'GET_INFO_ASYNC_PENDING',
FAILURE: 'GET_INFO_ASYNC_FAILURE',
loadingKey: 'getInfoAsyncPending',
errorCode: 'getInfoAsyncErrorCode',
stateKey: 'getInfoAsyncData'
}

GET_INFO_ASYNC will be the base mutation that handles SUCCESS, PENDING and FAILURE , and will decide how to update the state based on request status. loadingKey will be a boolean that indicates whether the request is in progress or not — useful for showing a loading animation. statusCode will store the requests status, 200 for success, a 401, 404, etc for an error. stateKey will be the property the response is mapped to.

This can be implemented like so:

// mutation-types.js 
import camelCase from 'lodash/camelCase'
const createAsyncMutation = (type) => ({
BASE: `${type}`,
SUCCESS: `${type}_SUCCESS`,
FAILURE: `${type}_FAILURE`,
PENDING: `${type}_PENDING`,
loadingKey: `${_.camelCase(type)}Pending`,
statusCode: `${_.camelCase(type)}StatusCode`,
stateKey: `${_.camelCase(type)}Data`
})

This creates the API outlined above for a given mutation type.

Handling the AJAX call

Next, a utility function to handle the request flow. The request lifecycle, again:

  1. Make the initial call. Set a loading flag to be true.
  2. The request succeeds. Set the loading flag to false, and set state.data to the response body. Set statusCode to 200.
  3. OR, the request failed. Still set loading to false . Set statusCode (401, 404, etc).

A first attempt at an implementation is below. See after for an explanation.

// async-util.jsimport axios from 'axios'const doAsync = (store, { url, mutationTypes }) => { 
// Send the pending flag. Useful for showing a spinner, etc
store.commit(mutationTypes.BASE, {
type: mutationTypes.PENDING, value: true
})
// make the ajax call.
return axios(url, {})
.then(response => {
let data = response

// the call was successful!
// commit the response and status code to the store.
// we will write the actual mutation logic next.
store.commit(mutationTypes.BASE, {
type: mutationTypes.SUCCESS,
data: data,
statusCode: response.status
})

// also sent pending to false, since the call is complete.
store.commit(mutationTypes.BASE, {
type: mutationTypes.PENDING, value: false
})
})
.catch(error => {
// there was an error. Commit the status code to the store.
// we will write the mutation logic soon.
store.commit(mutationTypes.BASE, {
type: mutationTypes.FAILURE,
statusCode: error.response.status
})
// since the call is complete, sent pending to false. store.commit(mutationTypes.BASE, {
type: mutationTypes.PENDING,
value: false
})
})
}

The first argument is the store, which is needed to commit the mutations as the AJAX call goes through it’s lifecycle. The second argument is an object, containing the url for the endpoint, and mutationTypes, which are created using the createAsyncMutation outlined just above (containing GET_ASYNC_INFO_SUCCESS, stateKey , and so on.

Note regardless of the success or failure of the call, we commit mutationTypes.BASE, and then in the payload, pass the type . This makes implementing the mutation later on more straight forward.


So far, we have createAsyncMutation and doAsync . Assuming we export them from wherever file they were defined, we can use in our store like such:

import { createAsyncMutation } from './mutation-types'
import { doAsync } from './async-util'
state = {}
mutations = {} // make these next.
const getInfoAsync = createAsyncMutation('GET_INFO_ASYNC')const actions = {
getAsync(store, url) {
return doAsync(
store, { url, mutationTypes: getInfoAsync })
}
}

Pretty concise! Creating another async action would only be another 2–3 lines, just pass in the url and the mutationType.

Next, we need to generate the mutations.

Generating the Mutations

Actually, we will just generate one mutation, using the BASE property from the object return from createAsyncMutation. In the mutation, we will handle SUCCESS, FAILURE, and PENDING using a switch statement.

  • SUCCESS will get the response, mapping to the stateKey from the createAsyncMutationobject. So for getInfoAsync, it’ll be getInfoAsyncData. It will also set getInfoAsyncStatusCode to 200.
  • PENDING will set the loadingKey , in this case getInfoAsyncPending, to true or false.
  • FAILURE will set simply set the statusCode.

A hardcoded implementation is shown below. The goal is to make this dynamically, for any amount of actions.

 GET_INFO_ASYNC_BASE (state, payload) => {
switch (payload.type) {
case GET_INFO_ASYNC_PENDING:
return Vue.set(state, types[type].loadingKey, payload.value)
case GET_INFO_ASYNC_SUCCESS:
Vue.set(state, getInfoAsyncStatusCode, payload.statusCode)
return Vue.set(state, getInfoAsyncData, payload.data)
case GET_INFO_ASYNC_FAILURE:
return Vue.set(state,
getInfoAsyncStatusCode,
payload.statusCode)
}
}
}

Note we use Vue.set to mutate the state. Since we are not declaring the values when the store is initialized, rather when the request is first made, we need to use Vue.set. Simply doing state.someDynamicVal = 4 will assign the value to the state, but Vue’s reactivity system will not be aware of it, and changes won’t be reflected in the UI.

Now the final piece of the puzzle: creating the mutations dynamically. This is easy, using the awesome ES6 computed properties. So far we only made one mutation set using createAsyncMutation, called getInfoAsync. Say we had two. We can generate the mutations for both like this:

const GET_INFO_ASYNC = createAsyncMutation('GET_INFO_ASYNC')
const GET_MORE_DATA = createAsyncMutation('GET_MORE_DATA')
// put them in a single object so we can iterate over them.
const allAsyncActions = {
'GET_INFO_ASYNC': GET_INFO_ASYNC,
'GET_MORE_DATA': GET_MORE_DATA
}
mutations = {}// Loop the object, and generate using ES6 computed properties.
Object.keys(types).forEach(type => {
mutations[types[type].BASE] = (state, payload) => {
switch (payload.type) {
case types[type].PENDING:
return Vue.set(state, types[type].loadingKey, payload.value)
case types[type].SUCCESS:
Vue.set(state, types[type].statusCode, payload.statusCode)
return Vue.set(state, types[type].stateKey, payload.data)
case types[type].FAILURE:
return Vue.set(
state,
types[type].statusCode,
payload.statusCode
)
}
}
})

A little hard to read at first glance, but basically using the properties generated in createAsyncMutation , the mutation is created and assigned to the mutations object. The pending, statusCode and data objects are also created in the store for us, prepended with mutation type — so getInfoAsyncPending, getInfoAsyncStatusCode, and getInfoAsyncData.

That’s it! Around 50 lines of code, and we have a simple system to the status of any number of asynchronous calls.


Many improvements can be made. Since the mutations are automatically created, we don’t have a chance to process the data before it hits the store. Often, you want to extract some of the data, clean, or process it in some way before it is committed to the state.

One alternative is to pass an extra property to the doAsync method, a callback method, which is invoked when the response is a success. The callback contains the custom processing logic. Say we hit an API for blog posts, but only care about the blog title, not the body, but we get it anyway in the response:

// callback to extract the desired data.
const getTitleOnly = (response) => {
return response.data.title
}
const actions = {
getBlogPost(store, url) {
return doAsync(store, {
url: url,
mutationTypes: types.GET_INFO_ASYNC,
callback: getTitleOnly
})
}
}
// inside of async-util.js
// if a callback is passed, invoke it on the response
// before committing to the store.
const doAsync = (store, { url, mutationTypes, callback }) => {
// ..
return axios(url, {})
.then(response => {
let data = response
// if we passed a callback, invoke it here.
if (callback) {
data = callback(response)
}
store.commit(mutationTypes.BASE, {
type: mutationTypes.SUCCESS,
data: data,
statusCode: response.status
})}

// handle failed call...
})

Some other things to consider would be how to handle POST requests (I only demonstrate GET here) and more complex scenarios.

I’ll start to move this into a library bit by bit, and hopefully typing out CRUD actions will be a thing of the past.

Thanks for reading! Any comments or ideas are more than welcome.

Lachlan Miller

Written by

I write about frontend, Vue.js, and TDD. You can reach me on @lmiller1990 on Github.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade