Home / Blog / 5 Component Design Patterns to Boost Your Vue.js Applications
5 Component Design Patterns to Boost Your Vue.js Applications

5 Component Design Patterns to Boost Your Vue.js Applications

Daniel Kelly
Daniel Kelly
Updated: December 13th 2024
This entry is part 1 of 2 in the series Vue Component Design

Guess what!? We’ve just published a new course all about component design patterns in Vue.js. It’s sure to improve your Vue codebase with battle-tested patterns for scalability and maintainability.

We created the course primarily for beginners to help them up their component development skills, but even more learned Vue devs might find a new pattern or two.

Go checkout the course now or keep reading for an overview of some of the patterns discussed in the videos.

1. The Branching Component Design Pattern

Extract complex conditional rendering into separate components. Instead of multiple v-if branches in one component, create dedicated components for each state. The resulting code is easier to read and maintain over time.

<!-- Before -->
<template>
  <div>
    <div v-if="loading">Loading...</div>
    <div v-else-if="error">Error: {{ error }}</div>
    <div v-else>
      <!-- Complex content -->
    </div>
  </div>
</template>

<!-- After -->
<template>
  <div>
    <LoadingState v-if="loading" />
    <ErrorState v-else-if="error" :message="error" />
    <ContentState v-else :data="data" />
  </div>
</template>

2. Slots and Template Props Design Pattern

Whenever you have a prop that gets passed directly to the template, that’s a good sign you should be using a slot instead. Why? They serve the same purpose, but the slot allows more flexibility.

<!-- AppButton.vue -->

<!--Before-->
<script setup>
defineProps({
  label: { type: String, default: "Click Me" }
})
</script>

<template>
  <button class="btn">
      <!-- the label makes a beeline to the template 
      and makes no stops along the way-->
    {{ label }}
  </button>
</template>

<!--After-->
<template>
  <button class="btn">
      <!-- ahh, that's better
      now we can pass in markup, icons, components whatever-->
        <slot></slot>
  </button>
</template>

3. List with ListItem Component Pattern

Separate list logic and item display into distinct components. This allows for bundling up list empty state, content, controls, and more into a contained List component. Extracting each item in the list to it’s own ListItem component, makes the List component easy to read, and provides more focus when developing and styling each item.

<!-- UsersList.vue -->
<script setup>
const props = defineProps({
  users: { type: Array, required: true }
});

const filter = ref("")
const filteredUsers = computed(()=>{
 // filter logic here
})
</script>

<template>
  <div>
    <!-- List Controls -->
      <UserFilters v-model="filter" />

      <!-- List Content -->
    <ul v-if="filteredUsers.length">

        <!-- items extracted to their own component (see below) -->
      <UserListItem
        v-for="user in filteredUsers"
        :key="user.id"
        :user="user"
      />
    </ul>

    <!-- Empty State-->
    <EmptyState v-else message="No users found" />
  </div>
</template>

<!-- UserListItem.vue -->
<script setup>
defineProps({
  user: { type: Object, required: true }
})
</script>

<template>
  <li class="user-item">
    <img :src="user.avatar" />
    <span>{{ user.name }}</span>
  </li>
</template>

4. Smart vs Dumb Components Pattern

Separate data handling from presentation. Smart components manage logic and data fetching, dumb components simply take in props to handle display. Furthermore, base components provide the fundamental building blocks of your application (such as cards, buttons, etc) and by convention start with v, base, or app.

<!-- Smart Component -->
<script setup>
import { ref } from 'vue'

const users = ref([])
const loading = ref(true)

async function fetchUsers() {
  users.value = await api.getUsers()
  loading.value = false
}

function createUser(){
 // open modal to create user or whatever
}
</script>

<template>
    <AppButton @click="createUser" >Create User</AppButton>
  <UserList
    :users="users"
    :loading="loading"
  />
</template>

<!-- Dumb Component -->
<script setup>
defineProps({
  users: Array,
  loading: Boolean
})
</script>

<template>
  <div class="user-list">
    <!-- Pure presentation logic -->
  </div>
</template>

5. Form Component Design Pattern

v-model on native input fields is awesome! We can do similar with whole forms. This approach accounts for the way users interact with forms: they expect committed data only after click of submit. It also works like a charm with native input validation as the submit event never fires until native validation passes.

<script setup>
// support v-model
const user = defineModel()

// clone the modelValue to local data
// and provide a fallback user if none provided
const form = ref(clone(user) || {
  name: '',
  emaill: ''
})

// only update the modelValue when the form is submitted
function handleSubmit() {
  user.value = clone(form.value);
}

// Reset form when prop changes
watch(user, () => form.value = clone(user.value))

const clone = (obj)=> JSON.parse(JSON.stringify(obj))
</script>

<template>
  <form @submit.prevent="handleSubmit">
      <!-- Bind the form inputs to the local data instead of the modelValue -->
    <input v-model="form.name" required/>
    <input v-model="form.email" />

    <!-- Bonus! You can even have dynamic submit button labels 
    based on the modelValue passed in-->
    <button type="submit">
        {{ user ? 'Update' : 'Create' }} User
    </button>
  </form>
</template>

Looking for More Patterns?

We cover all these patterns discussed above in more detail in our course: Vue Component Design. Plus get a preview of some more advanced patterns like the Tightly Coupled Components Pattern, Recursive Components, and Lazy Dynamic Components.

Besides our own course, Michael Thiessen’s Clean Components Toolkit is a great resource for further exploring not only component patterns but also patterns for stores, composables, and more.

Related Courses

Start learning Vue.js for free

Daniel Kelly
Daniel Kelly
Daniel is the lead instructor at Vue School and enjoys helping other developers reach their full potential. He has 10+ years of developer experience using technologies including Vue.js, Nuxt.js, and Laravel.

Comments

Latest Vue School Articles

Why Use TypeScript with Vue js?

Why Use TypeScript with Vue js?

Discover the benefits of using TypeScript with Vue.js. Learn why TypeScript in Vue improves development, scalability, and maintainability.
Daniel Kelly
Daniel Kelly
Composing Vue Layouts with Vue Router

Composing Vue Layouts with Vue Router

Learn how to compose Vue layouts using Vue Router. Explore best practices for Vue Router layouts to enhance app structure and scalability.
Daniel Kelly
Daniel Kelly

Our goal is to be the number one source of Vue.js knowledge for all skill levels. We offer the knowledge of our industry leaders through awesome video courses for a ridiculously low price.

More than 200.000 users have already joined us. You are welcome too!

Follow us on Social

© All rights reserved. Made with ❤️ by BitterBrains, Inc.