Make Your Components Easier to Think About

I hate thinking.

Well, actually, I love thinking, but only when I’m able to solve problems or make progress with it.

But often our code gets in the way of this. And as one workshop attendee said about reading code, “if you’re confused, it’s not your fault.”

Here are some ways you can make your code easier to think about, so you’re less confused and can actually get stuff done.

Extracting Components for Readability

One way to optimize your components for human understanding is by separating intention from implementation.

This is one of the most important reasons why you should create a new component — so you can replace a piece of code with a statement that describes what that code does.

Here’s a really basic component:

<template>
<div>
<p>{{ user.name }} ({{ user.age }})</p>
</div>
</template>
<script setup>
import { ref } from 'vue';
const user = ref({
name: 'John Doe',
age: 28,
});
</script>

We can extract a UserInfo component, which makes it exceedingly clear what the single line does — show the user’s info:

<template>
<div>
<UserInfo :user="user" />
</div>
</template>
<script setup>
import { ref } from 'vue';
import UserInfo from './UserInfo.vue';
const user = ref({
name: 'John Doe',
age: 28,
});
</script>

Yes, we have to deal with some extra boiler-plate in order to create this component:

<template>
<p>{{ user.name }} ({{ user.age }})</p>
</template>
<script setup>
import { defineProps } from 'vue';
const props = defineProps({
user: Object,
});
</script>

In the refactored code, we've extracted the user information into a separate UserInfo component. This separation makes the code more modular.

Our code is now much more readable.

This is just the simplest example, where the benefit is quite small. In a longer, more complex component, extracting it in this way will lead to even more benefit.

This is also one example of how we can write self-documenting code.

This is a central idea in the Clean Components Toolkit. Every pattern, technique, and principle asks the question, “how can we make our code more descriptive?”.

If you’re following a pattern that makes your code less descriptive, there’s a good chance it’s not a good pattern.

But self-documenting code extends beyond just component names — we can get more granular with it.

Writing more granular self-documenting code

Vue components should have clear and meaningful names for their props, data, computed properties, methods, and lifecycle hooks (and everything else). We also want our template code to be organized and readable through the use of proper indentation, formatting, and the infrequent comment.

When your code is self-documenting, it helps to announce the intention of what that code does.

This is may be an absurd example (who would write code like this?), but it takes some effort to figure out what’s going on in the code:

<template>
<div>
<button @click="c">Increment</button>
<p>{{ a }}</p>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
const a = ref(0);
function c() {
a.value++;
}
</script>

Yes, it’s a counter.

But with the following example, it’s immediately clear what it is:

<template>
<div>
<button @click="incrementCounter">Increment</button>
<p>{{ counterValue }}</p>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
const counterValue = ref(0);
function incrementCounter() {
counterValue.value++;
}
</script>

In the refactored code, we've improved the naming of the variables and methods to make them more descriptive.

Now we don’t need to waste our time and mental energy every time we come back to this component.

Splitting up components that are too long

When a component does too many things and becomes too long, it can be challenging to understand and maintain.

The Long Components Principle tells us that breaking down a long component into smaller, focused components can help improve readability, reusability, and testability.

Here’s a component from an e-commerce store that’s trying to do it all:

<template>
<div>
<h1>{{ title }}</h1>
<p>{{ description }}</p>
<ul>
<li v-for="item in items" :key="item.id">
{{ item.name }} - {{ item.price }}
<button @click="addToCart(item)">Add to cart</button>
</li>
</ul>
<div>
<h2>Cart</h2>
<ul>
<li v-for="item in cart" :key="item.id">
{{ item.name }} - {{ item.price }}
<button @click="removeFromCart(item)">Remove</button>
</li>
</ul>
<p>Total: {{ totalPrice }}</p>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue';
const title = ref('My Store');
const description = ref('Welcome to my store!');
const items = ref([
{ id: 1, name: 'Item A', price: 10 },
{ id: 2, name: 'Item B', price: 20 },
]);
const cart = ref([]);
function addToCart(item) {
cart.value.push(item);
}
function removeFromCart(item) {
const index = cart.value.indexOf(item);
if (index !== -1) {
cart.value.splice(index, 1);
}
}
const totalPrice = computed(() => {
return cart.value.reduce((sum, item) => sum + item.price, 0);
});
</script>

