Vue.js Upgrade Guide

Compare Vue.js versions and see exactly what changes, what requires action, and what you can start using.

77Changes
40Breaking
40Actions
21Features
1Migration tools

Breaking Changes

Component v-model API changed

Action required
breakinghigh impact

The default prop and event used by v-model on custom components changed from value and input to modelValue and update:modelValue.

Beforevue
<script>
export default {
  props: ['value'],
  methods: {
    updateValue(value) {
      this.$emit('input', value)
    }
  }
}
</script>
Aftervue
<script>
export default {
  props: ['modelValue'],
  emits: ['update:modelValue'],
  methods: {
    updateValue(value) {
      this.$emit('update:modelValue', value)
    }
  }
}
</script>
Migration

Replace the value prop with modelValue and emit update:modelValue instead of input. Replace .sync usage with v-model arguments where applicable.

Template filters were removed

Action required
breakinghigh impact

Vue 3 removed the filters syntax from templates. Expressions inside template interpolations and v-bind should use JavaScript expressions instead.

Beforevue
<template>
  <p>{{ accountBalance | currencyUSD }}</p>
</template>
Aftervue
<template>
  <p>{{ accountBalanceUSD }}</p>
</template>

<script>
export default {
  computed: {
    accountBalanceUSD() {
      return '$' + this.accountBalance
    }
  }
}
</script>
Migration

Replace template filters with methods or computed properties. If filters were registered globally, move the shared formatting logic to regular JavaScript functions or another shared utility.

Global API moved to application instances

Action required
breakinghigh impact

Vue 3 introduces createApp() and moves APIs that globally mutate Vue behavior to an application instance. Root applications are no longer created with new Vue().

Beforejavascript
import Vue from 'vue'
import App from './App.vue'

Vue.use(MyPlugin)
Vue.component('MyComponent', MyComponent)

new Vue({
  render: h => h(App)
}).$mount('#app')
Afterjavascript
import { createApp } from 'vue'
import App from './App.vue'

const app = createApp(App)

app.use(MyPlugin)
app.component('MyComponent', MyComponent)

app.mount('#app')
Migration

Create the root application with createApp(). Move globally mutating APIs such as Vue.use(), Vue.component(), Vue.directive(), Vue.mixin() and Vue.config to the returned application instance.

Key attribute behavior changed

Action required
breakingmedium impact

Vue 3 automatically generates unique keys for conditional branches. When using <template v-for>, the key should be placed on the <template> element instead of its children.

Beforevue
<template v-for="item in items">
  <div :key="item.id">
    {{ item.name }}
  </div>
</template>
Aftervue
<template
  v-for="item in items"
  :key="item.id"
>
  <div>
    {{ item.name }}
  </div>
</template>
Migration

Remove unnecessary manual keys from v-if/v-else branches where possible. For <template v-for>, move the key to the <template> element.

v-if now takes precedence over v-for

Action required
breakinghigh impact

When v-if and v-for are used on the same element, Vue 3 evaluates v-if before v-for. Vue 2 used the opposite precedence.

Beforevue
<template>
  <li
    v-for="user in users"
    v-if="user.isActive"
    :key="user.id"
  >
    {{ user.name }}
  </li>
</template>
Aftervue
<script>
export default {
  computed: {
    activeUsers() {
      return this.users.filter(
        user => user.isActive
      )
    }
  }
}
</script>

<template>
  <li
    v-for="user in activeUsers"
    :key="user.id"
  >
    {{ user.name }}
  </li>
</template>
Migration

Avoid using v-if and v-for on the same element. Prefer filtering the collection with a computed property before rendering it with v-for.

v-bind merge behavior now respects declaration order

Action required
breakingmedium impact

When an element has both individual attributes and v-bind="object", Vue 3 determines which value wins based on their declaration order.

Beforevue
<div
  id="red"
  v-bind="{ id: 'blue' }"
></div>
Aftervue
<div
  v-bind="{ id: 'blue' }"
  id="red"
></div>
Migration

Review elements that combine v-bind="object" with individual attributes. In Vue 3, place the binding that should take precedence later in the attribute list.

v-on.native modifier was removed

Action required
breakinghigh impact

Vue 3 removes the .native modifier from v-on. Component-emitted events should be declared with the emits option, while undeclared listeners can fall through to the component root element.

Beforevue
<template>
  <MyComponent
    @close="handleClose"
    @click.native="handleClick"
  />
</template>
Aftervue
<template>
  <MyComponent
    @close="handleClose"
    @click="handleClick"
  />
</template>

<script>
export default {
  emits: ['close']
}
</script>
Migration

