Vue app scaling, analysis of Vuex and comparison of state management solutions.

I am always interested in new tools that are inspired by other modern tools, that became popular earlier, because you can delegate 80/20 analysis of the world of web development to the creators of the new library and it will leave you with the best patterns for the development. Today we are taking a look at vuex, Vue’s state management library. vuex is inspired by Elm, the same solution that inspired Redux, probably the most popular state management solution in today’s Front end development. We are going to build an application, and comparing and analyzing it along the way.

Our app will be a person profile checker app. We will start with a modified version of a TV reviews application from the post http://jsmegatools.com/2017/12/01/creating-a-simple-app-with-vue-js/, whose source code is at the following url https://github.com/jsmegatools/creating-a-simple-app-with-vue.js. It is essentially the same logic with some variable’s names modified to reflect that we are dealing with persons instead of TV shows.

Our application is going to feature a PersonsList (a list of persons) and a Person (person details) routes/components.

Preparation.

Lets first install vuex

npm install vuex --save

Setting up a vuex store.

Let’s first set up a store. We will create a store.js file in our app’s src directory, with the following contents:

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    added: [],
    addedMap: {}
  },

  mutations: {
    addToCheckLater (state, payload) {
      if (state.addedMap[payload]) return
      state.addedMap = { ...state.addedMap, [payload]: true }
      state.added = [ payload, ...state.added ]
    }
  }
})

You see here state and mutation properties. State is similar to state in Redux.

mutations property basically functions as a rootReducer in redux. Mutation functions in Vue are functions that modify state, you may think of them like a particular case clause of reducers in Redux. In Redux a particular case clause gets executed in response to a certain action type. In Vue a mutation gets registered in response to its name either being used in mapMutations, being passed as a string to the store.commit() method (store.commit() is like store.dispatch() in Redux), or as a type property of an object passed to the store.commit method, the latter pattern is the most similar to actions passed to dispatch in redux/flux. And then a mutation gets executed after it is called as a method of a component in a template.

In our particular case we have a state with an `added` array which contains objects representing profiles that are added to check them later (they are displayed In a sidebar as mini cards) and an addedMap object, which allows us to quickly find an added profile by id without iteration.

addToCheckLater mutation checks whether a profile has already been added to check later, otherwise it adds it to `added` array for iteration and the addedMap for quick check.

As you see a store’s state is mutable, which different from redux, although I assign new objects and arrays to store’s properties, to follow view reactivity rules.

Though not required by vuex, we are going to follow a redux pattern here and have a single store responsible for the whole application, it will be application’s single source of truth.

The way to pass a store to our component.

The starting file of our app (main.js) now should look like this:

import Vue from 'vue'
import store from './store'
import App from './App'
import router from './router'

Vue.config.productionTip = false

/* eslint-disable no-new */
new Vue({
  el: '#app',
  router,
  store,
  template: '<App/>',
  components: { App }
})

Here we are injecting the store into the root component so it is available to all children in the hierarchy. React-redux lib’s equivalent of this action would be using a <Provider> component, which in turn relies on React’s context api.

Using vuex helpers in a component.

Right now PersonsList has some basic logic to show a list of persons. Let’s modify the component to have the following imports:

import { mapState, mapMutations } from 'vuex'

And the code inside component’s class should look like the following:

name: ‘PersonsList’,
computed: mapState({
  added (state) {
    return state.added.map(id => {
      return this.profilesMap[id]
    })
  },
  addedMap: 'addedMap'
}),
methods: {
  ...mapMutations([
    'addToCheckLater'
  ])
},
data: function () {
  return {
    profilesMap: Profiles.reduce((memo, next) => ({ [next.id]: next, ...memo }), {}),
    Profiles
  }
}

The main things to look at are computed (contains computed properties of a component), methods and data properties.

If you don’t know what is a computed property, you may need some explanation. A computed property is like a method, they both called when a template rerenders, but unlike a method, a computed property will execute its function code only when reactive values inside it (for example properties of data property of a component) have changed.

The data property is basically like React state, it holds values that are referenced inside a template and is used to trigger a rerender of a template.

