Abstracting Vuex/Redux Action patterns
Avoid typing the same trivial tests and implementations again and again
In a context of unidirectional dataflow implementations (like Redux or Vuex) is very common to see patterns like Request/Success/Error in async actions. Let’s see an example to understand this pattern (I’m going to use the Vue/Vuex implementation but you could apply the same concepts in any unidirectional ecosystem).
Fetching a list of films
We want to build a catalog with the movies of the incredible Studio Ghibli. They have this public API that we are going to call from a component using a Vuex action (RETRIEVE_FILMS action).
- We want to show a loading spinner to indicate that the request has started (FETCH_FILMS_REQUEST mutation).
- When the request finishes with success, we want to update the store with the received list of movies (FETCH_FILMS_SUCESS mutation).
- When the request fails we want to update the store to show an error (FETCH_FILMS_ERROR).
This is a very typical pattern but it’s not mandatory to use it. It will depend on our needs (for example, we could decide to have the loading state in the local scope of a component) but it’s very probable that we start using it again and again.
Testing the action
We are using Jest doubles to create a spy (commitSpy) that we will use to verify that Vuex called expected mutations.
Let’s see the implementation of the action (we came to this applying TDD, of course):
Note that we are using a factory function (retrieveFilmsAction) to create an object with the run method. Then we use buildRetrieveFilmsAction to inject the retrieveFilms function, which performs the query to the the API. And this is how we declare the action in Vuex store:
export const actions = {
[RETRIEVE_FILMS]: buildRetrieveFilmsAction()
}
Well done! The next step would be to implement the 3 mutations using TDD. It is low hanging fruit so I won’t show it here 🍇🍇 :)
More actions are coming…
We keep adding features to the app. Now we want to retrieve a list of people, places, and things found in the worlds of Ghibli. For that, we will need three new Vuex actions: RETRIEVE_PEOPLE, RETRIEVE_PLACES and RETRIEVE_THINGS.
Each new action will have its respective mutations:
- RETRIEVE_PEOPLE -> FETCH_PEOPLE_REQUEST, FETCH_PEOPLE_SUCESS, FETCH_PEOPLE_ERROR
- RETRIEVE_PLACES -> FETCH_PLACES_REQUEST, FETCH_PLACES_SUCESS, FETCH_PLACES_ERROR
- RETRIEVE_THINGS -> FETCH_THINGS_REQUEST, FETCH_THINGS_SUCESS, FETCH_THINGS_ERROR
Did you guess? Yes! We can avoid a lot of boring test and production code if we extract the REQUEST/SUCCESS/ERROR management to a generic function (queryAction):
And the generic test:
Do you get the point? Now we only need to implement our new actions like this:
export function retrievePeopleAction() {
return queryAction(retrievePeople, FETCH_PEOPLE_REQUEST, FETCH_PEOPLE_SUCCESS, FETCH_PEOPLE_ERROR).run
}export function retrievePlacesAction() {
return queryAction(retrievePlaces, FETCH_PLACES_REQUEST, FETCH_PLACES_SUCCESS, FETCH_PLACES_ERROR).run
}export function retrieveThingsAction() {
return queryAction(retrieveThings, FETCH_THINGS_REQUEST, FETCH_THINGS_SUCCESS, FETCH_THINGS_ERROR).run
}
We could even go a step forward and automatically add “FETCH_” prefixes and “REQUEST”, “SUCCESS”, “ERROR” sufixes.
Now, we won’t need to test the new actions following this pattern. (Of course, we will test when a component calls them in an integration test, but thats another story…).
Conclusion
Nothing prevent us from applying in frontend the same design principles that we are used to apply when we work in the backend but, sometimes we blindly follow the documentation examples and forget about it. Do you have some tricks like this one to share with us? Please do it! 🙏🏼
At the Coding Stones we help companies developing software and we also provide training and mentoring. If you liked this post and you want more, contact us, maybe we can play together with your band 🤘🏼🤘🏼