Remove .native modifiers and explicitly declare component-emitted events with the emits option. Review components that re-emit native events, because undeclared listeners can fall through to the root element.

Functional Components API changed

Action required
breakinghigh impact

Vue 3 removes the functional component option and the <template functional> SFC syntax. Functional components are now created as plain functions receiving props and context.

Beforejavascript
export default {
  functional: true,

  props: ['level'],

  render(h, { props, data, children }) {
    return h(
      `h${props.level}`,
      data,
      children
    )
  }
}
Afterjavascript
import { h } from 'vue'

const DynamicHeading = (props, context) => {
  return h(
    `h${props.level}`,
    context.attrs,
    context.slots
  )
}

DynamicHeading.props = ['level']

export default DynamicHeading
Migration

Remove functional: true and convert JavaScript functional components to plain functions. For SFCs using <template functional>, remove the functional attribute and migrate props/attrs access to the Vue 3 syntax. Consider using a normal stateful component when functional rendering is not specifically needed.

Async Components now use defineAsyncComponent

Action required
breakinghigh impact

Vue 3 introduces defineAsyncComponent() for explicitly creating async components. The component option is renamed to loader when using the advanced configuration format.

Beforejavascript
const AsyncComponent = () =>
  import('./MyComponent.vue')

export default {
  components: {
    AsyncComponent
  }
}
Afterjavascript
import { defineAsyncComponent } from 'vue'

const AsyncComponent = defineAsyncComponent(() =>
  import('./MyComponent.vue')
)

export default {
  components: {
    AsyncComponent
  }
}
Migration

Wrap async component loader functions with defineAsyncComponent(). If using the advanced async component configuration, rename the component option to loader and review loading/error component behavior.

Render Function API changed

Action required
breakinghigh impact

Vue 3 changes the render function API. The h function must be imported from Vue, VNode props use a flat structure, and registered components are resolved explicitly.

Beforejavascript
export default {
  render(h) {
    return h('div', {
      attrs: {
        id: 'app'
      },
      on: {
        click: this.handleClick
      }
    })
  }
}
Afterjavascript
import { h } from 'vue'

export default {
  render() {
    return h('div', {
      id: 'app',
      onClick: this.handleClick
    })
  }
}
Migration

Import h from vue instead of receiving it as a render argument. Update VNode props to the flat Vue 3 structure and use resolveComponent when resolving registered components by name.

Scoped slots were unified with regular slots

Action required
breakingmedium impact

Vue 3 removes $scopedSlots. All slots are exposed through $slots and are accessed as functions.

Beforejavascript
this.$scopedSlots.header
this.$slots.default
Afterjavascript
this.$slots.header()
this.$slots.default()
Migration

Replace $scopedSlots references with $slots and invoke slots as functions when accessing them programmatically.

$listeners was merged into $attrs

Action required
breakinghigh impact

Vue 3 removes the $listeners instance property. Event listeners are now included in $attrs using onXxx properties.

Beforevue
<input
  v-bind="$attrs"
  v-on="$listeners"
/>
Aftervue
<input v-bind="$attrs" />
Migration

Remove usages of $listeners. Component listeners that are not declared as emitted events are available through $attrs.

$attrs now includes class and style

Action required
breakingmedium impact

Vue 3 includes class and style attributes in $attrs, including when inheritAttrs is false.

Migration

Review components that manually forward $attrs or use inheritAttrs: false. Class and style are now included and may be forwarded together with other attributes.

Custom directive lifecycle hooks changed

Action required
breakinghigh impact

Vue 3 aligns custom directive lifecycle hooks with component lifecycle hooks and removes binding.expression.

Beforejavascript
Vue.directive('highlight', {
  bind(el, binding) {
    el.style.background = binding.value
  },

  inserted(el) {},

  componentUpdated(el) {},

  unbind(el) {}
})
Afterjavascript
app.directive('highlight', {
  beforeMount(el, binding) {
    el.style.background = binding.value
  },

  mounted(el) {},

  updated(el) {},

  unmounted(el) {}
})
Migration

Rename directive lifecycle hooks to the Vue 3 lifecycle equivalents. Replace bind with beforeMount, inserted with mounted, componentUpdated with updated, and unbind with unmounted.

Destroy lifecycle hooks were renamed

Action required
breakingmedium impact

Vue 3 renames beforeDestroy to beforeUnmount and destroyed to unmounted.

Beforejavascript
export default {
  beforeDestroy() {
    // cleanup
  },

  destroyed() {
    // destroyed
  }
}
Afterjavascript
export default {
  beforeUnmount() {
    // cleanup
  },

  unmounted() {
    // unmounted
  }
}
Migration

