loading...
Cover image for Architecting Vuex store for large-scale Vue.js applications
Locale.ai

Architecting Vuex store for large-scale Vue.js applications

haxzie profile image Musthaq Ahamad Updated on ・7 min read

At the heart of all large-scale Vue.js application lies the store which holds all its data. The Vuex store in a Vue.js application acts as a single source of truth which provides great performance and reactivity out of the box. As your application grows in complexity and code, Vuex stores get easily cluttered and become hard to manage. Architecting the state management of your application considering the best practices can solve most of the problems that grow with complexity.

In this blog post, we'll discuss some of the best practices and tips to architect state management on a large scale Vue.js application. We'll cover the following 5 concepts to help architect your store better.

  1. Structuring the store
  2. Modularizing the store
  3. Auto importing modules
  4. Resetting module state
  5. Global module state reset

1. Structuring the store

A Vuex store contains 4 main components:

  1. The state object
  2. Getter functions
  3. Actions
  4. Mutations

If you are not yet familiar with these 4 concepts, here's a quick teardown of the above. The state object holds the data of your application as a large JSON. The Getter functions help you to access these state objects outside the store, they can act as reactive computed properties. Mutations, as the name suggests is used to modify/mutate your state object. Actions are quite similar to mutations, but instead of mutating the state, Actions commit mutations. Actions can contain any arbitrary asynchronous code or business logic.

Vuex recommends the state object should only be mutated inside the Mutation functions. It is also recommended not to run any heavy or blocking code inside the Mutation functions since it's synchronous in nature. Instead, we should use Actions, which are to be designed asynchronous to carry out all the heavy load or make network requests and commit mutations. Actions are also the best place to keep your business logic and data processing logic. Since it can store the data back to the store or can be used to retrieve the data directly into your Vue components, actions are ideal for such use cases.

It is a good practice not to directly access the state object and use the Getter functions instead. The getter functions can easily be mapped into any Vue component using mapGetters as computed properties.


2. Modularizing the store

It's no wonder that with increased size and complexity, the store gets cluttered and hard to understand. Vuex provides out of the box ability to split your store into separate modules with specific purposes as per your application. differentiating the business logic with the help of store modules increases the maintainability of the application. So we need to make sure each module is name-spaced and not to access them using the global store scope.

Here's a quick example for authoring store module and how to combine all the modules in the main store.

Directory Structure

store/
   ├── index.js    ---> Main Store file
   └── modules/
       ├── module1.store.js
       ├── module2.store.js
       ├── module3.store.js
       ├── module4.store.js
       ├── module5.store.js
       └── module6.store.js
Enter fullscreen mode Exit fullscreen mode

Note that, each module is named as ModuleName.store.js this will help us to auto-import these modules and we'll discuss it in the next section.

Authoring Modules

We can move the network calls into a separate JavaScript file, we will discuss that in another blog post about architecting the network layer of the application. We can even separate out the state object, getters, actions and mutations into separate files for readability. It is good to keep all the related functions together and modularize away the store into modules further if it's still large and complex.

/* Module1.store.js */

// State object
const state = {
    variable1: value,
    variable2: value,
    variable3: value
}


// Getter functions
const getters = {
    getVariable1( state ) {
       return state.variable1;
    },
    getVariable2( state ) {
       return state.variable2;
    },
    ....
}


