Web Dev Stories
AboutArticlesProjectsBookmarksContact

written on 03/30/2018

Composing computed properties in Vue.js

If you have ever written a Vue.js application you surely used computed properties. Computed properties are existing to have some kind of opportunity to reduce the amount of logic in templates. A Vue.js component consists of three parts: The template, the script and the style. Those three parts can also be described as representation, logic, and style. If you fully live this standard you will not have any logic in your template. Just variables, methods calls, and other Vue.js bindings. Everything is in your <script> tag. Those can become really large but that is a sign for refactoring. But anyway let us concentrate on computed properties.

So basically the computed properties rely on your data and props of the component but can also react (what a good pun) on other elements like Vuex store properties. If you want to know more how the reactivity is working the Vue.js docs got you covered here.

A problem which occurs for many Vue.js projects is that computed properties get large. This is a big problem for readability. Clean Code, for example, states that functions should be small. This also applies to computed properties since those are just functions at the end which are called. Let us inspect this component:

1
export default {
2
name: "CommentContainer",
3
computed: {
4
commentsMapped(state) {
5
const parentComments = [];
6
const mappedComments = state.comments.comments.map(comment => {
7
const copy = Object.assign({}, comment);
8
copy.isOwner = comment.user.id === state.comments.userData.userId;
9
return copy;
10
});
11
mappedComments.forEach(comment => {
12
if (comment.parent === null) {
13
const copy = Object.assign({}, comment);
14
copy.textarea.showCommentButton = comment.textarea.text.length > 0;
15
parentComments.push(copy);
16
}
17
});
18
const result = [];
19
parentComments.forEach(parentComment => {
20
const copy = Object.assign({}, parentComment);
21
copy.subComments = mappedComments.filter(
22
x => x.parent === parentComment.id
23
);
24
result.push(copy);
25
});
26
return result;
27
}
28
}
29
};

I just reduced the component to include the computed property. To explain a bit. Normally this function is inside the mapState function provided by Vuex (docs). To explain the business logic here a bit is relatively easy. In the store, there is a list of comments. Those comments have a property which is called parentId. If this parentId is not null this means the comment is a sub comment. Just imagine a reply in the Facebook comments section:

The Facebook commenting system
The Facebook commenting system

In this component, we would have two types of comments. One main comment and one sub comment or reply.

The problem with our code above though is not the business logic. This is easy to understand if you know it. But by just looking at the code this is not clear. The first step would be to rename a lot of things. But about this, I will blog in the future since I have planned a blog series about Clean Code in Vue.js. In this blog, we will concentrate on what the code above is doing.

Picture showing all code compositions in one function
Picture showing all code compositions in one function

Even though this code, which was written by me (old code is always bad), was missing functional paradigms and many more cooll things, it worked. So now for the refactoring part. We decided already that we have three parts which are somehow influencing how the computed property is built. In the User Check, we iterate over every comment and see if the current user is the owner. If yes we assign the property to the object so we can see this later in the data structure which is important for editing or deleting a comment since this functionality should just be given by the user who created the comment. We could extract the first whole workflow into a different computed property named ownerAssignedComments . This computed property would look like the following snippet.

1
export default {
2
name: "CommentContainer",
3
computed: {
4
ownerAssignedComments(state) {
5
const mappedComments = state.comments.comments.map(comment => {
6
const copy = Object.assign({}, comment);
7
copy.isOwner = comment.user.id === state.comments.userData.userId;
8
return copy;
9
});
10
}
11
}
12
};

This does not look better. At least the inside but where it gets powerful is the bigger computed property.

1
export default {
2
name: "CommentContainer",
3
computed: {
4
commentsMapped(state) {
5
const mappedComments = this.ownerAssignedComments;
6
7
const parentComments = [];
8
mappedComments.forEach(comment => {
9
if (comment.parent === null) {
10
const copy = Object.assign({}, comment);
11
copy.textarea.showCommentButton = comment.textarea.text.length > 0;
12
parentComments.push(copy);
13
}
14
});
15
const result = [];
16
parentComments.forEach(parentComment => {
17
const copy = Object.assign({}, parentComment);
18
copy.subComments = mappedComments.filter(
19
x => x.parent === parentComment.id
20
);
21
result.push(copy);
22
});
23
return result;
24
},
25
ownerAssignedComments(state) {
26
const mappedComments = state.comments.comments.map(comment => {
27
const copy = Object.assign({}, comment);
28
copy.isOwner = comment.user.id === state.comments.userData.userId;
29
return copy;
30
});
31
}
32
}
33
};