Rename beforeDestroy to beforeUnmount and destroyed to unmounted.

data must be declared as a function

Action required
breakingmedium impact

Vue 3 requires the data option to be a function for component definitions.

Beforejavascript
const Component = {
  data: {
    count: 0
  }
}
Afterjavascript
const Component = {
  data() {
    return {
      count: 0
    }
  }
}
Migration

Convert object-based data options to functions that return the component state.

Mixin data is now merged shallowly

Action required
breakingmedium impact

Vue 3 changes data merging between components, mixins and extensions to a shallow merge instead of recursively merging nested objects.

Migration

Review components that rely on nested data objects being recursively merged from mixins or extends. Explicitly combine nested state where required.

Array watchers no longer trigger on mutation by default

Action required
breakinghigh impact

In Vue 3, watching an array triggers when the array is replaced. Watching mutations requires the deep option.

Beforejavascript
watch: {
  items() {
    this.handleItemsChanged()
  }
}
Afterjavascript
watch: {
  items: {
    handler() {
      this.handleItemsChanged()
    },
    deep: true
  }
}
Migration

Add the deep option to array watchers when the application needs to react to array mutations rather than only array replacement.

Transition enter and leave classes were renamed

Action required
breakingmedium impact

Vue 3 renames the initial transition classes to make the transition state names consistent.

Beforecss
.v-enter {
  opacity: 0;
}

.v-leave {
  opacity: 1;
}
Aftercss
.v-enter-from {
  opacity: 0;
}

.v-leave-from {
  opacity: 1;
}
Migration

Replace *-enter with *-enter-from and *-leave with *-leave-from. Update the related enter-class and leave-class component props as well.

TransitionGroup no longer renders a wrapper by default

Action required
breakingmedium impact

Vue 3 changes TransitionGroup so it does not render a wrapper element by default.

Migration

If the Vue 2 wrapper element is required for layout or styling, explicitly provide the tag prop on TransitionGroup.

Numeric keyCode modifiers were removed

Action required
breakingmedium impact

Vue 3 removes numeric keyCode modifiers and the config.keyCodes API.

Beforevue
<input @keyup.13="submit" />
Aftervue
<input @keyup.enter="submit" />
Migration

Replace numeric keyCode modifiers with named key modifiers. Remove custom config.keyCodes mappings and use KeyboardEvent key names or explicit event handling where necessary.

$on, $off and $once were removed

Action required
breakinghigh impact

Vue 3 removes the component instance event-emitter methods $on, $off and $once.

Migration

Replace Vue instances used as event buses with an external event emitter or another explicit state/event communication pattern.

$children was removed

Action required
breakingmedium impact

Vue 3 removes the $children instance property.

Migration

Replace code that depends on $children with explicit template refs, props, provide/inject, or another explicit parent-child communication mechanism.

propsData option was removed

Action required
breakingmedium impact

Vue 3 removes propsData when creating component instances. Root props can instead be passed as the second argument to createApp.

Beforejavascript
new Comp({
  propsData: {
    username: 'Evan'
  }
})
Afterjavascript
createApp(
  Comp,
  {
    username: 'Evan'
  }
)
Migration

Remove propsData usage. Pass root component props as the second argument to createApp or use normal component rendering APIs for child components.

Vue.set, Vue.delete, $set and $delete were removed

Action required
breakingmedium impact

Vue 3 proxy-based reactivity no longer requires special APIs for adding or deleting reactive object properties.

Beforejavascript
this.$set(user, 'name', 'Volkan')
this.$delete(user, 'legacy')
Afterjavascript
user.name = 'Volkan'
delete user.legacy
Migration

Replace Vue.set/$set with normal property assignment and Vue.delete/$delete with the JavaScript delete operator.

Mounted applications no longer replace the mount element

Action required
breakingmedium impact

Vue 3 mounts the rendered application inside the target container instead of replacing the target element itself.

Beforehtml
<div id="app"></div>

<!-- Vue 2 may replace #app with the rendered root -->
Afterhtml
<div id="app">
  <!-- Vue 3 renders the application here -->
</div>
Migration

Review CSS, DOM selectors and integrations that assume the mount container itself is replaced by the root component.

Custom element detection and is usage changed

Action required
breakingmedium impact

Vue 3 performs custom element checks during template compilation. The special is attribute is restricted to the reserved <component> element for component switching.

Migration

Move custom element detection to compilerOptions.isCustomElement in the compiler or build configuration. Review usages of is on native elements and use <component :is="..."> for dynamic Vue components.

