Vue 3

 New features, Breaking changes & a Migration path

~/ whoami

Vue.js Enthusiast

Proud VuejsAthens co-organizer

Currently working for Glovo Barcelona

fadamakis

What to expect

  • Notable Breaking Changes

  • New Features 

  • Composition API

  • Supporting Libraries

  • Migration Path

  • Additional Resources

Global API

Vue 2.x has a number of global APIs and configurations that globally mutate Vue’s behavior. For instance, to create a global component or directive, you would use the Vue.component and Vue.directive respectively.

Vue.component('button-counter', {
  data: () => ({
    count: 0
  }),
  template: `
  <button @click="count++">
    Clicked {{ count }} times.
  </button>
  `
})
Vue.directive('focus', {
  inserted: el => el.focus()
})
Vue.component
Vue.directive
Vue.mixin
Vue.use
Vue.config
Vue.use(MyPlugin)

Global configuration makes it difficult to share the same copy of Vue between multiple "apps" on the same page, but with different global configurations.

// this affects both root instances
Vue.mixin({
  /* ... */
})

const app1 = new Vue({ el: '#app-1' })
const app2 = new Vue({ el: '#app-2' })
import { createLocalVue, mount } from '@vue/test-utils'

// create an extended `Vue` constructor
const localVue = createLocalVue()

// install a plugin “globally” on the “local” Vue constructor
localVue.use(MyPlugin)

// pass the `localVue` to the mount options
mount(Component, { localVue })

In addition, Global configuration makes it easy to accidentally pollute other test cases during testing.

Global API

Global vs Instance API

import Vue from 'vue'
import Vuex from 'vuex'
import App from './app.vue'

Vue.use(Vuex)

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

const app = createApp(App)

app.use(store)

app.mount('#app')

Vue 2.x Global API

Vue 3.x  Instance API (app)

Vue.config
Vue.config.productionTip
Vue.config.ignoredElements
Vue.component
Vue.directive
Vue.mixin
Vue.use
Vue.prototype
app.config
removed
app.config.isCustomElement
app.component
app.directive
app.mixin
app.use 
app.config.globalProperties 

Fragments

<!-- Layout.vue -->
<template>
  <div>
    <header>...</header>
    <main>...</main>
    <footer>...</footer>
  </div>
</template>
<!-- Layout.vue -->
<template>
  <header>...</header>
  <main v-bind="$attrs">...</main>
  <footer>...</footer>
</template>

Vue 2.x

Vue 3.x

In Vue 3, components now have official support for multi-root node components.

 

Emitted Events

Emitted events can be defined on the component via the emits option.

This is not mandatory but should be considered a best practice since it enables self-documenting code.

<template>
  <div>
    <p>{{ text }}</p>
    <button v-on:click="$emit('accepted')">OK</button>
  </div>
</template>
<script>
  export default {
    props: ['text'],
    emits: ['accepted']
  }
</script>

Teleport

Sometimes a part of a component's template belongs to this component logically, while from a technical point of view, it would be preferable to move this part of the template somewhere else in the DOM, outside of the Vue app.

Teleport provides a clean way to allow us to control under which parent in our DOM we want a piece of HTML to be rendered, without having to resort to global state or splitting this into two components.

app.component('modal-button', {
  template: `
    <button @click="modalOpen = true">
        Open full screen modal! (With teleport!)
    </button>

    <teleport to="body">
      <div v-if="modalOpen" class="modal">
        <div>
          I'm a teleported modal! 
          (My parent is "body")
          <button @click="modalOpen = false">
            Close
          </button>
        </div>
      </div>
    </teleport>
  `,
  data() {
    return { 
      modalOpen: false
    }
  }
})

key attribute

The key special attribute is used as a hint for Vue's virtual DOM algorithm to keep track of a node's identity. That way, Vue knows when it can reuse and patch existing nodes and when it needs to reorder or recreate them.

<template v-for="item in list" :key="item.id">
  <div>...</div>
</template>
<div v-if="condition" key="yes">Yes</div>
<div v-else key="no">No</div>
<template v-for="item in list">
  <div :key="item.id">...</div>
  <span :key="item.id">...</span>
</template>
<template v-for="item in list" :key="item.id">
  <div>...</div>
  <span>...</span>
</template>

Vue 2.x

Vue 3.x

In Vue 3, the key should be placed on the <template> tag instead of each child.

 

Key attribute (v-for)

