Thursday, March 11, 2021

Vue.js - Optimization

Before deploying, we have the following items needing to be done:

1. Test and fix the bugs

2. Refactor our codes following the best practice

3. Tune the performance


Async Components



To deal with performance (page load time), Vue provides 'Async Components' to shorten the waiting time.

We only load components if needed.

Below is an example without using Async Components.

Ex:

<template>
    <the-navigation v-on:set-active-page="setActivePage"></the-navigation>
     <main>
         <component :is="activePage"></component>
    </main>
</template>

<script>
import { defineAsyncComponent } from 'vue';
import TheNavigation from './components/nav/TheNavigation.vue';
import UsersList from './components/users/UsersList.vue';
 import ProductsList from './components/products/ProductsList.vue';

export default {
     components: {
         TheNavigation,
        UsersList,
         ProductsList,
     },
    data() {
         return {
            ...
        };
     },
    provide() {
         return {
             users: this.users,
             products: this.products,
        };
     },
    methods: {
         setActivePage(page) {
             this.activePage = page;
        },
     },
};
</script>


We can adjust it to use Async Component as below:

Ex:

<template>
     <the-navigation v-on:set-active-page="setActivePage"></the-navigation>
     <main>
         <component :is="activePage"></component>
    </main>
</template>

<script>
import { defineAsyncComponent } from 'vue';
import TheNavigation from './components/nav/TheNavigation.vue';
import UsersList from './components/users/UsersList.vue';

export default {
     components: {
         TheNavigation,
         UsersList,

        // Using async component
         ProductsList: defineAsyncComponent(() =>
             import('./components/products/ProductsList.vue')
        ),
     },
    data() {
         return {
            ...
         };
    },
     provide() {
         return {
             users: this.users,
             products: this.products,
        };
     },
    methods: {
         setActivePage(page) {
             this.activePage = page;
        },
     },
};
</script>



Lazy Loading



Also, base on route components, vue-router provides lazy loading to split each route's components into small chunks.

Those chunks will be loaded only if needed. Therefore, it will help to improve the page loading itme.

This is an example without lazy loading.

Ex:
import { createRoutercreateWebHistory } from 'vue-router';
import UsersList from './pages/UsersList.vue';
import ProductsList from './pages/ProductsList.vue';
import ProductDetail from './components/products/ProductDetail.vue';
import NotFound from './pages/NotFound.vue';

const router = createRouter({
     history: createWebHistory(),
    routes: [
         {
             path: '/'redirect: '/users',
        },
         {
             path: '/users',
             component: UsersList,
        },
         {
             path: '/products',
             component: ProductsList,
        },
         {
             name: 'product-detail',
             path: '/products/:id',
             component: ProductDetail,
        },
         {
             path: '/:notFound(.*)'component: NotFound,
        },
     ]
});

export default router


We can adjust it as below.

Ex:

import { createRoutercreateWebHistory } from 'vue-router';
import UsersList from './pages/UsersList.vue';
import NotFound from './pages/NotFound.vue';

const router = createRouter({
     history: createWebHistory(),
     routes: [
         {
             path: '/'redirect: '/users',
        },
         {
             path: '/users',
             component: UsersList,
        },
         {
             path: '/products',
             component: () => import('./pages/ProductsList.vue')
        },
         {
             name: 'product-detail',
             path: '/products/:id',
             component: () => import('./components/products/ProductDetail.vue'),
        },
         {
             path: '/:notFound(.*)'component: NotFound,
        },
     ]
});

export default router


After that, you can open developer tools to monitor network activities.

Then you will see if we click 'product' or 'product-detail' page on the first time, 0.js or 1.js will be downloaded.


Using lint to improve code quality



The easiest way to improve your code quality is to utilize some tools like ES Lint to analyze your code.

Take below as an example, it works per se, but it is not the best practice to write some kind of code like that.

ESLint have defined some rules for us to analyze our code, and provide some feedback.

Ex:

if (false) { }
else {
     console.log("test")
}


Errors:

    Unexpected constant condition no-constant-condition

    Empty block statement no-empty

Wednesday, March 10, 2021

Vue.js - Reactivity

When learning Vue, we are impressed by the magic Vue brings to us.

I am curious that how Vue make it? And what technique it uses?

Vue uses proxy (provided by modern JavaScript) to implement its Reactivity System.

Take the below example to simulate how to update 'displayMessage' in template once there is a change from the 'inputText' of input box.

Ex:


const data = {
    inputText: "",
     displayMessage: "",
}

