Vue

Vue

Vue 3

Refs

  1. Composition API - An animated explanation
  2. Vue2 to Vue3 — What’s changed?
  3. Why vite ?
  4. A definitive guide to Vue 3 components
  5. ViteJS
  6. What is the difference between “vite” and “vite preview”?

Vue 3 VS Vue 2

Composition API vs Options API

Composition API vs Options API
Options API is function concerned.
Composition API is logic concerned.
For complex components, code of same logic may be scattered in props, data, methods, mounted, which makes it hard to maintain. So in Vue 3, Composition API is introduced, code of same logic is put together in setup.
See which to choose.

createApp vs vue instance

In Vue 2, we mount a App.vue instance to #app:

1
2
3
new Vue({
store, router, render: h => h(App)
}).$mount('#app')

In Vue 3, we createApp from a App.vue and mount to #app:

1
createApp(App).use(store).use(router).mount('#app')

Why use createApp over new Vue ?

In Vue 2, any directives created using Vue object will be usable by all application instances. This becomes a problem when our web app has multiple Vue application instances but we want to limit certain functionality to specific instances.

1
2
3
4
5
6
7
8
// The only way to create a directive in Vue 2
Vue.directive('directive', {
// ...
});

// Both of the application instances can access the directive
const appOne = new Vue(App1).mount('#app1');
const appTwo = new Vue(App2).mount('#app2');

Vue 3 solves this problem by creating directives on app instance instead of Vue object:

1
2
3
4
5
6
7
8
9
10
11
12
// Both of the application instances can access the directive
const appOne = Vue.createApp(App1);
appOne.directive('directive', {
// only availalble "appOne" instance */
});
appOne.mount('#app1');

const appTwo = Vue.createApp(App2);
appTwo.directive('directive', {
// only availalble to "appTwo"
});
appTwo.mount('#app2');

Vite

Vue 3’s scaffolding tool migrated from vue-cli to ‘create-vue’, which is based on vite and has a much faster building speed than webpack. See why vite is much faster

For more information about vite, check out my post named Vite on this blog.

Vue 3 features

Composition API

  • Difference with Options API already explained above.
  • Use <script setup> or <script>setup() to indicate Composition API. Difference between <script setup> and <script>setup() is that no return is required in <script setup> to pass objects to template.
  • A typical <script setup> SFC(Single File Component) goes here:
    script_setup_SFC

FAQ

ref() vs reactive()

  • reference:
    ref vs reactive in Vue 3
    Reactivity Core

  • Usage
    ref: Returns a deep reactive mutable object, point to inner value with .value. If an object assigned, reactive() is called. Use shallowRef() to avoid deep conversion.
    reactive: Returns a deep reactive proxy of the object.

  • Example

ref:

1
2
3
4
5
const count = ref(0)
console.log(count.value) // 0

count.value++
console.log(count.value) // 1

reactive:

1
2
const obj = reactive({ count: 0 })
obj.count++

Key Points

  • reactive() only takes objects, NOT JS primitives
  • ref() is calling reactive() behind the scenes, objects work for both
  • BUT, ref() has a .value property for reassigning, reactive() does not have this and therefore CANNOT be reassigned

Use

ref() when..

  • it’s a primitive
  • it’s an object you need to later reassign (like an array - more info here)

reactive() when..

  • it’s an object you don’t need to reassign, and you want to avoid the overhead of ref()

In Summary

ref() seems like the way to go since it supports all object types and allows reassigning with .value. ref() is a good place to start, but as you get used to the API, know that reactive() has less overhead, and you may find it better meets your needs.

ref() Use-Case

You’ll always use ref() for primitives, but ref() is good for objects that need to be reassigned, like an array.

1
2
3
4
5
6
7
setup() {
const blogPosts = ref([]);
return { blogPosts };
}
getBlogPosts() {
this.blogPosts.value = await fetchBlogPosts();
}

The above with reactive() would require reassigning a property instead of the whole object.

1
2
3
4
5
6
7
setup() {
const blog = reactive({ posts: [] });
return { blog };
}
getBlogPosts() {
this.blog.posts = await fetchBlogPosts();
}

reactive() Use-Case

A good use-case for reactive() is a group of primitives that belong together:

1
2
3
4
5
const person = reactive({
name: 'Albert',
age: 30,
isNinja: true,
});

the code above feels more logical than

1
2
3
const name = ref('Albert');
const age = ref(30);
const isNinja = ref(true);