As you could have noticed mapState and mapMutations helpers are part of vuex api. We use mapState to define the computed properties of a component. mapState is similar to mapStateToProps from react-redux library, what mapState does is it creates an object with keys corresponding to a component’s computed properties’ names and values corresponding to either functions that return parts of state (those parts are possibly modified by various operations), or names of state’s properties.

In our particular case we have a added computed property, which returns objects that represent the profiles that  are added to the “check later” sidebar. Also we have an addedMap property, it allows us to check by id, whether a profile has been added to the check later sidebar or not, quickly, without iteration.

Next we have mapMutations, returning us an array of methods for the component. MapMutations is similar to react-redux’s mapActionsToProps, in that it returns an object of functions. But the functions are mutations, rather than action creators as is the case in Redux.

In our case we have an addToCheckLater mutation, which adds a particular profile’s object to the `added` array of the store.

Finally we have the data object of our component, that is its local state.

Right now the template for our component’s template looks like this:

<ul class="profiles-list">
  <li class="profile" v-for="item in Profiles" :key="item.id">
    <router-link :to="`/profile/${item.id}`">
      <img :src="`static/${item.photo}`" />
    </router-link>
    {{ item.first_name }} {{ item.last_name }}
  </li>
</ul>

Let’s modify it so that it is possible to add a person, to a sidebar, containing profiles that are possible to view later.

<ul class="profiles-list">
  <li class="profile" v-for="item in Profiles" :key="item.id">
    <router-link :to="`/profile/${item.id}`">
      <img :src="`static/${item.photo}`" />
    </router-link>
    {{ item.first_name }} {{ item.last_name }}
    <button @click="addToCheckLater(item.id)" :disabled="addedMap[item.id]">Check Later</button>
  </li>
</ul>

What we have done here is we have created a button that will allow us to add a person to check later. It will call addToCheckLater method, provided by mapMutations helper. The button becomes disabled after a user has added a person to check later sidebar.

Let’s also add code for the sidebar itself:

<div class="profiles-list-component">
<div class="check-later-container" v-if="added.length">
  <ul class="check-later">
    <li class="profile-later" v-for="item in added" :key="item.id">
      <router-link :to="`/profile/${item.id}`">
        <img :src="`static/${item.photo}`"/>
      </router-link>
      {{ item.first_name }} {{ item.last_name }}
    </li>
  </ul>
</div>

Here we have a link for every item in the sidebar that takes us to that user’s profile.

Lets also add an ability to remove items from the “check later” sidebar. Add the following button to remove the item to the list item corresponding to a sidebar item:

<button @click="removeFromCheckLater(item.id)">Don't check</button>

Then to mapMutation we need to add another string:

'removeFromCheckLater'

And to the store we need to add the following:

removeFromCheckLater (state, payload) {
  // This does not get noticed by Vuex, until we reassign it
  delete state.addedMap[payload]
  // Now it gets noticed
  state.addedMap = { ...state.addedMap };
  state.added.splice(state.added.indexOf(payload), 1)
  state.added = [ ...state.added ]
}

First we delete an item from addedMap object. Due to the way vue implements reactivity (it uses Object.defineProperty) and how JavaScript works, vue has no way of detecting that property was deleted, to make it detect we need to create a new object with all remaining properties of state.addedMap. It is easy with the help of ES6 spread operator.

{ …state.addedMap }

And we assign that new object to state.addedMap again. You can also emulate property deletion by assigning null or undefined to the removed item in addedMap, this way you don’t have to write delete and reassigning new object to addedMap, so 2 lines become one:

state.addedMap[someProperty] = null // or undefined

Vue will detect changes as you change the property in this case. But in this case state.addedMap.hasOwnProperty(keyOfNulledProperty) will return true, and if you do not want this you should stick to the variant with delete operator.

Similar method applies to the `added` array: we use splice, then array spread operator and then reassign to the `added` property.

Right now we can add/delete Items to the ‘check later’ sidebar, but we don’t have the sidebar itself. We should fix this issue!

Add the following to the beginning of our template:

<div class="check-later-container" v-if="added.length">
  <ul class="check-later">
    <li class="profile-later" v-for="item in added" :key="item.id">
      <router-link :to="`/profile/${item.id}`">
        <img :src="`static/${item.photo}`"/>
      </router-link>
      {{ item.first_name }} {{ item.last_name }}
      <button @click="removeFromCheckLater(item.id)">Don't check</button>
    </li>
  </ul>
</div>

Here we check whether he have any profiles to check later via v-if directive, then we make every profile link to the corresponding detail page for a user. Also we have a button with a click handler that triggers removeFromCheckLater method and with it a corresponding mutation in our application store.

We can improve our UI by letting a user to close/open the sidebar. It is going to be easy to do with Vue. Since for now the sidebar is only present on one page we are going to use the PersonList component’s local state to keep track of the sidebar state, no need to bother with store here.

First we need to add a showSidebar data property.

data: function () {
  return {
    profilesMap: Profiles.reduce((memo, next) => ({ [next.id]: next, ...memo }), {}),
    Profiles,
    showSidebar: true
  }
}

Then we need to add an additional check in v-if directive:

<div class="check-later-container" v-if="added.length && showSidebar">

We also need to add a toggleSidebar method to the component:

toggleSidebar () {
  this.showSidebar = !this.showSidebar
}

And finally add a menu button with a click event handler:

<div class="menu-button" v-if="added.length" @click="toggleSidebar()"></div>

Separating functionality of a component.

As you can see, as we have added, functionality to our PersonList component its source have become more bloated and less manageable. Maybe that is not a big deal now, but as you scale an application more, this becomes more and more of a concern. So what should we do in this situation? You might have already guessed, move some of the functionality into a separate component and use composition to hold our app together.

What kind of functionality can we extract from the PersonList? Right now it is apparent that the sidebar definitely could have made use of its own component. Let’s create a sidebar component.

First, let’s create a Sidebar.vue file, which will contain our component. We need to move the elements that represent sidebar functionality from ProfilesList template to Sidebar template and the template will look like this:

<template>
  <div>
    <div class="menu-button" v-if="added.length" @click="toggleSidebar()"></div>
    <div class="check-later-container" v-if=“added.length && showSidebar">
      <ul class="check-later">
        <li class="profile-later" v-for="item in added" :key="item.id">
          <router-link :to="`/profile/${item.id}`">
            <img :src="`static/${item.photo}`"/>
          </router-link>
          {{ item.first_name }} {{ item.last_name }}
          <button @click="removeFromCheckLater(item.id)">Don't check</button>
        </li>
      </ul>
    </div>
  </div>
</template>

You might have noticed that though we have removed two elements ( with classes menu-button and check-later-container) from ProfilesList, we have one root element in Sidebar component, which serves as a wrapper for the two. This is necessary because Vue does not permit more than one root element in a template and will throw an error in that case. Now, recent versions of React permit usage of arrays and fragments to use multiple root elements in a component (inside a return value of the render method), in Vue you can use render functions for returning multiple roots, otherwise you have to wrap your multiple roots in one element.

We are going to make use of mock data and vuex helpers in Sidebar component also, therefore we should add their imports:

import Profiles from '../mock_data'
import { mapState, mapMutations } from 'vuex'

Now it’s just a matter of copy/paste of the necessary properties from ProfilesList. After that the component’s export will look like this

export default {
  name: 'Sidebar',
  computed: mapState({
    added (state) {
      return state.added.map(id => {
        return this.profilesMap[id]
      })
    }
  }),
  methods: {
    ...mapMutations([
      'removeFromCheckLater'
    ]),
    toggleSidebar () {
      this.showSidebar = !this.showSidebar
    }
  },
  data: function () {
    return {
      profilesMap: Profiles.reduce((memo, next) => ({ [next.id]: next, ...memo }), {}),
      showSidebar: true
    }
  }
}

And now we can remove the functionality related to the Sidebar from ProfilesList component. ProfileList now looks like this:

export default {
name: 'ProfilesList',
computed: mapState({
  addedMap: 'addedMap'
}),
methods: {
  ...mapMutations([
    'addToCheckLater'
  ])
},
data: function () {
  return {
    Profiles
  }
}

The only thing that’s left is to use the new sidebar component in ProfilesList component. Here is how we are going to do this. First we are going to import the Sidebar component:

import Sidebar from './Sidebar'

Then we will add a components property to the ProfilesList component:

components: {
  sidebar: Sidebar
}

sidebar is the name that is going to represent the Sidebar component inside ProfilesList component (unlike web components in Vue a custom component doesn’t have to contain dash in its name). In Vue using a child component in another component is a matter of placing it in a component’s template. In our case in place of removed sidebar elements we simply put the following

<sidebar></sidebar>

And our ProfilesList component’s template is reduced to the following:

<template>
  <div class="profiles-list-component">
    <sidebar></sidebar>
    <div class="profiles-list-container">
      <ul class="profiles-list">
        <li class="profile" v-for="item in Profiles" :key="item.id">
          <router-link :to="`/profile/${item.id}`">
            <img :src="`static/${item.photo}`" />
          </router-link>
          {{ item.first_name }} {{ item.last_name }}
          <button @click="addToCheckLater(item.id)" :disabled="addedMap[item.id]">Check Later</button>
        </li>
      </ul>
    </div>
  </div>
</template>

Now whats’ left is to move styles, used by the Sidebar component to its file and we have successfully completed separating functionality of the sidebar from the ProfilesList component.

Using vuex getters.

It all works well with the sidebar, but somebody might not like having the sidebar open all the time, maybe they want a dropdown with a button in upper right corner of a screen. We can create this dropdown.

To hold this dropdown button we need some kind of a toolbar at the top (that in a real app can also contain navigation) of the screen. The button is going to be right aligned.

Our first move here is to create Toolbar.vue file for the toolbar component. The content is going to be identical to the Sidebar component, with some minor tweaks. The template will look like this:

<div class="check-later-toolbar">
  <div class="check-later-button" @click="toggleDropdown()">
    <div class="has-check-later" v-if="added.length"></div>
    <ul class="check-later" v-if="added.length && showDropdown">
      <li class="profile-later" v-for="item in added" :key="item.id">
        {{ item.first_name }} {{ item.last_name }}
        <button @click="removeFromCheckLater(item.id)">Don't check</button>
      </li>
    </ul>
  </div>
</div>

We have toggleDropdown instead of toggleSidebar and showDropdown instead of sidebar. We don’t use images here, and we show an indicator on a button if there are any profiles added to check later. The styles will differ from the sidebar too.

We have some shared code between the sidebar and the toolbar, we can make use of vuex’s getters to handle this situation. We need to import mock data (in a real app you would want to update store’s state after fetching the profile data, instead of directly importing mock data) in store.js to calculate profilesMap, therefore we do not need Profiles(mock data) and profilesMap in Sidebar.vue and Toolbar.vue. We also do not need mapState, since we are using getters. Move mock data import and profilesMap to the store like this:

import Profiles form ‘./mock_data’

…

state: {
…
  profilesMap: Profiles.reduce((memo, next) => ({ [next.id]: next, ...memo }), {})
}

Then we need to add getters to the store, with `added` method that contains the code copied from Sidebar and Toolbar:

getters: {
 added (state) {
   return state.added.map(id => {
     return state.profilesMap[id]
   })
 }
}

With that we can change the `added` computed property of both Sidebar and Toolbar to this:

added () {
  return this.$store.getters.added
}

This pattern of using getters resembles Reselect library that is used with React + Redux, in that it is used for reusing and caching code, responsible for preparing state values for a component. There is also a mapGetters helper in vuex, that allows you to simplify the code a bit. In our case we can use it like this:

computed: mapGetters([
  'added'
])

And that’s all folks! Hope you find this post useful. There is more to vuex than I have described here, but it should give you a quick overview of the library. The code for the final version can be found at the following URL: https://github.com/jsmegatools/Vue-app-scaling-analysis-of-Vuex-and-comparison-of-state-management-solutions.