Priority A Rules: Essential
These rules help prevent errors, so learn and abide by them at all costs. Exceptions may exist, but should be very rare and only be made by those with expert knowledge of both JavaScript and Vue.
Use multi-word component names
User component names should always be multi-word, except for root App components. This prevents conflicts with existing and future HTML elements, since all HTML elements are a single word.
Bad
template
<!-- in pre-compiled templates -->
<Item />
<!-- in in-DOM templates -->
<item></item>Good
template
<!-- in pre-compiled templates -->
<TodoItem />
<!-- in in-DOM templates -->
<todo-item></todo-item>Use detailed prop definitions
In committed code, props should be treated as a key part of the component's contract and be defined in as much detail as possible.
Why this matters
Detailed prop definitions make a component's API easier to understand, easier to use correctly, and easier to change safely.
In TypeScript, a type-based defineProps() declaration can also be used when the prop contract is fully described by its types.
Bad
js
// This is only OK when prototyping
const props = defineProps(['status'])Good
js
const props = defineProps({
status: String
})js
// Even better!
const props = defineProps({
status: {
type: String,
required: true,
validator: (value) => {
return ['syncing', 'synced', 'version-conflict', 'error'].includes(
value
)
}
}
})Declare emitted events
Treat emitted events as part of a component's public contract, and declare them explicitly.
Why this matters
Explicit event declarations document how a component communicates outward and make parent-child interactions easier to follow.
In Options API, declare events with the emits option. In <script setup>, declare them with defineEmits().
Use the object syntax when event payloads need validation, and an array of event names when they do not.
In TypeScript, a typed defineEmits() declaration can also be used so the event contract is checked by the type system as well as documented in the component. Vue 3.3+ also supports a named tuple syntax for the same declaration.
Bad
vue
<template>
<button @click="$emit('submit')">Submit</button>
</template>Good
vue
<script setup>
defineEmits(['submit'])
</script>
<template>
<button @click="$emit('submit')">Submit</button>
</template>vue
<script setup lang="ts">
const form = {
email: ''
}
const emit = defineEmits<{
// Type-based declaration
(e: 'submit', payload: { email: string }): void
// Vue 3.3+: alternative, more succinct syntax
// submit: [payload: { email: string }]
}>()
</script>
<template>
<button @click="emit('submit', { email: form.email })">Submit</button>
</template>Keep parent-child data flow explicit
Pass data down with props, and communicate requested changes back up with emitted events.
Why this matters
Vue components are easier to understand and maintain when state ownership is clear. Prop mutation and other implicit parent-child coupling make updates harder to reason about and components harder to reuse.
This includes v-model, which still follows the same prop-and-event communication pattern with shorthand syntax.
If a child needs editable local state, derive or initialize it from the prop instead of mutating the prop itself.
Bad
js
const props = defineProps({
open: Boolean
})
function requestClose() {
props.open = false
}Good
js
const props = defineProps({
open: Boolean
})
const emit = defineEmits(['update:open'])
function requestClose() {
emit('update:open', false)
}Use component-scoped styling
Keep component styles within the component boundary unless a style is intentionally global.
Why this matters
Component-scoped styling reduces accidental coupling, makes style ownership clearer, and lowers the chance that a change in one component will unexpectedly affect another.
Bad
template
<template>
<button class="btn btn-close">×</button>
</template>
<style>
.btn-close {
background-color: red;
}
</style>Good
template
<template>
<button class="button button-close">×</button>
</template>
<!-- Using the `scoped` attribute -->
<style scoped>
.button {
border: none;
border-radius: 2px;
}
.button-close {
background-color: red;
}
</style>template
<template>
<button :class="[$style.button, $style.buttonClose]">×</button>
</template>
<!-- Using CSS modules -->
<style module>
.button {
border: none;
border-radius: 2px;
}
.buttonClose {
background-color: red;
}
</style>Notes
- The
scopedattribute is not the only option. CSS modules, native CSS@scope, and disciplined class-based conventions such as BEM can all work. - App-level and layout-level styles may be global when they are intentionally shared.
Use computed for derived state
Use computed for synchronous derived state instead of storing and synchronizing it manually, and use watch, watchEffect(), or lifecycle hooks for side effects.
Why this matters
Computed state should describe what values mean, not perform work. Keeping derivation pure and synchronous makes reactive code easier to reason about and keeps side effects in the places Vue expects them.
Bad
js
const fullName = computed(() => {
const value = `${user.value.firstName} ${user.value.lastName}`
analytics.track('full-name-changed', value)
return value
})Good
js
const fullName = computed(() => {
return `${user.value.firstName} ${user.value.lastName}`
})
watch(fullName, (value) => {
analytics.track('full-name-changed', value)
})