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