Published on

Vue - State Management

In this article, state management best practices will be explained.

State Categories

Component State

This is the state that only a component needs, and it is not meant to be shared anywhere else. But you can pass it as a props to the children components if needed. Most of the time, you want to start from here and lift the state up if needed elsewhere. In Vue components, this can be accomplished by using data and computed properties.

Data

When a Vue instance is created, it adds all the properties found in its data object to Vue's reactivity system. When the values of those properties change, the view will reacts and updates to match the new values.

Common Mistakes
It should be noted that properties in data are only reactive if they existed when the instance was created. That means if you add a new property after the instance has been created, it will not trigger any view updates.
If you know you will need a property later, but it starts out empty or non-existent, you will need to set some initial value depending on its type. For example, the initial value can be 0 for a Number type, empty string ("") for a String type, false value for a Boolean type, empty array () for an Array type, etc.

data() {
    return {
        name: '',
        age: 0,
        isRegistered: false,
        skills: [],
        error: null
    }
}

Computed

Suppose you have a message property in the data, but you need to manipulate the value prior to displaying it on the view template. Although you could write it as an in-template expressions, they are meant for simple operations. Putting too much logic in your templates can make them bloated and hard to maintain. For example:

<div id="example">
  <p>Original message: "{{ message }}"</p>
  <p>Reversed message: "{{ message.split('').reverse().join('') }}"</p>
</div>

At this point, the template is no longer simple and declarative. This is where a computed property could prevent us from writing any complex logic in the view template. Let's change the above example's implementation into this:

<div id="example">
  <p>Original message: "{{ message }}"</p>
  <p>Reversed message: "{{ reversedMessage }}"</p>
</div>
data() {
    return {
        message: "Hello World!"
    }
},
computed: {
    reversedMessage() {
        return this.message.split('').reverse().join('');
    }
}

Now you may wonder, we can achieve the same result by invoking a method with the similar approach, so what is the difference? The difference is that computed properties are cached based on their reactive dependencies. A computed property will only re-evaluate when some of its reactive dependencies have changed. In comparison, a method invocation will always run the function whenever a re-render happens.

Computed Caching vs Methods

Passing Data to Child Components with Props

As mentioned before, we can pass the component state (data and computed properties) as a props to the children components if needed. Props are custom attributes you can register on a component. When a value is passed to a props attribute, it becomes a property on that component instance. A component can have as many props as you would like and by default, any value can be passed to any prop and we can access this value on the component instance, just like with data.

Therefore, I recommend you to understand how the communication between parent and child components can be established by reading these documentations:

Parent to child communicationOfficial guide - Props

Child to parent communicationOfficial guide - Events

Application State

What if a variable inside the data is a value that needed to be accessed from another component that might completely not related with? For example, we may need a component to be responsible in displaying the variable value and another (sibling component) to manipulate it. If we want to share a variable value between multiple components, this variable doesn’t only become component level data but also application level data. There are several ways to accomplish communication between any components and manage this application level state.

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.

Using Vuex as a state management

Components Can Still Have Local State Using Vuex doesn't mean you should put all the state in Vuex. Although putting more state into Vuex makes your state mutations more explicit and debuggable, sometimes it could also make the code more verbose and indirect. If a piece of state strictly belongs to a single component, it could be just fine leaving it as local/component state. You should weigh the trade-offs and make decisions that fit the development needs of your app.

Simple Global Store

If you've never built a large-scale SPA and jump right into Vuex, it may feel verbose and daunting. That's perfectly normal - if your app is simple, you will most likely be fine without Vuex. A simple store pattern may be all you need. This can be performed by creating a store pattern that involves sharing a data store between components. The store will then manage the state of our application as well as the methods that are responsible in changing the state.

Simple state management from scratch

EventBus

EventBus is an instance that is used to enable isolated components to subscribe and publish custom events between one another, hence it can be used to share a data communication flow between multiple components. It’s a pattern to achieve very common thing: transform data not only from parent to child components, but also in reverse order, even between any components. Event bus allows us to also emit events between components and simply use methods for them.

Using event bus in Vue.js to pass data between components