<div v-if="condition" key="yes">Yes</div>
<div v-else key="no">No</div>
<div v-if="condition">Yes</div>
<div v-else>No</div>

Vue 2.x

Vue 3.x

In Vue 3, unique keys are now automatically generated on conditional branches if you don't provide them.

 

Key attribute (v-if)

v-if vs v-for Precedence

If used on the same element, v-if will have higher precedence than v-for

<template v-if="list.length" v-for="item in list" :key="item.id">
  <div>...</div>
</template>

Functional Components

In Vue 2, functional components had two primary use cases:

  • to return multiple root nodes
  • as a performance optimization, because they initialized much faster than stateful components
  • to create dynamic components

However, in Vue 3, the performance of stateful components has improved to the point that the difference is negligible. In addition, stateful components now also include the ability to return multiple root nodes. 

<template functional>
  <component
    :is="`h${props.level}`"
    v-bind="attrs"
    v-on="listeners"
  />
</template>

<script>
export default {
  props: ['level']
}
</script>

Functional Components

In 3.x, the performance difference between stateful and functional components has been drastically reduced and will be insignificant in most use cases. As a result, the migration path for developers using functional on SFCs is to remove the attribute and rename all references of props to $props and attrs to $attrs.

<template>
  <component
    v-bind:is="`h${$props.level}`"
    v-bind="$attrs"
  />
</template>

<script>
export default {
  props: ['level']
}
</script>

Functional Components

Now in Vue 3, all functional components are created with a plain function. In other words, there is no need to define the { functional: true } component option.

They will receive two arguments: props and context. The context argument is an object that contains a component's attrs, slots, and emit properties.

In addition, rather than implicitly provide h in a render function, h is now imported globally.

import { h } from 'vue'

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

DynamicHeading.props = ['level']

export default DynamicHeading

Async Components

const asyncPage = () => import('./NextPage.vue')
const asyncPage = {
  component: () => import('./NextPage.vue'),
  delay: 200,
  timeout: 3000,
  error: ErrorComponent,
  loading: LoadingComponent
}

Vue 2.x

Previously, async components were created by simply defining a component as a function that returned a promise.

Async Components

Now, in Vue 3, since functional components are defined as pure functions, async components definitions need to be explicitly defined by wrapping it in a new defineAsyncComponent helper

const asyncPage = () => import('./NextPage.vue')
const asyncPage = {
  component: () => import('./NextPage.vue'),
  delay: 200,
  timeout: 3000,
  error: ErrorComponent,
  loading: LoadingComponent
}

Vue 2.x

import { defineAsyncComponent } from 'vue'

// Async component without options
const asyncPage = defineAsyncComponent(() => import('./NextPage.vue'))

// Async component with options
const asyncPageWithOptions = defineAsyncComponent({
  loader: () => import('./NextPage.vue'),
  delay: 200,
  timeout: 3000,
  errorComponent: ErrorComponent,
  loadingComponent: LoadingComponent
})

Vue 3.x

Scoped Slots

this.$scopedSlots removed and they are now unified into the $slots option.

 

$attrs changes

In Vue 2, you can access attributes passed to your components with this.$attrs, and event listeners with this.$listeners. In combination with inheritAttrs: false, they allow the developer to apply these attributes and listeners to some other element instead of the root element.

<template>
  <label>
    <input type="text" v-bind="$attrs" v-on="$listeners" />
  </label>
</template>
<script>
  export default {
    inheritAttrs: false
  }
</script>

In Vue 3, event listeners are now just attributes, prefixed with on, and as such are part of the $attrs object, so $listeners has been removed.

<template>
  <label>
    <input type="text" v-bind="$attrs" />
  </label>
</template>
<script>
export default {
  inheritAttrs: false
}
</script>

Vue 2.x

Vue 3.x

$attrs changes

In fact $attrs now contains all attributes passed to a component, including class and style.

 

<template>
  <label>
    <input type="text" v-bind="$attrs" />
  </label>
</template>
<script>
export default {
  inheritAttrs: false
}
</script>

Vue 3.x

<my-component id="my-id" class="my-class"></my-component>
<label>
  <input type="text" id="my-id" class="my-class" />
</label>

Component lifecycle hooks

<script>
export default {
  beforeDestroy() {
    console.log('beforeDestroy has been called')
  }
  destroyed() {
    console.log('destroyed has been called')
  }
}
</script>

Vue 2.x

Vue 3.x

The beforeDestroy  & destroyed lifecycle hooks has been renamed to beforeUnmount & unmounted

 

