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 getters
  • fetchCustomerError in state and getters
  • setFetchCustomerPending in mutations
  • setFetchCustomerError in mutations
  • fetchCustomer 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 while updateCustomer does not.
  • fetchCustomer is a GET request while updateCustomer is a POST.
  • fetchCustomer sets response data in the store while updateCustomer 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 or axios.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, say mapPayload(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.

Wil je iets waarmaken met Infi?

Wil jij een eigen webapplicatie of mobiele app waarmee jij het bij anderen maakt?

Waargemaakt door de nerds van Infi.
Nerds met liefde voor softwareontwikkeling en die kunnen communiceren. En heel belangrijk: wat we doen, doen we met veel lol!

Wij willen het fixen. Laat jij van je horen?

Voor wie heb je een vraag?