Prop default factories no longer have access to this

Action required
breakingmedium impact

Vue 3 removes component instance access through this inside prop default factory functions. Raw incoming props are provided as an argument instead.

Beforejavascript
export default {
  props: {
    theme: {
      default() {
        return this.defaultTheme
      }
    }
  }
}
Afterjavascript
import { inject } from 'vue'

export default {
  props: {
    theme: {
      default(props) {
        return inject(
          'theme',
          'default-theme'
        )
      }
    }
  }
}
Migration

Remove component instance access from prop default factories. Use the raw props argument when another incoming prop is needed, or inject() when injected values are required.

Attribute coercion behavior changed

Action required
breakingmedium impact

Vue 3 removes the special enumerated-attribute coercion behavior. For non-boolean attributes, false is serialized as "false" instead of removing the attribute.

Beforevue
<div :aria-hidden="false"></div>

<!-- Vue 2 could remove some
     non-boolean attributes
     when bound to false -->
Aftervue
<div :aria-hidden="false"></div>

<!-- Vue 3 renders:
     aria-hidden="false"

     Use null or undefined
     to remove the attribute. -->
Migration

Review bindings that depend on false removing a non-boolean attribute. Use null or undefined when the attribute should be removed.

Plain template elements are now rendered natively

Action required
breakinglow impact

A <template> element without a Vue special directive is treated as a native template element in Vue 3 instead of transparently rendering only its inner content.

Migration

Review plain <template> elements that do not use v-if, v-else, v-for or v-slot. Remove unnecessary template wrappers when the Vue 2 transparent-wrapper behavior was intended.

VNode lifecycle event prefix changed

Action required
breakingmedium impact

Vue 3 replaces the hook: prefix used for VNode lifecycle events with vue:. These events can also be used on HTML elements.

Beforevue
<ChildComponent
  @hook:updated="onUpdated"
/>
Aftervue
<ChildComponent
  @vue:updated="onUpdated"
/>
Migration

Replace hook: lifecycle event prefixes with vue:. Also account for the beforeDestroy/beforeUnmount and destroyed/unmounted lifecycle renames where applicable.

inline-template was removed

Action required
breakingmedium impact

Vue 3 removes the inline-template attribute that allowed child component content to act as the component template.

Beforevue
<MyComponent inline-template>
  <div>
    {{ message }}
  </div>
</MyComponent>
Aftervue
<MyComponent v-slot="{ message }">
  <div>
    {{ message }}
  </div>
</MyComponent>
Migration

Replace inline-template usage with regular component templates, external template sources, or scoped/default slots depending on the original use case.

$destroy was removed

Action required
breakingmedium impact

Vue 3 removes the $destroy component instance method. Applications should not manually manage the lifecycle of individual component instances.

Migration

Remove manual $destroy calls and control component lifetime through normal declarative rendering and application unmounting.

Global APIs are now tree-shakeable named exports

Action required
breakinghigh impact

Vue 3 restructures a number of global APIs as named ES module exports so unused APIs can be removed by tree-shaking.

Beforejavascript
import Vue from 'vue'

Vue.nextTick(() => {
  // DOM update complete
})

const state = Vue.observable({
  count: 0
})
Afterjavascript
import {
  nextTick,
  reactive
} from 'vue'

nextTick(() => {
  // DOM update complete
})

const state = reactive({
  count: 0
})
Migration

Replace affected Vue.* global API calls with named imports from vue. For example, import nextTick directly and replace Vue.observable with reactive.

defineComponent() function overload type changed

Action required
breakinglow impact

Vue 3.3 repurposes the rarely used defineComponent() function overload to support improved generic component typing, changing its TypeScript type behavior.

Migration

Review code that passes a function directly to defineComponent(). This overload was rarely used, but its type signature changed in Vue 3.3.

Official sources

Global JSX namespace registration was removed

Action required
breakinghigh impact

Vue 3.4 no longer registers the global JSX namespace by default, avoiding global type collisions with other JSX ecosystems such as React.

Beforevue
{
  "compilerOptions": {
    "jsx": "preserve"
  }
}
Aftervue
{
  "compilerOptions": {
    "jsx": "preserve",
    "jsxImportSource": "vue"
  }
}
Migration

TSX users should set jsxImportSource to vue in tsconfig.json or add an @jsxImportSource vue pragma per file. Projects that depend on the old global JSX namespace can explicitly reference vue/jsx.

Reactivity Transform was removed from Vue core

Action required
breakinghigh impact

Vue 3.4 removes the experimental Reactivity Transform feature, including compile-time macros such as $ref(), from Vue core.

