Saturday, October 6, 2018

Vue.js 2 State Management With Vuex — Introduction

If your Vue.js application grows bigger and consists of multiple components you might run into the problem of how to share data across the those components and make sure that components which are using the same data are always updated if data is changing.

 However, if your application is small you can solve this problem by emitting events to inform other components about data changes. If you applications grows bigger keeping track if all those events becomes difficult and it’s hard to keep the overview of components which need to trigger events and components which need to listen to those events.

 To solve that problem you can use Vuex which makes it possible to use a centralized state management in your application. Vuex is a library which helps you to enforce a Flux-like application architecture in your Vue.js 2 application.

 So what exactly is meant by state and centralized state management? Simply, you can think of state as just data you use in your application. So a centralized state is just data you’re using by more than one component (application level state).

 Vuex introduces a centralized state management by implementing the following core concepts (Store concept):
  • State Tree: An object containing the data
  • Getters: Used to access data from the state tree of the store
  • Mutations: Handler functions that perform modifications of data in the state tree
  • Actions: Functions that commit mutations. The main difference to Mutations is that Actions can contain asynchronous operations
Don’t worry if this concept seems a little bit strange at first. We’ll explore the various building blocks in detail in the following. Take a look at this diagram form the Vuex docs:
This overview is great for understanding the flows of actions and data in the store concept. From the diagram you can see that the data flow is unidirectional. A Vue component initiates a state change by dispatching an Action. The Action method can be used to perform asynchronous operation (e.g. accessing a web service to retrieve data) and then commit a Mutation. The Mutation performs the modification of the state tree. A new state becomes available is used by components to display the changed data.

Want to dive deeper into Vue.js 2 and Vuex — Check out the great online course :
Vue.js 2 The Complete Guide (incl. Vuex).

Let’s see how everything works by building a sample application next.

Setting Up A Vue.js 2 Project And Installing Vuex

The easiest way to set up a new Vue.js 2 project is to use Vue CLI. To install Vue CLI on your system use the following command:

 $ npm install --global vue-cli

 Now we're ready to use the vue command to initiate a new Vue project:

 $ vue init webpack vuex-test-01

 Next, change into the newly created project folder:
$ cd vuex-test-01

 Use NPM to install the default dependencies
$ npm install

 Finally you can start up the development web server by using the npm command in the following way:
$ npm run dev

 In the next step we need to add the Vuex library to the project.

 Vuex is an official Vue package, but not part of the core library. If you want to use Vuex in your project, you first need to install it via NPM:

 $ npm install vuex --save

 This makes sure that this package is downloaded in the node_modules folder of the project and that this dependency is added to package.json file.

Creating A Store

Having installed Vuex, we’re now ready to create a first store in our application. Within the src folder create a new subfolder named store. Within that folder create a new file store.js and insert the following code:
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
const state = {
  count: 0
}
export default new Vuex.Store({
  state
})
First, we need to use two import statements to make Vue and Vuex available. Second, a call of

 Vue.use(Vuex)

 is needed to activate Vuex for our application.

 The initial state of the store is defined by the state object. To keep things simple we're only defining one property count and set the initial value to 0. The state object is used to create the store instance by the method Vuex.Store and passing in an object containing state.

 Now we're able to inject the store into our Vue application instance. Open file main.js and adapt the code as follows:
import Vue from 'vue'
import App from './App'
import store from './store/store'
Vue.config.productionTip = false
new Vue({
  el: '#app',
  store,
  template: '<App/>',
  components: { App }
})
First you need to import store. Next, add store to the configuration object which is passed to the Vue constructor. By providing the store option to the root instance, the store will be injected into all child components of the root and will be available on them as this.$store.

Accessing State

Using the $store Object

Now, that the $store object is available in all components we’re able to use the following expression in one of our component templates, e.g. in Hello.vue:

 {{ $store.state.count }}

 This will insert the value of count (0) in the HTML output which is generated and displayed

Using Getters

Now you’ve learnt how to create a store, define the state and access state properties by using the $store object. The next step is to access state by adding Getters to the store. You can compare Getters to Computed Properties which are available on component level. For example we can use a Getter to evaluate if the value of count is even or odd by adding the following code to store.js:
const getters = {
  evenOrOdd: state => state.count % 2 === 0 ? 'even' : 'odd'
}
To make sure that the defined Getter is part of the Store, we need to add this Getters to the store configuration object which is passed to the call of Vuex.Store at the end of the file:
export default new Vuex.Store({
  state,
  getters
})
To use the evenOrOdd Getter in our component we need to make the following change in our component implementation in file Hello.vue:
<script>
import { mapGetters } from 'vuex'
export default {
  name: 'hello',
  computed: mapGetters([
    'evenOrOdd'
  ])
}
</script>
Notice, that we’re adding the mapGetters function from the vuex package first. Then we’re calling mapGetters, passing in as a parameter a array listing the Getters we would like to use and assign the return value to the computed property of the component configuration object.

 Now we can make use of evenOrOdd by including it in the template like you can see in the following:

 Counter: {{ $store.state.count }} times, count is {{ evenOrOdd }}.

 If the value of count in the store is even it returns the string “even”. If the value is odd it returns the string “odd”.