const handler = {
     set(targetkeyvalue) {
         if (key == "inputText") {
// If inputText has been assigned a new value, copy that value to displayMessage
             target.displayMessage = value
        }
     }
}

const proxy = new Proxy(datahandler)
// Simulate that if there is a change from input element
proxy.inputText = "Good Morning!"
// Log the value of displayMessage to see if it get changed or not
console.log("displayMessage"proxy.displayMessage);


We can watch all props in handler and setup some logic to update other properties (like computed in component).


Virtual DOM



Refer to this:

In short, touching DOM is expansive comparing with performing updates in JavaScript.

Therefore, in order to have the better performance, Vue will create and keep a Virtual DOM which is the JS version copy from the real DOM.

Once there is a change request, then Vue can change Virtual DOM quickly and compare the difference between the real DOM.

After that, Vue will only update some part of UI in real DOM if it is needed.


Template Refs



Below is the normal way we bind data to properties and update relating view.

Ex:

// index.html

    <section id="app">
        <input
            type="text"
            v-model="inputTextValue"
        >
        <button v-on:click="onUpdateBtnClicked">Update</button>
        <p>{{ displayMessage }}</p>
    </section>


// app.js

const app = Vue.createApp({
     data() {
         return {
             inputTextValue: "",
            displayMessage: "Hello World!",
         };
    },
     methods: {
         onUpdateBtnClicked() {
             this.displayMessage = this.inputTextValue;
        },
     },
});

app.mount("#app");


Vue provided another way called 'Template Refs' to accees template directly.

Ex:

// index.html

    <section id="app">
        <input
            type="text"
            ref="inputText"
        >
        <button v-on:click="onUpdateBtnClicked">Update</button>
        <p>{{ displayMessage }}</p>
    </section>


// app.js

const app = Vue.createApp({
    data() {
        return {
             inputTextValue: "",
             displayMessage: "Hello World!",
        };
     },
    methods: {
         onUpdateBtnClicked() {
             this.displayMessage = this.$refs.inputText.value;
        },
     },
});

app.mount("#app");



Vue Lifecycle



Vue provides Lifecyle Hooks to let us able to add some logic during different stages.

For example, your app need to fetch some data from web api during initial stage.

Or you might need to perform some general business logic for each update.

Wednesday, March 3, 2021

Vue.js - Vuex

So far we have learned how to handle data (or we called state) for a single component (via data properties) or multiple components (by Provide and Inject pattern).


Local State:

    Affect one component.

    Ex: component instance properties


Global State:

    Affect multiple components or entire app.

    Ex: authenticate data (JWT)


However, it is really difficult to manage state in global level

    1. You might need to add lots of logic in App.vue

    2. It is hard to track the state change.

    3. It is error-prone for updating state


Then Vuex can help!

    1. Outsourced state management

    2. Predictable state management/flow

    3. Less errors (clearly defined data flow)


Installation



$ npm install vuex@next



Before using Vuex



It is an basic example to use 'properties' for local state. (Without Vuex)

Ex:

// App.vue

<template>
    <the-counter></the-counter>
</template>

<script>
import TheCounter from './components/TheCounter.vue';

export default {
     components: {
         'the-counter': TheCounter,
    },
};
</script>


// components/TheCounter.vue


<template>
<!-- Bind to properties -->
     <h1>{{ counter }}</h1>
     <button v-on:click="increase">Add 1</button>
</template>

<script>
export default {
// Using properties
     data() {
         return {
             counter: 0,
        };
     },
    methods: {
         increase() {
             this.counter++;
        },
     },
};
</script>



Using state



We can use 'createStore' provided by Vuex to create a store for our Vue app.

A 'store' can maintain all the state (data) in Vue application level.

Ex:

// main.js

import { createApp } from 'vue';
import { createStore } from 'vuex';
import App from './App.vue';
// Create store
const store = createStore({
     state() {
         return {
             counter: 0,
        };
     }
});

const app = createApp(App);

// Install plugin
app.use(store);

app.mount('#app');


// components/TheCounter.vue

<template>
<!-- Using state in store -->
    <h1>{{ $store.state.counter }}</h1>
     <button v-on:click="increase">Add 1</button>
</template>

<script>
export default {
     methods: {
         increase() {
// Update state directly
             this.$store.state.counter++;
        },
     },
};
</script>



Using mutations



According to previous example, it is not ideal to change the state of store directly.

Imagine that if your app get bigger and lots of place change the state directly.

Then it is hard to control and track where and why the state got changed.