Beforevue
<script setup>
let count = $ref(0)

count++
</script>
Aftervue
<script setup>
import { ref } from "vue"

const count = ref(0)

count.value++
</script>
Migration

Replace Reactivity Transform macros with standard Vue reactivity APIs, or migrate to the Vue Macros plugin if the transform syntax should be retained.

app.config.unwrapInjectedRef was removed

Action required
breakinglow impact

Vue 3.4 removes app.config.unwrapInjectedRef. Injected refs are unwrapped according to the behavior that had already become the default in Vue 3.3.

Beforevue
app.config.unwrapInjectedRef = false
Migration

Remove usages of app.config.unwrapInjectedRef. Vue 3.4 no longer allows opting out of the injected-ref unwrapping behavior.

Official sources

Deprecated @vnodeXXX event listeners became compiler errors

Action required
breakingmedium impact

Vue 3.4 removes the deprecated @vnodeXXX template event syntax. The replacement @vue:XXX lifecycle listener syntax must be used.

Beforevue
<MyComponent
  @vnodeMounted="onMounted"
/>
Aftervue
<MyComponent
  @vue:mounted="onMounted"
/>
Migration

Replace deprecated @vnodeXXX lifecycle event listeners with the corresponding @vue:XXX syntax.

Official sources

Deprecated v-is directive was removed

Action required
breakingmedium impact

Vue 3.4 removes the deprecated v-is directive.

Beforevue
<tr v-is="'vue:my-row'"></tr>
Aftervue
<tr is="vue:my-row"></tr>
Migration

Replace v-is with the is attribute and the vue: prefix where Vue component resolution on native elements is required.

Official sources

New Features

Components can have multiple root nodes

featuremedium impact

Vue 3 introduces fragment support, allowing components to render multiple root nodes instead of requiring a single root element.

Beforevue
<template>
  <div>
    <header>...</header>
    <main>...</main>
  </div>
</template>
Aftervue
<template>
  <header>...</header>
  <main>...</main>
</template>
Migration

The wrapper element can be removed when it only existed to satisfy the Vue 2 single-root requirement. For multi-root components, review attribute inheritance because Vue cannot automatically determine which root should receive fallthrough attributes.

Components can declare emitted events

featuremedium impact

Vue 3 introduces the emits option, allowing components to explicitly declare the events they emit, similar to how props declares accepted properties.

Beforevue
<script>
export default {
  props: ['text'],

  methods: {
    accept() {
      this.$emit('accepted')
    }
  }
}
</script>
Aftervue
<script>
export default {
  props: ['text'],

  emits: ['accepted'],

  methods: {
    accept() {
      this.$emit('accepted')
    }
  }
}
</script>
Migration

Declare the events emitted by each component using the emits option. This is especially important when migrating components that previously relied on the .native modifier or re-emitted native DOM events.

Composition API introduced

featurehigh impact

Vue 3 introduces the Composition API, providing APIs such as setup(), ref(), reactive(), computed() and lifecycle hooks for organizing component logic by feature instead of option type.

Beforevue
<script>
export default {
  data() {
    return {
      count: 0
    }
  },

  computed: {
    doubled() {
      return this.count * 2
    }
  },

  methods: {
    increment() {
      this.count++
    }
  }
}
</script>
Aftervue
<script>
import {
  ref,
  computed
} from 'vue'

export default {
  setup() {
    const count = ref(0)

    const doubled = computed(
      () => count.value * 2
    )

    const increment = () => {
      count.value++
    }

    return {
      count,
      doubled,
      increment
    }
  }
}
</script>
Migration

Existing Options API components do not need to be rewritten. Composition API is an additional way to author components and can be adopted incrementally where it improves logic reuse or organization.

Teleport component introduced

featuremedium impact

Vue 3 introduces the built-in <Teleport> component, allowing part of a component template to be rendered into a DOM node outside of the component’s own DOM hierarchy.

Beforevue
<template>
  <div class="page">
    <MyModal v-if="showModal" />
  </div>
</template>
Aftervue
<template>
  <div class="page">
    <!-- page content -->
  </div>

  <Teleport to="body">
    <MyModal v-if="showModal" />
  </Teleport>
</template>
Migration

No migration is required. Teleport can replace custom portal-style solutions when UI such as modals, overlays or notifications needs to render outside the component DOM hierarchy.

Suspense component introduced

featuremedium impact Experimental

Vue 3 introduces the built-in <Suspense> component for coordinating asynchronous dependencies in a component tree and displaying fallback content while they resolve.

Beforevue
<template>
  <div v-if="loading">
    Loading...
  </div>

  <AsyncComponent v-else />