// Actions 
const actions = {
    fetchVariable1({ commit }) {
        return new Promise( (resolve, reject) => {
               // Make network request and fetch data
               // and commit the data
               commit('SET_VARIABLE_1', data); 
               resolve();
        }
    },
    ....
}
// Mutations
const mutations = {
    SET_VARIABLE_1(state, data) {
       state.variable1 = data;
    },
    SET_VARIABLE_2(state, data) {
       state.variable2 = data;
    },
    ....
}
export default {
    namespaced: true,
    state,
    getters,
    actions,
    mutations
}
Enter fullscreen mode Exit fullscreen mode

Combining Modules

/** store/index.js **/
import Vue from 'vue';
import Vuex from 'vuex';
import createLogger from 'vuex/dist/logger';
import Module1 from './modules/module1.store';
import Module2 from './modules/module2.store';
...
Vue.use(Vuex);
const debug = process.env.NODE_ENV !== 'production';
export default new Vuex.Store({
   modules: {
      Module1,
      Module2,
      ...
   },
   strict: debug,
   plugins: debug? [ createLogger() ] : [],
}
Enter fullscreen mode Exit fullscreen mode

3. Auto importing store modules

As I mentioned, if the modules are getting more and more complicated, we need to further split them into individual modules to reduce the complexity. When the number of modules increases, it becomes really hard to manage these modules individually and manually import each and every one of them. We'll have a small JS file inside the modules subdirectory to do this job for us. This file will take care of bringing all the modules together.

To make this happen, it's recommended to follow a strict naming pattern for the module files. After all, having a standard naming pattern will increase the maintainability of the entire project. For making things easier, our modules can be named using camelCase followed by .store.js extension. eg. userData.store.js and we need to add an index.js file inside the modules sub-directory to find all these modules and export them into the main store.

store/
   ├── index.js    ---> Main Store file
   └── modules/
       ├── index.js   --> Auto exporter
       ├── module1.store.js
       └── module2.store.js
Enter fullscreen mode Exit fullscreen mode

Auto export script

/**
 * Automatically imports all the modules and exports as a single module object
 */
const requireModule = require.context('.', false,  /\.store\.js$/);
const modules = {};

requireModule.keys().forEach(filename => {

    // create the module name from fileName
    // remove the store.js extension and capitalize
    const moduleName = filename
                   .replace(/(\.\/|\.store\.js)/g, '')
                   .replace(/^\w/, c => c.toUpperCase())

    modules[moduleName] = requireModule(filename).default || requireModule(filename);
});

export default modules;
Enter fullscreen mode Exit fullscreen mode

Now, our auto-export script is in place, we can import this in our main store and have access to all modules.

import Vue from 'vue'
import Vuex from 'vuex'
import createLogger from 'vuex/dist/logger'

// import the auto exporter
import modules from './modules';

Vue.use(Vuex);
const debug = process.env.NODE_ENV !== 'production';

export default new Vuex.Store({
  modules, // all your modules automatically imported :)
  strict: debug,
  plugins: debug ? [createLogger()] : [] // set logger only for development
})
Enter fullscreen mode Exit fullscreen mode

Once you have used the auto importer in your main store, all the new modules getting added to the modules sub-directory will be automatically imported. For example, if you have a file named user.store.js this will be imported as a store module name-spaced as User. You can use this name-space to map the Getters and Actions into your components use mapGetters and mapActions.


4. Resetting module state

If you have worked with Vue+Vuex applications which manage a lot of data in the store modules. You might have come across a scenario where you need to reset the state of the store. It is quite common to have a reset feature when you have user authentication in your application so that you can reset the store when the user logs out.

To reset the store we need to separate out the state object to an initial state and copy that to the main state. we can use a simple function that returns the initial state to achieve this. So, in your store module, create a function called initialState() that returns the actual state object.

const initialState = () => ({
    variable1: value,
    variable2: value,
    variable3: value
});

const state = initialState();
Enter fullscreen mode Exit fullscreen mode

Now we have a separate initial state, any changes we make to the state will not affect the actual initial value. So, we can use this to reset the store. Create a mutation function that basically mutates the entire store object with the initial state.

const initialState = () => ({
    variable1: value,
    variable2: value,
    variable3: value
});

const state = initialState();

// Getters

// Actions

// Mutations
const mutations = {
    RESET(state) {
      const newState = initialState();
      Object.keys(newState).forEach(key => {
            state[key] = newState[key]
      });
    },
    // other mutations
}
Enter fullscreen mode Exit fullscreen mode

Once we have the RESET mutation in place we can use this function to reset the store easily either by calling an action or directly committing the RESET mutation.

// Actions
const actions = {
   reset({ commit }) {
       commit('RESET');
   },
}
Enter fullscreen mode Exit fullscreen mode

5. Global module state reset

What if we need to reset the entire store? including all the modules? If you have followed the 4th and 5th points on setting up the auto importer and module state reset mutation in all your modules, we can use the following action in our main store file to reset all the modules at once.

import Vue from 'vue'
import Vuex from 'vuex'
import createLogger from 'vuex/dist/logger'
import modules from './modules';

Vue.use(Vuex);
const debug = process.env.NODE_ENV !== 'production';

export default new Vuex.Store({
  modules,
  actions: {
    reset({commit}) {
      // resets state of all the modules
      Object.keys(modules).forEach(moduleName => {
        commit(`${moduleName}/RESET`);
      })
    }
  },
  strict: debug,
  plugins: debug ? [createLogger()] : [] // set logger only for development
});
Enter fullscreen mode Exit fullscreen mode

Note the action we created is in the main store file and not inside any module. This action can be triggered anywhere from your Vue component using the following line of code.

this.$store.dispatch('reset');
Enter fullscreen mode Exit fullscreen mode

What’s next?

Liked this article? leave a like and follow to be in the loop. In our upcoming blog posts, we’ll discuss in-depth how to architect the networking aspects of our Vue.js application. We'll cover techniques used to how to manage auth credentials, intercepters and error handling in network requests.

To, get a better understanding of what we do at Locale.ai, read more about the unexplored territories of Geo-Spatial Analytics here.

Special thanks to Chris Fritz for his amazing talk 7 secret patterns Vue consultants don’t want you to know which gave us some of the ideas we used in this article.


Hi there! I work as a UX Engineer at Locale.ai solving Geo-Spatial problems for our B2B customers. If you think you love solving UX problems for users, love designing and want to work with a team of enthusiastic individuals, check out the job openings we have at Locale.
Wanna talk? You can find me on Twitter, Instagram and GitHub.

Originally posted on haxzie.com

Discussion

pic
Editor guide
Collapse
ausmurp profile image
Austin Murphy

On a large scale Vue application, one thing you should absolutely do is utilize SOLID code patterns. Consider creating 3 base store classes to manage CRUD, CRUD arrays, and paged state. Trust me, that will save you a ton of time, and boilerplate.

Collapse
haxzie profile image
Musthaq Ahamad Author

Awesome. Thanks for the suggestion! Would love to learn more about this. Any pointers?

Collapse
ausmurp profile image
Austin Murphy

This is where Typescript proves it's worth, as you can simply create these 3 classes, each extending the previous:
BaseStore<T>
ListStore<T>
PagedStore<T>

You have to use dynamic modules for this to work. Then your base store contains functions to mutate state as a whole, setState(T state), changeState(indexOrProp: string | number), clearState(), etc. And state$ across all 3 classes is:
T
Array<T>
Page<T>

Collapse
syaleni profile image
Siavash Habibi

Would love to see a post on that! I'm working on a project where I'm making a dashboard for a db with more than 20 years of sales data and all I worry about is how large the data can be and how to deal with it. I was thinking about making a base store and then extend the base store for each year... But even one year of data is large enough to impair the app if it's not handled properly.

Collapse
maftalion profile image
Matthew Aftalion

I really like the auto-importer. One optimization though:
Since you're appending all your module files with .store.js you can just change your regex to:
const requireModule = require.context('.', false, /\.store.js$/);
and that allows you to get rid of if (filename === './index.js') return;

Collapse
haxzie profile image
Musthaq Ahamad Author

Yes! Thanks for the suggestion Matthew ✨

Collapse
psnehanshu profile image
Snehanshu Phukon

Nice article Musthaq.
Can you please elaborate on why we should use getters instead of directly accessing state properties?

Collapse
haxzie profile image
Musthaq Ahamad Author

There is a good chance that you might actually try to mutate it outside the store. The store object should be treated as an immutable object and should only be mutated inside the mutation handlers. Since we have used strict: true in our main store file. Vuex will actually throw errors when you try to mutate it outside the mutation handlers. You will likely come across this error a lot if you try to mutate it outside ~ Do not mutate vuex store state outside mutation handlers.

Considering this, it's in our best interest to expose only the getters outside the store :)

Collapse
psnehanshu profile image
Snehanshu Phukon

I understand that, we shouldn't mutate state outside of mutations. But mutating is one thing and reading is different. What I am concerned with is reading. I can directly read store object from this.$store.state, so why do you think it is a good practice to use getters?

Thread Thread
haxzie profile image
Musthaq Ahamad Author

It is totally fine to use this.$store.state, or mapState to map the state properties into a component. But, considering the chances that these might get referenced or mutated, we can take a safe stand. Using getters too much is also a problem since they are actually computed properties for your store they need a bit of extra computation. The pro side of using getters is that we can move more of the logic, (if any involved in processing the state properties) inside your getters and have a safe stand on preventing it from getting mutated.

Collapse
nkoik profile image
Nikos Koikas

Cause you could return a filter or map or whatever you want from state property in vuex and not in component privately

Collapse
cooper09 profile image
cooper09

I'm trying to use your boiler plate but I can seem to initialize my state data to appear in the HelloWord component.

I'm trying to attach screenshots of the HelloWorld component and the template.store.js to illustrate the point. It doesn't look like I can import more than one image so I'll send just the component screen shot.

If you need more info let me know...

Collapse
haxzie profile image
Collapse
cooper09 profile image
cooper09

do you have an email?

Collapse
haxzie profile image
Musthaq Ahamad Author

github.com/haxzie/haxzie.com/issues/3

You can add the screenshots here. I'll try to help 🙂

Collapse
rolandcsibrei profile image
Roland Csibrei

Thanks for the article. There's a typo in mutations. Variable1. Have a nice day!

Collapse
haxzie profile image
Musthaq Ahamad Author

Thanks Ronald :)

Collapse
khalyomede profile image
Khalyomede

Auto module export is on fire!

Collapse
wormss profile image
WORMSS

I believe state can be supplied as a function, so no need to run state = { ...initalState() }. Just state: initalState

Collapse
haxzie profile image
Musthaq Ahamad Author

Thanks for the suggestion! ❤️