We should use mutations provided by Vuex to update state.

Ex:

// main.js

const store = createStore({
     state() {
         return {
             counter: 0,
        };
     },
// Define mutations
    mutations: {
         increase(state) {
             state.counter++;
        }
     }
});


// components/TheCounter.vue

<template>
    <h1>{{ $store.state.counter }}</h1>
     <button v-on:click="increase">Add 1</button>
</template>

<script>
export default {
     methods: {
         increase() {
// Send a commit for triggering mutations
             this.$store.commit('increase');
        },
     },
};
</script>



Passing data to mutations is possible



From previous example, we might want to control the increase value when button was clicked.

Refer to this.

Ex:

// main.js

const store = createStore({
    state() {
         return {
             counter: 0,
        };
     },
    mutations: {
         // In most cases, payload should be object (flexibility)
         increase(statepayload) {
             state.counter += payload.amount;
        }
     }
});


// components/TheCounter.vue

<template>
    <h1>{{ $store.state.counter }}</h1>
     <button v-on:click="increase">Add 1</button>
</template>

<script>
export default {
     methods: {
         increase() {
             // It is Object-style commit
            this.$store.commit({
                 type: 'increase',
                 amount: 5,
             });
        },
     },
};
</script>



Using getters?



If we need to compute a state with others (like the case when using 'computed' in component), we need to copy computing logic to all components if used. Duplicate code is not what we want.

Vuex provide getter for us.

Ex:

// main.js

const store = createStore({
    state() {
         return {
             counter: 0,
        };
     },
    getters: {
         // Computing the final value of counter
         finalCounter(state) {
             return state.counter * 2;
        }
     },
    mutations: {
         increase(statepayload) {
             state.counter += payload.amount;
        }
     }
});


// components/TheCounter.vue

<template>
     <!-- Using computed value -->
    <h1>{{ counter }}</h1>
     <button v-on:click="increase">Add 1</button>
</template>

<script>
export default {
     computed: {
         counter() {
             // Using getter to get the final counter
            return this.$store.getters.finalCounter;
         },
    },
     methods: {
         increase() {
            this.$store.commit({
                 type: 'increase',
                 amount: 5,
            });
         },
    },
};
</script>



Using actions



In realistic project, we need to send data to server to update database, and we will update the view depends on the web api response.

And since web api call is asynchronous, we cannot use mutations (it must be synchronous) to update state.

Vuex provide actions for us.

Ex:

// main.js

const store = createStore({
    state() {
         return {
             counter: 0,
        };
     },
    getters: {
         finalCounter(state) {
             return state.counter * 2;
        }
     },
    mutations: {
         increase(statepayload) {
             state.counter += payload.amount;
        }
     },
    actions: {
         increase(contentpayload) {
             // Force to have a delay for exp only
             setTimeout(function () {
                 content.commit('increase'payload)
            }, 5000)
         }
    }
});


// components/TheCounter.vue
<template>
     <h1>{{ counter }}</h1>
    <button v-on:click="increase">Add 1</button>
</template>

<script>
export default {
     computed: {
         counter() {
            return this.$store.getters.finalCounter;
         },
    },
     methods: {
         increase() {
             // Trigger actions
            this.$store.dispatch({
                 type: 'increase',
                 amount: 5,
             });
        },
     },
};
</script>


NOTE:

Refer to this.

You cat dispatch action to another action!

For example, you can dispatch to succeed/failure actions after web api call actions.


Mapper Helper



So far you might feel that the code is verbose such as we have used lots of code like 'this.$store.getters.xxx' and 'this.$store.dispatch(xxx)'.

Vuex provides mapper helper to save your code.

Ex:

// components/TheCounter.vue
<template>
    <!-- bind to getters -->
     <h1>{{ finalCounter }}</h1>
    <!-- bind to actions -->
     <button v-on:click="increase({ amount: 5 })">Add 1</button>
</template>

<script>
import { mapGettersmapActions } from 'vuex';
export default {
     computed: {
// map helper for getters
         ...mapGetters(['finalCounter']),
    },
     methods: {
// map helper for actions
         ...mapActions(['increase']),
    },
};
</script>



Modules



If our app get bigger, you may want to find a way to manage the state based on features.

Below is an example if  we have counter and auth features.

Ex:

// main.js