</template>
Aftervue
<template>
  <Suspense>
    <template #default>
      <AsyncComponent />
    </template>

    <template #fallback>
      Loading...
    </template>
  </Suspense>
</template>
Migration

No migration is required. Suspense can coordinate loading states for supported asynchronous dependencies, but it should be adopted with care because the API is experimental.

Custom Renderer API introduced

featurelow impact

Vue 3 exposes a custom renderer API that allows Vue components and its reactivity system to target rendering environments other than the browser DOM.

Migration

No migration is required for normal Vue applications. The API is intended primarily for authors building custom renderers for non-DOM environments.

Component state can be used directly in SFC CSS

featurelow impact

Vue 3 supports the v-bind() CSS function in Single-File Component style blocks, allowing CSS values to react to component state.

Beforevue
<template>
  <div :style="{ color: themeColor }">
    Hello
  </div>
</template>

<script>
export default {
  data() {
    return {
      themeColor: 'red'
    }
  }
}
</script>
Aftervue
<template>
  <div class="message">
    Hello
  </div>
</template>

<script>
export default {
  data() {
    return {
      themeColor: 'red'
    }
  }
}
</script>

<style>
.message {
  color: v-bind(themeColor);
}
</style>
Migration

No migration is required. CSS v-bind() can be adopted when reactive component state should be consumed directly inside an SFC style block.

onServerPrefetch added to the Composition API

featurelow impact

Vue 3.1 adds onServerPrefetch(), allowing Composition API components to register asynchronous work that should be resolved during server-side rendering.

Aftervue
<script>
import {
  ref,
  onServerPrefetch
} from 'vue'

export default {
  setup() {
    const data = ref(null)

    onServerPrefetch(async () => {
      data.value = await fetchData()
    })

    return {
      data
    }
  }
}
</script>
Migration

No migration is required. Use onServerPrefetch() when Composition API logic needs to fetch or prepare asynchronous data during server-side rendering.

Official sources

<script setup> became stable

featurehigh impact

Vue 3.2 stabilizes <script setup>, a compile-time syntax for using the Composition API inside Single-File Components with less boilerplate.

Beforevue
<script>
import { ref } from "vue"

export default {
  setup() {
    const count = ref(0)

    const increment = () => {
      count.value++
    }

    return {
      count,
      increment
    }
  }
}
</script>
Aftervue
<script setup>
import { ref } from "vue"

const count = ref(0)

const increment = () => {
  count.value++
}
</script>
Migration

No migration is required. Existing setup() components remain valid. <script setup> can be adopted incrementally to reduce Composition API boilerplate in Single-File Components.

:slotted() selector became stable

featurelow impact

Vue 3.2 stabilizes the :slotted() pseudo-class for scoped SFC styles, allowing a component to explicitly style content passed through slots.

Aftervue
<template>
  <slot />
</template>

<style scoped>
:slotted(.message) {
  color: red;
}
</style>
Migration

No migration is required. Use :slotted() when scoped component styles need to target elements provided by a parent through slots.

Web Components support with defineCustomElement

featuremedium impact

Vue 3.2 introduces defineCustomElement(), allowing Vue components to be packaged and registered as native Custom Elements.

Aftervue
import {
  defineCustomElement
} from "vue"
import MyElement from "./MyElement.ce.vue"

const MyCustomElement =
  defineCustomElement(MyElement)

customElements.define(
  "my-element",
  MyCustomElement
)
Migration

No migration is required. Use defineCustomElement() when Vue components need to be distributed or consumed as native Web Components.

Effect scopes introduced

featuremedium impact

Vue 3.2 introduces effectScope() for grouping reactive effects such as computed values and watchers so they can be disposed together.

Aftervue
import {
  effectScope,
  computed,
  watch
} from "vue"

const scope = effectScope()

scope.run(() => {
  const doubled = computed(
    () => state.count * 2
  )

  watch(
    () => state.count,
    () => {
      // react to changes
    }
  )
})

scope.stop()
Migration

No migration is required. effectScope() is primarily useful for reusable composables and libraries that need explicit control over groups of reactive effects.

Web Streams SSR API introduced

featurelow impact

Vue 3.2 expands server-side rendering APIs with renderToWebStream() for environments that support the Web Streams API.

Aftervue
import {
  renderToWebStream
} from "@vue/server-renderer"

const stream =
  renderToWebStream(app)
Migration

No migration is required. The streaming API can be adopted by SSR runtimes that support Web Streams.

defineOptions() introduced

featuremedium impact

Vue 3.3 introduces defineOptions(), allowing additional component options such as inheritAttrs to be declared directly inside <script setup>.

