A New State Management for Vue.js

You might have felt like me choosing a state management solution for your Vue.js application: Relieved, that there is exactly one, well designed and officially supported package. One simple decision, where in todays Frontend Development the amount of choices to be made can be overwhelming.

However, with growing experience and bigger-getting applications, it seemed to me that one could develop a new state management for Vue.js that would make enough of a difference to justify one more decision being made.

My attempt to develop such a solution resulted in two plugins:

The combination of both should bring you:

  • Simplicity
    Just this.MyModel.key and this.MyModel.update(payload). No huge API, that exposes implementation details (state, getters, commit, dispatch).
    Easy Hot Module Replacement and Lazy-Loading.
  • Flexible scope
    It is designed to support application-wide and local state, and can still be hydrated from SSR or localStorage.
  • Learning & refactoring
    The state is composed of Vue components. That means: almost no new APIs and patterns to learn, plus seamless refactoring of your application.
  • Power
    All plugins and native Vue capabilities are accessible by design, without any configuration ( this.$router, this.$apollo, created()...).
  • History
    VueHistory gives you a detailed view of what’s going on, even in complex scenarios, handling async processes, error tracking and deepl call chains.

At the end of this article I hope you’ll not only want to npm install right away, but that both plugins will become your daily companion in developing applications with Vue.js.

How does this work?

In Vue States what would be a module in Vuex becomes a Vue component that we will call a Model.

Define

export default {
  name: 'Inventory',
  // state -> data()
data() {
return {
products: [],
}
},
  // getters -> computed 
computed: {
productsCount() {
return this.products.length
},
},
  // mutations and actions -> methods
methods: {
async loadProducts() {
this.saveProducts(
// plugins ($apollo, $router, ...) are accessible!
await this.$apollo.query({ query: productsQuery })
)
},
saveProducts(products) {
this.products = products
}
},
}

Why using Vue components? They are incredible easy to write, perfectly integrated — and they offer a simple and well-known API, hiding all implementation details. Also think about how easy it becomes to integrate that state management into an existing Vue.js application.

Provide

Models can be provided from any component down to its children. The syntax is simple:

import Inventory from '@/models/Inventory'
import Cart from '@/models/Cart'
export default {
   name: 'ExampleShop'
   // both Models will be provided down the tree
// they are accessible as `this.Inventory` or `this.Cart` on the
// "ExampleShop" and on every child component that injects them.
   models: {
Inventory,
Cart,
}
}

By using the provide/inject pattern, we can not only realize application-wide state, but adjust the states scope to any branch of the Component Tree (let’s say the current route, a form, …).

In the example below a “User” Model is provided as application-wide state. The Inventory and the Cart are scoped to the ExampleShop and could be used multiple times concurrently.

Models provided down the Component-Tree.

Inject

<template>
<section>
<div
v-for="product of Inventory.products"
:key="product.id"
>
{{ product.name }}
<button @click="Cart.addProduct(product.id)">
Add
</button>
</div>
</section>
</template>
<script>
export default {
   name: 'ProductList'
   injectModels: ['Inventory', 'Cart'],
}
</script>

Every child component can inject the model using the injectModels option. The key must match the one used in models: {} and is then added to this , making the code very readable.

Keeping track

Vue History in action.

What Vue History does is to wrap all methods on components it is activated on (using the option{ history: true } ). It tracks nested calls, errors and promises to give you an understanding of what happens in the Models and why (what caused it). Gone are the days when you wondered what action caused the action that caused the mutation that caused the error.

To work properly, Vue History enforces two simple rules:

Rule 1: Methods only

Data may only be changed by methods. No direct double-data-binding, no state changes from outside a Model. Otherwise tracking wouldn’t be reliable.

Rule 2: Synchronous state changes

As there is no distinction between mutations and actions, state could be updated asynchronously inside a method. Vue History prevents this, since otherwise you wouldn’t know at what time the state actually changed. To store the result of an asynchronous process (let’s say an API call) you just need to call another method:

import api from '@/api'
export default {
data() {
return {
apiCalls: 0,
data: null
}
},
methods: {
async loadDataGood() {
// valid
this.apiCalls += 1
// good
this.saveData(await api.loadData())
},
async loadDataBad() {
// valid so far
this.apiCalls += 1
// bad - you wouldn't know at what time 'data' was mutated
this.data = await api.loadData()
},
saveData(data) {
this.data = data
},
},
}

When to use

If you need to choose, there are a lot of points to be considered. How experienced are you and your team? How big and complex is the application? What is its expected lifetime and what the dynamics in development and maintenance? Does it need to be rock-solid, or can you try new things?

I would recommend using Vue States and Vue History especially in complex applications, since I believe that with those two the increase in complexity of managing state will be less coupled to the size of an application. Also if you just like the simplicity, the integration with other plugins and the analytics via Vue History — and as long as you are carefully considering the implications of provide-inject when creating non-global state.

Otherwise, there can be good reasons to rely on existing best practices, a big community and battle-tested solutions, which Vue States can’t provide so far.

Lastly, you can use Vue States alongside any other state management, just to give it a try ;-). There are now also some basic examples at https://github.com/JohannesLamberts/vue-states-examples that might help to get started.

Bottom line

I hope the introduction above gave you a good impression of what this State Management is capable of and how simple it is to use. Be sure to check the README.md for more.

npm install @sum.cumo/vue-states @sum.cumo/vue-history

In the initial version of this article, the plugin was referred to as “Vue Models”. We renamed the the plugin to “Vue States” to avoid confusion with a similarly named package on npm. Also the “When to use” section was added.