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.
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>
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>
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>
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>
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>
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.
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!
© All rights reserved. Made with ❤️ by BitterBrains, Inc.