<script>
export default {
  beforeUnmount() {
    console.log('beforeUnmount has been called')
  }
  unmounted() {
    console.log('unmounted has been called')
  }
}
</script>

Custom Directives

Vue 2.x

Vue 3.x

The hook functions for directives have been renamed to better align with the component lifecycle.

  • bind - Occurs once the directive is bound to the element. Occurs only once.
  • inserted - Occurs once the element is inserted into the parent DOM.
  • update - This hook is called when the element updates, but children haven't been updated yet.
  • componentUpdated - This hook is called once the component and the children have been updated.
  • unbind - This hook is called once the directive is removed. Also called only once.
  • created - new! This is called before the element's attributes or event listeners are applied.
  • bind → beforeMount
    inserted → mounted
  • beforeUpdate: new! This is called before the element itself is updated, much like the component lifecycle hooks.
  • update → removed! There were too many similarities to updated, so this is redundant. Please use updated instead.
  • componentUpdated → updated
    beforeUnmount: new! Similar to component lifecycle hooks, this will be called right before an element is unmounted.
  • unbind unmounted

Custom Directives

Vue 2.x

The hook functions for directives have been renamed to better align with the component lifecycle.

created
bind beforeMount
inserted mounted
beforeUpdate
update
componentUpdated updated
beforeUnmount
unbind unmounted

Vue 3.x

Transition Class Change

.v-enter,
.v-leave-to {
  opacity: 0;
}

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

.v-leave-from,
.v-enter-to {
  opacity: 1;
}

Vue 2.x

Vue 3.x

The v-enter transition class has been renamed to v-enter-from and

the v-leave transition class has been renamed to v-leave-from.

.v-enter

.v-enter-active

.v-enter-to

.v-enter-from

.v-enter-active

.v-enter-to

Events API

In 2.x, Vue instance could be used create global event listeners used across the whole application via the event emitter API ($on, $off and $once)

// eventBus.js
import Vue from 'vue'
const eventBus = new Vue()
export default eventBus
// ChildComponent.vue
import eventBus from './eventBus'

export default {
  mounted() {
    // adding eventBus listener
    eventBus.$on('custom-event', () => {
      console.log('Custom event triggered!')
    })
  },
  beforeDestroy() {
    // removing eventBus listener
    eventBus.$off('custom-event')
  }
}
// ParentComponent.vue
import eventBus from './eventBus'

export default {
  methods: {
    callGlobalCustomEvent() {
      eventBus.$emit('custom-event') 
    }
  }
}

In 3.x, $on, $off and $once instance methods are removed. Application instances no longer implement the event emitter interface. Functionality should be replaced by a 3rd party library like mitt or tiny-emitter

Filters

<template>
  <h1>Bank Account Balance</h1>
  <p>{{ accountBalance | currencyUSD }}</p>
</template>

<script>
  export default {
    props: {
      accountBalance: {
        type: Number,
        required: true
      }
    },
    filters: {
      currencyUSD(value) {
        return '$' + value
      }
    }
  }
</script>
<template>
  <h1>Bank Account Balance</h1>
  <p>{{ accountInUSD }}</p>
</template>

<script>
  export default {
    props: {
      accountBalance: {
        type: Number,
        required: true
      }
    },
    computed: {
      accountInUSD() {
        return '$' + this.accountBalance
      }
    }
  }
</script>

Vue 2.x

Vue 3.x

Filters are removed from Vue 3.0 and no longer supported.

They should be replaced with computed properties or methods.

Composition API

Composition API

// src/components/UserRepositories.vue
export default {
  components: { RepositoriesFilters, RepositoriesSortBy, RepositoriesList },
  props: {
    user: {
      type: String,
      required: true
    }
  },
  data () {
    return {
      repositories: [], 
      searchQuery: ''.,
      filters: { ... },
    }
  },
  computed: {
    filteredRepositories () { ... }, 
    repositoriesMatchingSearchQuery () { ... },
  },
  watch: {
    user: 'getUserRepositories'
  },
  methods: {
    getUserRepositories () {
      // using `this.user` to fetch user repositories
    }, 
    updateFilters () { ... }, 
  },
  mounted () {
    this.getUserRepositories()
  }
}
// ConsumingComponent.js
import MyMixin from "./MyMixin.js";

