Why we still need state management systems in Vue 3 — but not the way you think 🐣
This blog post is a contribution to the Vuecember 2020 at sum.cumo with a bunch of interesting topics up until 24.12.2020.
State management is one of the topics that keep me thinking since I announced Vue States here on Medium. Which tools and patterns we choose for state management greatly affects the design of our applications.
Current systems like Vuex have their benefits:
[Vuex is] well-integrated with Vue Devtools, offers a lot of useful features like plugins or subscriptions, has well-established best practices and is known by almost every Vue developer (by Filip Rakowski on VueSchool)
However, they have also a whole bunch of drawbacks — the biggest of all being a huge increase in complexity. They mostly support only global state and they make things like testing and refactoring much more difficult.
🎙 Current developments
The introduction of the Composition API revived the discussion about state management patterns and tools:
- Vue.js core team member Eduardo San Martin Morote developed pinia, an experimental state management system “based on the composition api with devtools support”.
- Filip Rakowski from Vue Storefront argued in a newer blogpost that you could “forget about [Vuex]” and just use composition functions.
- By now there are quite a few blogposts showing how to use the composition API for state management — like “Application State Management with Vue 3” by Markus Oberlehner, who recommends embracing local state and utilizing the context provider pattern.
- Vue.js core team member Kia King Ishii presented Vuex 4, which will be compatible with Vue 3 while keeping the current API. In the same talk, he shared what a new API for Vuex 5 could look like that would “leverage more of Vue 3’s exposed reactivity API”.
I tried new approaches as well with an experimental vue-state-composer, but ultimately, no solution looked promising enough to provide both the flexibility and the tooling I search for.
🐣 Imagining
After lots of thoughts 🤔, tea 🍵 and experiments 🧪 I believe the Composition API does allow us to build solutions with greater flexibility and tooling — reducing the drawbacks we currently face.
Such a solution should…
- encapsulate state and logic in plain composition functions.
- support not only global state, but local state and the context-provider pattern as well.
- support fine-grained client-side hydration by third-party composition functions like NuxtJS’ ssrRef.
- provide a Vue.js devtools integration of comparable or improved quality.
- restrict the flexibility of composition functions only as much as is required for devtools to work reliably and to provide the necessary guidance.
This approach will look similar to stores written with Vuex 5 and the Composition API. However, in my opinion, the solution is not a single, self-contained state management system for every conceivable use case.
Instead, I advocate developing a set of independent tools & patterns that collectively enable state management using the Composition API in a wide variety of use cases.
The biggest concerns I continue to have about using the Composition API for state management deal with how the necessary insights can be gathered for the Vue.js Devtools. Vuex’s previous API and also the Options API made it easy to install hooks and watchers. Gaining the same information using the Composition API is much more challenging.
In case the Composition API ist new to you should checkout the RFC. Much of the examples and discussion will not be self-explanatory, but assume knowledge of new approaches to state management. Therefore I also recommend reading about implementations with plain composition functions and to checkout pinia as well as Vuex 5.
Basic Example
All further code samples will be based on this useCounter
example:
Global and local state
As stated before, a solution should support global state, local state and the context-provider pattern. There are already lots of examples how to implement all three with the Composition API and I will just present some options in addition:
The context-provider pattern is pretty straightforward with Vue 3. However, it could be further simplified by generating the necessary composition functions automatically:
Global state must be isolated in case of server-side rendering, but that separation must not be the concern of a state management system. We can rely on NuxtJS’ context for that. Custom server-side rendering applications can implement a similar mechanism.
I also want to point out that this is only necessary if the state is actually mutated on the server. Otherwise, it could be shared between requests and it does not need to be hydrated. This is usually the case for all session-based data like user
, cart
and authentication
:
Client-side hydration
For hydration after server-side rendering we can rely on NuxtJS’ ssrRef:
For custom server-side rendering applications, equivalent composition functions can be developed.
Note that this requires more knowledge, but also gives more fine-grained control over how much payload is sent to the client. Stores containing user- or authentication data are usually mutated only in the browser and don’t need client-side hydration.
A full hydration could be provided as well, but should be separated and thereby optional. For example:
Devtools
What I usually want to know is where, when and how state was changed. The Vuex devtools show which mutations have been commited to which store, at what time that happened and what the payload was. You can analyse the state before and after each mutation and you can also use the time travel feature to reset the store to a previous state.
Let’s look at the full example once more:
To gather the information we need, we could provide a wrapper for composition functions like useCounter
. They would watch the state and wrap all exported methods to track incoming calls. The Vue.js Devtools would then display all this information in the component explorer and in the new timeline.
However, this approach alone would be very limited in handling the flexibility of composition functions. I believe that we need tools like babel to extend both flexibility and insights during development.
Imagine a more complex composition function:
In this example — using the approach described above — we could be aware of the private state generated with ref
, but we could not provide any insight apart from its current value. It would be impossible to track internal calls to increment
. And unless useAuthentication
is wrapped in withDevtools
as well, it would be impossible to keep apart which composition function made calls to reactive
, ref
etc.
A babel plugin however would probably allow us to detect the variable names 'state'
and 'autoIncrementInterval'
as labels to be displayed in the Vue.js Devtools. We could track internal calls that would otherwise stay unnoticed. And we would also be able to isolate everything that happens inside useCounter
from what’s implemented outside.
Update 05.01.2021: There is now a very basic proof of concept at https://github.com/JohannesLamberts/vue-composition-function-insights, which displays the current state of all const VariableDeclarations.
Restrictions
The flexibility of composition functions poses a huge challenge to gather and display the required information. For it to be meaningful and reliable the flexibility could be restricted as necessary where withDevtools
is applied.
Possible restrictions are:
- Functions must be synchronous
Calls toref
andreactive
could then be tracked more easily and reliably. - Functions must call either
ref
orreactive
exactly once
If a composition function creates multiple reactive objects the state displayed in the Vue.js devtools probably becomes harder to understand. - Return values must split state and methods
If a function returned a structure like{ state, ...methods }
, where the location of each is well known, tracking method calls and changes to the exposed state would become easier and more reliable.
Closing
With the release of Vue 3, movement has come into the entire ecosystem.
The topic of state management is not exempt from this.
With this blog post I hope to have not only raised questions, but also provided new approaches.
And for some it might just be an overview of the new options and current developments on the topic 🙂.
Now my question is: What do you think about all this? Does this approach make sense and would you want to work with it? Is all this realistic — or not?
❤ In any case I’d love to hear your opinion ❤