Let’s use the Component Boundaries Pattern from Clean Components Toolkit to help break this up.

This pattern tells us that there are usually boundaries already defined in our code that we can use to split up our component. We just have to look for them.

The easiest way to find these boundaries is by looking at the template. Doing this, we find three separate components we can extract:

  • StoreHeader
  • ItemList
  • Cart
<template>
<div>
<!-- StoreHeader -->
<h1>{{ title }}</h1>
<p>{{ description }}</p>
<!-- ItemList -->
<ul>
<li v-for="item in items" :key="item.id">
{{ item.name }} - {{ item.price }}
<button @click="addToCart(item)">Add to cart</button>
</li>
</ul>
<!-- Cart -->
<div>
<h2>Cart</h2>
<ul>
<li v-for="item in cart" :key="item.id">
{{ item.name }} - {{ item.price }}
<button @click="removeFromCart(item)">Remove</button>
</li>
</ul>
<p>Total: {{ totalPrice }}</p>
</div>
</div>
</template>

Here’s the new, simplified component — it’s now much easier to understand what’s going on:

<template>
<div>
<StoreHeader :title="title" :description="description" />
<ItemList :items="items" @addToCart="addToCart" />
<Cart :cart="cart" @removeFromCart="removeFromCart" />
</div>
</template>
<script setup>
import { ref } from 'vue';
import StoreHeader from './StoreHeader.vue';
import ItemList from './ItemList.vue';
import Cart from './Cart.vue';
const title = ref('My Store');
const description = ref('Welcome to my store!');
const items = ref([
{ id: 1, name: 'Item A', price: 10 },
{ id: 2, name: 'Item B', price: 20 },
]);
const cart = ref([]);
function addToCart(item) {
cart.value.push(item);
}
function removeFromCart(item) {
const index = cart.value.indexOf(item);
if (index !== -1) {
cart.value.splice(index, 1);
}
}
</script>

Our StoreHeader component:

<template>
<div>
<h1>{{ title }}</h1>
<p>{{ description }}</p>
</div>
</template>
<script setup>
const props = defineProps({
title: String,
description: String,
});
</script>

The new ItemList component:

<template>
<ul>
<li v-for="item in items" :key="item.id">
{{ item.name }} - {{ item.price }}
<button @click="$emit('addToCart', item)">Add to cart</button>
</li>
</ul>
</template>
<script setup>
const props = defineProps({
items: Array,
});
</script>

And finally, the Cart:

<template>
<div>
<h2>Cart</h2>
<ul>
<li v-for="item in cart" :key="item.id">
{{ item.name }} - {{ item.price }}
<button @click="$emit('removeFromCart', item)">Remove</button>
</li>
</ul>
<p>Total: {{ totalPrice }}</p>
</div>
</template>
<script setup>
import { defineProps, computed } from 'vue';
const props = defineProps({
cart: Array,
});
const totalPrice = computed(() => {
return props.cart.reduce((sum, item) => sum + item.price, 0);
});
</script>

You may have noticed a tradeoff here.

We take one complex component and turn it into multiple simple components. Likely, we’ll end up with slightly more code, but it’s code that’s far easier to understand — and easier to maintain and modify.

There’s a unifying principle here between the examples I’ve shown you.

Optimize for the dumbest, most frustrated, most tired version of yourself**

I have a tendency to write clever code (at least, it makes me feel smart).

But when I come back the next day with less motivation or less energy, it’s a slog to figure out what’s going on. We want to write code that is so clear we can understand it and work with it productively, even on our worst days.

This can be achieved through a combination of good naming and organization, as well as clear and concise documentation and examples. Also, using consistent patterns and conventions can help to reduce cognitive load and make it easier to continue working on a project after a break or interruption.

Conclusion

Optimizing your components for human understanding is crucial for maintainability, readability, and overall project success.

By using good naming conventions, extracting components and methods, organizing code, and following best practices, you can make your Vue components more accessible and easier to work with.

This article covered a few ways to do this, but there are way more.

The Clean Components Toolkit has 21 different tools like these — patterns, techniques, and concepts that will help you write better Vue components.

Each is covered in a lot of detail, including step by step refactoring examples that show you how to apply the tool to a real world code example.

So if you’re interested in learning more about how you can improve your Vue code, the Clean Components Toolkit is the best way to go.