If you’re still lost, this simple guide helped me: https://www.danvega.dev/blog/2020/02/12/vue3-ref-vs-reactive/

An argument for only ever using ref(): https://dev.to/ycmjason/thought-on-vue-3-composition-api-reactive-considered-harmful-j8c

The decision-making behind why reactive() and ref() exist as they do and other great information, the Vue Composition API RFC: https://vuejs.org/guide/extras/composition-api-faq.html#why-composition-api

ref unwrap

reference:
Reactivity Core

Vue Cli Plugins and Presets

reference: Plugins and Presets

  • What are plugins ?
    Plugins modify the internal webpack configuration and inject commands to vue-cli-service.

Most of the features listed during the project creation process are implemented as plugins.

If you inspect a newly created project’s package.json, you will find dependencies that start with @vue/cli-plugin-. These are plugins.

  • Add a plugin to an existing project
    Use vue add [plugin name] to add a plugin to an existing project.
    For example, use vue add eslint to add eslint linter to the project.

  • What is a Vue Cli preset ?
    A JSON object that contains pre-defined options and plugins for creating a new project.

Example:

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"useConfigFiles": true,
"cssPreprocessor": "sass",
"plugins": {
"@vue/cli-plugin-babel": {},
"@vue/cli-plugin-eslint": {
"config": "airbnb",
"lintOn": ["save", "commit"]
},
"@vue/cli-plugin-router": {},
"@vue/cli-plugin-vuex": {}
}
}

How to get query string in vue 3 ?

  • In vue 2, we got this.$route.query to get query string.
  • In vue 3, first import { useRoute } from 'vue-router', then useRoute().query to get query string.

Useful commands

  • Use vue ui to start a ui interface inside a project created by vue-cli.
  • Use vue add typescript to add typescript plugin to a vue-cli project and transform it to a typescript project.

Vue Component Communication

Ref

Component communication has three forms:

  1. Parent -> Child
  2. Child -> Parent
  3. Global
  • Parent send messages to child through props. Any change to prop is reflected immediately. Prop is immutable in child so vice versa not viable. This is a one-way communication.

Vue Plugin

  • Plugin use case: global methods/properties/assets(directives/filters/transitions)/mixin/Vue instance/library

Vue 2

Reference

1
2
3
4
5
6
// main.js
Vue.use(MyPlugin, { someOption: true }) // calls MyPlugin.install

