loading...

Structuring store with the right feet using vue/vuex

chrismichael profile image Chris Michael ・5 min read

This publication will be focused on how to structure our store in a simple and effective way. I will be assuming that you have a fairly reasonable knowledge using the vue framework and the handling of Lifecycle Hooks

What is Vuex?

Vuex is a state management pattern based on the singleton architecture that allows us to have centralized and accessible data in any component within the application but without being able to be modified without some control.

Why should we use Vuex?

By using a centralized application data store, the complete state of the application can be represented in one place, which makes the application more organized. By using a unidirectional data flow, mutations and access to the data of the scope component only to the required data, it becomes much simpler to reason about the role of the component and how it should affect the state of the application.

Store structure

store.js

It is better to keep all the Vuex logic in a separate file. We will keep our logic in a file called store.js. In our store.js file we have a store object that is our Vuex store.

The store is an instance of the Vuex.store object, which consists of four objects. The four objects are the state, actions, mutations and getters.
Since these objects have been defined in different files, they must be imported into the store.js file and then passed in the store instance.

import Vue from 'vue';
import Vuex from 'vuex';
import {state} from './state';
import {actions} from './actions';
import {mutations} from './mutations';
import {getters} from './getters'; 

Vue.use(Vuex);
export default new Vuex.Store({
  state,
  mutations,
  actions,
  getters
});

state.js

The state is an object that contains the state of the application data.

export const state = {
  data: [],
  isLoading: true
};

mutations.js

Mutations is also an object that contains methods that affect the state and only care to control the states, to manipulate it.

A mutation may have two arguments as state and payload:

  • State has the current state of an application.
  • Payload is an optional one, which will give the data to mutate.
export const mutations = {
   SET_DATA(state , payload){
     state.data = payload;  
   },
   IS_LOADING(state , payload){
     state.isLoading = payload;
   }
 };

actions.js

Actions are methods used to cause mutations and execute asynchronous code. Responsible for preparing everything necessary for a mutation to confirm a change in the state.

The actions expect a promise to be resolved, hence we make a return of the promise that returns axios. When axios returns the data, we can execute commits with the type of mutation that we want to carry out. Instead of mutating the state, actions commit mutations, in this case, using the mutator SET_DATA. And the mutator IS_LOADING that will help to know if there is still data to load.

import axios from 'axios';
const BASE_API_URL= '...';
const API_URL_ENDPOINT = '...';
const A = axios.create({ baseURL: String(BASE_API_URL) });
export const actions = {
  GET_DATA({commit}){
    A.get(API_URL_ENDPOINT).then((res) =>{
      commit('SET_DATA' , res.data);
      commit('IS_LOADING' , false);
    }).catch((err) =>{
      console.error(err)
    });
  }
};

getters.js

Getters contain the methods used to abstract access to the state and to do some preprocessing tasks, if necessary (data calculation, filtering, etc …).

Vuex allows us to define "getters" in the store. Like computed properties, a getter's result is cached based on its dependencies, and will only re-evaluate when some of its dependencies have changed.

  • Getters will receive the state as their 1st argument

For the following example, we will be using Lodash's library. Lodash is a well-known JavaScript utility library that makes it easy to manipulate arrays and objects, as well as functions, strings, etc.

So, since we are interested in filtering some data in particular, we will use the function `.filter()`._

.filter(collection, [predicate=.identity])

Iterates over elements of collection, returning an array of all elements predicate returns truthy for. The predicate is invoked with three arguments: (value, index|key, collection).

var _ = require('lodash');
export const getters = {
  FILTER_SPESIFIC_DATA: (state) =>{
    return _.filter(state.data , (data) =>{
      // ....
    });
  }
};

Working in the view folder

Home.vue

  • When a component needs to make use of multiple store state properties or getters, declaring all these computed properties can get repetitive and verbose. To deal with this we can make use of the mapState helper which generates computed getter functions for us.

  • The easy way to access getters in your component, however, is through Vuex's mapGetter helper method. This allows you to mount getters to top-level computed properties in your component.

Note that mapState returns an object. How do we use it in combination with other local computed properties? Normally, we'd have to use a utility to merge multiple objects into one so that we can pass the final object to computed. However with the object spread operator (which is a stage-4 ECMAScript proposal), we can greatly simplify the syntax, equally applies with mapGetters.

Spread Properties

Spread properties in object initializers copies own enumerable properties from a provided object onto the newly created object.

<template>
  <div class="content">
    <div v-if="isLoading"> loading...</div>
    <div v-else>
       <div v-for="(content , index) in data" :key="index">
         // ....
       </div>
    </div> 
  </div>
</template>
<script>
  import {mapState , mapGetters} from 'vuex';
  import store from '../store/store.js';

  export default{
    name: 'home',
    computed:{
      ...mapState(['data' , 'isLoading']),
      ...mapGetters(['FILTER_SPESIFIC_DATA']),
      filteredDataObj(){
        return FILTER_SPESIFIC_DATA()
      }
    },
    created(){
      store.dispatch('GET_DATA')
    }
  };
</script>
<style scoped>
  .content{
    ....
  }
</style>

Finally…..

main.js

Now, to access the store you'll either have to import it in all of your components, or you can inject it into the root Vue instance to have it automatically injected into every other component in your app as this.$store or importing the store.js file from the store folder.

import Vue from 'vue';
import Vuex from 'vuex';
import App from 'App.vue';
import { store } from '../store/store.js';
Vue.config.productionTip = false;
Vue.use(Vuex);
new Vue({
  store,
  render: h => h(App)
}).$mount('#app');

Conclusion

Keep in mind that vuex helps us maintain and test our application better, but we also have to be aware that vuex involves new levels of abstraction that will make us need to work with new concepts that will make the learning curve less accessible for developed juniors to our projects. So be careful.

References

The Progressive JavaScript Framework

I'll be glad that you like this article! 💖

Posted on by:

chrismichael profile

Chris Michael

@chrismichael

Strong interest in projects that require conceptual and analytical thinking. Fully committed to the design and development of implementations of new algorithms to optimize or for learning purposes.

Discussion

markdown guide
 

❤️ Very grateful for your comments ..

I must mention that we can improve our GET_DATA action by adding the setTimeout function, this will help us to execute the function in a time interval and load our data at that time.

GET_DATA({commit}){
  A.get(API_URL_ENDPOINT).then((res) =>{
    commit('SET_DATA' , res.data);
    setTimeout(() =>{
      commit('IS_LOADING' , false);
    } , 1000)
  }).catch((err) =>{
      console.error(err)
    });
  }
 

great article, but i think actions who does async tasks would return a promise.

GET_DATA({commit}){
  return A.get(API_URL_ENDPOINT).then((res) =>{
    commit('SET_DATA' , res.data);
    setTimeout(() =>{
      commit('IS_LOADING' , false);
    } , 1000)
  }).catch((err) =>{
    console.error(err)
  });
}

anyway, i think IS_LOADING would be set to false, even if the http request throws a error.

async GET_DATA({commit}){
  try {
    const { data } = await A.get(API_URL_ENDPOINT)
    commit('SET_DATA' , res.data)
  } catch () {
    console.error(err)
  } finally () {
    setTimeout(() =>commit('IS_LOADING' , false), 1000)
  }
}
 

Solid guide! I would warn people not to get too complex with your Vuex file structure right from the get-go though, as all that added complexity really doesn't pay off until the rest of your app gets as complex.

 

I find Vuex so much easier to understand than Redux.

 

It is better to avoid using Vuex helpers and go this.$store.getters/commit/dispatch. Makes large code bases way more readable and easy understood.