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.
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:
fetchCustomerPendingin state and getters
fetchCustomerErrorin state and getters
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
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:
fetchCustomerformats the URL while
fetchCustomeris a GET request while
updateCustomeris a POST.
fetchCustomersets response data in the store while
setupApiCall method and insert functions where needed. We are going to need to pass three functions to account for the three differences in behavior:
call(url, data)This will be our axios method, eg.
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
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
setupApiCall and observe a couple of things we find missing:
- The hook
calltakes 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
formatUrlmay also need to get data from the store, so it also should depend on context.
- By the same token,
handleResponsemay 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:
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).
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.