Vue JS 3  - The Practical Guide

Learn about most of the new features introduced in Vue 3 JS practical way!

TODO: provide alt

Useful Links

Watch free Vue 3 course on: https://academy.eincode.com

Github repository with starting project: https://github.com/Jerga99/vue-3-updates

We are going to cover:

  1. Global Api
  2. Composition
  3. Data option
  4. Root nodes
  5. Filters
  6. Suspense
  7. Reactivity
  8. v-models
  9. Teleport
  10. vue router

Global API

Let’s start in the entry point of our application, main.js file. In Vue 2 you were used to mount Vue component like this:

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

in Vue 3 mounting looks like this:

import { createApp } from 'vue'
import App from './App.vue'

const app = createApp(App)

app.mount('#app')
main.js

Why this change ? If you would create multiple Vue instances in the old version, then all application instances would share the same global configuration.

// this affects both root instances
Vue.mixin({
  /* ... */
})
// this affects both root instances
Vue.directive('...', {
  ...
})
const app1 = new Vue({ el: '#app-1' })
const app2 = new Vue({ el: '#app-2' })

That’s not the case for Vue 3. Now you can create multiple instance each with the separate configuration.

Composition API

Inspired by React, Vue 3 is introducing composition api / “hook” functions. Vue components themself are very flexible and re-usable. With the use of new api you can go one step further. Composition API can be very beneficial in the larger applications. I will illustrate it on simple example.

First traditional way, a simple component that fetches and displays some data:

<script>
import { fetchResources } from '@/actions'
import ResourceDetail from '@/components/ResourceDetail'
import ResourceList from '@/components/ResourceList'
export default {
  components: {
    ResourceDetail,
    ResourceList,
  },
  data() {
    return {
      title: 'Your resources',
      selectedResource: null,
      resources: []
    }
  },
  async created() {
    this.resources = await fetchResources()
  },
  computed: {
    hasResources() {
      return this.resourceCount > 0
    },
    activeResource() {
      return this.selectedResource || (this.hasResources && this.resources[0]) || null
    },
    resourceCount(){
      return this.resources.length
    }
  },
  methods: {
    selectResource(resource) {
      this.selectedResource = {...resource}
    }
  }
}
</script>
ResourceApp.vue

You can check the whole code here: Github

Let’s use composition API now, first check the code then the explanation.

import { ref, onMounted, computed } from 'vue'
import { fetchResources } from '@/actions'

export default function useResources() {
  const resources = ref([])
  const getResources = async () => resources.value = await fetchResources()

  onMounted(getResources);

  const resourceCount = computed(() => resources.value.length)
  const hasResources = computed(() => resourceCount.value > 0 )

  return {
    resources,
    resourceCount,
    hasResources
  }
}
useResources.js

Here you can see very simple composition function responsible for fetching resources data. Composition functions usually start with the “use” keyword so developers will know it’s not just an ordinary function.

  1. “ref” will create a reactive object. If you want to get a raw value from the ref, you need to access it under the “value” property. See following example:

var a = 7;
var b = a;
b = 10;
// a = 7
// b = 10

var a = ref(7);
var b = a;
b.value = 100;
// a = 100
// b = 100

That's why I have created resources array as a ref. If an item from the array gets added or removed we want to reflect these changes in our application.

This image from Vue Docs is worth thousand of words, when using “ref” you are passing by reference.

2. getResources will just fetch the data and assign it to resources

3. onMounted lifecycle function will be called when component will be added to the DOM.

4. computed properties are evaluated when their dependencies(resources or resourceCount) will change

5. In the last step we will return data/functions we want to expose to a component that will use useResources hook function.

Now is the time to hook-in the composition function.

<script>
import ResourceDetail from '@/components/ResourceDetail'
import ResourceList from '@/components/ResourceList'
import useResources from '@/composition/useResources';
export default {
  components: {
    ResourceDetail,
    ResourceList,
  },
  data() {
    return {
      title: 'Your resources',
      selectedResource: null
    }
  },
  setup() {
    return {
      ...useResources()
    }
  },
  computed: {
    activeResource() {
      return this.selectedResource || (this.hasResources && this.resources[0]) || null
    }
  },
  methods: {
    selectResource(resource) {
      this.selectedResource = {...resource}
    }
  }
}
</script>
Resources.vue

As you can see here I am calling useResources in the new “setup” function. Whatever returned in the setup will get into your component “this” context.

That’s why we can access all computed properties and resources data as before. Now they are provided from return of useResources function.

The new setup component option is executed before the component is created, once the props are resolved, and serves as the entry point for composition API's.
Because the component instance is not yet created when setup is executed, there is no this inside a setup option.