Beforevue
<script>
export default {
  inheritAttrs: false
}
</script>

<script setup>
// component logic
</script>
Aftervue
<script setup>
defineOptions({
  inheritAttrs: false
})
</script>
Migration

No migration is required. Components that previously needed a second normal <script> block solely for supported component options can use defineOptions().

Reactive Props Destructure introduced experimentally

featuremedium impact Experimental

Vue 3.3 introduces experimental support for destructuring defineProps() while preserving reactivity and allowing native default-value syntax.

Migration

No migration is required. This feature was experimental in Vue 3.3 and should be treated according to the behavior and stability of the target Vue version.

defineModel() introduced experimentally

featuremedium impact Experimental

Vue 3.3 introduces the experimental defineModel() compiler macro for declaring a component v-model prop and its corresponding update event with less boilerplate.

Beforevue
<script setup>
const props = defineProps({
  modelValue: String
})

const emit = defineEmits([
  "update:modelValue"
])
</script>
Aftervue
<script setup>
const modelValue =
  defineModel<string>()
</script>
Migration

No migration is required. In Vue 3.3 defineModel() was experimental, so adoption should account for the stability of the target Vue release.

toRef() normalization improved and toValue() introduced

featuremedium impact

Vue 3.3 enhances toRef() so it can normalize values, getters and existing refs, and introduces toValue() for normalizing values, refs and getters into values.

Aftervue
import {
  ref,
  toRef,
  toValue
} from "vue"

const existing = ref(1)

toRef(1)
toRef(() => props.count)
toRef(existing)

toValue(1)
toValue(existing)
toValue(() => props.count)
Migration

No migration is required. Composables can use toValue() when accepting values, refs or getters, and the normalization form of toRef() when a ref representation is needed.

Reactivity APIs gained better getter support

featuremedium impact

Vue 3.3 improves getter handling in reactivity utilities, making getter-based reactive inputs easier and more efficient to consume in composables.

Beforevue
useFeature(
  computed(() => props.id)
)
Aftervue
useFeature(
  () => props.id
)
Migration

No migration is required. Composables can increasingly accept getters directly instead of requiring callers to allocate intermediate computed refs.

defineModel() became stable

featurehigh impact

Vue 3.4 promotes defineModel() from experimental to stable, providing a concise way to declare a component v-model prop and its corresponding update event.

Beforevue
<script setup lang="ts">
const props = defineProps<{
  modelValue: string
}>()

const emit = defineEmits<{
  "update:modelValue": [value: string]
}>()
</script>
Aftervue
<script setup lang="ts">
const model = defineModel<string>()
</script>
Migration

No migration is required. Projects that avoided defineModel() while it was experimental can now adopt the stable API. Vue 3.4 also removes the previous local option and supports local mutation by default.

Same-name v-bind shorthand introduced

featurelow impact

Vue 3.4 supports a shorthand for bindings where the attribute name and JavaScript variable name are identical.

Beforevue
<img
  :id="id"
  :src="src"
  :alt="alt"
/>
Aftervue
<img
  :id
  :src
  :alt
/>
Migration

No migration is required. The shorthand can be adopted where a v-bind argument has the same name as the bound variable.

Official sources

MathML support introduced

featurelow impact

Vue 3.4 adds built-in recognition and rendering support for MathML elements.

Aftervue
<template>
  <math>
    <mfrac>
      <mi>a</mi>
      <mi>b</mi>
    </mfrac>
  </math>
</template>
Migration

No migration is required. MathML can be used directly in Vue templates.

Official sources

TypeScript

SFC macros support imported and complex TypeScript types

typescripthigh impact

Vue 3.3 expands type support in macros such as defineProps() and defineEmits(), allowing imported types and a broader set of complex TypeScript types to be used directly.

Beforevue
<script setup lang="ts">
interface Props {
  name: string
  age?: number
}

defineProps<Props>()
</script>
Aftervue
<script setup lang="ts">
import type { UserProps } from "./types"

defineProps<UserProps>()
</script>
Migration

No migration is required. Existing local type declarations remain valid. Imported and supported complex types can now be used directly with SFC compiler macros.

Generic components supported in <script setup>

typescripthigh impact

Vue 3.3 adds native support for declaring generic Single-File Components using the generic attribute on <script setup>.

Aftervue
<script
  setup
  lang="ts"
  generic="T extends string | number"
>
defineProps<{
  items: T[]
  selected: T
}>()
</script>
Migration

No migration is required. Generic components can now express relationships between prop types directly in <script setup>.

defineEmits gained a more ergonomic TypeScript syntax

