Your Web News in One Place

Help Webnuz

Referal links:

Sign up for GreenGeeks web hosting
June 12, 2018 12:47 pm

How to Build Complex, Large-Scale Vue.js Apps With Vuex

It's so easy to learn and use Vue.js that anyone can build a simple application with that framework. Even novices, with the help of Vue's documentation, can do the job. However, when complexity comes into play the things get a bit more serious. The truth is that multiple, deeply nested components with shared state can quickly turn your application into an unmaintainable mess.

The main problem in a complex application is how to manage the state between components without writing spaghetti code or producing side effects. In this tutorial you'll learn how to solve that problem by using Vuex: a state management library for building complex Vue.js applications.

What is Vuex?

Vuex is a state management library specifically tuned for building complex, large-scale Vue.js applications. It uses a global, centralized store for all the components in an application taking advantage of its reactivity system for instant updates.

The Vuex store is designed in such a way that it is not possible to change its state from any component. This ensures that the state can only be mutated in a predictable manner. Thus your store becomes a single source of truth: every data element is only stored once and is read-only to prevent the application's components from corrupting the state that is accessed by other components.

Why Do You Need Vuex?

You may ask: Why I need Vuex at first place? Can I just put the shared state in a regular JavaScript file and import it in my Vue.js application?

You can, of course, but compared to a plain global object Vuex store has some significant advantages and benefits:


  • The Vuex store is reactive. Once components retrieve a state from it, they will reactively update their views every time the state changes.

  • Components cannot directly mutate the store's state. The only way to change the store's state is by explicitly committing mutations. This ensures every state change leaves a trackable record which makes the application easier to debug and test.


  • You can easily debug your application thanks to the Vuex integration with Vue's DevTools extension.


  • The Vuex store gives you a bird's eye view on how everything is connected and affected in your application.


  • It's easier to maintain and synchronize the state between multiple components, even if component hierarchy changes.


  • Vuex makes direct cross-components communication possible.


  • If some component is destroyed, the state in Vuex store will remain intact.

Getting Started With Vuex

Before we get started I want to make clear several things: First, to follow along this tutorial you need to have a good understanding of Vue.js and its components system, or at least minimal experience with the framework. 

Also, the aim of this tutorial is not to show you how to build actual complex application; the aim is to focus your attention more on Vuex concepts and how you can use it to build complex applications. For that reason, I'm going to use very plain and simple examples without any redundant code. Once you fully grasp the Vuex concepts you will be able to apply them on any level of complexity.

Finally, I'll be using ES2015 syntax. If you are not familiar with it, you can learn it here.

And now, let's get started!

Setting Up a Vuex Project

The first step to get started with Vuex is to have Vue.js and Vuex installed on your machine. There are several ways to do that but we'll use the easiest one. Just create an HTML file and put the needed CDN links:

I use some CSS to make the components look nicer, but you don't need to worry about that CSS code. It only helps you to gain a visual notion about what is going on. Just copy and paste the following inside the <head> tag:

Now, let's create some components to work with. Inside the <script> tag, right above the closing </body> tag, put the following Vue code:

Here, we have a Vue instance, a parent component, and two child components. Each component have a heading "Score:" where we'll output the app state.

The last thing you need to do is to put a wrapping <div> with id="app" right after the opening <body>, and then place the parent component inside:

So, the preparation work is done. We're ready to move on.

Exploring Vuex

State Management

In real life we deal with complexity by using strategies to organize and structure the content we want to use. We group related things together in different sections, categories, etc. Just as how in a book library, the books are categorized and put in different sections so that we can easily find what we are looking for. The same is true for Vuex. It arranges the application data and logic related to state in four groups or categories: state, getters, mutations, and actions.

State and mutations are the base for any Vuex store:



  • state is an object that holds the state of the application data.


  • mutations is also an object containing methods which affect the state.

Getters and actions are like logical projections of state and mutations:



  • getters contains methods used to abstract the access to the state, and to do some preprocessing jobs, if needed (data calculating, filtering, etc.)


  • actions are methods used to trigger mutations and execute asynchronous code.

Let's explore the following diagram to make the things a bit clear:

Vuex State Management Workflow Diagram

In the left side, we have an example of Vuex store, which we'll create later on in this tutorial. In the right side, we have a Vuex workflow diagram, which shows how the different Vuex elements work together and communicate each another.

In order to change the state, a particular Vue component must commit mutations (eg. this.$store.commit('increment', 3)), and then, those mutations change the state (score becomes 3). After that, the getters are automatically updated thanks to Vue's reactive system and they render the updates in the component's view (with this.$store.getters.score). 

