Within the past year, my development team at Caktus worked on a project that required a front-end framework to build a fast, easy-to-use product for a client. After a discussion of frameworks such as React, Vue, and Angular, and their approaches, our team settled on using Vue.js, along with a Django back-end with Django REST Framework (DRF). Initially, we chose Vue because we were more familiar with it, rather than its current competitor React, but as we worked on the product, we ended up having a number of team discussions about how to organize our code well and avoid extra code debt. This blog outlines some of the development patterns we chose as we worked through a number of issues, such as simplifying a multitude of almost identical Vuex mutations, finding a consistent way of holding temporary state, and working with nested objects on the front-end and back-end.
Note: this blog post assumes familiarity either with Vue.js, or a similar front-end framework, like React or AngularJS.
Issue 1: Almost Identical Mutations
In our use of Vue.js, we chose to use the Vuex library for state management in a store, as recommended by the Vue documentation. Following Vuex documentation, we created a number of mutations to alter the state of objects inside of our store. For example, a user in our store (in this blog post’s examples, we’ll focus on this user object) could look like:
{
'first_name': '',
'middle_name': '',
'last_name': '',
'email': '',
'date_of_birth': '',
}
And, so following the Vuex docs, we created mutations for updating each of the user’s properties in the store, like this:
updateUserFirstName (state, firstName) {
state.user.first_name = firstName
},
updateUserMiddleName (state, middleName) {
state.user.middle_name = middleName
},
updateUserLastName (state, lastName) {
state.user.last_name = lastName
},
updateUserEmail (state, email) {
state.user.email = email
},
updateUserDateOfBirth (state, dateOfBirth) {
state.user.date_of_birth = dateOfBirth
}
This worked great when having only a few objects in the store, but this pattern became increasingly repetitive when both the number of objects in the store (user, company, address, etc.) and the number of properties for each object (first name, middle name, last name, gender, date of birth, etc.) increased. It became clear that having a mutation for each property (updateUserFirstName, updateUserMiddleName, updateUserLastName, updateUserDateOfBirth, etc.) led to redundant lines of mutation code. As a result, we wanted to find a less repetitive, and a more DRY (Don’t Repeat Yourself) way of doing so, in order to keep our codebase more readable and maintainable. After some discussion and coding, we created a generic and flexible mutation that would allow us to update any property of the user object:
updateUserAttributes (state, updatesObject) {
/* Update specific attributes of the user in the store. */
state.user = Object.assign(deepcopy(state.user), updatesObject)
},
This way, we could update the user’s first name and last name by calling
this.$store.commit(
'updateUserAttributes',
{ 'first_name': 'Newfirstname', 'last_name': 'Newlastname' }
)
from any of our Vue components, which is both less lines of code (less code debt), and easier to write.
Note: The code above uses a deepcopy() function, which is a utility function we wrote for copying an object, and all of its nested fields and objects so that the copied object’s nested data no longer points to the original nested data, using lodash’s cloneDeep() method. As we found out earlier, simply using Object.assign() creates a new object with nested fields that point to the original object’s nested fields, which becomes problematic if the new object gets committed to the store, but then the original object’s nested field gets changed, surprisingly leading to the same field change on the new object.
Issue 2: Using Store Or Components For Temporary State
Another issue we discussed at length had to do with how to manage the state of what the user was doing on a particular page. For example, if the user is editing their personal information on a page, we could hold that data in the store, and end up updating the data in the store as the user types on their keyboard. Alternatively, we could let the Vue component hold onto the data, and save them in the back-end when the user presses 'Save,' and wait for the next page load to load it into the store. Or, we could let the Vue component hold onto the data and update it in the store and the back-end when the user clicks ‘Save.’ Here’s a breakdown of pros and cons to each approach:
only refer to store | update store and backend on 'Save' | update backend on 'Save' |
---|---|---|
only 1 place to manage state | multiple places to manage state (store for permanent state and components for temporary state) | multiple places to manage state permanent state and components for temporary state) |
user sees changes immediately | user sees changes on when clicking 'Save' | user sees changes in other components on page load |
user sees that clicking 'Save' changes the page | user may not want to see changes in multiple components while typing | user may not want to see changes in multiple components while typing |
forcing the user to click 'Save' gives a clearer indication of what the user is doing | forcing the user to click 'Save' gives a clearer indication of what the user is doing |
Though making the changes instantly in the store took away the need to manage data in the Vue component, we ultimately decided that it would be more reusable and useful to save the changes both in the front-end store and in the back-end API when the user clicked a ‘Save’ button. Moreover, having the user click a ‘Save’ button seemed a more clear indication that they want to save a piece of data than just typing a value in a field. As a result, we ended up utilizing Vue components’ data to hold the local state of the objects, and sent those objects to both the back-end and the store when the user clicked ‘Save.’ In such a pattern, a user detail page could look something like this:
<template> ... </template> <script> export default { name: 'UserDetailPage', data () { return { localUser: {} } }, methods: { saveUser () { /* Make an API call to the backend, and update the store. */ ... } } } </script>
So the data that the user was editing would be held in the localUser object, and when the user clicks ’Save,’ the data would be sent to both the back-end and the store.
Issue 3: Nested Objects
A third issue we had a number of discussions about was how to manage an object’s relations to other objects. From our user example above, a user likely has an address (which is its own object), and the user could have a number of siblings, who have their own addresses, and the user could work at a company, which also has an address, which could have employees, who have their own addresses. Very quickly, the user object could have many layers of nested data, which can become rather unmanageable:
store = {
user: {
'id': 100,
'first_name': '',
'middle_name': '',
'last_name': '',
'email': '',
'date_of_birth': '',
'address': {
'id': 123,
'street_line_1': '',
'street_line_2': '',
'city': '',
...
},
'siblings':
[
{
'id': 200,
'first_name': '',
...
},
{
'id': 2,
'first_name': '',
...
},
...
]
}
}
One solution for avoiding unmanageable nesting would be to manage each object’s nested relations with only the related object’s ID in the nested fields:
store = {
user: {
'first_name': '',
'middle_name': '',
'last_name': '',
'email': '',
'date_of_birth': '',
'address': 123,
'siblings': [200, 2],
},
addresses:
[
{
'id': 123,
'street_line_1': '',
...
}
]
}
This makes each object’s data much smaller in the store, and decreases repetition of data (for example, when a user’s address matches their sibling’s address, we don’t need to have multiple places with the same data; only referring to the object’s ID should be enough).
However, this approach makes communication between the front-end and the back-end more complex. For instance, the user may want to edit their own information on their detail page to:
- change their email
- change their address
- add a child
In such a case, the front-end may have to make three API calls (one to create an address, one to create a child, and one to update user.email, user.address, and user.children), which could take significantly longer than just making one API call to update everything related to the user. As an aside, learn more about creating an API endpoint in this post.
Seeing the pros and cons of each approach, we had some discussion about which way to develop, and after working through several API endpoints, we ended up with the nested approach, though we also tried to limit the amount of nesting that the back-end would be forced to handle. Django REST Framework does support editing an object’s relations to other objects by writing custom update() or create() serializer methods (read more here) and we were able to write these methods for some of our serializers. As a result, we were left with some nested data, but tried to be cognizant of how many nested relations we were using, and to limit them when possible.
Team Discussions Led to Sustainable Decisions
Throughout the course of the project, we continued to have conversations about the development patterns we were forming and following, and we attempted to make decisions that would lead to a more maintainable app with less code debt. Through these team discussions, we were able to come up with solutions that worked for us in terms of handling very similar Vuex mutations, handling temporary state, and working with nested data. As a consequence, we were able to deliver a product that satisfied our client’s needs, and is maintainable for our future work. We have such conversations as we continue to build apps that are well-built, maintainable, and work well for our users.