Vue 3 Breaking Changes Checklist for Migration
AI generated
<v/>
{ }
Vue 2 → Vue 3 · Breaking Changes · Migration · Checklist
Vue 3 Breaking Changes Checklist
every relevant change with before-and-after code

Vue 3 breaking changes rarely hide in obvious code, but in v-model syntax, removed filters, the reworked global API and the retired event bus. This checklist works through the most important changes systematically, each with a concrete before-and-after example, so nothing gets overlooked during migration.

20 min read v-model · global API · filters · event bus · Teleport Vue 2.7 · Vue 3.4+

1. Why a checklist beats trial and error

Vue 3 breaking changes differ from ordinary deprecation warnings in that they often misbehave silently at runtime instead of failing at build time. A filter that worked silently in Vue 2 is simply ignored in Vue 3, or interpreted as an expression error in the template, without the console showing a clear cause. That exact difference makes trial and error inefficient during a Vue 3 migration: most breaking changes only get discovered once a feature is visibly broken in the browser.

A systematic checklist of Vue 3 breaking changes flips this approach around. Instead of waiting for visible symptoms, every known change gets deliberately searched for in the code before it becomes a problem. The sections below cover the changes most commonly overlooked in practice: v-model syntax, the new global API, removed filters, the retired event bus, and renamed lifecycle hooks.

2. v-model: new syntax and multiple bindings

Arguably the most visible item among the Vue 3 breaking changes concerns v-model on custom components. In Vue 2, v-model implicitly bound to the value prop and the input event. In Vue 3, that becomes, by default, the modelValue prop and the update:modelValue event, which at first glance looks like a cosmetic rename but affects every component implementing its own v-model behavior. Anyone who overlooks this change gets no error message, just a v-model that stops updating the component.

The second part of this change is simultaneously an improvement: Vue 3 allows multiple v-model bindings on the same component using named arguments like v-model:title and v-model:content. This replaces the common Vue 2 workaround of manually rebuilding extra two-way bindings through props and $emit. During migration it is worth specifically searching for components that used several .sync modifiers, since these are now replaced by multiple v-model bindings.


// Vue 2: v-model implicitly bound to value prop and input event
export default {
  props: ['value'],
  emits: ['input'],
  methods: {
    updateValue(newValue) {
      this.$emit('input', newValue)
    },
  },
}
// <MyInput v-model="text" />

// Vue 3: v-model bound to modelValue prop and update:modelValue event
export default {
  props: ['modelValue'],
  emits: ['update:modelValue'],
  methods: {
    updateValue(newValue) {
      this.$emit('update:modelValue', newValue)
    },
  },
}
// Multiple v-model bindings with named arguments - new in Vue 3
// <MyForm v-model:title="title" v-model:content="content" />

3. Global API: Vue.use() becomes createApp()

One of the more fundamental Vue 3 breaking changes concerns the global API. In Vue 2, every plugin and every global component was registered through the global Vue import, for example Vue.use(plugin) or Vue.component('name', component). The problem: this registration was globally valid for the entire process, which could cause shared state between independent requests during server-side rendering with multiple concurrent requests.

Vue 3 solves this with createApp(), which creates an isolated app instance. Every registration, whether a plugin, a global component, or a global directive, now hangs off this instance instead of the global Vue object. For migration, this means every Vue.use() and Vue.component() call must be translated into a corresponding call on the app object from createApp(), usually bundled in the application's central entry file.


// Vue 2: global registration mutates the shared Vue object
import Vue from 'vue'
import MyPlugin from './my-plugin'
import GlobalButton from './components/GlobalButton.vue'

Vue.use(MyPlugin)
Vue.component('GlobalButton', GlobalButton)
Vue.directive('focus', { inserted(el) { el.focus() } })

new Vue({ render: (h) => h(App) }).$mount('#app')

// Vue 3: createApp() creates an isolated instance, no global mutation
import { createApp } from 'vue'
import MyPlugin from './my-plugin'
import GlobalButton from './components/GlobalButton.vue'
import App from './App.vue'