How To Create a Global Event Bus in Vue 2

EventBus is not a recommended approach to application wide data management
Although EventBus can achieve the desirable data communication flow, however this approach can be hard to debug as the data cannot be tracked on the developer tools, since each of the events emitted aren't being tracked and can be fired from anywhere in our application. This makes things hard to maintain and can be really quickly make the code frustrating to work with and can become a source of bugs.
> So please, use it with caution and as your last choice.
> Vue.js: why event bus is bad idea

Using Vuex as a state management

As we have learned before, the simplicity of establishing data communication between parent and child components quickly breaks down when we have multiple components that share a common state, for example:

  1. Multiple views may depend on the same piece of state
  2. Actions from different views may need to mutate the same piece of state For problem 1, passing props can be tedious for deeply nested components, and simply doesn't work for sibling components.

For problem 2, we often find ourselves resorting to solutions such as reaching for direct parent/child instance references or trying to mutate and synchronize multiple copies of the state via events.

Both of these patterns are brittle and quickly lead to unmaintainable code. These are the problems Vuex trying to solve, with the idea of defining and separating the concepts involved in state management and enforcing rules that maintain independence between views and states, we give our code more structure and maintainability.

Vuex - Workflow diagram

This diagram shows the workflow of the Vuex library:

Vuex - Core concepts

These official guides will help you understand the concepts of Vuex and how the things work.

Vuex - State management structure in a large scale application

Most of the time, our application grow bigger since there are many additional features and changes in the requirements. Large applications can often grow in complexity, due to multiple pieces of state scattered across many components and the interactions between them. Hence, we need to manage the state in such a well planned structure. This task can be quite hard, but Vuex helps to make this task simpler.

Large-scale Vuex application structures

Vuex - Files structure example

src/
|
+-- store/            # vuex store

    |

    +-- index.js      # main file to initialize vuex store. import the store modules here.

    |

    +-- modules/      # store modules inside the app, e.g: customer and services

        |

        +-- customer/               # customer module in the vuex store

        |   |

        |   +-- index.js                # to access this state: this.$store.state.customer.<state name>

        |   |

        |   +-- getters.js              # to access this getters: this.$store.getters["customer/<getter name>"]

        |   |

        |   +-- mutations.js            # to access this mutations: this.$store.commit("customer/<mutation name>", parameter)

        |   |

        |   +-- actions.js              # to access this actions: this.$store.dispatch("customer/<action name>", parameter)

        |

        +-- payment/                 # payment module in the vuex store. store module can have its own modules too, e.g: purchase and refund

            |

            +-- index.js                # import the purchase and refund store modules here

            |

            +-- purchase/               # purchase module in the payment module. set namespaced property to true in the main store module file.

            |   |

            |   +-- index.js                # to access this state: this.$store.state.payment.purchase.<state name>

            |   |

            |   +-- getters.js              # to access this getters: this.$store.getters["payment/purchase/<getter name>"]

            |   |

            |   +-- mutations.js            # to access this mutations: this.$store.commit("payment/purchase/<mutation name>", parameter)

            |   |

            |   +-- actions.js              # to access this actions: this.$store.dispatch("payment/purchase/<action name>", parameter)

            |

            +-- refund/                 # refund module in the payment module. set namespaced property to true in the main store module file.

                |

                +-- index.js                # to access this state: this.$store.state.payment.refund.<state name>

                |

                +-- getters.js              # to access this getters: this.$store.getters["payment/refund/<getter name>"]

                |

                +-- mutations.js            # to access this mutations: this.$store.commit("payment/refund/<mutation name>", parameter)

                |

                +-- actions.js              # to access this actions: this.$store.dispatch("payment/refund/<action name>", parameter)

In the example above, you will notice that it's getting cumbersome to access the "nested" store module's state, getters, mutations and actions. To resolve this, Vuex already provided helpers such as mapState, mapGetters, mapMutations and mapActions.

Best practices for Vuex mapping

Be careful of overusing the Vuex getters
> Vuex getters are great, but don’t overuse them

Author
  • Label
    Author
    Name
    Gingsir Pradipta

Copyright © 2020- Xtremax