Isn’t that amazing ? I have just separated fetching of data into it’s own hook function and we can go even further.

Code: Github

Let’s create now search functionality.

import { ref, computed } from 'vue'

export default function useSearchResource(resources) {
  const searchQuery = ref('')

  const setSearchQuery = searched => {
    searchQuery.value = searched
  }

  const searchedResources = computed(() => {
    if (!searchQuery.value) {
      return resources.value
    }

    const lcSearch = searchQuery.value.toLocaleLowerCase();

    return resources.value.filter(r => {
      const title = r?.title.toLocaleLowerCase()
      return title.includes(lcSearch)
    })
  })

  return {
    setSearchQuery,
    searchedResources
  }
}
useSearch.js
export default function useResources() {
  const resources = ref([])
  const getResources = async () => resources.value = await fetchResources()

  onMounted(getResources);

  const resourceCount = computed(() => resources.value.length)
  const hasResources = computed(() => resourceCount.value > 0 )

  const { searchedResources, setSearchQuery } = useSearchResources(resources)

  return {
    resources: searchedResources,
    resourceCount,
    hasResources,
    setSearchQuery
  }
}
useResources.js

Let’s break down useSearch.

  1. searchQuery contains an empty string value, ref is used so computed property searchedResources can react to changes of searchQuery string
  2. setSearchQuery is a simple function that assigns a searched value to searchQuery
  3. searchedResources is executed whenever value of searchQuery or resources will change
  4. searchedResources responsibility is filtering resources. Every resource contains title, if searchedQuery string is included in the resource title, then resource is added to searchedResources array.
  5. both setSearchQuery and searchedResourced are returned from the function.
  6. useSearchResources is executed from inside of useResources, values of setSearchQuery and searchedResources are taken and returned so they can be used in the component.

Now you could see how easy is to plug-in hook function without polluting component code.

Only thing we need to do now is to implement input to get a search value and assign it to searchedQuery.

<template>
...
<input
  @keyup="handleSearch"
  type="text"
  class="form-control"
  placeholder="Some title" />
...
</template>
<script>
...
methods: {
  ...
  handleSearch(e) {
    this.setSearchQuery(e.target.value)
  }
}
...
</script>
Resources.vue

And that’s it! Now, whenever setSearchQuery is executed and value of searchQuery is changed then the searchedResources are re-executed and filtered out.

To get more informations watch the full course on: https://academy.eincode.com/

& Code: Github

That’s the new composition API, now let’s take a look on other features.

Data Option

In Vue 2 you could define data option with either an object or a function. In Vue 3 it will be possible only with a function. This way it gets standardized.

<script>
  export default {
    data() {
      return {
        someData: '1234'
      }
    }
  }
</script>
index.vue

Filters removed

There is no longer possible to write “pipes” into templates.

<h1>{{title | capitalized }} </h1>

Such an expression is not a valid Javascript and it requires additional implementation cost on Vue side. Following expression is very easily transformed in a computed property or a function.

computed: {
 capitalizedTitle() {
   return title[0].toUpperCase + title.slice(1);
  }
}

Multiple root nodes

In Vue 2 you had to have always one root node wrapping the rest of other nodes.

<template>
  <div>
    <h1>...</h1>
    <div class="container">...</div>
    ...
  </div>
</template>

in Vue 3 that’s not the case.

<template>
  <h1>...</h1>
  <div class="container">...</div>
  ...
</template>

Suspense

Suspense is a special Vue component used for resolving components depending on asynchronous data.

First, generic example:

<Suspense>
  <template #default>
    <AsyncComponent>
  </template>
  <template #fallback>
    Loading Data...
  </template>
</Suspense>

with a new composition api, a setup can be set as an async function. Suspense is displaying fallback template until setup is resolved.

Let’s see it on the specific example

<template>
  Welcome, {{user.name}}
</template>
<script>
  import { fetchUser } from '@/actions';
  export default {
    async setup() {
      const user = await fetchUser();
      return { user }
    }
  }
</script>
UserPanel.vue

Then in wrapper component:

<Suspense>
  <template #default>
    <UserPanel/>
  </template>
  <template #fallback>
    Loading user ...
  </template>
</Suspense>
Header.vue

Code: Github

Reactivity

Reactivity in Vue 3 has been updated rapidly . There is no need to use Vue.set or Vue.delete any longer. To achieve reactivity you can simply use native functions to manipulate arrays and objects.

// in composition API
const resources = ref([])
const addResource = newResource =>      resources.value.unshift(newResource)
const removeResource = atIndex => resources.value.splice(atIndex ,1)
const reactiveUser = ref({name: 'Filip'})
const changeName = () => reactiveUser.value.name = 'John'