const app = createApp(App)
app.use(MyPlugin)
app.component('GlobalButton', GlobalButton)
app.directive('focus', { mounted(el) { el.focus() } })
app.mount('#app')

4. Removed filters: replaced by computed and methods

Filters were a popular way in Vue 2 to format values directly in the template using the pipe syntax {{ value | filterName }}. Among the Vue 3 breaking changes, the complete removal of filters is one of the changes that fails least obviously: Vue 3 simply interprets the pipe character as an invalid JavaScript expression, producing a cryptic compilation warning instead of clearly pointing to the removed filter.

The Vue core team's suggested replacement: formatting logic moves either into a computed property, if it depends on reactive data, or into a plain method, if it can be called as a pure function with arguments. For globally used filters, for example currency formatting used in many places across the project, a global composable or an imported utility function used everywhere is a better fit than trying to emulate a global filter.


// Vue 2: filter used in template with pipe syntax
// Template: <p>{{ price | currency }}</p>
export default {
  filters: {
    currency(value) {
      return `€${value.toFixed(2)}`
    },
  },
}

// Vue 3: formatting logic as a plain function, imported where needed
// utils/format.js
export function formatCurrency(value) {
  return `€${value.toFixed(2)}`
}

// Component using it directly in the template via a method or computed
import { formatCurrency } from '@/utils/format'
// Template: <p>{{ formatCurrency(price) }}</p>

5. Event bus removed: alternatives for component communication

The global event bus, usually implemented in Vue 2 through an empty new Vue() instance with $on, $off and $emit, has been fully removed among the Vue 3 breaking changes, since $on and $off were dropped from the instance API. This hits projects hard that used the event bus as a quick solution for communication between distant components, without using props and events or a central store.

As a direct replacement for simple cases, a small custom composable built on reactive() or external libraries like mitt, which reproduce the same API surface with on, off and emit, works well. For more complex communication patterns, moving to Pinia is the more sustainable solution, since centralized state stays more traceable than loosely coupled events whose triggers are hard to follow in the code.

6. Renamed lifecycle hooks

Two lifecycle hooks were renamed, which is easily overlooked in the Vue 3 breaking changes list because both variants still work: destroyed is now unmounted, and beforeDestroy is now beforeUnmount. Vue 3 supports both names in the Options API to some extent for compatibility reasons, but new projects and the Composition API equivalents onUnmounted() and onBeforeUnmount() use exclusively the new terminology.

The name change reflects a conceptual clarification: a Vue component is never truly destroyed in the sense of object destruction, it is simply removed from the DOM, hence "unmounted". During migration, a simple search-and-replace pass over destroyed and beforeDestroy is enough to check off this part of the Vue 3 breaking changes, provided the Composition API is not involved, where only the new names exist anyway.

7. Teleport, fragments and multiple root elements

Vue 2 forced exactly one root element per component template, which regularly led to unnecessary wrapping div elements just to satisfy that rule. Among the Vue 3 breaking changes, lifting this restriction is a pure improvement: fragments allow multiple root elements in a template, which lets you remove superfluous wrapper divs that often produced unwanted CSS behavior like extra flexbox children.

Teleport is a completely new capability that lets parts of a template be rendered at a different point in the DOM on purpose, for example directly onto body for modals and tooltips, without changing the logical component hierarchy in the code. Vue 2 projects usually solved this with external libraries like portal-vue, whose functionality is now covered directly in core through <Teleport to="body"> and can be replaced during migration.

8. Further breaking changes: attributes, slots, reactivity

Alongside the major changes, there are smaller Vue 3 breaking changes that still regularly cause bugs in daily work. Attribute inheritance onto root elements behaves differently with multiple root elements than in Vue 2, so it must be explicitly defined which element receives $attrs whenever a component has more than one root element. $listeners was completely removed and merged into $attrs, which affects event handlers that were separately passed through in Vue 2 via v-on="$listeners".

