State Management in Vue 3
State Management ist eines der Themen, die mich intensiv beschäftigen, seit ich mit Vue States eine eigene Library als Alternative zu Vuex entwickelt habe. Welche Tools und Patterns wir für das State Management wählen, hat großen Einfluss auf das Design unserer Anwendungen.
Aktuelle Systeme wie Vuex haben ihre Vorteile:
[Vuex ist] gut in die Vue Devtools integriert, bietet eine Menge nützlicher Features wie Plugins oder Subscriptions, hat etablierte Best Practices und ist fast jedem Vue-Entwickler bekannt (von Filip Rakowski auf VueSchool)
Allerdings haben sie auch eine ganze Reihe von Nachteilen. Der Größte ist wohl die Komplexität, die sie einem Projekt hinzufügen. Sie unterstützen meist nur globale Zustände und erschweren Testing und Refactoring.
🎙 Aktuelle Entwicklungen
Die Einführung der Composition API hat die Diskussion über Patterns und Tools zum State Management neu belebt:
- Vue.js Core Team Member Eduardo San Martin Morote hat pinia entwickelt, ein experimentelles State Management System “basierend auf der Composition API und mit devtools support”.
- Vue.js Core Team Member Natalia Tepluhina zeigte auf der Vuejs Amsterdam 2020, dass man “Vuex möglicherweise nicht braucht” und präsentierte Wege den ApolloClient für das State Management zu verwenden.
- Filip Rakowski von Vue Storefront schreibt in einem neueren Blogpost, dass man einfach die Composition API verwenden könne und Vuex nicht weiter benötigen würde.
- Inzwischen gibt es eine ganze Reihe von Blogposts, die zeigen, wie man sich die Composition API zum State Management verwenden lässt — wie beispielsweise “Application State Management with Vue 3” von Markus Oberlehner, der empfiehlt, mehr Zustände lokal zu behandeln, und das context provider pattern zu verwenden.
- Vue.js Core Team Member Kia King Ishii präsentierte Vuex 4, das kompatibel mit Vue 3 sein wird, während die aktuelle API weitgehend unverändert bleibt. Im selben Talk zeige er, wie eine neue API für Vuex 5 aussehen könnte die "mehr Vorteile aus Vue 3’s reactivity API zieht".
Ich habe mit einem experimentellen vue-state-composer ebenfalls neue Ansätze versucht.
Letztendlich wirkte allerdings keine Lösung auf mich vielversprechend genug, um sowohl all die Flexibilität als auch die Tools zu bieten, die ich mir in der Entwicklung wünsche.
🐣 Ideen
Nach vielen Überlegungen 🤔, heißem Tee 🍵 und diversen Experimenten 🧪 bin ich der Meinung, dass die Composition API es uns ermöglicht, Lösungen nicht nur mit mehr Flexibilität, sondern auch mit besserem Tooling zu entwickeln, als bislang.
So eine Lösung sollte:
- Zustand und Logik in einfachen composition functions kapseln.
- nicht nur globalen Zustand, sondern auch lokalen Zustand und das context-provider pattern unterstützen.
- mehr Kontrolle über client-side hydration bieten und third-party composition functions wie NuxtJS’ ssrRef nutzen.
- eine Vue.js devtools Integration von gleichbleibender oder gesteigerter Qualität bereitstellen
- die Flexibilität von composition functions nur so weit reduzieren, wie unbedingt notwendig damit devtools zuverlässig funktionieren und um bei der Entwicklung die notwendige Orientierung zu geben.
Dieser Ansatz wird ähnlich aussehen wie stores, die mit Vuex 5 und der Composition API geschrieben werde.
Meiner Meinung nach ist die Lösung aber kein einzelnes, in sich geschlossenen State Management System für jeden denkbaren Anwendungsfall.
Stattdessen plädiere ich dafür, eine Reihe voneinander unabhängiger Tools & Patterns zu entwickeln, die in ihrer Gesamtheit ein sauberes State Management mit der Composition API ermöglichen.
Die größten Bedenken, die ich bezüglich des Einsatzes der Composition API für das State Management weiterhin habe, beschäftigen sich damit wie die notwendigen Insights für die Vue.js Devtools gesammelt werden.
Die bisherige API von Vuex und auch die Options API haben es einfach gemacht, Hooks und Watcher zu installieren.
Bei Verwendung der Composition API die gleichen Informationen zu gewinnen ist eine ungleich größere Herausforderung.
Falls die Composition API noch unbekannt ist, empfehle ich zuerst den RFC anzusehen. Viele der Beispiele und Diskussionen werden nicht selbsterklärend sein, sondern setzen Kenntnisse über neue Ansätze zum State Management voraus. Daher empfehle ich auch, über state management with reinen composition functions zu lesen und einen Blick auf pinia und Vuex 5 zu werfen.
Ein einfaches Beispiel
Alle weiteren Codebeispiele werden auf diesem einfachen useCounter
Beispiel basieren:
export function useCounter() %7B
const state = reactive(%7B
count: 0,
%7D)
function increment() %7B
state.count++
%7D
return %7B
state,
increment
%7D
%7D
Globale und lokale Zustände
Wie oben geschrieben, sollte eine Lösung globalen Zustand, lokalen Zustand und das context-provider pattern unterstützen. Es gibt bereits viele Beispiele, wie man alle drei mit der Composition API implementieren kann, und ich werde nur einige Optionen zusätzlich vorstellen:
Das context-provider pattern ist mit Vue 3 verhältnismäßig geradlinig in der Umsetzung.
Es könnte jedoch weiter vereinfacht werden, indem die notwendigen composition functions automatisch generiert werden:
export const %7B
provider: useCounterProvider,
injector: useCounterContext
%7D = asProvideInject(useCounter)
Der globale Zustand muss bei serverseitigem Rendering isoliert werden. Diese Trennung muss aber nicht notwendigerweise die Aufgabe eines State Management Systems sein.
Wir können hierfür beispielsweise NuxtJS' context verwenden.
Nicht NuxtJS basierte SSR-Applikationen können einen ähnlichen Mechanismus implementieren.
// NuxtJS' API will probably change with version 3
export default (_context, inject) => %7B
inject('counter', useCounter())
%7D
Ich möchte auch aufzeigen, dass dies nur dann notwendig ist, wenn der Zustand tatsächlich auf dem Server mutiert wird. Andernfalls könnte er auch zwischen Anfragen geteilt werden und muss in jedem Falle nicht im client hydriert werden.
Dies ist in der Regel der Fall für alle sessionbasierten Daten wie user
, cart
und authentication
:
// in pure single-page-applications
export const counter = useCounter()
// in server-side rendering applications
// where state is not mutated server-side (i.e. authentication)
// one could skip request isolation
export const counter = process.server
// withLockedState would raise an Exception if state was changed
// to ensure requests are not leaking
? withLockedState(useCounter)()
: useCounter()
Client-side hydration
Für die client-side hydration nach dem server-side rendering können wir NuxtJS' ssrRef verwenden:
export function useCounter() %7B
const state = reactive(%7B
count: ssrRef(0),
%7D)
// ...
%7D
Nicht NuxtJS basierte SSR-Applikationen können auch hier vergleichbare composition functions entwickeln.
Dieser Ansatz erfordert zwar mehr Wissen, ermöglich aber auch eine feinere Kontrolle darüber, wie viel Payload an den Client gesendet werden.
Stores, die Benutzer- oder Authentifizierungsdaten enthalten, werden normalerweise nur im Browser mutiert und benötigen keine client-side hydration.
Eine vereinfachte, vollständige Hydrierung könnte ebenfalls umgesetzt werden werden, sollte aber separiert und optional sein.
Beispielsweise:
// hydration-plugin.js
import %7B ssrRef %7D from '@nuxtjs/composition-api'
export const withHydration = createWithHydration(%7B ref: ssrRef %7D)
// useCounter.js
function useCounterBase() %7B
/* implementation */
%7D
export const useCounter = withHydration(useCounterBase)
Devtools
Was ich in der Regel wissen möchte ist, wo, wann und wie ein Zustand geändert wurde.
Die Vuex Devtools zeigen, welche Mutationen in welchen Store commited wurden, zu welchem Zeitpunkt das geschah und was die Payload war. Der Zustand vor und nach jeder Mutation kann analysiert werden und mit dem Time-travel-Feature lässt sich der Store auf einen früheren Zustand zurückzusetzen.
Schauen wir uns das vollständige Beispiel noch einmal an:
export function useCounter() %7B
const state = reactive(%7B
count: 0,
%7D)
function increment() %7B
state.count++
%7D
return %7B
state,
increment
%7D
%7D
Um die benötigten Informationen zu sammeln, ließe sich ein Wrapper für composition functions wie useCounter
bereitstellen. Diese würden den Zustand überwachen und alle exportierten Methoden wrappen, um eingehende Aufrufe zu registrieren.
Die Vue.js Devtools würden dann all diese Informationen im Komponenten-Explorer und in der neuen Timeline anzeigen.
function useCounterBase() %7B
/* implementation */
%7D
export const useCounter = withDevtools('Counter', useCounterBase)
Allerdings wäre dieser Ansatz für sich genommen sehr beschränkt, wenn die volle Flexibilität von composition functions unterstützt werden soll.
Ich glaube, dass wir Werkzeuge wie babel brauchen, um sowohl die Flexibilität als auch die Insights während der Entwicklung zu erweitern.
Schauen wir uns eine komplexere composition function an:
export function useCounter() %7B
const state = reactive(%7B
count: 0,
%7D)
const %7B isAuthenticated %7D = useAuthentication()
const autoIncrementInterval = ref(null)
function increment() %7B
state.count++
%7D
function startAutoInrement() %7B
if(!isAuthenticated.value) %7B
throw new Error('Cannot start auto increment unless authenticated')
%7D
autoIncrementInterval = setInterval(increment, 100)
%7D
return %7B
state,
increment,
startAutoInrement,
%7D
%7D
In diesem Beispiel - mit dem oben beschriebenen Ansatz - könnten wir zwar den privaten Zustand registrieren, der mit ref
erzeugt wird. Wir hätten aber keine Informationen um in den Vue.js Devtools den nötigen Kontext zu geben. Es wäre unmöglich, interne Aufrufe von increment
zu verfolgen. Und solange useAuthentication
nicht selbst auch in withDevtools
gewrapped ist, wäre es unmöglich, auseinanderzuhalten, welche composition function Aufrufe von reactive
, ref
usw. ausgelöst hat.
Ein babel-Plugin würde uns vermutlich erlauben, die Variablennamen state
und autoIncrementInterval
als Labels zu erkennen, die in den Vue.js Devtools angezeigt werden. Wir könnten interne Aufrufe verfolgen, die sonst unbemerkt blieben. Und wir wären auch in der Lage, alles, was innerhalb von useCounter
passiert, von dem zu isolieren, was außerhalb implementiert ist.
export const useCounter = withDevtools('Counter', () => %7B
// code inside `withDevtools` could be transformed with babel
// to reduce limitations while gathering insights
%7D)
Restrictions
Die Flexibilität der Kompositionsfunktionen stellt eine große Herausforderung dar, um die benötigten Informationen zu erfassen und anzuzeigen. Damit dies sinnvoll und zuverlässig ist, könnte die Flexibilität bei der Anwendung von withDevtools
nach Bedarf eingeschränkt werden.
Mögliche Restriktionen sind beispielsweise:
- Functions müssen synchron sein
Aufrufe vonref
undreactive
können dann einfacher und zuverlässiger getracked werden. - Functions müssen entweder ref oder reactive genau ein mal aufrufen
Wenn eine composition function mehrere reaktive Objekte erzeugt, deren Zustand in den Vue.js devtools dargestellt wird, ist dies schwieriger zu interpretieren. - Im Rückgabewert müssen state und methods getrennt werden
Wenn eine composition function eine Struktur wie%7B state, ...methods %7D
zurückgibt, wird es einfacher und zuverlässiger, den nach außen gegebenen Zustand und calls zu Methoden zu tracken.
Closing
Mit der Veröffentlichung von Vue 3 ist Bewegung ins gesamte Ecosystem gekommen.
Das Thema State Management ist davon nicht ausgenommen.
Mit diesem Blogpost hoffe ich nicht nur Fragen aufgeworfen, sondern auch neue Lösungsansätze geliefert zu haben.
Und für manche ist er vielleicht einfach ein Überblick über die neue Optionen und aktuellen Entwicklungen zum Thema 😀
Nun ist meine Frage: Was meinst Du zu dem Ganzen? Macht dieser Ansatz Sinn und würdest Du damit arbeiten wollen? Ist all das realistisch - oder nicht?
❤ In jedem Fall freue mich über Feedback und Fragen im begleitenden Blogpost auf Medium. ❤