#vuejs #vuecember2020 #vue3

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:

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 von ref und reactive 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. ❤