skip to content

In Vue 3, what does a custom directive hook receive for `v-tooltip:top.delay="hint"`, and when is `binding.oldValue` available?

level: middleimportance: should knowfreq 45%

answer

  1. el, binding, vnode, prevVnode
  2. arg after the colon
  3. modifiers as a flag object
  4. oldValue only while updating
  5. instance is the template's owner

basics

~20 s

Each 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 s

A 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
vue
<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

for a junior

Know that value, arg and modifiers come from how the directive is written in the template.

for a middle

Explain oldValue and prevVnode availability, what instance refers to, and dynamic arguments.

for a senior

Design directives that keep per-element state safely and avoid redundant work on every re-render.

for a principal

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