Mutations cannot execute asynchronous code, because this would make it impossible to record and track the changes in debug tools like Vue DevTools. To use asynchronous logic you need to put it in actions. In this case a component first will dispatch actions (this.$store.dispatch('incrementScore', 3000)) where the asynchronous code is executed, and then those actions will commit mutations, which will mutate the state. 

Create a Vuex Store Skeleton

After we explored how Vuex work, let's create the skeleton for our Vuex store. Put the following code above the ChildB component registration:

To provide global access to the Vuex store from every component we need to add the store property in the Vue instance:

Now, we can access the store from every component with this.$store variable.

So far, if you open the project with CodePen in the browser, you should see the following result.

App Skeleton

State Properties

The state object contains all of the shared data in your application. Of course, if needed, each component can have its own private state too.

Imagine that you want to build a game application and you need a variable to store the game's score. So, you put it in the state object:

Now, you can access the state's score directly. Let's go back to the components and reuse the data from the store. In order to be able to reuse reactive data from the store's state, you should use computed properties. So, let's create a score() computed property in the parent component:

In parent component's template put the {{ score }} expression:

And now, do the same for the two child components.

Vuex is so smart that it will do all the work for us to reactively updates the score property whenever the state changes. Try to change the score's value and see how the result updates in all of the three components.

Creating Getters

It is, of course, good that you can reuse the this.$store.state keyword inside the components, as you saw above. But imagine the following scenarios:


  1. In a large-scale application, where multiple components access the state of the store by using this.$store.state.score, you decide to change the name of score. This means that you have to change the name of the variable inside each and every component that uses it! 

  2. You want to use a computed value of the state. For example, let's say you want to give the players a bonus of 10 points when the score reaches 100 points. So, when the score hits 100 points, 10 points bonus are added. This means each component has to contain a function that reuses the score and increment it with ten. You will have repeated code in each component, which is not good at all!

Fortunately, Vuex offers a working solution to handle such situations. Imagine the centralized getter that accesses the store's state and provides a getter function to each of the state's items. If needed, this getter can apply some computation to the state's item. And if you need to change the name of some of the state's properties, you only change them in one place, in this getter. 

Let's create a score() getter:

A getter receives the state as its first argument, and then uses it to access the state's properties.

Note: Getters also receive getters as the second argument. You can use it to access the other getters in the store.

In all components, modify the score() computed property to use the score() getter instead of state's score directly.

Now, if you decide to change the score to result you need to update it only in one place: in the score() getter. Try it out in this CodePen!

Creating Mutations

Mutations are the only way allowed to change the state. Triggering changes simply means committing mutations in component methods.

A mutation is pretty much an event handler function that is defined by name. Mutation handler functions receive a state as a first argument. You can pass an additional second argument too, which is called the payload for the mutation. 

Let's create a increment() mutation:

Mutations cannot be called directly! To perform a mutation, you should call the commit() method with a name of the corresponding mutation and possible additional parameters. It might be just one, as is the step in our case, or they might be multiple wrapped in an object.

Let's use the increment() mutation in the two child components by creating a method named changeScore():

We are committing a mutation instead of changing this.$store.state.score directly, because we want to explicitly track the change made by the mutation. This way we make our application logic more transparent, traceable and easy to reason about. In addition, it makes it possible to implement tools, like Vue DevTools or Vuetron, that can log all mutations, take state snapshots, and perform time-travel debugging.

Now, let's put the changeScore() method into use. In each template of the two child components, create a button and add a click event listener to it:

When you click the button, the state will be incremented by three, and this change will be reflected in all components. Now we have effectively achieved direct cross-components communication, which is not possible with the Vue.js built-in "props down, events up" mechanism. Check it out in our CodePen example.

Creating Actions

An action is just a function that commit a mutation. It changes the state indirectly which allows for executing of asynchronous operations. 

Let's create a incrementScore() action:

Actions get the context as first parameter, which contains all methods and properties from the store. Usually we just extract the parts we need by using ES2015 argument destructing. The commit method is one we need very often. Actions also get a second payload argument, just like mutations.

In the ChildB component modify the changeScore() method:

To call an action we use dispatch() method with a name of the corresponding action and additional parameters just as with mutations.

Now, the "Change Score" button from ChildA component will increment the score by three. The identical button from ChildB component will do the same, but after 3 seconds delay. In the first case we execute synchronous code and we use a mutation, but in the second case we execute asynchronous code and we need to use an action instead. See how it all works in our CodePen example.

Vuex Mapping Helpers

Vuex offers some useful helpers which can streamline the process of creating state, getters, mutations, and actions. Instead of writing those functions manually we can tell Vuex to create them for us. Let's see how it works.

Instead of writing the score() computed property like this:

We just use the mapState() helper like this:

And the score() property is created automatically for us.

The same is true for the getters, mutations, and actions. 

