Files
agent-skills/skills/vue-best-practices/reference/directive-arguments-read-only.md
Jason Woltje f5792c40be feat: Complete fleet — 94 skills across 10+ domains
Pulled ALL skills from 15 source repositories:
- anthropics/skills: 16 (docs, design, MCP, testing)
- obra/superpowers: 14 (TDD, debugging, agents, planning)
- coreyhaines31/marketingskills: 25 (marketing, CRO, SEO, growth)
- better-auth/skills: 5 (auth patterns)
- vercel-labs/agent-skills: 5 (React, design, Vercel)
- antfu/skills: 16 (Vue, Vite, Vitest, pnpm, Turborepo)
- Plus 13 individual skills from various repos

Mosaic Stack is not limited to coding — the Orchestrator and
subagents serve coding, business, design, marketing, writing,
logistics, analysis, and more.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 16:27:42 -06:00

181 lines
5.3 KiB
Markdown

---
title: Treat Directive Hook Arguments as Read-Only
impact: MEDIUM
impactDescription: Modifying directive arguments causes unpredictable behavior and breaks Vue's internal state
type: gotcha
tags: [vue3, directives, hooks, read-only, dataset]
---
# Treat Directive Hook Arguments as Read-Only
**Impact: MEDIUM** - Apart from `el`, you should treat all directive hook arguments (`binding`, `vnode`, `prevVnode`) as read-only and never modify them. Modifying these objects can cause unpredictable behavior and interfere with Vue's internal workings.
If you need to share information across hooks, use the element's `dataset` attribute or a WeakMap.
## Task Checklist
- [ ] Never mutate `binding`, `vnode`, or `prevVnode` arguments
- [ ] Use `el.dataset` to share primitive data between hooks
- [ ] Use a WeakMap for complex data that needs to persist across hooks
- [ ] Only modify `el` (the DOM element) directly
**Incorrect:**
```javascript
// WRONG: Mutating binding object
const vBadDirective = {
mounted(el, binding) {
// DON'T DO THIS - modifying binding
binding.value = 'modified' // WRONG!
binding.customData = 'stored' // WRONG!
binding.modifiers.custom = true // WRONG!
},
updated(el, binding) {
// These modifications may be lost or cause errors
console.log(binding.customData) // undefined or error
}
}
// WRONG: Mutating vnode
const vAnotherBadDirective = {
mounted(el, binding, vnode) {
// DON'T DO THIS
vnode.myData = 'stored' // WRONG!
vnode.props.modified = true // WRONG!
}
}
```
**Correct:**
```javascript
// CORRECT: Use el.dataset for simple data
const vWithDataset = {
mounted(el, binding) {
// Store data on the element's dataset
el.dataset.originalValue = binding.value
el.dataset.mountedAt = Date.now().toString()
},
updated(el, binding) {
// Access previously stored data
console.log('Original:', el.dataset.originalValue)
console.log('Current:', binding.value)
console.log('Mounted at:', el.dataset.mountedAt)
},
unmounted(el) {
// Clean up dataset if needed
delete el.dataset.originalValue
delete el.dataset.mountedAt
}
}
// CORRECT: Use WeakMap for complex data
const directiveState = new WeakMap()
const vWithWeakMap = {
mounted(el, binding) {
// Store complex state
directiveState.set(el, {
originalValue: binding.value,
config: binding.arg,
mountedAt: Date.now(),
callbacks: [],
observers: []
})
},
updated(el, binding) {
const state = directiveState.get(el)
if (state) {
console.log('Original:', state.originalValue)
console.log('Current:', binding.value)
// Can safely modify state object
state.updateCount = (state.updateCount || 0) + 1
}
},
unmounted(el) {
// WeakMap auto-cleans when element is garbage collected
// but explicit cleanup is good for observers/listeners
const state = directiveState.get(el)
if (state) {
state.observers.forEach(obs => obs.disconnect())
directiveState.delete(el)
}
}
}
```
## Using Element Properties
```javascript
// CORRECT: Use element properties with underscore prefix convention
const vTooltip = {
mounted(el, binding) {
// Store on element with underscore prefix to avoid conflicts
el._tooltipInstance = createTooltip(el, binding.value)
el._tooltipConfig = { ...binding.modifiers }
},
updated(el, binding) {
// Access and update stored instance
if (el._tooltipInstance) {
el._tooltipInstance.update(binding.value)
}
},
unmounted(el) {
// Clean up
if (el._tooltipInstance) {
el._tooltipInstance.destroy()
delete el._tooltipInstance
delete el._tooltipConfig
}
}
}
```
## What You CAN Modify
You are allowed to modify the `el` (DOM element) itself:
```javascript
const vHighlight = {
mounted(el, binding) {
// CORRECT: Modifying el directly is allowed
el.style.backgroundColor = binding.value
el.classList.add('highlighted')
el.setAttribute('data-highlighted', 'true')
el.textContent = 'Modified content'
},
updated(el, binding) {
// CORRECT: Update el when binding changes
el.style.backgroundColor = binding.value
}
}
```
## Binding Object Properties (Read-Only Reference)
The `binding` object contains:
- `value` - Current value passed to directive (read-only)
- `oldValue` - Previous value (only in beforeUpdate/updated) (read-only)
- `arg` - Argument passed (e.g., `v-dir:arg`) (read-only)
- `modifiers` - Object of modifiers (e.g., `v-dir.foo.bar`) (read-only)
- `instance` - Component instance (read-only)
- `dir` - Directive definition object (read-only)
```javascript
const vExample = {
mounted(el, binding) {
// READ these properties, don't modify them
console.log(binding.value) // Read: OK
console.log(binding.arg) // Read: OK
console.log(binding.modifiers) // Read: OK
console.log(binding.instance) // Read: OK
// Store what you need for later
el.dataset.directiveArg = binding.arg || ''
el.dataset.hasModifierFoo = binding.modifiers.foo ? 'true' : 'false'
}
}
```
## Reference
- [Vue.js Custom Directives - Hook Arguments](https://vuejs.org/guide/reusability/custom-directives#hook-arguments)
- [MDN - HTMLElement.dataset](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/dataset)