Files
2026-04-17 23:26:01 +00:00

163 lines
3.7 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
title: Directive Best Practices
impact: MEDIUM
impactDescription: Custom directives are powerful but easy to misuse; following patterns prevents leaks, invalid usage, and unclear abstractions
type: best-practice
tags: [vue3, directives, custom-directives, composition, typescript]
---
# 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 the `v-` 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. Dont write to them.
```ts
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:**
```vue
<MyInput v-focus />
```
**GOOD:**
```vue
<!-- 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.
```ts
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.
```ts
const vAutofocus = (el) => el.focus()
```
## Use the `v-` Prefix and Script Setup Registration
```vue
<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:**
```ts
// Untyped directive value and no template type augmentation
export const vHighlight = {
mounted(el, binding) {
el.style.backgroundColor = binding.value
}
}
```
**GOOD:**
```ts
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:**
```ts
const vTooltip = {
mounted(el, binding) {
el.setAttribute('data-tooltip', binding.value)
el.classList.add('has-tooltip')
}
}
```
**GOOD:**
```ts
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.