Directive Best Practices
Impact: MEDIUM - Directives are for low-level DOM access. Use them sparingly, keep them side-effect safe, and prefer components or composables when you need stateful or reusable UI behavior.
Task List
- Use directives only when you need direct DOM access
- Do not mutate directive arguments or binding objects
- Clean up timers, listeners, and observers in
unmounted - Register directives in
<script setup>with thev-prefix - In TypeScript projects, type directive values and augment template directive types
- Prefer components or composables for complex behavior
Treat Directive Arguments as Read-Only
Directive bindings are not reactive storage. Don’t write to them.
const vFocus = {
mounted(el, binding) {
// binding.value is read-only
el.focus()
}
}Avoid Directives on Components
Directives apply to DOM elements. When used on components, they attach to the root element and can break if the root changes.
BAD:
<MyInput v-focus />GOOD:
<!-- MyInput.vue -->
<script setup>
const vFocus = (el) => el.focus()
</script>
<template>
<input v-focus />
</template>Clean Up Side Effects in unmounted
Any timers, listeners, or observers must be removed to avoid leaks.
const vResize = {
mounted(el) {
const observer = new ResizeObserver(() => {})
observer.observe(el)
el._observer = observer
},
unmounted(el) {
el._observer?.disconnect()
}
}Prefer Function Shorthand for Single-Hook Directives
If you only need mounted/updated, use the function form.
const vAutofocus = (el) => el.focus()Use the v- Prefix and Script Setup Registration
<script setup>
const vFocus = (el) => el.focus()
</script>
<template>
<input v-focus />
</template>Type Custom Directives in TypeScript Projects
Use Directive<Element, ValueType> so binding.value is typed, and augment Vue's template types so directives are recognized in SFC templates.
BAD:
// Untyped directive value and no template type augmentation
export const vHighlight = {
mounted(el, binding) {
el.style.backgroundColor = binding.value
}
}GOOD:
import type { Directive } from 'vue'
type HighlightValue = string
export const vHighlight = {
mounted(el, binding) {
el.style.backgroundColor = binding.value
}
} satisfies Directive<HTMLElement, HighlightValue>
declare module 'vue' {
interface ComponentCustomProperties {
vHighlight: typeof vHighlight
}
}Handle SSR with getSSRProps
Directive hooks such as mounted and updated do not run during SSR. If a directive sets attributes/classes that affect rendered HTML, provide an SSR equivalent via getSSRProps to avoid hydration mismatches.
BAD:
const vTooltip = {
mounted(el, binding) {
el.setAttribute('data-tooltip', binding.value)
el.classList.add('has-tooltip')
}
}GOOD:
const vTooltip = {
mounted(el, binding) {
el.setAttribute('data-tooltip', binding.value)
el.classList.add('has-tooltip')
},
getSSRProps(binding) {
return {
'data-tooltip': binding.value,
class: 'has-tooltip'
}
}
}Prefer Declarative Templates When Possible
If a standard attribute or binding works, use it instead of a directive.
Decide Between Directives and Components
Use a directive for DOM-level behavior. Use a component when behavior affects structure, state, or rendering.