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>

🧠 Test Your Knowledge

What is the recommended naming convention for Vue components?