const store = createStore({
    state() {
         return {
             counter: 0,
             isLoggedIn: false,
        };
     },
    getters: {
         finalCounter(state) {
             return state.counter * 2;
        },
         isUserAuthenticated(state) {
             return state.isLoggedIn;
        }
     },
    mutations: {
         increase(statepayload) {
             state.counter += payload.amount;
        },
         setAuth(statepayload) {
             state.isLoggedIn = payload.isAuth;
        }
     },
    actions: {
         increase(contentpayload) {
             content.commit('increase'payload)
        },
         login(content) {
             content.commit('setAuth', { isAuth: true })
        },
         logout(content) {
             content.commit('setAuth', { isAuth: false })
        },
     }
});

const app = createApp(App);
app.use(store);
app.mount('#app');


We can use module to separate it to make the code clean and readable.

Ex:
// main.js
// Counter feature module
const counter = {
    state() {
         return {
             counter: 0,
        };
     },
    getters: {
         finalCounter(state) {
             return state.counter * 2;
        },
     },
    mutations: {
         increase(statepayload) {
             state.counter += payload.amount;
        },
     },
    actions: {
         increase(contentpayload) {
             content.commit('increase'payload)
        },
     },
}

// Root Module
const store = createStore({
// Setup feature modules
     modules: {
         'counter': counter,
    },
     state() {
         return {
             isLoggedIn: false,
        };
     },
    getters: {
         isUserAuthenticated(state) {
             return state.isLoggedIn;
        }
     },
    mutations: {
         setAuth(statepayload) {
             state.isLoggedIn = payload.isAuth;
        }
     },
    actions: {
         login(content) {
             content.commit('setAuth', { isAuth: true })
        },
         logout(content) {
             content.commit('setAuth', { isAuth: false })
        },
     }
});

const app = createApp(App);
app.use(store);
app.mount('#app');


Note that once you separate the modules, the feature module need to use 'rootState' to access state from root module.


Name clashes?



Refer to this and that.

Ex:

// main.js

const counter = {
    // Enable namespaced feature
     namespaced: true,
    state() {
         return {
             counter: 0,
        };
     },
    getters: {
         finalCounter(state) {
             return state.counter * 2;
        },
     },
    mutations: {
         increase(statepayload) {
             state.counter += payload.amount;
        },
     },
    actions: {
         increase(contentpayload) {
             content.commit('increase'payload)
        },
     },
}


// components/TheCounter.vue

<template>
    <!-- bind to getters -->
     <h1>{{ finalCounter }}</h1>
    <!-- bind to actions -->
     <button v-on:click="increase({ amount: 5 })">Add 1</button>
</template>

<script>
import { mapActions } from 'vuex';
export default {
     computed: {
         // Using getters with namespace feature
        finalCounter() {
             return this.$store.getters['counter/finalCounter'];
         },
    },
     methods: {
         // Using map helper with namespace feature
        ...mapActions('counter', ['increase']),
     },
};
</script>



Structuring



According to previous example, you may notice that main.js get bigger and bigger, and it is hard to manage now.

We can organize our app as below.

// Structure

    src
     ├─── components
     ├─── store
     │       ├─── modules
     │       │       └─ counter
     │       │             ├── actions.js
     │       │             ├── getters.js
     │       │             ├── index.js
     │       │             └── mutations.js
     │       ├─── actions.js
     │       ├─── getters.js
     │       ├─── index.js
     │       └─── mutations.js
     ├─── App.vue
     └─── main.js



Ex:

// main.js

import { createApp } from 'vue';
import App from './App.vue';
import store from './store/index.js';

const app = createApp(App);
// Install plugin
app.use(store);
app.mount('#app');


// store/index.js

import { createStore } from 'vuex';
import counterModule from './modules/counter/index.js';
import rootGetters from './getters.js'
import rootMutations from './mutations.js';
import rootActions from './actions.js';

// Root Modules
export default createStore({
     modules: {
// Setup Counter Feature Module
         'counter': counterModule,
    },
     state() {
        return {
             isLoggedIn: false,
         };
    },
     getters: rootGetters,
    mutations: rootMutations,
     actions: rootActions,
});


// store/actions.js

export default {
    login(content) {
         content.commit('setAuth', { isAuth: true })
    },
     logout(content) {
        content.commit('setAuth', { isAuth: false })
     },
}


// store/modules/counter/index.js

import counterActions from './actions.js';
import counterGetters from './getters.js';
import counterMutations from './mutations.js';

// Counter Feature Module
export default {
    namespaced: true,
     state() {
         return {
             counter: 0,
        };
     },
    getters: counterGetters,
     mutations: counterMutations,
    actions: counterActions,
}