Following best practices in Vue.js development ensures maintainable, scalable, and performant applications.
Component Design
Single Responsibility Principle
Each component should have a single, well-defined purpose:
vue
<!-- ❌ Bad: Too many responsibilities -->
<template>
<div>
<UserForm />
<UserList />
<UserStats />
<UserNotifications />
</div>
</template>
<!-- ✅ Good: Focused component -->
<template>
<div>
<UserList />
</div>
</template>Component Composition
Compose smaller components into larger ones:
vue
<!-- components/BaseButton.vue -->
<template>
<button
:class="[variant, size]"
:disabled="disabled"
@click="$emit('click')"
>
<slot />
</button>
</template>
<script setup>
defineProps({
variant: { type: String, default: 'primary' },
size: { type: String, default: 'md' },
disabled: { type: Boolean, default: false }
})
defineEmits(['click'])
</script>
<!-- Usage -->
<BaseButton variant="secondary" size="lg" @click="handleClick">
Click me
</BaseButton>Props and Emits
Define Props Explicitly
vue
<script setup>
// ✅ Good: Explicit prop definitions
const props = defineProps({
title: {
type: String,
required: true
},
count: {
type: Number,
default: 0
},
user: {
type: Object,
default: () => ({ name: 'Guest' })
}
})
// Define emits
const emit = defineEmits(['update', 'delete'])
</script>Use v-model for Two-Way Binding
vue
<!-- Parent -->
<template>
<ChildComponent v-model:title="pageTitle" />
</template>
<!-- Child -->
<template>
<input
:value="title"
@input="$emit('update:title', $event.target.value)"
/>
</template>
<script setup>
defineProps(['title'])
defineEmits(['update:title'])
</script>Composables
Extract Reusable Logic
javascript
// composables/usePagination.js
import { ref, computed } from 'vue'
export function usePagination(items, itemsPerPage = 10) {
const currentPage = ref(1)
const totalPages = computed(() =>
Math.ceil(items.value.length / itemsPerPage)
)
const paginatedItems = computed(() => {
const start = (currentPage.value - 1) * itemsPerPage
const end = start + itemsPerPage
return items.value.slice(start, end)
})
function nextPage() {
if (currentPage.value < totalPages.value) {
currentPage.value++
}
}
function prevPage() {
if (currentPage.value > 1) {
currentPage.value--
}
}
function goToPage(page) {
currentPage.value = Math.max(1, Math.min(page, totalPages.value))
}
return {
currentPage,
totalPages,
paginatedItems,
nextPage,
prevPage,
goToPage
}
}Compose Composables
javascript
// composables/useAsyncOperation.js
export function useAsyncOperation() {
const loading = ref(false)
const error = ref(null)
async function execute(asyncFn) {
loading.value = true
error.value = null
try {
return await asyncFn()
} catch (err) {
error.value = err
throw err
} finally {
loading.value = false
}
}
return { loading, error, execute }
}
// Usage
const { loading, error, execute } = useAsyncOperation()
const { data } = await execute(() => fetch('/api/users'))Performance Optimization
Lazy Loading Components
vue
<script setup>
import { defineAsyncComponent } from 'vue'
const HeavyComponent = defineAsyncComponent(() =>
import('./HeavyComponent.vue')
)
</script>
<template>
<HeavyComponent />
</template>v-once for Static Content
vue
<template>
<div v-once>
<h1>{{ staticTitle }}</h1>
<p>{{ staticContent }}</p>
</div>
</template>Computed vs Methods
vue
<script setup>
import { ref, computed } from 'vue'
const items = ref([])
// ✅ Good: Use computed for derived state
const filteredItems = computed(() =>
items.value.filter(item => item.active)
)
// ❌ Bad: Using method in template
function getFilteredItems() {
return items.value.filter(item => item.active)
}
</script>
<template>
<div v-for="item in filteredItems" :key="item.id">
{{ item.name }}
</div>
</template>State Management
Local State vs Global State
javascript
// ✅ Good: Use local state for component-specific data
const localCount = ref(0)
// ✅ Good: Use Pinia for shared state
import { useUserStore } from '@/stores/user'
const userStore = useUserStore()Avoid Props Drilling
vue
<!-- ❌ Bad: Props drilling -->
<template>
<Parent :user="user" />
</template>
<!-- ✅ Good: Use provide/inject or Pinia -->
<script setup>
import { provide } from 'vue'
import { useUserStore } from '@/stores/user'
const userStore = useUserStore()
provide('user', userStore.user)
</script>Naming Conventions
Component Naming
javascript
// ✅ Good: Multi-word, descriptive names
components/UserProfile.vue
components/BaseButton.vue
components/SearchInput.vue
// ❌ Bad: Single-word or generic names
components/User.vue
components/Button.vue
components/Input.vueEvent Naming
vue
<script setup>
// ✅ Good: Descriptive event names
const emit = defineEmits(['user-updated', 'form-submitted', 'item-deleted'])
// ❌ Bad: Generic event names
const emit = defineEmits(['update', 'submit', 'delete'])
</script>Error Handling
Global Error Handler
javascript
// main.js
app.config.errorHandler = (err, instance, info) => {
console.error('Global error:', err)
console.error('Component:', instance)
console.error('Info:', info)
// Send to error tracking service
trackError(err, { info })
}Error Boundaries
vue
<template>
<div v-if="error">
<h2>Something went wrong</h2>
<button @click="reset">Try again</button>
</div>
<slot v-else />
</template>
<script setup>
import { onErrorCaptured } from 'vue'
const error = ref(null)
onErrorCaptured((err) => {
error.value = err
return false // Prevent error propagation
})
function reset() {
error.value = null
}
</script>TypeScript Integration
Typing Props
vue
<script setup lang="ts">
interface User {
id: number
name: string
email: string
}
interface Props {
user: User
title: string
count?: number
}
const props = withDefaults(defineProps<Props>(), {
count: 0
})
</script>Typing Emits
vue
<script setup lang="ts">
interface Emits {
(e: 'update', value: string): void
(e: 'delete', id: number): void
}
const emit = defineEmits<Emits>()
</script>Testing
Component Testing
javascript
// tests/components/Button.spec.js
import { mount } from '@vue/test-utils'
import Button from '@/components/Button.vue'
describe('Button', () => {
it('emits click event', async () => {
const wrapper = mount(Button)
await wrapper.trigger('click')
expect(wrapper.emitted('click')).toBeTruthy()
})
it('disables button when disabled prop is true', () => {
const wrapper = mount(Button, {
props: { disabled: true }
})
expect(wrapper.find('button').attributes('disabled')).toBeDefined()
})
})Code Organization
Feature-Based Structure
src/
├── features/
│ ├── auth/
│ │ ├── components/
│ │ ├── composables/
│ │ ├── stores/
│ │ └── types/
│ └── users/
│ ├── components/
│ ├── composables/
│ └── stores/
├── shared/
│ ├── components/
│ ├── composables/
│ └── utils/
└── router/Following these best practices will help you build maintainable, scalable, and performant Vue.js applications.