In Vue 3, what does a custom directive hook receive for `v-tooltip:top.delay="hint"`, and when is `binding.oldValue` available?
answer
- el, binding, vnode, prevVnode
- arg after the colon
- modifiers as a flag object
- oldValue only while updating
- instance is the template's owner
basics
~20 sEach hook gets el, binding, vnode and, in beforeUpdate and updated, prevVnode. For v-tooltip:top.delay="hint" the binding has value = hint's value, arg = 'top', modifiers = { delay: true }, plus instance and dir; oldValue is set only in beforeUpdate and updated.
solid answer
~40 sA Vue 3 directive hook is called as `(el, binding, vnode, prevVnode)`. For `v-tooltip:top.delay="hint"`, `binding.value` is the evaluated expression, `binding.arg` is `'top'`, `binding.modifiers` is `{ delay: true }`, `binding.instance` is the component whose template uses the directive, and `binding.dir` is the directive definition. `binding.oldValue` and `prevVnode` exist only in `beforeUpdate` and `updated`, and `oldValue` is filled on every update whether or not the value changed, so compare the two yourself. The argument can be dynamic (`v-tooltip:[side]`), and the value may be an object literal when you need several inputs. Treat everything except `el` as read-only; keep per-element state on `el.dataset` or a `WeakMap` keyed by `el`.
code
vue · 22 lines<script setup>
import { ref } from 'vue'
const hint = ref('Saves the draft')
const side = ref('top')
const vTooltip = {
mounted(el, binding) {
el.title = binding.value
el.dataset.side = binding.arg ?? 'bottom'
el.dataset.delay = binding.modifiers.delay ? '300' : '0'
},
updated(el, binding) {
if (binding.value !== binding.oldValue) el.title = binding.value
el.dataset.side = binding.arg ?? 'bottom'
}
}
</script>
<template>
<button v-tooltip:[side].delay="hint">Save</button>
</template>go deeper
Know that value, arg and modifiers come from how the directive is written in the template.
Explain oldValue and prevVnode availability, what instance refers to, and dynamic arguments.
Design directives that keep per-element state safely and avoid redundant work on every re-render.
Set conventions for directive inputs, such as object values versus args and modifiers, across a component library.
## The hook signature Every hook of a Vue 3 custom directive is called with up to four arguments: 1. **`el`** — the element the directive is bound to; the only argument you may modify. 2. **`binding`** — an object describing how the directive was written and what it evaluates to. 3. **`vnode`** — the virtual node for the bound element. 4. **`prevVnode`** — the previous render's vnode, passed only to `beforeUpdate` and `updated`. ## Anatomy of the binding object Take `<button v-tooltip:top.delay="hint">Save</button>`: | Property | Value in the example | Notes | |---|---|---| | `value` | the current value of `hint` | the evaluated expression, any type | | `oldValue` | previous value of `hint` | only in `beforeUpdate` and `updated` | | `arg` | `'top'` | text after the colon; can be dynamic | | `modifiers` | `{ delay: true }` | one `true` flag per dot modifier | | `instance` | the component whose template contains the button | not the button, not a child | | `dir` | the directive definition object | useful when sharing helpers across hooks | Two details are frequently asked: - **`oldValue` is not a change signal.** In `beforeUpdate` and `updated` it holds the previous value whether or not it changed, because these hooks run whenever the owning component re-renders. Guard expensive work with `if (binding.value !== binding.oldValue)`. - **`instance` is the template owner.** If `v-tooltip` sits on an element inside `<UserMenu>`'s template, `instance` is `UserMenu`'s public instance. ## Dynamic arguments and object values The argument can be bound: `v-tooltip:[side]="hint"` evaluates `side` on each render, so `binding.arg` follows component state. When a directive needs several inputs, pass an object literal: ```vue-html <button v-tooltip="{ text: hint, placement: 'top', delay: 300 }">Save</button> ``` A new object literal is created on every render, so `value !== oldValue` is always true for it. Compare the fields you care about instead of object identity. ## Keeping state between hooks The docs advise treating every argument except `el` as read-only. A directive has no instance of its own, so state such as a tooltip instance or a timer id must live somewhere keyed to the element: - `el.dataset` for small string values, as the Vue guide suggests; - a module-level `WeakMap` keyed by `el` for objects and functions, which is released when the element is garbage-collected. Do not store per-element state in a variable in the directive module itself; one definition serves every element that uses it. ## The deep option A directive object can set `deep: true`. Vue then traverses the bound value during render, so mutations nested inside an object value re-render the owner and fire `beforeUpdate` and `updated`. It is rarely needed and costs a full traversal per render. ## A worked hook ```js const tips = new WeakMap() export const vTooltip = { mounted(el, { value, arg, modifiers }) { tips.set(el, createTip(el, value, arg ?? 'bottom', modifiers.delay ? 300 : 0)) }, updated(el, { value, oldValue }) { if (value !== oldValue) tips.get(el)?.setText(value) }, unmounted(el) { tips.get(el)?.destroy() tips.delete(el) } } ``` `createTip` stands for any tooltip helper; the point is the flow: read `arg` and `modifiers` once, react to `value` changes in `updated`, and release resources in `unmounted`. ## What each hook can rely on | Hook | `binding.value` | `binding.oldValue` | `prevVnode` | |---|---|---|---| | `created`, `beforeMount`, `mounted` | current | `undefined` | not passed | | `beforeUpdate`, `updated` | current | previous render's value | previous vnode | | `beforeUnmount`, `unmounted` | last value | not documented; do not rely on it | not passed | The practical consequences: - Initial setup reads `value`, `arg` and `modifiers` in `mounted` (or `beforeMount` if nothing needs the element in the document). - Change handling lives in `updated`, always guarded by a comparison, because the hook also fires when an unrelated piece of the owner's state changed. - Teardown in `unmounted` should not depend on `binding` at all; look up what you stored for `el` and release it.
- Why does `binding.value !== binding.oldValue` always hold in a Vue 3 directive bound to an inline object literal?The template creates a new object on every render, so the previous and current values are different objects even when their fields are equal. Compare the specific fields, or bind a stable object from component state, before doing expensive work in `updated`.
- In Vue 3, where should a custom directive keep a per-element object such as a third-party tooltip instance?In a `WeakMap` keyed by the element, or on `el.dataset` if a string suffices. A variable in the directive module would be shared by every element using the directive, and mutating `binding` is discouraged. Delete the entry and destroy the object in `unmounted`.
saying these in an interview costs you the question
- Expects oldValue to be available in mounted
- Treats oldValue being set as proof the value changed
- Thinks binding.instance is the element or a child component
- Stores per-element state in a module-level variable
- Believes modifiers arrive as an array of strings