--- 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)