export default {
  mixins: [MyMixin],
  data: () => ({
    myLocalDataProperty: null
  }),
  methods: {
    myLocalMethod () { ... }
  }
}
// MyMixin.js
export default {
  data: () => ({
    mySharedDataProperty: null
  }),
  methods: {
    mySharedMethod () { ... }
  }
}
export default {
  data: () => ({
    mySharedDataProperty: null
    myLocalDataProperty: null
  }),
  methods: {
    mySharedMethod () { ... },
    myLocalMethod () { ... }
  }
}

Composition API

Composition API

Options API

Composition API

//Counter.vue
export default {
  data() {
    return {
      count: 0
    }
  },
  computed: {
    double () {
      return this.count * 2;
    }
  },
  methods: {
    increment() {
      this.count++;
    }
  }
}
// Counter.vue
import { ref, computed } from "vue";

export default {
  setup() {
    const count = ref(0);
    const double = computed(() => count.value * 2)
    function increment() {
      count.value++;
    }
    return {
      count,
      double,
      increment
    }
  }
}

Composition API

// useCounter.js
import { ref, computed } from "vue";

export default function () {
  const count = ref(0);
  const double = computed(() => count.value * 2)
  function increment() {
    count.value++;
  }
  return {
    count,
    double,
    increment
  }
}
// Counter.vue
import { ref, computed } from "vue";

export default {
  setup() {
    const count = ref(0);
    const double = computed(() => count.value * 2)
    function increment() {
      count.value++;
    }
    return {
      count,
      double,
      increment
    }
  }
}

Composition API

Composition Function

Composition API

// useCounter.js
import { ref, computed } from "vue";

export default function () {
  const count = ref(0);
  const double = computed(() => count.value * 2)
  function increment() {
    count.value++;
  }
  return {
    count,
    double,
    increment
  }
}
// MyComponent.js
import useCounter from "./useCounter.js";

export default {
  setup() {
    const { count, double, increment } = useCounter();
    return {
      count,
      double,
      increment
    }
  }
}

Composition API

Composition Function

Composition API

// useCounter.js
import { ref, computed } from "vue";

export default function () {
  const count = ref(0);
  const double = computed(() => count.value * 2)
  function increment() {
    count.value++;
  }
  return {
    count,
    double,
    increment
  }
}
// MyComponent.js
import useCounter from "./useCounter.js";

export default {
  setup() {
    return {
     ...useCounter()
    }
  }
}

Composition API

Composition Function

Composition API

import { ref, watch } from 'vue'

const counter = ref(0)
watch(counter, (newValue, oldValue) => {
  console.log('The new counter value is: ' + counter.value)
})

Reacting to Changes with watch

 

import { fetchData } from '@/api'
import { ref, onMounted } from 'vue'

// in our component
setup (props) {
  const todos = ref([])
  const getData = async () => {
    todos.value = await fetchData(props.user)
  }

  onMounted(getData)

  return {
    todos
  }
}

Lifecycle hooks

Composition API

  • Less & Cleaner code
  • Flexibility
  • Built with functions
  • Better typescript support
  • Tooling friendly
  • Learning curve
  • Two ways of doing the same thing

Supporting Libraries

Vue CLI - As of v4.5.0, vue-cli now provides the built-in option to choose Vue 3 when creating a new project. You can upgrade vue-cli and run vue create to create a Vue 3 project today.

 

Vuex - Vuex 4.0 provides Vue 3 support with largely the same API as 3.x. The only breaking change is how the plugin is installed.

 

Vue Router - Vue Router 4.0 provides Vue 3 support and has a number of breaking changes of its own.
 

Devtools Extension - The new version is currently in beta and only supports Vue 3 (for now). Vuex and Router integration is also work in progress.

Supporting Libraries

Vetur - VSCode official extension Vetur provides comprehensive IDE support for Vue 3.

 

Ionic Vue - Ionic Vue is built on top of Vue 3.x If you've built an app with early versions of Ionic Vue, you'll want to upgrade to the latest release and upgrade your Vue dependencies.

 

Browser Compatibility

IE11 dropped from the main bundle.

Need to add an extra polyfill for proxies in order to support IE

Migration guide

  1. Learn about new features and breaking changes 
  2. Replace deprecated features when possible (Event bus, filters)
  3. Upgrade to Vue 2.7 * 
  4. Refactor every breaking change to work the Vue 3 way
  5. Upgrade to Vue 3 🥳🥳🥳

* Vue 2.7

This version will have deprecation warnings for every feature that is not compatible with Vue 3 and will guide you with documentation links on how to handle every case.

Additional resources

Conclusion

Made with Slides.com