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>
181 lines
5.3 KiB
Markdown
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)
|