new Vue({
//... options
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// MyPlugin.js
export default {
install: (Vue, options) {
// 1. add global method or property
Vue.myGlobalMethod = function () {
// some logic ...
}

// 2. add a global asset
Vue.directive('my-directive', {
bind (el, binding, vnode, oldVnode) {
// some logic ...
}
...
})

// 3. inject some component options
Vue.mixin({
created: function () {
// some logic ...
}
...
})

// 4. add an instance method
Vue.prototype.$myMethod = function (methodOptions) {
// some logic ...
}
}
}

Vue 3

Ref

1
<h1>{{ $translate('greetings.hello') }}</h1>
1
2
3
4
5
6
7
8
9
10
11
// main.js
import { createApp } from 'vue'
import i18nPlugin from './plugins/i18n'

const app = createApp({})

app.use(i18nPlugin, {
greetings: {
hello: 'Bonjour!'
}
})
1
2
3
4
5
6
7
8
9
10
11
12
13
// plugins/i18n.js
export default {
install: (app, options) => {
// inject a globally available $translate() method
app.config.globalProperties.$translate = (key) => {
// retrieve a nested property in `options`
// using `key` as the path
return key.split('.').reduce((o, i) => {
if (o) return o[i]
}, options)
}
}
}

Vuex

  • A global state manage with reactive props.
  • Note:
    • Do not change props directly. Commit mutations otherwise changes will not be seen.

Vuex workflow

Vuex workflow

State

  • Map state in computed.
1
2
3
4
5
6
7
8
9
10
11
// store.js
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export default new Vuex.Store({
state: {
count: 100
}
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<!-- component.vue -->
<script>
export default {
// 1. map with count
computed: {
count () {
return this.$store.state.count
}
}
// 2. map with mapState
computed: mapState({
// arrow functions can make the code very succinct!
count: state => state.count,
// passing the string value 'count' is same as `state => state.count`
countAlias: 'count',
// to access local state with `this`, a normal function must be used
countPlusLocalState (state) {
return state.count + this.localCount
}
})
// 3. mapState and name short
computed: mapState([
// map this.count to store.state.count
'count'
])

// 4. mapState and local count methods merged
computed: {
localCount () {},
...mapState([
// map this.count to store.state.count
'count'
])
}
}
</script>

Getters

  • getters is a computed property for global state.
  • getters are called without brackets.
  • getters are also mapped in computed.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// store.js
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export default new Vuex.Store({
state: {
count: 100,
todos: [
{ id: 1, text: 'Todo 1', done: true },
{ id: 2, text: 'Todo 2', done: false }
]
},
getters: {
doneTodos: (state) => state.todos.filter(todo => todo.done),
doneTodosLength: (state, getters) => getters.doneTodos.length,
// method style getter:
getTodoById: (state) => id => state.todos.find(todo => todo.id == id)
},
mutations: {
},
actions: {
},
modules: {
}
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
<!-- component.vue -->
<template>
<div id="app">
<p>{{ $store.getters.doneTodos }}</p>
<p>{{ $store.getters.doneTodosLength }}</p>
<p>{{ doneTodos1 }}</p>
<p>{{ $store.getters.getTodoById(2) }}</p>
<p>{{ doneTodos }}</p>
<p>{{ doneTodosLength }}</p>
<p>{{ doneCount }}</p>
</div>
</template>

<script>
import { mapGetters } from 'vuex'
export default {
data () {
return {
localCount: 1
}
},
computed: {
doneTodos1 () {
return this.$store.getters.doneTodos
},
// mix the getters into computed with object spread operator
...mapGetters([
'doneTodos',
'doneTodosLength'
]),
...mapGetters({
// map with a different name
doneCount: 'doneTodosLength'
})
}

}
</script>

<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>

Mutations

  • Mutation is the only way to change state.
  • Mutation must be synchronous.

If we have a mutation like this:

1
2
3
4
5
6
7
mutations: {
someMutation (state) {
api.callAsyncMethod(() => {
state.count++
})
}
}

Now imagine we are debugging the app and looking at the devtool’s mutation logs. For every mutation logged, the devtool will need to capture a “before” and “after” snapshots of the state. However, the asynchronous callback inside the example mutation above makes that impossible: the callback is not called yet when the mutation is committed, and there’s no way for the devtool to know when the callback will actually be called - any state mutation performed in the callback is essentially un-trackable!

  • Mutations are mapped in methods.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { mapMutations } from 'vuex'

export default {
// ...
methods: {
...mapMutations([
'increment', // map `this.increment()` to `this.$store.commit('increment')`

// `mapMutations` also supports payloads:
'incrementBy' // map `this.incrementBy(amount)` to `this.$store.commit('incrementBy', amount)`
]),
...mapMutations({
add: 'increment' // map `this.add()` to `this.$store.commit('increment')`
})
}
}

Actions

  • actions do not mutate state directly, they only commit mutations.
  • actions can be asynchronous.
  • actions are mapped in methods.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
// store.js
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export default new Vuex.Store({
state: {
count: 0
},
mutations: {
increment (state) {
state.count++
},
increment1 (state, payload) {
state.count += payload.amount
}
},
actions: {
increment (context) {
context.commit('increment')
},
// simpler:
increment1 ({ commit }) {
commit('increment')
},
// with async
increment2 ({ commit }) {
setTimeout(() => {
commit('increment')
}, 1000)
},
// with payload
increment3 ({ commit }, payload) {
commit('increment1', payload)
}
}
})


// more pratical example:
actions: {
checkout ({ commit, state }, products) {
// save the items currently in the cart
const savedCartItems = [...state.cart.added]
// send out checkout request, and optimistically
// clear the cart
commit(types.CHECKOUT_REQUEST)
// the shop API accepts a success callback and a failure callback
shop.buyProducts(
products,
// handle success
() => commit(types.CHECKOUT_SUCCESS),
// handle failure
() => commit(types.CHECKOUT_FAILURE, savedCartItems)
)
}
}

actions: {
actionA ({ commit }) {
return new Promise((resolve, reject) => {
setTimeout(() => {
commit('someMutation')
resolve()
}, 1000)
})
}
}

store.dispatch('actionA').then(() => {
// ...
})

// action calls action
actions: {
// ...
actionB ({ dispatch, commit }) {
return dispatch('actionA').then(() => {
commit('someOtherMutation')
})
}
}

// assuming `getData()` and `getOtherData()` return Promises

actions: {
async actionA ({ commit }) {
commit('gotData', await getData())
},
async actionB ({ dispatch, commit }) {
await dispatch('actionA') // wait for `actionA` to finish
commit('gotOtherData', await getOtherData())
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<!-- component -->

<script>
import { mapActions } from 'vuex'

export default {
methods: {
increment () {
this.$store.dispatch('increment')
// dispatch with a payload
this.$store.dispatch('increment3', {
amount: 10
})

// dispatch with an object
this.$store.dispatch({
type: 'incrementAsync',
amount: 10
})
},
...mapActions(['increment']),

...mapActions({add: 'increment'})
},
mounted () {
this.$store.dispatch('increment3', {
amount: 10
})
this.add()
}
}
</script>

Modules

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// typical mutli module
const moduleA = {
state: () => ({ /*...*/ }),
mutations: { /*...*/ },
actions: { /*...*/ },
getters: { /*...*/ }
}

const moduleB = {
state: () => ({ /*...*/ }),
mutations: { /*...*/ },
actions: { /*...*/ }
}

store.state.a // -> `moduleA`'s state
store.state.b // -> `moduleB`'s state

export default new Vuex.Store({
modules: {
a: moduleA,
b: moduleB
}
})
  • Access root state in module:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

const moduleA = {
state: () => ({ count: 20 }),
mutations: {
increment (state) {
// state is moduleA's local state
state.count++
console.log(this.state.rootCount)
// this refers to global store and this.state.count points to global state count
}
},
actions: {
incrementByRootCount ({ state, commit, rootState}) {
if (rootState.count > state.count) {
commit('sumWithRootCount')
}
}
},
getters: {
sumWithRootCount (state, getters, rootState) {
return state.count + rootState.count
}
}
}

const moduleB = {
state: () => ({ /*...*/ }),
mutations: { /*...*/ },
actions: { /*...*/ }
}

export default new Vuex.Store({
state: {
rootCount: 10
},
modules: {
a: moduleA,
b: moduleB
}
})
// store.state.a -> `moduleA`'s state
// store.state.b -> `moduleB`'s state
  • By default, actions, mutations and getters are all registered under global scope. Calling them may call all the same named correspondent in modules. Be careful not to have same name.
  • Namespacing is introduced to avoid naming conflicts.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)


export default new Vuex.Store({
state: {
countRoot: 200
},
modules: {
modA: {
namespaced: true,
state: {
countA: 100
// state is already namespaced and not affected.
// store.state.modA.countA
},
getters: {
isCountAPositive (state, getters, rootState, rootGetters) {
console.log(rootState.countRoot)
return state.countA > 0
// getters refers to modA's local getters
// rootState and rootGetters refers to root state and getters
// store.getters['modA/isCountAPositive']
}
},
actions: {
addAsyncA ({ dispatch, commit, getters, rootGetters }) {
setTimeout(() => { commit('addA') }, 1000)
// $store.dispatch('modA/addAsyncA') in vue component
// dispatch('someAction') dispatches module action by default
// dispatch('someAction', null, {root: true}) to dispatch a root action
// commit('mutation') to commit a module mutation
// commit('mutation', null, {root: true}) to commit a root mutation
}
},
mutations: {
addA (state) {
state.countA++
}
// ccommit('modA/addA')
},
modules: {
// here goes nested modules
nestedA: {
// if not namespaced, nested modules share namespace with parent
// if namespaced, add nested module namespace name in the same pattern
// like getters['modA/nestedA/getterName']
/* ... */
}
}
},

}
})

Others

Add global property to Vue instance

1
2
3
4
// Vue 2
Vue.prototype.$http = axios.create({ /* ... */ })
// Vue 3
app.config.globalProperties.$http = axios.create({ /* ... */ })

Global css

In main.js file: import './assets/css/main.css'

Scoped css

Ref

By default, styles wrapped by <style> tags are global.

When a <style> tag has the scoped attribute, its CSS will apply to elements of the current component only.

It works by adding a unique data-v attribute to the component:

1
2
3
4
5
6
7
8
9
<style scoped>
.example {
color: red;
}
</style>

<template>
<div class="example">hi</div>
</template>
1
2
3
4
5
6
7
8
9
<style>
.example[data-v-f3f3eg9] {
color: red;
}
</style>

<template>
<div class="example" data-v-f3f3eg9>hi</div>
</template>

created vs mounted

When created, DOM has not yet been mounted. No DOM operation can be done.

Created is generally used for fetching data from backend API and setting it to data properties. But in SSR mounted() hook is not present you need to perform tasks like fetching data in created hook only.

Author

Chendongtian

Posted on

2022-06-27

Updated on

2023-02-20

Licensed under

Comments