The future of State Management in Vue.js

With Vue 3 gaining traction and becoming the new default, many things are changing, and the ecosystem is still shaping. Until recently, the choice for state management was clearly Vuex. But with the new composition API architecture, reactivity mechanism and some new players in the game, the choice now might be different.

Let's explore different ways of managing the state depending on your application size and try to predict what the future of State Management in Vue.js will look like.

Reactivity API

In options API, we can declare reactive data for a component using the data() option. Internally the object returned is wrapped with the reactive helper. This helper is also available as a public API.

If you have a piece of state that should be shared by multiple instances, you can use reactive() to create a reactive object and then import it from multiple components:

With this approach, data are centralized and can be reused across components. This can be a simple option with a minimal footprint for a small application.

Composables

A similar concept, that composition API brought to the table, is using a composable. This pattern, which is very popular in the React world, combined with the powerful reactivity mechanism of Vue can produce some elegant and reusable composables like the following.

Which can be consumed like this:

Full working example of using composables in CodeSandbox

This pattern was originally introduced to replace mixins since composition is much preferred over inheritance nowadays. But it can also be used to share state between components. This is the main idea behind many libraries that have emerged to replace Vuex.

(There is a more elaborate example about using composables for state management later in this article)

Vuex 4

Vuex is not going away. It supports Vue 3 with the same API and minimal breaking changes (which probably other libraries should take note of). The only change is that installation must happen on a Vue instance instead of the Vue prototype directly.

Vuex 4 will still be maintained. However, it’s unlikely that new functionalities will be added to it. It's a good option if you already have a project using Vuex 3 and want to defer the migration to something else for later.

Introducing Pinia

Pinia started as an experiment but quickly became the obvious choice for Vue 3. It offers more than Vuex does with better architecture and a more intuitive syntax that leverages the composition API.

On top of dev tools support (state inspection, timeline with actions and the ability to time travel) and the extensibility of using plugins that Vuex also provides, pinia is type-safe and modular by design. These were the two biggest pain points when using Vuex.

Additionally, the syntax to define your stores is very similar to Vuex modules, which keeps the mental effort of migrating minimal, while the API, when using that store, uses hooks that are very explicit and close to the Vue 3 way of doing things using the composition API.

As you might have noticed, the big difference is that mutations are completely gone. They were often perceived as extremely verbose without any real advantage of using them. Additionally, namespacing is no longer needed. With the new way of importing a store, everything is namespaced by design. This means that in Pinia, you don't have a store with multiple modules but multiple stores that are imported and used on demand.

Pinia Setup Stores

Pinia supports an alternative syntax to define stores. It uses a function that defines reactive properties and methods and returns them very similar to the Vue Composition API’s setup function.

In Setup Stores:

  • ref()s become state properties
  • computed()s become getters
  • function()s become actions

Setup stores bring a lot more flexibility than Options Stores as you can create watchers within a store and freely use any composable.

Thanks to /r/coolcosmos for mentioning Pinia setup stores on this Reddit discussion.

A more advanced and realistic example

Since it looks like Pinia will be around for a while, it is worth spending some time with a more advanced example.

Let us create a hypothetical fellowship store that can hold a list of heroes with the ability to add heroes, kill them (sorry Boromir) and filter them based on that property.

If you are familiar with Vuex it shouldn’t be that difficult to follow this code.

First, every store requires a key which acts as a namespace. In this case, this key is fellowship.

The state is a function that holds all the reactive data of this store and the getters are functions with access to the store as the first parameter. Both state and getters are identical to Vuex.

This statement doesn't apply to actions. The context parameter has gone, and actions have access to the state and getters directly through their context(this). As you might have noticed, actions directly manipulate the state, which was strictly forbidden in Vuex.

Lastly, mutations are completely removed since state manipulation is now happening in actions.

Using that pinia store is simple:

All the magic is happening inside the setup function. The imported useFellowship hook is executed and returned. This will make it available to the component, including both methods and the template. Access to the state getters and actions are done directly using this object.

Of course, this component should be broken into smaller reusable ones but left like this for demo purposes.

If a different component needs to access the same state, it can be done in a similar manner.

You can find the whole application in the following CodeSandbox. Feel free to fork and experiment yourself with it.

Migrating from Vuex to Pinia

Pinia docs are optimistic that code can be reused between the libraries, but the truth is that the architecture is very different, and refactoring will definitely be required. First of all, while in Vuex, we had one store with multiple modules, Pinia is built around the concept of multiple stores. The easiest way to transition that concept to be used with Pinia is that each module you used previously is now a store.

Additionally, mutations do not exist anymore. Instead, these should be converted to actions that directly access and mutate the state.

Actions no longer accept context as their first parameters. They should be updated to access state or any other context property directly. The same applies for rootState, rootGetters etc. since the concept of a single global store doesn't exist. If you want to use another store you need to explicitly import it.

It's evident that for large projects, migration will be complicated and time-consuming, but hopefully, a lot of boilerplate code will be eliminated, and the store will follow a more clean and modular architecture. The conversion can be done module by module rather than converting everything at once. You can actually mix Pinia and Vuex together during the migration so that this approach can work.

Conclusion

Predicting the future is not easy, but as of today, Pinia is the safest bet. It offers a modular architecture, type safety by design and eliminates boilerplate code. If you are starting a new project with Vue 3, Pinia is the recommended choice.

If you are already using Vuex, you can upgrade to version 4 before migrating to Pinia since the process seems straightforward but takes a significant amount of time.

If you want to read more, you can find some additional handpicked material following up. And if you liked this article, don't forget to 👏 or comment with your own experience.

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Fotis Adamakis

Fotis Adamakis

620 Followers

Front End Engineer @ Glovo // Vue.js Athens Meetup Coorganizer