Vuex features you should know if you really care about your store
Software Development
Wojciech Bak
2020-07-21

Vuex features you should know if you really care about your store

Frontend applications, especially the more complex ones, have to process a lot of data. Programmers introduce various design patterns to make their projects readable and maintainable. In most the common scenarios of dealing with an MVC, we want to separate the data from the visual parts of the app.

That's the reason why store has become so useful. It's up to you whether you use React + Redux or Vue + Vuex – the main goal is the same, namely keeping your data structured, accessible and safe at the same time.

In this article, I'm going to show you a few examples of how to keep your Vuex store clean and efficient.

Before we start, let's assume that:

  • you have some experience with modern JavaScript,
  • you basically know what Vue is and how to use props, computed, etc.,
  • you are familiar with Vuex (actions, mutations, etc.) and want to make your apps better.

Vuex, like the majority of core Vue projects, is pretty well-documented and you can find many useful hacks in official docs. We have extracted some essential information from it for you.

A basic Vuex store implementation looks like this:

// main.js

import Vue from 'vue'
import Vuex from 'vuex'
import App from "./App";

Vue.use(Vuex)

const store = new Vuex.Store({
  state: {
    data: null;
  },
  actions: {
      someAction: ({ commit }, data) {
          commit("SOME_MUTATION", data);
      }
  },
  mutations: {
    SOME_MUTATION (state, data) {
        state.data = data;
    }
  }
});

new Vue({
  el: "#app",
  render: h => h(App),
  store
});

Usually, when your app gets bigger, you have to apply routing, some global directives, plugins, etc. It makes the main.js file much longer and more difficult to read. It's a good practice to keep the store in an external file, like here:

// store.js
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex);

const state = {
    data: null;
};

const actions = {
    someAction: ({ commit }, data) {
        commit("SOME_MUTATION", data);
    }
};

const mutations = {
    SOME_MUTATION (state, data) {
        state.data = data;
    }
};

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

1. Modules

What should you do when the store.js file gets enormous and difficult to work on? Actually, there is a really cool Vuex feature – modules. They are dedicated to splitting your data into separate files.

Imagine that you work on some corporate app, in which you have few domains of data, for example:

  • user (manage all authorizations and permissions),
  • route parameters (manage global parameters before requests to API),
  • sales (for your SalesMegaChart component visible in a monthly/quarterly/yearly context),
  • orders (visible after clicking on the SalesMegaChart bar).
Get free code review

...and maybe a few more. Now you have serious reasons to introduce some modularity in your store.

First of all, move the store.js file to a newly created store/ directory and rename it index.js. Optionally, if you want to keep everything packed into modules, remove state, actions and mutations from the main file.

// store/index.js

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

Vue.use(Vuex);

export default new Vuex.Store({
    modules: {
        // modules will go here
    }
});

Then, next to the `store/index.js` file, create the first module – `store/user.js`.

import ApiService from '../services/api.service';

const state = {
    loggedIn: false,
    loginError: null,
    user: null
};

const actions = {
    login: async ({ commit }, data) {
        try {
            const response = await ApiService.post('/login', data);
            const { user } = response.data;

            COMMIT("SAVE_USER", user);
            COMMIT("LOGIN_SUCCESS");
        } catch (error) {
            commit("LOGIN_ERROR", error);
        }
    }
};

const mutations = {
    SAVE_USER (state, user) {
        state.user = user;
    },

    LOGIN_SUCCESS (state) {
        state.loggedIn = true;
    },

    LOGIN_ERROR (state, error) {
        state.loginError = error;
        state.loggedIn = false;
    }
};

export const user {
    state,
    actions,
    mutations
}

And now, load the ready module into the main `store/index.js` file:

import Vue from 'vue'
import Vuex from 'vuex'
import { user } from './user';

Vue.use(Vuex);

export default new Vuex.Store({
    modules: {
        user
    }
});

Congratulations! Now you have a really nice-looking store implementation. You can also access the data from the component (e.g., UserProfile.vue) like this:

<template\>
    <div class="user-profile">
        <h2\>{{ user.name }}!</h2>
        <!-- component template goes here -->
    </div>
</template>

<script> import { mapActions } from 'Vuex';

    export default {
        name: 'UserProfile',

        computed: mapState({
            user: state => state.user
            // user: 'user' <-- alternative syntax
        })
    }
