Vue Best Practices
Write clean, maintainable, and scalable Vue code
✨ Best Practices
Vue best practices help you write clean, maintainable code. Follow naming conventions, component structure guidelines, proper prop validation, and composition patterns to build scalable applications that are easy to understand and maintain.
<!-- Well-structured component -->
<script setup>
const props = defineProps({
title: { type: String, required: true }
})
</script>
Core Best Practices
Naming Conventions
Use clear, consistent names
// PascalCase for components
const UserProfile = {}
// camelCase for variables
const userName = 'John'
Component Structure
Organize code logically
<script setup>
// 1. Imports
// 2. Props/Emits
// 3. State
// 4. Computed
// 5. Methods
</script>
Prop Validation
Always validate props
defineProps({
age: {
type: Number,
required: true
}
})
Single Responsibility
One component, one purpose
<!-- ✅ Good -->
<UserAvatar />
<UserName />
<UserBio />
🔹 Component Naming
Use clear and consistent naming conventions:
<!-- ✅ GOOD: Multi-word component names -->
<script setup>
// components/UserProfile.vue
// components/TodoList.vue
// components/SearchBar.vue
</script>
<!-- ❌ BAD: Single-word names -->
<script setup>
// components/User.vue (too generic)
// components/Todo.vue (conflicts with HTML)
// components/Search.vue (unclear)
</script>
Naming Rules:
- Components: PascalCase (UserProfile.vue)
- Props: camelCase in script, kebab-case in template
- Events: kebab-case (update-user)
- Files: PascalCase or kebab-case consistently
🔹 Prop Validation
Always validate props for reliability:
<script setup>
// ✅ GOOD: Detailed prop validation
const props = defineProps({
title: {
type: String,
required: true
},
age: {
type: Number,
default: 0,
validator: (value) => value >= 0
},
status: {
type: String,
default: 'active',
validator: (value) => ['active', 'inactive'].includes(value)
},
user: {
type: Object,
required: true,
default: () => ({ name: '', email: '' })
}
})
// ❌ BAD: No validation
const props = defineProps(['title', 'age', 'status'])
</script>
🔹 Component Organization
Structure your component code logically:
<script setup>
// 1. Imports
import { ref, computed, watch, onMounted } from 'vue'
import { useRouter } from 'vue-router'
// 2. Props and Emits
const props = defineProps({
userId: { type: Number, required: true }
})
const emit = defineEmits(['update', 'delete'])
// 3. Composables
const router = useRouter()
// 4. Reactive State
const user = ref(null)
const loading = ref(false)
// 5. Computed Properties
const fullName = computed(() =>
`${user.value?.firstName} ${user.value?.lastName}`
)
// 6. Methods
async function fetchUser() {
loading.value = true
user.value = await fetch(`/api/users/${props.userId}`)
.then(r => r.json())
loading.value = false
}
// 7. Watchers
watch(() => props.userId, () => {
fetchUser()
})
// 8. Lifecycle Hooks
onMounted(() => {
fetchUser()
})
</script>
<template>
<!-- Template content -->
</template>
<style scoped>
/* Scoped styles */
</style>
🔹 Composables Pattern
Extract reusable logic into composables:
// composables/useUser.js
export function useUser(userId) {
const user = ref(null)
const loading = ref(false)
const error = ref(null)
async function fetchUser() {
loading.value = true
error.value = null
try {
const response = await fetch(`/api/users/${userId}`)
user.value = await response.json()
} catch (e) {
error.value = e.message
} finally {
loading.value = false
}
}
return { user, loading, error, fetchUser }
}
<!-- Using the composable -->
<script setup>
import { useUser } from '@/composables/useUser'
const props = defineProps({
userId: { type: Number, required: true }
})
const { user, loading, error, fetchUser } = useUser(props.userId)
onMounted(() => {
fetchUser()
})
</script>
🔹 Template Best Practices
Write clean and readable templates:
<template>
<div>
<!-- ✅ GOOD: Simple expressions -->
<p>{{ fullName }}</p>
<!-- ❌ BAD: Complex logic in template -->
<p>{{ user.firstName + ' ' + user.lastName.toUpperCase() }}</p>
<!-- ✅ GOOD: Use computed for complex logic -->
<p>{{ formattedName }}</p>
<!-- ✅ GOOD: Descriptive event handlers -->
<button @click="handleSubmit">Submit</button>
<!-- ❌ BAD: Inline complex logic -->
<button @click="() => { /* complex logic */ }">Submit</button>
<!-- ✅ GOOD: Use v-show for frequent toggles -->
<div v-show="isVisible">Content</div>
<!-- ✅ GOOD: Use v-if for conditional rendering -->
<div v-if="user">{{ user.name }}</div>
</div>
</template>
🔹 Event Handling
Proper event naming and handling:
<!-- Child Component -->
<script setup>
// ✅ GOOD: Descriptive event names
const emit = defineEmits(['update-user', 'delete-user', 'close-modal'])
function handleUpdate() {
emit('update-user', { id: 1, name: 'John' })
}
// ❌ BAD: Generic event names
// const emit = defineEmits(['update', 'delete', 'close'])
</script>
<!-- Parent Component -->
<template>
<UserForm
@update-user="handleUserUpdate"
@delete-user="handleUserDelete"
/>
</template>
🔹 Scoped Styles
Always use scoped styles to avoid conflicts:
<template>
<div class="user-card">
<h2 class="title">{{ user.name }}</h2>
</div>
</template>
<style scoped>
/* ✅ GOOD: Scoped styles */
.user-card {
padding: 20px;
border: 1px solid #ddd;
}
.title {
color: #333;
font-size: 24px;
}
/* Use :deep() for child components */
:deep(.child-class) {
color: blue;
}
</style>