You can see that the commentsMapped computed property shrunk down in size already. It is more clear what it is doing also. The next two parts of the computed property which are Create Parent Comments and Assign Parent-Child Comments both rely on mappedComments. This means we cannot put them into a computed property since both would trigger at the same time and would interfere when we want to use them both. A refactor into a method would be ideal in this case. Better to say in two methods. The first method should be called getParentComments and takes one argument which is the mappedComments or ownerAssignedComments. The second method would be named assignSubComments and would take two arguments which are the newly created parentComments and the mappedComments or ownerAssignedComments . The component would look like the following snippet.

1
export default {
2
name: "CommentContainer",
3
computed: {
4
commentsMapped(state) {
5
const ownerAssignedComments = this.ownerAssignedComments;
6
const parentComments = this.getParentComments(ownerAssignedComments);
7
const childParentComments = this.assignSubComments(
8
parentComments,
9
ownerAssignedComments
10
);
11
return childParentComments;
12
},
13
ownerAssignedComments(state) {
14
const mappedComments = state.comments.comments.map(comment => {
15
const copy = Object.assign({}, comment);
16
copy.isOwner = comment.user.id === state.comments.userData.userId;
17
return copy;
18
});
19
}
20
},
21
methods: {
22
getParentComments(ownerAssignedComments) {
23
const parentComments = [];
24
ownerAssignedComments.forEach(comment => {
25
if (comment.parent === null) {
26
const copy = Object.assign({}, comment);
27
copy.textarea.showCommentButton = comment.textarea.text.length > 0;
28
parentComments.push(copy);
29
}
30
});
31
return parentComments;
32
},
33
assignSubComments(parentComments, ownerAssignedComments) {
34
const childParentComments = [];
35
parentComments.forEach(parentComment => {
36
const copy = Object.assign({}, parentComment);
37
copy.subComments = ownerAssignedComments.filter(
38
x => x.parent === parentComment.id
39
);
40
childParentComments.push(copy);
41
});
42
return childParentComments;
43
}
44
}
45
};

Now the computed property is constructed out of another computed property and two method invocations which return another data structure. These functions are pure functions btw. which is recommended to use for those type of functions. They are easy to test and extend. I definitely know this code could use some functional sugar but for the example, it is not required. All in all the code of the commentsMapped computed property is a lot more readable now than before since it is shorter and more explicit.

Now we can have a look into the future: The Pipeline Operator

The Pipeline Operator is a proposal of the community to include in JavaScript. Who is familiar with functional paradigms or languages will know this kind of pattern. For all others I recommend you to read the proposal. It is awesome 🚀. Basically, it is an easier way to chain methods and the results. This would make our example even easier to read in the computed property commentsMapped.

1
export default {
2
name: 'CommentContainer',
3
computed: {
4
commentsMapped(state) {
5
const ownerAssignedComments = this.ownerAssignedComments;
6
return ownerAssignedComments
7
|> this.getParentComments
8
|> (_ => this.assignSubComments(_, ownerAssignedComments);
9
}
10
}
11
};

More functional, cleaner, still understandable and faster to read.

After all composing computed properties by chaining different properties, methods and so on makes the computed properties more readable a lot. Also, it is somehow better to refactor and test applications. One disadvantage is that more getters and setters in the Vue.js instances are created. But after all readability of code is more important than performance in most cases.

Thanks for reading this. You rock 🤘 If you have any feedback or want to add something to this article just comment here. You can also follow me on twitter or visit my personal site to stay up-to-date with my blog articles and many more things.

You might also like

© Kevin Peters 2021

Imprint