Cleaning Up Control Flow

This is an excerpt from my course Clean Components. You can learn more about it here.

It is Lesson 3 of Module 1 — Cleaning up the template.

In Vue we have some directives that help us to control what gets rendered to the page: v-for and v-if.

We call these things control flow, because they control the flow of how code is executed. In Vue, they control the flow of what gets rendered to the page.

Cleaning up how we use v-if and v-for is very similar to what we saw in Lesson 1, and the next 3 lessons cover how we can do that.

First we'll cover some of the reasons why we need clean this up, as well as some things that are unique to control flow directives, such as why we want our children to be lonely, and how it solves problems with nesting.

Then the next two lessons will explore how we can refactor components that use v-for and v-if to clean them up.

In this lesson you'll learn:

  • Why we want our children to be lonely (not all of them though)
  • How we can get rid of nesting that makes our code unreadable

Lonely Children

Similar to what we learned in Lesson 1 - Different Tasks, it is often helpful to clean up a component by refactoring it so that these directives are acting upon a single child components.

They'll have no siblings, so they'll be lonely (unless we can find friends for them somewhere 🤔).

There are several benefits we gain by doing this:

  • It reduces nesting
  • It simplifies logic, specifically computed props and lifecycle hooks

We'll explain these more right away, but first I want to show you what I mean by lonely children.

Instead of having many elements inside of a v-for, we'll just have one:

<template>
<div v-for="item in items" :key="item.id">
<h2>{{ item.title }}</h2>
<p>{{ item.discription }}</p>
<button @click="buy(item.id)">Buy now!</button>
</div>
</template>

This component could be refactored into something like this:

<template>
<ItemView
v-for="item in items"
:key="item.id"
:item="item"
/>
</template>

With a v-if we would do something almost exactly the same:

<template>
<div v-if="item">
<h2>{{ item.title }}</h2>
<p>{{ item.discription }}</p>
<button @click="buy(item.id)">Buy now!</button>
</div>
</template>

The end result looks identical, but with a v-if instead of a v-for:

<template>
<ItemView
v-if="item"
:item="item"
/>
</template>

If we have a v-else or v-else-if, we just put those branches into their own components as well.

As you can see, figuring out how to refactor in this way isn't too complicated as long as you can find the control flow statements in your template.

The reason this works out nicely has to do with component seams, which we'll cover in great detail in Module 4 - Component Seams, but it's worth mentioning here.

Looking at the v-for and v-if directives gives us very clear seams where we can break out a new component. This is because anything that can be repeated using a v-for or conditionally rendered using v-if is a naturally self-contained unit.

You'll see what I mean when we get to Module 4.

As promised, I now need to explain some of the benefits of having lonely children paired with control flow directives like v-for and v-if.

Getting rid of nesting

One of the main reasons to do this is that it gets rid of nesting.

If you have more than 4 or 5 levels of nesting, things get really complicated to understand.

Your code ends up being quite cluttered, and it's hard to see how closing tags match up:

<template>
<div
v-for="row in rows"
:key="row.id"
>
Row {{ row.value }}
<div
v-for="column in row.columns"
:key="column.id"
>
List of column values:
<span
v-for="value in column.values"
:key="value"
>
{{ value }}
</span>
</div>
</div>
</template>

But forcing all control flow directives to have a custom component that we've created as their single and only child (mostly) eliminates this problem.

In our example, the root component becomes this:

<template>
<RowComponent
v-for="row in rows"
:key="row.id"
:row="row"
/>
</template>

Then RowComponent is this:

<!-- RowComponent.vue -->
<template>
<div>
Row {{ row.value }}
<ColumnComponent
v-for="column in row.columns"
:key="column.id"
:column="column"
/>
</div>
</template>

In turn, `ColumnComponent' will be:

<!-- ColumnComponent.vue -->
<template>
<div>
List of column values:
<span
v-for="value in column.values"
:key="value"
>
{{ value }}
</span>
</div>
</template>

As you can see, doing this simplifies our components greatly, and nesting is no longer an issue here.

Simplified Logic

The other benefit is that logic can be simplified by doing this approach.

We'll cover how to clean up and extract logic from our components in the next modules, so we won't go into too much detail here.

The main benefit here is with computed props.

There is also a small benefit with lifecycle hooks. Because each item is it's own component, each item can have it's own independent lifecycle hooks.

In our previous example, if we wanted to add a computed prop that formats the value of the column, it would be really awkward.

<template>
<div
v-for="row in rows"
:key="row.id"
>
Row {{ row.value }}
<div
v-for="column in row.columns"
:key="column.id"
>
List of column values:
<span
v-for="value in column.values"
:key="value"
>
{{ value }}
</span>
</div>
</div>
</template>

We would have to map through each row, each columns field on each row, and then each value on each of those values. Then that would have to become our new rows value.

It would look something similar to this (somewhat simplified):

computedRows() {
return this.rows.map(row => ({
...row,
columns: row.columns.map(column => ({
...column,
values: column.values.map(value => value.toUpperCase()),
})),
}));
}

Don't even bother trying to understand this, it's not even worth it!

Once our component is broken up we get something much simpler. By avoiding nesting we avoid complicated and gross computed props.

This is a much simpler approach:

<!-- ColumnComponent.vue -->
<template>
<div>
List of column values:
<span
v-for="value in column.values"
:key="value"
>
{{ value }}
</span>
</div>
</template>
export default {
name: 'ColumnComponent',
props: {
column: {
type: Object,
required: true,
},
},
computed: {
formattedValue() {
return this.values.map(value => value.toUpperCase());
},
},
};

You may disagree with me, but this computed prop is a lot more understandable:

formattedValue() {
return this.values.map(value => value.toUpperCase());
}

Summary

The control flow directives, v-if and v-for, can be cleaned up in the same way you would any other code in a template.

But there are some more specific benefits that you get from cleaning them up, especially if you pair them with lonely children.

You're able to get rid of most of the nesting in your components, as well as getting benefits of simplified logic.

Now that you understand these benefits, let's look at how we can refactor these directives!

We'll start with the v-for directive first, then move on to the v-if directive in the lesson afterward.

You can find out more about the course here.