typescriptmedium impact

Vue 3.3 allows type-based defineEmits declarations to use event names as object keys and tuple types for event arguments.

Beforevue
const emit = defineEmits<{
  (e: "change", id: number): void
  (e: "update", value: string): void
}>()
Aftervue
const emit = defineEmits<{
  change: [id: number]
  update: [value: string]
}>()
Migration

No migration is required. The previous call-signature syntax remains supported. The tuple syntax can be adopted for more concise event typing.

Typed slots with defineSlots()

typescripthigh impact

Vue 3.3 introduces the defineSlots() compiler macro for providing IDE and vue-tsc type information about slot names and slot props.

Aftervue
<script setup lang="ts">
defineSlots<{
  default?: (
    props: { message: string }
  ) => any

  item?: (
    props: { id: number }
  ) => any
}>()
</script>
Migration

No migration is required. defineSlots() can be adopted when slot names and slot props should be type checked.

JSX import source support added

typescriptmedium impact

Vue 3.3 adds support for TypeScript's jsxImportSource option, allowing Vue JSX typing to be opted into explicitly and reducing conflicts with other JSX ecosystems.

Aftervue
{
  "compilerOptions": {
    "jsx": "preserve",
    "jsxImportSource": "vue"
  }
}
Migration

Vue 3.3 still provides the global JSX namespace for backwards compatibility, but TSX users should configure jsxImportSource for forward compatibility because the default global registration was planned for removal in Vue 3.4.

defineComponent() gained generic component support

typescriptmedium impact

Vue 3.3 improves TypeScript support for authoring generic components with defineComponent(), complementing generic support in <script setup>.

Migration

No migration is required. Library and component authors can use the improved generic typing when component APIs need to preserve relationships between types.

Official sources

Performance

Reactivity performance improved

performancemedium impact

Vue 3.2 includes major reactivity performance improvements, reducing overhead in dependency tracking and reactive effect execution.

Migration

No migration is required. Existing applications receive the reactivity performance improvements after upgrading.

Template compiler performance improved

performancelow impact

Vue 3.2 improves template compiler performance, reducing compilation costs for templates and Single-File Components.

Migration

No migration is required. Applications and build tooling benefit automatically after upgrading.

Runtime memory usage reduced

performancelow impact

Vue 3.2 includes runtime optimizations that reduce memory usage and improve performance when creating and updating component trees.

Migration

No migration is required. The runtime optimizations apply automatically after upgrading.

Template parser rewritten for substantially better performance

performancemedium impact

Vue 3.4 introduces a rewritten template parser that is approximately twice as fast, improving Single-File Component and template compilation performance.

Migration

No migration is required. Applications and build tooling receive the parser performance improvements after upgrading.

Official sources

Reactivity system became more efficient

performancehigh impact

Vue 3.4 refactors the reactivity system so computed values and effects trigger more accurately and avoid unnecessary executions in a number of common cases.

Migration

No migration is required. Existing applications benefit automatically from the more efficient reactivity implementation.

Official sources

Tooling

SFC compiler source map support improved

toolinglow impact

Vue 3.1 includes compiler improvements for generating and propagating source maps for Single-File Components.

Migration

No migration is required. Tooling built on @vue/compiler-sfc can benefit from the improved source map support.

Official sources

SSR package became ESM-bundler compatible

toolinglow impact

Vue 3.2 improves server-side rendering support, including an ESM-bundler build of @vue/server-renderer for better integration with modern build tools.

Migration

No migration is required for most applications. SSR tooling and frameworks can use the improved server-renderer packaging with modern ESM-based build pipelines.

SSR hydration mismatch diagnostics improved

toolingmedium impact

Vue 3.4 improves hydration mismatch checks and development diagnostics, providing clearer information about mismatched server and client output.

Migration

No migration is required. SSR applications receive more informative hydration mismatch diagnostics after upgrading.

Production hydration mismatch details can be enabled

toolinglow impact

Vue 3.4 adds the false compile-time feature flag for including detailed hydration mismatch information in production builds.

Migration

No migration is required. SSR tooling can opt into detailed production hydration mismatch diagnostics when the additional information is useful.

Official sources

Migration Tools

Migration Build for Vue 2 applications

migrationmedium impact

Vue 3.1 introduced the @vue/compat migration build, which provides Vue 2 compatible behavior with runtime warnings for APIs and behaviors that need to be migrated.

Migration

Projects migrating from Vue 2 can temporarily alias vue to @vue/compat and use compatibility warnings to migrate incrementally. The compatibility build is intended as a migration aid and should be removed after migration is complete.