</script>

2. Namespaces

Now that you know how to use modules, you should also become familiar with Vuex’s namespacing. On the previous step, we created the store/user.js file with the user module.

The data structure defined in the user.js file is accessible from components, but you can spot that all user data goes directly to the global state context, like here:

computed: mapState({
    user: state => state.user
    // user: 'user' <-- alternative way
})

When you define more modules, you'll probably get confused about which object comes from which module. Then you should use namespaced modules and define them in this way:

export const user {
    namespaced: true, // <-- namespacing!
    state,
    actions,
    mutations
}

From now on, all your user data (state variable from store/user.js file) will be handled under the state.user reference:

computed: mapState({
    user: state => state.user.user
    // user: 'user/user' <-- alternative way
})

A few steps later, you can achieve for the component something like this:

import { mapActions } from 'Vuex';

export default {
    name: 'Dashboard',

    computed: mapState({
        sales: 'sales/data',
        orders: 'orders/data',
        sortBy: 'orders/sortBy',
        loggedIn: 'user/loggedIn'
    }),

    methods: mapActions({
        logout: 'user/logout',
        loadSales: 'sales/load',
        loadOrders: 'orders/load'
    }),

    created() {
        if (this.loggedIn) {
            loadSales();
            loadOrders();
        }
    }
}

Bravo! So fresh, so clean... But don't worry, refactoring never ends. Ready for the next steps?

3. Communication between modules

In the first step, I you showed some action in the user module:

const actions = {
    login: async ({ commit }, data) {
        try {
            const response = await ApiService.post('/login', data);
            const { user } = response.data;

            COMMIT("SAVE_USER", user);
            COMMIT("LOGIN_SUCCESS");
        } catch (error) {
            commit("LOGIN_ERROR", error);
        }
    }
};

In case of failure, we're adding login error to our store – what's next?

Here we have a few options and the choice depends on which option suits your needs better. The simplest way used the v-if directive, thanks to which an error message can be displayed if there's an error in your store.

<template>
    <div class="dashboard"\>
        <!-- dashboard component template -->
        <div
            v-if="error"
            class="error-message"
        > {{ error.message }} </div>
    </div>
</template>
<script> import { mapActions } from 'Vuex';

export default {
    name: 'Dashboard',

    computed: mapState({
        error: "user/loginError"
    })
}
</script>

Again, imagine that you have many modules and each try/catch syntax generates a new error in your store. Obviously, you're going to abuse the DRY rule this way.

How can you make your error handling processes more generic?

Let's define the common module and put some logic in there that would be used globally.

// store/common.js

const state = {
    errors: []
};

const actions = {
    error: {
        root: true,
        handler({ commit }, error) {
            commit("ERROR", error);
        }
    }
},

const mutations = {
    ERROR (state, error) {
        /* this way we're gonna have the newest error on top of the list */
        state.errors = [error, ...state.errors];
    }
};

export const common {
    namespaced: true,
    state,
    mutations
}

Now, we can adapt the user module (and other modules as well):

try {
    // some action
} catch (error) {
    commit("common/ERROR", error, { root: true });
}

or in more elegant way, using our global action:

try {
    // some action
} catch (error) {
    dispatch("error", error);
}

This syntax of commit and dispatch calls seems self-explanatory, but you can read more about these tricks here.

When you have all errors in one place, you can easily load them to your Dashboard component:

computed: mapState({
    errors: 'common/errors'
}),

watch: {
    /* this will be invoked after each "common/ERROR" mutation, where we only add new errors to the store, one by one */
    errors() {
        this.showErrorMessage(this.errors[0]);
    }
}

The previous example with the common module handling errors is already an efficient solution, but you can go even further.

As you can see, we're watching changes on the common/errors array in the store. In cases like these, when you need to determine some action on a particular mutation, you can use Vuex plugins or even Higher Order Components (HOC).

I will discuss the plugins and HOCs in the next article. Meanwhile, thank you for reading this entry, hopefully, you enjoyed the examples we’ve prepared.

Stay tuned and keep coding!

Read more:

- How to improve Vue.js apps? Some practical tips

- GraphQL: lessons learned in production

- Shopify, Spree or Solidus? Check why Ruby on Rails can help you develop your e-commerce

Software development consulting