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,
}