in the classical component:

<script>
export default {
  data() {
    return {
      resources: [1,2,3],
      person: { name: 'Filip' }
    }
  }
  methods: {
    addResource(newResource) {
      this.resources.unshift(newResource)
    },
    removeResource(atIndex) {
      this.resources.splice(atIndex ,1)
    },
    changeName(newResource) {
      this.person.name = 'John'
    }
  }
}
</script>
As you can see here to add an item we can use array function push/unshift
To remove an item use splice
To access and change an item property use dot notation

All of the changes will be reactive.

Code: Github

Multiple v-models

Now you are able to use multiple v-models on a custom component:

<ChildComponent v-model:prop1="prop1" v-model:prop2="prop2"/>

is shorthand for:

<ChildComponent
  :prop1="prop1"
  @update:prop1="prop1 = $event"
  :prop2="prop2"
  @update:prop2="prop2 = $event"
/>

Now in specific example

<resource-form
  v-model:title="title"
  v-model:description="description"
  v-model:type="type"
  v-model:link="link"
  @on-form-submit="submitForm"
/>
ResourceNew.vue

and in the form component:

<template>
  <form>
    <div class="mb-3">
      <label htmlFor="title">Title</label>
      <input
        :value="title"
        @input="changeTitle($event.target.value)"
        type="text" />
    </div>
    <div class="mb-3">
      <select
        :value="type"
        @change="changeType($event.target.value)">
        <option
          v-for="type in types"
          :key="type"
          :value="type">{{type}}</option>
      </select>
    </div>
    <button
      @click="submitForm"
      class="btn btn-primary btn-lg btn-block"
      type="button">
      Submit
    </button>
  </form>
</template>
export default {
  props: {
    title: String,
    description: String,
    link: String,
    type: String,
  },
  data() {
    return {
      types: ['blog', 'book', 'video']
    }
  },
  methods: {
    submitForm() {
      this.$emit('on-form-submit');
    },
    changeTitle(title) {
      this.$emit('update:title', title)
    },
    changeType(type) {
      this.$emit('update:type', type)
    }
    ...
  }
}
ResourceForm.vue

We are handling input changes and emitting the events to update the values in the parent component. Don’t forget to emit the event in format: “update:prop1” because <ChildComponent v-model:prop1="prop1" /> is transformed to <ChildComponent v-model:prop1="prop1" @update:prop1="prop1 = $event" /> . In previous example you could see that even handlers update:title, update:type… are automatically provided.

Code : Github

Teleport

Provides the way how to render parts of the template outside of current context.

To “teleport” the content we need to use teleport component and wrap the content inside.

<teleport to="#teleportContent">
  <div class="teleport-body">I am Teleported!</div>
</teleport>

this content will be “teleported” into the node with id of “teleportContent

<div id="teleportContent"></div>

only condition is that a node where we are teleporting to exists before content wrapped in the teleport is defined.

we can target id, data, class

<!-- ok -->
<teleport to="#some-id" />
<teleport to=".some-class" />
<teleport to="[data-teleport]" />

<!-- Wrong -->
<teleport to="h1" />
<teleport to="some-string" />

Vue Router

Same as Vue also Vue router has been updated. Let’s see the main changes:

In Vue 2:

import Vue from 'vue'
import VueRouter from 'vue-router';

import SomePage1 from './pages/SomePage1';
import SomePage2 from './pages/SomePage2';

Vue.use(VueRouter);

const routes = [
  { path: '/', redirect: '/path' },
  { path: '/path', name: 'HomePage', component: SomePage1 },
  { path: '/some/path', component: SomePage2 }
]

const router = new VueRouter({
  mode: 'history',
  linkExactActiveClass: 'active',
  routes
})

export default router;
router.js

then in main.js

import router from './router'; 
new Vue({render: h => h(App),  
  router,  
  components: { App }})
.$mount('#app')
main.js

in Vue 3:

import { createRouter, createWebHistory } from 'vue-router'
import SomePage1 from './pages/SomePage1'
import SomePage2 from './pages/SomePage2'

const routes = [
  { path: '/', redirect: {name: 'HomePage'}},
  { path: '/path', name: 'HomePage', component: SomePage1 },
  { path: '/some/path', component: SomePage2 }
]

const router = createRouter({
  history: createWebHistory(),
  linkExactActiveClass: 'active',
  routes
})

export default router;
router.js
import router from './router'
const app = createApp(App)
app.use(router)
app.mount('#app')
main.js

Code: Github

We have just covered some of the main Vue 3 features. To see the full list of updates visit official Vue documentation.

To watch the full Vue 3 course covering this and other features visit my academy page: https://academy.eincode.com

Best of luck!

Filip