Modifying State

synchronously By Using Mutations

The next step is to modify the state. To directly modify state properties Mutations are used. Mutations are passed the current state and an optional payload. Mutations must be synchronous. Let’s define two Mutations in file store.js to increment and decrement:
const mutations = {
  increment (state) {
    state.count++
  },
  decrement (state) {
    state.count--
  }
}
Again, add the mutations object to the object which is passed to Vuex.Store:
export default new Vuex.Store({
  state,
  getters,
  mutations
})

asynchronously By Using Actions

In many cases it’s not sufficient to modfiy the state with a Mutation synchronously. Instead you need to perform asynchonous operations to retrieve values from a backend (e.g. accesing a data base or a web service) and then updating the state with that new value. In that case we need to add Actions to our store, as you can see in the following code listing:
import Vuex from 'vuex'
import Vue from 'vue'
Vue.use(Vuex)
const state = {
  count: 0
}
const mutations = {
  increment (state) {
    state.count++
  },
  decrement (state) {
    state.count--
  }
}
const actions = {
  increment: ({ commit }) => commit('increment'),
  decrement: ({ commit }) => commit('decrement'),
  incrementIfOdd ({ commit, state }) {
    if ((state.count + 1) % 2 === 0) {
      commit('increment')
    }
  },
  incrementAsync ({ commit }) {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        commit('increment')
        resolve()
      }, 1000)
    })
  }
}
const getters = {
  evenOrOdd: state => state.count % 2 === 0 ? 'even' : 'odd'
}
export default new Vuex.Store({
  state,
  getters,
  actions,
  mutations
})
Here you can see that the actions object is containing four functions:
  • increment
  • decrement
  • incrementIfOdd
  • incrementAsync
Each Action handler function can receive two parameters, a context object giving us access to a commit method and the current state and a optional payload object.
actions: {
  increment (context, payload) {
    ...
  }
}
The second parameter payload is only need if you want to pass in further information, e.g. values used to set a new state. The context object gives you access to the commit method, so that you can commit Mutations like you can see in the following:
context.commit('increment')
In our example we’re using the ES6 object deconstruction and arrow function feature, so that the code can be rewritten to:
increment: ({ commit }) => commit('increment')
The incrementAsync Action function demonstrates how to execute asynchronous code as part of an Action function.

 Now let’s see how we can apply the four action methods in the corresponding template:
<template>
  <div class="container">
    <div class="row text-center">
        <h3>Clicked: {{ $store.state.count }} times, count is {{ evenOrOdd }}.</h3>
        <button class="btn btn-success" @click="increment">+</button>
        <button class="btn btn-danger" @click="decrement">-</button>
        <button class="btn" @click="incrementIfOdd">Increment if odd</button>
        <button class="btn" @click="incrementAsync">Increment async</button>
    </div>
  </div>
</template>
<script>
import { mapGetters, mapActions } from 'vuex'
export default {
  name: 'hello',
  computed: mapGetters([
    'evenOrOdd'
  ]),
  methods: mapActions([
    'increment',
    'decrement',
    'incrementIfOdd',
    'incrementAsync'
  ])
}
</script>
Here you can see that first we’re extending the import statement to also import the mapActions function. The return value of the mapActions call is assigned to the methods property of the component configuration object. We need to pass an array to mapActions containing strings with the names of the Actions we would like to make available to the component.

 With this code in place we’re able to include four buttons in the template code and bind the click event of those buttons the the four Action functions. The result in the browser should now look like the following:
If you click on the plus and minus button you can immediately see that the output changes. The counter is updated and the output text contains the information if the new value is odd or even. If you click on button “Increment if odd” the counter is only incremented if the current counter value is odd. If you click on “Increment async” you’ll see that the counter is incremented with a delay of 1000 ms.

Summary

If your Vue application grows bigger and consists of multiple components which needs to share data, introducing a central state management might be the right approach for you project. By using the Vuex library it’s easy to implement central state management in your Vue app by introducing a central store in your application.

 With this tutorial you’ve gained a basic overview about the centralized state management concept and you’ve learned how to apply Vuex in your application. In one of the next tutorials we’ll use that knowledge and dive deeper into that topic by implementing a real-world Vue.js application with Vuex.
This post has been published first on CodingTheSmartWay.com.

No comments:

Post a Comment