Architecture | Reactive Vue 3 State
Introduction
There’s been a lot of discussion about state management in the upcoming Vue 3 framework. Some writers go as far as declaring Vuex dead. Reactivity is all we need, is the claim. Just like blockchain was supposed to cure all problems of modern civilisation ;-) Jokes aside, it does look like an intriguing possibility, so we’ve taken a challenge and explored it in this article.
TL;DR
Vue 3 Reactivity System, now free of UI confines, can be efficiently employed as a powerful tool to handle state. This requires extra plumbing though, with no batteries included. If you want to get straight to technical details, scroll down to Reactive proposal for application state chapter below.
Source code for the article can be found at https://bitbucket.org/letsdebugit/vue-3-state. It uses the simple build-less architecture presented in my recent article about Vue JS 3 Application Without Build.
No need for Vuex?
The official Vue 3 documentation mentions a simple yet efficient pattern for small-scale state management. Find it here: https://v3.vuejs.org/guide/state-management.html#simple-state-management-from-scratch.
It’s quite sufficient for many small applications, while it’s advised to use Vuex for more complex scenarios. But the community is exploring alternative ways to manage state. Some of them come up with a bold conclusion: Vuex is no longer needed. On closer inspection I’m still not convinced. However ingenious, many proposals are essentially just another singleton with a bunch of methods, only made reactive. In all fairness this was perfectly doable with Vue 2 - yet we kept using Vuex. Why? Because there are deeper reasons why we got Vuex, Redux etc. Allow me now to stir a controversy:
A singleton with a bunch of methods is not a replacement for Vuex, even if it’s reactive.
What makes a good application state
A viable solution for application state must include the following:
- Centralized state available to all components. We used to call it a store.
- Functions to safely modify the state, routinely called mutations.
- Ability to modify state in async functions such as API calls. Vuex makes it possible through actions, Redux offers thunks.
- Prevent programmer from shooting himself in the foot by modifying state directly.
It is the last requirement which lacks in majority of the proposals. And for me it is a crucial requirement.
All the effort of writing proper state and mutations is waste of time, if my colleagues are free to modify and hack state directly, causing inconsistencies and elusive bugs.
Vuex helps enforce this with strict
mode, which throws exceptions on attempts to illegally alter the state. Redux resorts to immutability - any local alterations will simply be discarded with the next mutation cycle.
Finally, the mere fact of using Vuex (or Redux etc.) enforces structure and discipline. It does come with the inevitable boilerplate code. But then one simply has to follow the structure - design a state store, define state mutations, organize application logic into actions, create state getters when state gets complex etc.
It’s this power of convention which makes your state easier for others to understand and use.
Those who worked on large Angular project without a third party state library immediately know what I’m talking about. Theoretically it is possible to manage state in Angular application using only Angular service and state-as-service pattern. But this is completely voluntary. There are no comprehensive guidelines and patterns of state management in official documentation. The inevitable outcome is a total mess of a state. Mysterious bugs and overwhelmed programmers eventually leaving the ship. Maybe others were luckier, but I have yet to see a large-scale pure Angular application with properly managed state.
That Vue and React both propose a ready pattern for state manegement, cannot be overestimated. Yes, it comes with quirks and boilerplate. But it gives us predictable architecture and much needed rigour. Properly used, it will prevent less experienced programmer from inadvertent or purposeful misbehaviour. Redux and Vuex have trained a whole generation of programmers into full awareness of how to properly manage application state.
Vuex, Redux and alikes are doing great job healing mental damage inflicted by Angular’s state-is-not-our-problem policy. Yet so many people still keep questioning these libraries and asking: “What have the Romans ever done for us?”
Reactive proposal for application state
Having praised Vuex, let’s try and come up with a simple reactive alternative which meets the criteria specified above. While I’m quite happy with Vue’s own simple pattern for state management, I’d really appreciate a store that is safe from unauthorized alterations. My additional goal is to have as little boilerplate as possible.
Usage
This is how I wish I could define a fictional state store representing user session:
import { createStore } from './vue-state.js'
export default createStore({
userName: '',
sessionToken: null,
async login (userName, password) {
await this.logout()
this.sessionToken = await SessionAPI.validate(userName, password)
this.userName = userName
},
async logout () {
if (this.isLoggedIn) {
await SessionAPI.invalidate(sessionToken)
}
this.userName = null
this.sessionToken = null
}
})
Apparently, the only boilerplate here is a call to createStore
method. This is entirely in line with Vue 3 conventions, where we already have things like createApp
or createRouter
.
Here’s how the store can be used in a component:
import sessionStore from '../store/session.js'
export default {
setup () {
const { state, login, logout } = sessionStore
return {
state,
login,
logout
}
}
}
In HTML template we can refer to the store like this:
<div v-if="state.userName">
{{ state.userName }}
<button @click="logout">Log out</button>
</div>
Additionally, when programmer attempts to alter state manually, I want the code to throw an exception:
import sessionStore from '../store/session.js'
export default {
setup () {
const { state } = sessionStore
// This must throw!
state.userName = 'bob'
}
}
Implementation
To achieve the above, actually very little code is needed. Thanks to the reactive magic of Vue 3, all it takes is 30 lines of code:
import { reactive, readonly, computed } from 'vue'
export function createStore (store = {}) {
// Prepare state properties
const entries = Object.entries(store)
const state = reactive(entries
.filter(entry => typeof entry[1] !== 'function')
.reduce((all, [key, value]) => {
all[key] = value
return all
}, {})
)
// Prepare mutation functions, make the state available in their body through `this`
const mutations = entries
.filter(entry => typeof entry[1] === 'function')
.reduce((all, [name, handler]) => {
all[name] = (...args) => handler.call(state, ...args)
return all
}, {})
// Add mutations to state object, so that mutations can call each other through `this`
for (const [name, mutation] of Object.entries(mutations)) {
state[name] = mutation
}
return {
...mutations,
// Return state as readonly, so the outside world cannot alter the state directly.
get state () {
return readonly(state)
}
}
}
Next Steps
An important addition in my proposal is extract
method returned with the store. It is similar to Vuex mapState
, and it can be used to extract values from state - either direct state properties or state-based expressions, for example:
import sessionStore from '../store/session-store.js'
const { sessionToken, userName } = sessionStore.extract('sessionToken', 'userName')
const { userNameCaps } = sessionStore.extract({
userNameCaps: state => (state.userName || '').toUpperCase()
})
You might ask, why not simply use destructuring, for example:
import { state } from '../store/session-store.js'
const { sessionToken, userName } = state
This will work, but with a big caveat. The returned values will not be reactive. If you try use them in data bindings, you won’t see any changes in the UI, when their value changes. This is nothing unexpected to Vue 3 experts. It’s the same behaviour as when extracting values from props
object in setup
method. Vue 3 documentation at https://composition-api.vuejs.org/api.html#setup explicitly warns:
Do NOT destructure the props object, as it will lose reactivity.
This is where our extract
method helps. It not only extracts the required values from the state, but it also wraps them as computed
. This way they can be safely used in data bindings or other computed expressions. Of course, if you don’t need reactivity in a particular scenario, you will be fine with simple destructuring.
Please refer to the source code at https://bitbucket.org/letsdebugit/vue-3-state for implementation details.
Sample application
We’ve provided a sample application where the proposed solution for state management has been employed. To make it less trivial than the usual single-button click-me application, it has the following features:
- Two views and UI router
- Reusable components
- Two state stores -
greetingStore
andsessionStore
, used by the views and components as the ultimate source of truth
You can run the sample application here.
It uses a simple architecture presented in my article about Vue JS 3 Application Without Build. To run it, you don’t need npm
or webpack
or anything else than a modern browser. Serve the source folder with something like serve or python -m SimpleHTTPServer 8000
and there it is!
Summary
We have come up with a simple solution for managing state which seems to meet the basic criteria for a state store. It shows clearly that Vue 3 Reactivity System can be indeed employed as a tool to handle state in Vue 3 applications. Hopefully we’ll be able to deploy it soon as a reusable npm
package!
References
The article is also available at my blog Let’s Debug It.
The complete source code can be found at https://bitbucket.org/letsdebugit/vue-3-state. Feel free to clone and reuse this code. Any suggestions or questions are most welcome!
I’d like to thank Jake Whelan for inspiration and valuable insights. And the usual credits and thanks to the creators of the awesome Vue JS framework.