A pattern to handle ajax requests in Vuex
After using both Redux and Vuex quite a lot, I am starting to see some patterns emerging. Here is one I noticed in my Vuex applications, and how I extracted it into a few utility functions. Below is an action I write all too often:
const actions = {
fetchApiData (store) {
// sets `state.loading` to true. Show a spinner or something.
store.commit('API_DATA_PENDING')
return axios.get('someExternalService')
.then(response => {
// sets `state.loading` to false
// also sets `state.apiData to response` store.commit('API_DATA_SUCCESS', response.data)
})
.catch(error => {
// set `state.loading` to false and do something with error
store.commit('API_DATA_FAILURE', error)
})
}
}
Writing this for every single ajax call was repetitive and made my actions super long.
I set out to implement something that would give me the following:
- Handle all three states of an ajax request — success, failure, and pending
- Be able to easily create new actions with as little boilerplate as possible
- Reusable — almost all SPAs will use this kind of functionality
Three states: success, failure and pending
The first step is implementing a function that will generate the following for a mutation called GET_INFO_ASYNC
:
const GET_INFO_ASYNC = {
SUCCESS: 'GET_INFO_ASYNC_SUCCESS',
FAILURE: 'GET_INFO_ASYNC_FAILURE',
PENDING: 'GET_INFO_ASYNC_PENDING',
loadingKey: getInfoAsyncPending,
dataKey: getInfoAsyncData}
The below function does so, for any given type
. I also include loadingKey
and dataKey
— in my store’s state, the pending status will be saved in loadingKey
, and the response in dataKey
.
// src/mutation-types.jsimport _ from 'lodash'const createAsyncMutation = (type) => ({
SUCCESS: `${type}_SUCCESS`,
FAILURE: `${type}_FAILURE`,
PENDING: `${type}_PENDING`,
loadingKey: _.camelCase(`${type}_PENDING`),
stateKey: _.camelCase(`${type}_DATA`)
})
This lets us create all three mutations by just calling createAsyncMutation(‘GET_INFO’)
.
Next, another method to handle the ajax call and mutations for us.
// src/async-util.jsimport axios from 'axios'const doAsync = (store, { url, mutationTypes }) => {
store.commit(mutationTypes.PENDING) axios(url)
.then(response => {
store.commit(mutationTypes.SUCCESS, response.data)
})
.catch(error => {
store.commit(mutationTypes.FAILURE)
})
}export default doAsync
We simply pass the store, url for the service we are accessing, and the mutation-types
created using createAsyncMutation
. This solves the action boilerplate problem. We can use the above utility in to compose actions like so:
const actions = {
getInfoAsync(store) {
doAsync(store, {
url: 'https://jsonplaceholder.typicode.com/posts/1',
mutationTypes: types.GET_INFO_ASYNC
})
},
}
}
The last thing is to create the mutations:
const mutations = {
[types.GET_INFO_ASYNC.SUCCESS] (state, data) {
state[types.GET_INFO_ASYNC.loadingKey] = false
Vue.set(state, [types.GET_INFO_ASYNC.dataKey], data)
}, [types.GET_INFO_ASYNC.PENDING] (state) {
Vue.set(state, types.GET_INFO_ASYNC.loadingKey, true)
}
}
This handles the loadingKey
as well as setting the data. Note we use Vue.set
, simply doing state[types.GET_INFO_ASYNC.loadingKey]
would not work with Vue’s reactivity system — properties need to be created on the initial load or Vue.set
to be reactive.
There is still a little boilerplate associated with the above mutations. However I sometimes do some processing of data in mutations — whether this belongs in actions or not depends on a number of factors. An example might be a toggleTodo
mutation, where you find the todo
using the payload from the external API, then toggle done
to false, or something like that.
The above code can be used by the app like so:
// App.vue<template>
<div id="app">
<p>
Pending: {{ $store.state.getInfoAsyncPending }}
</p>
<p>
{{ $store.state.getInfoAsyncData }}
</p>
</div>
</template><script>
export default {
created () {
this.$store.dispatch('getInfoAsync')
}
}
</script>
The next thing I would like to do is extract this into a plugin, to make it even more flexible and reusable. Maybe next post!
Gist of source code here. See the file main.js
for instructions on setting the demo up, or post can I can upload the repo :)
I am interested in hearing how other people use different patterns or methods to make actions more concise and reduce the boilerplate associated with them, so please leave a comment!