For slots, this.$scopedSlots and this.$slots were separate objects in Vue 2, but merge into a single this.$slots in Vue 3, with every slot now consistently treated as a function instead of a VNode array. Reactivity itself was switched from Object.defineProperty to ES2015 Proxies, so Vue 3 automatically detects array index assignments and new properties added to objects, without the Vue.set() calls required in Vue 2.

9. Vue 2 and Vue 3 breaking changes compared

The table below summarizes the most important Vue 3 breaking changes as a checklist, sorted by how often they turn up in real migration projects.

Area Vue 2 Vue 3 Failure mode without a fix
v-model value / input modelValue / update:modelValue Component stops updating
Global API Vue.use() / Vue.component() app.use() / app.component() Plugin or component missing
Filters {{ value | filter }} computed() or a method Cryptic compiler error
Event bus new Vue() with $on/$emit mitt or Pinia $on is not a function
Lifecycle destroyed / beforeDestroy unmounted / beforeUnmount Hook never gets called

This table works well as the basis for one migration ticket per row: each row can be quantified with a grep search across the project and closed as its own manageable work item, instead of tackling all Vue 3 breaking changes at once.

Mironsoft

Systematic Vue 2 to Vue 3 migrations

No more Vue 3 breaking changes slipping into production?

We check your Vue 2 project systematically against the full list of Vue 3 breaking changes and deliver a prioritized migration roadmap instead of nasty surprises in production.

Breaking-change audit

Grep-based search for v-model, filters, event bus and global API usage

Prioritized roadmap

Changes ranked by risk and effort, no big-bang migration

Code review

Verify migration code before it ships to production

10. Summary

The most important Vue 3 breaking changes fall into five concrete, easily searchable categories: v-model syntax on custom components, the global API rework toward createApp(), the complete removal of filters, the retired event bus via $on/$off, and renamed lifecycle hooks. Each of these changes can be deliberately found through a grep search in the project before it shows up as a broken feature in the browser.

The decisive advantage of a checklist over trial and error: migrations become plannable, because each item can be checked off individually and handled as its own ticket. Anyone who also keeps an eye on the smaller changes around attribute inheritance, slots and reactivity significantly reduces the risk of unexpected regressions when migrating past the Vue 3 breaking changes.

Vue 3 Breaking Changes Checklist — The Essentials at a Glance

v-model syntax

modelValue instead of value, update:modelValue instead of input. Multiple v-model bindings now possible with named arguments.

Global API

createApp() replaces global Vue.use() registration. Prevents shared state between SSR requests.

Filters and event bus removed

Filters replaced by computed() or functions. Replace the event bus with mitt or Pinia.

Lifecycle and reactivity

destroyed becomes unmounted. Proxy-based reactivity detects new properties automatically, no more Vue.set() needed.

11. FAQ: Vue 3 Breaking Changes Checklist

1Why does v-model stop updating my component?
Vue 3 expects modelValue and update:modelValue instead of value and input. Switch the component to the new names.
2What replaces Vue.use() and Vue.component()?
createApp() creates an isolated app instance. Registrations happen via app.use() and app.component().
3Why doesn't my filter work anymore?
Filters were completely removed. Move formatting logic into computed() or an imported function.
4How do I replace an event bus?
$on/$off removed. mitt for simple cases, Pinia for more complex communication between components.
5Are destroyed and beforeDestroy still usable?
Partly in the Options API for compatibility. unmounted and beforeUnmount are the official new names.
6Do I have to use multiple root elements?
No, optional. Fragments allow it, a single root element remains valid.
7What happens to $listeners?
Removed and merged into $attrs. Passed along via v-bind="$attrs" now.
8Can I replace portal-vue with Teleport?
Almost always yes. Teleport covers the same functionality natively, with comparable syntax.
9Do I still need Vue.set()?
No, proxy-based reactivity detects new properties automatically. Vue.set() is redundant in pure Vue 3 projects.
10Is there a tool for automatic detection?
The official migration build warns at runtime about some changes, but does not replace a full grep search in your own code.