3.7 KiB
title, impact, impactDescription, type, tags
| title | impact | impactDescription | type | tags | |||||
|---|---|---|---|---|---|---|---|---|---|
| Directive Best Practices | MEDIUM | Custom directives are powerful but easy to misuse; following patterns prevents leaks, invalid usage, and unclear abstractions | best-practice |
|
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.