Tackling Boilerplate in Vuex
Building an SPA with Vue and Vuex can result in a lot of duplicate code because of similarities in state, mutations and actions. In this article we look at an example, and see what we can do to reduce this boilerplate.
Introduction
If you’re writing an SPA with a client side framework and a store for centralized state management, chances are you have found yourself writing the same lumps of code over and over again. That’s because similarity in state behavior for different parts of your app is reflected in similarity in code. And as your app grows, so does the boilerplate, along with your frustration. This article explores the problem of growing boilerplate with building a Vuex store, and offers a strategy that provides multiplicity in behavior while preventing multiplicity in code.
Scenario
Imagine you are writing a Vue application with a Vuex store that provides actions for making HTTP calls. For each call, your app needs to know if the call is pending and if there was an error. This means that every time you add an action, you need to add the following pieces to the store:
- state to indicate whether the call is pending and if there was an error,
- getters for that state,
- mutations for that state,
- an action that makes the HTTP call and uses the getters and mutations.
This gets tiresome very quickly, as we’ll see in the next example…
Example Vuex module
Consider the following Vuex module:
Ugh, that’s a lot of code for just two HTTP calls! And just look at all that duplicate code: The logic for fetching and updating a customer is nearly identical. Why does state management have to be so bothersome? Surely we can do better.
Refactor 1: state, getters and mutations
Let’s take a moment to review all the Vuex properties involved with one of our actions; fetching a customer:
fetchCustomerPending
in state and gettersfetchCustomerError
in state and getterssetFetchCustomerPending
in mutationssetFetchCustomerError
in mutationsfetchCustomer
in actions
Hmm, I recognize a pattern here. It seems that our naming convention is such that we can derive the store property names from the action name:
'<name>Pending'
in state and getters'<name>Error'
in state and getters'set<Name>Pending'
in mutations'set<Name>Error'
in mutations'<name>'
in actions
I also remember this wacky javascript feature called property accessor bracket notation. Javascript objects are just hashmaps with strings as keys. Let’s leverage that power and write a method that configures our store module:
Using this function to refactor our module setup:
Nice! That’s a big improvement, and the resulting store is identical to the one we had before. But there are still two actions that are very similar, so we can do even better.
Refactor 2: actions
Comparing the two actions we find three differences:
fetchCustomer
formats the URL whileupdateCustomer
does not.fetchCustomer
is a GET request whileupdateCustomer
is a POST.fetchCustomer
sets response data in the store whileupdateCustomer
does not.
In javascript we can pass functions around as easy as pie. So let’s setup the actions in our setupApiCall
method and insert functions where needed. We are going to need to pass three functions to account for the three differences in behavior:
formatUrl(url, payload)
call(url, data)
This will be our axios method, eg.axios.get
oraxios.post
handleResponse(context, response)
Let’s give it a whirl and see what it looks like:
And then our store module setup is reduced to:
I am not sure if I am completely happy with this yet. All those paramaters are making my head spin. And did you notice those comments? I put them there because without them it’s not obvious what those arrow functions are for. Moreover, I do not want to specify dummy formatUrl
and handleResponse
paramaters when there’s no formatting going on and no response being handled. Let’s address these concerns.
Refactor 3: options
Wouldn’t it be nice if instead of passing all the callbacks to setupApiCall
individually, we pass a single optional object parameter (say options
) that contain all the callbacks as optional properties. It would look something like this:
Notice that if a callback property is either not present in options or invalid, it will set a default in formatOptions
. Now the usage of setupApiCall
is a bit more expressive:
While that step didn’t really reduce the amount of code, it did make it a bit more manageable and – more importantly – scalable. By suppling optional hooks through an object, we can easily add more to make our setupApiCall
more powerful.
Refactor 4: more hooks
Let’s review setupApiCall
and observe a couple of things we find missing:
- The hook
call
takes the payload as second parameter, but maybe it needs something else entirely. We should use a hook here to create the call parameter, saymapPayload(context, payload)
. We pass it the context in case we need any data from the store to create our call parameter. - The hook
formatUrl
may also need to get data from the store, so it also should depend on context. - By the same token,
handleResponse
may require something from the payload. - The developer should have an option to handle the error. There’s room for another hook there.
Putting it all together, we arrive at our final result:
Our setupApiCall
has become pretty powerful and versatile. Using this function to configure our actions allows us to uncover if any HTTP call is pending or gave an error at any point in our app without having to spend too much time writing all the necessary pieces of the store. At the same time, the hooks we can supply through options provide some good versatility for how different requests should be handled. As an added benefit, the naming of the store pieces are guaranteed to be consistent (eg, a fetchCustomerPending
getter, a setFetchCustomerPending
mutation, etc.) without which the consistency of the names would have to depend on the discipline of the developer(s).
Other applications
While the example only demonstrates a particular use case, the takeaway should be the general strategy. In other words, the example is not about how to manage pending and error states for HTTP requests (you are already doing that anyway, right?), but how you can avoid many lines of Vuex code for state management similarities. However, you may prefer sticking to the code we started out with if you prioritize readability over DRY, given that this strategy does obscure the Vuex code to some extent. But keep in mind this example demonstrates only two HTTP calls, and you may feel different when dealing with twenty or two hundred. In summary, if you ever find yourself managing different pieces of state in a similar fashion – be they cookies, localstorage or just data in memory – it may be worthwhile writing a store-configuring function like the one presented here.