9 Tips to Write Better Vue Components

Sep 27 2022 | By Taha Shashtari

Components are the basic building blocks of frontend frameworks. Designing them better will make your app easier to change and understand.

In this article, I will share the top 9 tips I learned after working a lot on Vue apps in the past several years.

1. You probably don't need to create a component

Before creating a component, see if it's for reusability and adding a state to some UI or if it's only for organizing and dividing the code.

If it's the latter, then you don't need to create it as it will just be adding more unnecessary work, like passing props and emitting events.

Not only that, but it also requires us to navigate to that file to see what it contains instead of seeing it directly in the parent component, which is much cleaner.

2. Use slots instead of props to display content

Let's say you have a reusable button component that takes its text via props:

<BaseButton label="Delete Item"/>

If you want to display an icon in it, you have to add more props like <BaseButton label="Delete Item" icon="delete" /> and update the component to display that icon.

But with slots, you can just display the label however you want each time you use that component.

<BaseButton>
  Delete Item <Icon name="delete" />
</BaseButton>

Or maybe you just need to make a certain word bold. With slots, you can use <strong> tag directly in the text instead of parsing it in the component.

3. Group the component with what triggers it

Sometimes there are two separate components that are used together in a certain scenario. It's better to put them together in a new component to make reusing and moving them easier.

A common example is modal components. We usually show the modal when a specific button is clicked. Instead of adding the showModal state and import the modal with its button every time we want to reuse it (or move it to other place), it will be easier to have a single component that shows the button that when the user clicks, it shows the related modal.

<!-- CreateItemButton.vue -->
<template>
  <Modal v-if="showModal" @close="showModal = false" />
  <BaseButton @click="showModal = true">
    Create Item
  </BaseButton>
</template>

<script setup>
const showModal = false
</script>

4. Use teleport to display fixed positioned elements from anywhere

Continuing from the previous example, if we want to display the modal correctly, we need to make sure that the modal is using the correct z-index and it's displayed in the correct position in the HTML code so it's always displayed on top of everything on the page.

We can easily avoid this issue by always displaying the modal directly as a child of the <body> element regardless where we are using it in the components structure.

The Teleport component allows us to do that.

All you have to do is wrap the modal component with <Teleport to="body">.

<!-- BaseModal.vue -->
<template>
  <Teleport to="body">
    <div class="modal"></div>
  </Teleport>
</template>

This component is part of Vue 3 built-in components. If you are using Vue 2, check out PortalVue.

5. Group related props in an object

The component's prop list is a major part of the component interface. And the clearer the interface is the easier it's to use and reason about.

One way to improve the prop list is to group related properties together.

Take this component for example:

<PostCard
  :title="post.title"
  :date="post.date"
  :layout="currentLayout"
  :image="post.imageUrl"
  <!-- more props -->
/>

We need to take a few seconds to know what props are related to a post here. But we can make it clearer by grouping post-related props together like this:

<PostCard :post="post" :layout="currentLayout" />

So now we quickly know that the layout is not part of the post data.

Not only that, but also we make updating props much easier with this approach. For example, adding or removing post related props, doesn't require us to update the component prop list.

6. Give each loop item its own state

A good reason to create a new component is to give a piece of UI its own state. A common place we need to do this is in v-for loops.

Let me demonstrate this with an example:

<template>
  <div>
    <div v-for="(item, index) in items" :key="item.id">
      <input type="checkbox" v-model="checkedItems[index]">
    </div>
  </div>
</template>
<script setup>
const checkedItems = ref(items.map(item => item.checked))
</script>

To keep track of the checked items, we had to create an array and fill it with initial values from items. But this code is not robust enough. To make it better we have to make items accessible through their ids instead of the index because indexes are not reliable and can change. For example, what if you added a support for reordering the items with drag and drop?

To simplify this code we can introduce a new component that holds a single state for each item. Like this:

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

The item component can look like this:

<template>
  <div>
    <input type="checkbox" v-model="checked">
  </div>
</template>

<script setup>
const props = defineProps({ item: Object })
const checked = ref(props.item.checked)
</script>

Another benefit with this approach is that we add all the item's related data, computed properties, and methods in a single place that is easy to understand and change.

7. Move loading data as close as possible to its UI

Whether you are loading with GraphQL or any other API, it's better to put that code as close as possible to the UI that's using it. That's for two reasons:

  1. Moving the UI component with its data becomes much easier. You just move the component without looking for its dependencies.
  2. It's always easier to understand the code when all the pieces are put together in a single place – you see the UI and where its data is coming from.

Sometimes, there are multiple components using the same fetched data. In this case it's ok to move the fetching code up a level. So, there will be the parent component, where you fetch the data, and the child components that you pass the data to.

But always make sure it's a single level. If it's not, then look for a way to improve your components design and the relationship between them.

8. Pure UI components should not access app state

There are two types of frontend components: pure UI components and app-specific components.

Pure UI components are like buttons, inputs, etc. They should not know anything about your app. Their job is merely for displaying the UI – they take the data through props.

App-specific components are the components that know about your app state, whether it's a local state or the global state (through state management libraries such as Pinia).

Separating such components makes it much easier to reuse UI components in other places in your app, or even in other apps.

This tip applies if you are building your own UI components. If you are using an external library such as Vuetify or Quasar , then you don't have to worry about that – these components are designed with that in mind.

9. Don't specify width or margin in the component

When creating a component, you should think of it as a piece of UI that can be used as any other native element.

Letting the user specify the spaces around the component is a good way to achieve this.

Let's say your component has a top margin on its root element, and the user wants to display it below a certain element but without a top margin. To do this, the user has to set a negative margin with the same value as the component's margin, like margin-top: -50px; not to mention that the user has to match the selector specificity in some cases (or maybe use !important).

And the same is for the width. If users want to make the component responsive, they have to override its width and max-width for that.

So it makes sense to always give the user that control by not setting the width and margin inside the component.

Subscribe to get my latest posts