Structuring A Massive Vuex Store


Dive deeply into the architecture of our massive Vuex store. This solution will always be easy to scale, read, and maintain no matter how huge your app is.


Hello vue.js London! I'm so glad for speaking here today. My name is Domagoj Widojk. Feel free to call me Dom because it's just way easier to pronounce. I work at Orbital Witness, a cool London tech startup somewhere between prop tech and legal tech, and I also live in London. Today I will be talking about structuring a massive massive UX store. How to create an architecture that is scalable, flexible and maintainable. Currently we're using this architecture at Orbital Witness. We have zero problems with it. Let's dive in. I will start with an example of a simple store. I will be expanding that store in every step and presenting you the challenges we need to solve and obviously sell them. We're going to start with the simplest store possible. We're using create store from UX and we're creating an empty store with empty state actions, mutations and getters. I won't be explaining those. I assume that you already know that. I will focus on how to create a store and make it scalable, make it appropriate for usage in huge apps. Let's add some properties here now. Usually in your huge apps you're going to have thousands of different properties. Right now only two of them are enough. Username and organization name and obviously some mutations to set both. What would happen if this file had thousands of properties? You can imagine just like adding adding adding things here and file will just become bigger and bigger and bigger. We don't want to do that. If we're going to do that, why don't we just keep all of our code in app.vue and forget about components? Jokes aside. Let's see how can we fix this problem. To do so, we're going to use a native UX feature called modules. modules allow us to separate the concerns, to isolate different parts of the store and to make mini stores within our massive store. Take a look at user module for example. Here we took everything connected to the user. So name and its mutation to set that name. We are also passing actions and getters as an empty object because we don't have any right now. And what is really important here is that namespace attribute which is set to true. Namespace attribute allows us to register those properties for that mini store to local namespace of that module. In that way we can access those properties from any other module which is good because namespace set to true prevents us from accidentally accessing some of the local modules properties. You might think of that as a limitation, right? Because sometimes you actually need to trigger some methods from the different modules but you can't right now. Well, you can but you need to have an intent to do it. So it is possible but you just need to have an intent. But this is definitely good because we're preventing those accidental triggering. You can have same state properties with the same name or mutations, actions, getters with the same name. Namespace set to true allows us that and its default value is false. In that way all properties will be registered at the global namespace. Maybe you want to do that but just be careful about it. Organization module is literally the same. So we took everything connected to organization, set namespace to true and we're exporting default that object. If we take a look at index.js file, the things have changed a bit. So now we have user module and organization module. We need to import them and then we need to pass them to modules object for mapping those imported modules to a certain name. Right now it's user and organization. This is cool. So we've already separated our store in different modules. They are isolated. We can't access them by an accident but there's still something we need to solve. Let's see how can we use these mutations right now. We as developers hate hardcoded strings. They're not maintainable, they're not scalable. If you need to change something in your project you literally have to search the whole codebase and replace it. You can do search replace over the whole codebase and they make crazy mess. It's a recipe for bugs. Don't use hardcoded strings. Right now we don't have any other options because we can trigger mutations in two ways. First one, directly accessing the store object and triggering for example actions and mutations you would trigger commit and dispatch from methods from that store object. You still need to pass hardcoded strings there but I definitely don't recommend doing it that way. We have UX helpers, map mutations, map getters, map actions and map state for that. Why is that good? Well imagine massive apps with many many many components and files and massive massive store. If you're accessing that store, that global store which controls your core application directly it's just not clear. You're gonna have thousands and tens of thousands or even more references directly to this dot store object and it will just all be a mess. With map mutations and all the other helpers you literally see oh this component my random component is using this part of the store and it's clear you have it all in one place. You still have to use hardcoded strings right now but we're gonna solve that. Take a look at map mutations. Because our modules are namespaced we need to pass module name as a first argument. That's why we in the first scenario passed user. Second argument is an array of mutations so just one string here set username. Let's solve this. To do so we need to extract the strings. It's pretty pretty simple. You need to do three things. Create a const, export it and then set that const value as a mutation name. That's it. The same thing in organization model creating a const, exporting it so that you can import it in the other files and setting it as a mutation name. That's it for the modules. The other thing we need to add is module object which will just be a list of all of our module names. Then we need to import that object to our index.js file and just use values from that object rather than hardcoded strings for user and organization. Pretty simple right? Let's see how we improved our usage by doing this. If we take a look at our random component now we obviously need to import set user name, set organization name and module object. But right now in map mutations we're not passing hardcoded strings anymore. We're passing a module name and the second argument we're not passing an array anymore rather an object where we map a mutation name to a name we want to use in our component. So right now it's set organization name and set user name. You can set it to any way you want. It's completely up to you. Good! So our store is split into modules right now. We have those mini stores. Their usage is so clear. Not only that we have map mutations and possibly all the other helpers like mega address map actions, we also need to import those methods to use them. So immediately when you open your component at the top of the file you will see, okay, I use set user name here and set organization name here and it's so clear. Oh, this component uses these parts of the store. It's really important when your app scales that you have that clear, clear picture rather than just accessing store object directly. And because of namespace set to true our store is isolated too. We don't have hard-coded strings. Yeah, best thing. But there's still one problem we need to solve. Even though our app is split into modules now, those modules can be pretty, pretty big and we need to somehow solve that and split them even more. To do so, we need to take a look at our current folder structure first. At the top we have root store folder. Then within it we have index.js, modules.js, organization module.js and user module.js. This is pretty cool for small apps, mid-sized apps, but it's not good for massive apps. That's why we need to refactor this structure. This is our desired structure. Store is the same, so that root folder is at root, index.js and modules.js inside, but right now we've added another folder called modules. And within that folder we're going to have multiple folders with the separate modules. So right now we have organization and user. Within organization we have actions, getters, index, mutations and types. And within the user we have the same things, I just didn't want to expand it because it would look bad. Let's take a look now at the details of these files. How can we do that? So the title says splitting the modules. That's basically what we need to do right now. We need to grab certain parts of our modules and just create separate files for them, export default those objects and we're good to go. First we have types.js file. You remember those hardcoded strings. So this types.js, basically it's a list of all consts, actions, mutations and getters. Everything we have and use in this module. It serves as a kind of documentation as well. You can look here. Oh, okay, this module is using these methods. It's pretty reasonable, pretty clear. It's not clotted with any logic, just consts here. So yeah, as I said, a nice sort of a documentation. Then we have mutations. We obviously need to import consts from types and set mutation names to those const values. Obviously you're going to have multiple mutations here. Right now we have only a single one and then you need to export default that object. With actions, currently we don't have any actions. That's why we're exporting default an empty object. That is possible to happen for some new modules. I highly recommend exporting default an empty object rather than not having anything at all here or not having a file at all, just so you can be consistent and then when you add something inside, you already have that boilerplate for it. Everything with actions is the same as with mutations. The same goes on for getters. Right now we're just exporting an empty object, but the process is the same as with mutations and actions. Finally, we have our index.js. That's not that index.js in root store folder. That's index.js within organization folder, within our module. Here we need to import actions, mutations and getters. Then we need to create state object. You might think, oh, we can maybe create a separate file from state and import it too. That's true. You can do that. You don't have to do that. We don't do it because state object doesn't have any logic connected to it. We love that every time we open an index.js file that we can see everything there. It's just what we do. You can create a separate file for it. You don't have to. It's completely up to you. I definitely recommend creating separate files for actions, getters and mutations because there is a lot of logic behind those methods. We trust those logic. That's why we don't want to look at it. We just want to import it and have a clean look of the state. Finally, we need to export default.object by setting, of course, namespace to true, passing state, mutations, actions and getters. The process is the same for all modules. I won't repeat the same thing for user module right now because it's literally the same. What we need to do now is update our root store. To do so, we need to import user module, import organization module and import our module object with all the names. Then we're going to declare global state, global actions, global mutations and global getters. Be careful with them because they are accessible at the global namespace. That's the place where they will be registered. Right now, they're empty, but the process is the same as with the ones which are namespaced. The process for creating them. Then we need to create modules folder. We're going to map user module to username and organization module to organization name. We're getting from the module const. Finally, we need to use create store from vuex. We need to export default.object with state, actions, mutations, getters, global ones and modules. That's it. We have it now. We have that clean, scalable and flexible architecture of the store for your massive app. Let's take a look at it. At the top, we have index.js file, our root store. That root store has its own global state, actions, mutations and getters. But from the second layer, that root store imports all the different modules you have. Right now, we have only organization and user, but you're going to have much more, trust me. And then all those different kinds of modules from the second layer import their own isolated actions, getters, mutations, state from the third layer. Right now, at Orbital Witness, we don't have any problems with this architecture. These three layers are enough. But if you're a pro player, then you can actually create a fourth layer. So you can split, for example, your mutations into different clusters. You can separate them by concerns and obviously import it to some index file and then import it to the third layer. And that's something for massive, massive apps, I would say. We don't need it right now. We could do it, but we're not doing it right now. If you have something like massive, massive, massive app, then you can create a fifth layer. And this process, you can dig as deep as you want. You can have as many layers as you want. This architecture supports massive apps, the massivest apps you can see. And that's everything I wanted to say here. Thank you all for your attention. It was so nice speaking here. Feel free to drop any questions. If you don't have anything on your mind right now, you can find my information on the conference's website under speakers. You have my email there. We can connect on Twitter. Also, check out my GitHub profile because this code is publicly available there. So you can use it as a boilerplate for your new app. Or if you want to refactor your current store, you can take a look at these practices. You don't need to refactor the whole store at once. So take your time, take days, weeks, months, whatever you need. And yeah, you can do it partially. That's everything from me. Thanks a lot. vue.js London.
21 min
20 Oct, 2021

Check out more articles and videos

We constantly think of articles and videos that might spark Git people interest / skill us up or help building a stellar career

Workshops on related topic