Files
agent-skills/skills/vue-best-practices/reference/directive-vue2-migration-hooks.md
Jason Woltje f5792c40be feat: Complete fleet — 94 skills across 10+ domains
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>
2026-02-16 16:27:42 -06:00

5.4 KiB

title, impact, impactDescription, type, tags
title impact impactDescription type tags
Vue 3 Directive Hooks Renamed from Vue 2 HIGH Using Vue 2 hook names in Vue 3 causes directives to silently fail gotcha
vue3
vue2
migration
directives
hooks
breaking-change

Vue 3 Directive Hooks Renamed from Vue 2

Impact: HIGH - Vue 3 renamed all custom directive lifecycle hooks to align with component lifecycle hooks. Using Vue 2 hook names will cause your directives to silently fail since the hooks won't be called. Additionally, the update hook was removed entirely.

This is a breaking change that requires updating all custom directives when migrating from Vue 2 to Vue 3.

Task Checklist

  • Rename bind to beforeMount
  • Rename inserted to mounted
  • Replace update with beforeUpdate or updated (update was removed)
  • Rename componentUpdated to updated
  • Rename unbind to unmounted
  • Add beforeUpdate if you need the old update behavior

Hook Name Mapping

Vue 2 Vue 3
bind beforeMount
inserted mounted
update removed
componentUpdated updated
unbind unmounted
(none) created
(none) beforeUpdate
(none) beforeUnmount

Vue 2 (old):

// Vue 2 directive - WILL NOT WORK IN VUE 3
Vue.directive('demo', {
  bind(el, binding, vnode) {
    // Called when directive is first bound to element
  },
  inserted(el, binding, vnode) {
    // Called when element is inserted into parent
  },
  update(el, binding, vnode, oldVnode) {
    // Called on every VNode update (REMOVED in Vue 3)
  },
  componentUpdated(el, binding, vnode, oldVnode) {
    // Called after component and children update
  },
  unbind(el, binding, vnode) {
    // Called when directive is unbound from element
  }
})

Vue 3 (new):

// Vue 3 directive - Correct hook names
app.directive('demo', {
  created(el, binding, vnode) {
    // NEW: called before element's attributes or event listeners are applied
  },
  beforeMount(el, binding, vnode) {
    // Was: bind
  },
  mounted(el, binding, vnode) {
    // Was: inserted
  },
  beforeUpdate(el, binding, vnode, prevVnode) {
    // NEW: called before the element itself is updated
  },
  updated(el, binding, vnode, prevVnode) {
    // Was: componentUpdated
    // Note: 'update' was removed - use this or beforeUpdate instead
  },
  beforeUnmount(el, binding, vnode) {
    // NEW: called before element is unmounted
  },
  unmounted(el, binding, vnode) {
    // Was: unbind
  }
})

Migration Examples

Simple Focus Directive

// Vue 2
Vue.directive('focus', {
  inserted(el) {
    el.focus()
  }
})

// Vue 3
app.directive('focus', {
  mounted(el) {
    el.focus()
  }
})

Directive with Cleanup

// Vue 2
Vue.directive('click-outside', {
  bind(el, binding) {
    el._handler = (e) => {
      if (!el.contains(e.target)) binding.value(e)
    }
    document.addEventListener('click', el._handler)
  },
  unbind(el) {
    document.removeEventListener('click', el._handler)
  }
})

// Vue 3
app.directive('click-outside', {
  beforeMount(el, binding) {  // or mounted
    el._handler = (e) => {
      if (!el.contains(e.target)) binding.value(e)
    }
    document.addEventListener('click', el._handler)
  },
  unmounted(el) {
    document.removeEventListener('click', el._handler)
  }
})

Directive with Updates

// Vue 2 - using update hook
Vue.directive('color', {
  bind(el, binding) {
    el.style.color = binding.value
  },
  update(el, binding) {
    // Called on every VNode update
    el.style.color = binding.value
  }
})

// Vue 3 - update removed, use function shorthand or updated
app.directive('color', (el, binding) => {
  // Function shorthand: called for both mounted AND updated
  el.style.color = binding.value
})

// Or with object syntax
app.directive('color', {
  mounted(el, binding) {
    el.style.color = binding.value
  },
  updated(el, binding) {
    // Use updated instead of update
    el.style.color = binding.value
  }
})

Why update Was Removed

In Vue 2, update was called on every VNode update (before children updated), while componentUpdated was called after. The distinction was confusing and rarely needed. In Vue 3:

  • beforeUpdate is called before the element updates
  • updated is called after the element and all its children have updated
// Vue 3 - if you need both before and after
app.directive('track-updates', {
  beforeUpdate(el, binding) {
    console.log('Before update, old value:', binding.oldValue)
  },
  updated(el, binding) {
    console.log('After update, new value:', binding.value)
  }
})

vnode Structure Changes

In Vue 3, the vnode and prevVnode arguments also have different structure:

// Vue 2
{
  update(el, binding, vnode, oldVnode) {
    // vnode.context was the component instance
    console.log(vnode.context)
  }
}

// Vue 3
{
  updated(el, binding, vnode, prevVnode) {
    // Use binding.instance instead of vnode.context
    console.log(binding.instance)
  }
}

Reference