To create the score() getter we use mapGetters() helper:

To create changeScore() method we use mapMutations() helper like this:

When used for mutations and actions with payload argument, we must pass that argument in the template where we define the event handler:

If we want changeScore() to use an action instead of a mutation we use mapActions() like this:

Again, we must define the delay in the event handler:

Note: All mapping helpers return an object. So, if we want to use them in combination with other local computed properties or methods we need to merge them into one object. Fortunately with the object spread operator (...)  we can do it without using any utility. 

In our CodePen you can see an example of how all mapping helpers are used in practice.

Making the Store More Modular

It seems that the problem with complexity constantly obstructs our way. We solved it before by creating Vuex store, where we made the state management and components communication easy. In that store we have everything in one place, easy to manipulate and easy to reason about. However, as our application grows this easy to manage store file becomes larger and larger, and, as a result, harder to maintain. Again, we need some strategies and techniques for improving the application structure by returning it to its easy to maintain form. In this section, we'll explore several techniques which can help us in this undertaking.

Using Vuex Modules

Vuex allows us to split the store object into separate modules. Each module can contain its own state, mutations, actions, getters, and other nested modules. After we create the needed modules, we register them in the store.

Let's see it in action:

In the above example, we created two modules, one for each child component. The modules are just plain objects, which we register as scoreBoard and resultBoard in the modules object inside the store. The code for childA is the same as that in the store from the previous examples. In the code for childB we add some changes in values and names.

Let's now tweak ChildB component to reflect the changes in the resultBoard module. 

In ChildA component the only thing we need to modify is the changeScore() method:

As you can see, splitting the store into modules makes it much more lightweight and maintainable, while still keeps its great functionality. Check out the updated CodePen to see it in action.

Namespaced Modules

If you want or need to use one and the same name for particular property or method in your modules, then you should consider namespacing them. Otherwise you may observe some strange side effects, such as executing all the actions with same names, or getting the wrong state's values. 

To namespace a Vuex module you just set the namespaced property to true.

In the above example we made the property and method names the same for the two modules. And now we can use a property or method prefixed with the name of the module. For example, if we want to use the score() getter from the resultBoard module, we type it like this: resultBoard/score. If we want the score() getter from the scoreBoard module, then we type it like this: scoreBoard/score

Let's now modify our components to reflect the changes we made. 

As you can see in our CodePen example, we can now use the method or property we want and get the result we expect.

Splitting the Vuex Store Into Separate Files

In the previous section, we improved the application structure to some extent by separating the store into modules. We made the store cleaner and more organized, but still all of the store code and its modules lie in one and the same big file. 

So the next logical step is to split the Vuex store in separate files. The idea is to have an individual file for the store itself and one for each of its objects, including the modules. This means having separate files for the state, getters, mutations, actions, and for each individual module (store.jsstate.js, getters.js, etc.) You can see an example of this structure at the end of the next section.

Using Vue Single File Components

We've made the Vuex store as modular as we can. The next thing we can do is to apply the same strategy to the Vue.js components too. We can put each component in a single, self-contained file with a .vue extension. To learn how this works you can visit the Vue Single File Components documentation page

So, in our case, we'll have three files: Parent.vueChildA.vue, and ChildB.vue

Finally, if we combine all three techniques we'll end up with the following or similar structure:

In our tutorial GitHub repo you can see the completed project with the above structure.

Recap

Let's recap some main points you need to remember about Vuex:

Vuex is a state management library, which help us to build complex, large-scale applications. It uses a global, centralized store for all the components in an application. To abstract the state we use getters. Getters are pretty much like computed properties and are ideal solution when we need to filter or calculate something on runtime.

Vuex store is reactive and components cannot directly mutate the store's state. The only way to mutate the state is by commiting mutations, which are synchronous transactions. Each mutation should perform only one action, must be as simple as possible, and only responsible for updating just a piece of state.

Asynchronous logic should be encapsulated in actions. Each action can commit one or more mutations, and one mutation can be committed by more than one action. Actions can be complex but they never change the state directly.

Finally, modularity is the key to maintainability. To deal with complexity and make our code modular we use the "divide and conquer" principle and the code splitting technique.

Conclusion

So folks, that's it. You already know the main concepts behind Vuex and you are ready to start applying them in practice.  

For the sake of brevity and simplicity, I omitted intentionally some details and features of Vuex, so you'll need to read the full Vuex documentation to learn everything about Vuex and its feature set.


Original Link:

Share this article:    Share on Facebook
No Article Link

TutsPlus - Code

Tuts+ is a site aimed at web developers and designers offering tutorials and articles on technologies, skills and techniques to improve how you design and build websites.

More About this Source Visit TutsPlus - Code