Seit einigen Jahren werden die Ansprüche an Websites zunehmend komplexer. Da immer mehr Business-Logik ins Frontend wandert, ist es sehr wichtig, dass alle Informationen gebündelt an einer Stelle gesammelt und aktualisiert werden.

Hierfür hat sich das State Management Pattern etabliert. Dabei wird die Anwendung in einen State strukturiert, der alle Daten als zentrale Quelle beinhaltet: die verschiedenen Views der Website und die Actions, die Veränderungen an dem State ausführen. Wird das Pattern sauber implementiert, können alle Daten der Webanwendung einheitlich gehalten werden. Das bedeutet, dass eine Benutzereingabe zuverlässig im State gespeichert wird und sowohl für die Anwendung als auch für mögliche Backends oder die DevTools zur Verfügung steht.

Abbildung - Vuex vs. Pinia - State Management Pattern

Es ist natürlich möglich, das State Management Pattern eigenständig umzusetzen. Es bietet sich jedoch an, eine vorgefertigte Bibliothek zu verwenden, um sich diese Kosten und Mühen einzusparen. Je verwendeter Technologie für die Webanwendung stehen verschiedene Optionen im Raum. Für das JavaScript-Framework Vue gibt es seit 2016 Vuex, und es hat sich weithin als Standard etabliert.

Seit 2019 steht die State-Management-Bibliothek Pinia als Alternative zur Verfügung. Seinen Ursprung hat sie im Entwicklungsteam von Vuex, wo Ideen für die nächste Version gesammelt und ausprobiert wurden. Da die Änderungen zu sehr auseinander liefen, beschloss das Team, die neuen Entwürfe unter dem Paketnamen Pinia separat zu veröffentlichen.

Mittlerweile ist die Arbeit an neuen Vuex-Versionen abgebrochen worden, und es findet keine weitere Entwicklung unter diesem Namen statt. Außerdem enthält die Vuex-Website ein offizielles Statement, das darauf hinweist, dass es nur noch Maintenance-Updates geben wird und die empfohlene Store-Bibliothek jetzt Pinia ist.

Designunterschiede von Vuex und Pinia

Abbildung - Vuex vs. Pinia - Designunterschiede

Im Vergleich zu Vuex bietet Pinia eine sehr einfache API an. Diese orientiert sich an der Composition-API von Vue. Außerdem wurde es für TypeScript-Projekte mit Pinia ermöglicht, die Typisierung in den meisten Fällen automatisch generieren zu lassen. Um die Typsicherheit für Vuex zu gewährleisten, muss ein Wrapper implementiert werden. Diese Typisierung muss für den Store, sowie seinen State, die Mutations, Actions und auch Getter angelegt werden.

Da Pinia aus dem Vuex-Projekt entstanden ist, differenziert sich der Aufbau der beiden Bibliotheken nur minimal. In dem übergreifenden Konzept fällt das Erstellen von Mutations weg. Diese hatte man ursprünglich integriert, um das Debuggen über die DevTools zu ermöglichen. Mittlerweile ist dies auch ohne die Mutations problemlos möglich, und der extra Schreibaufwand kann eingespart werden.

Pinia wurde mit der Composition-API als Designvorbild entworfen, welche ein Bestandteil von Vue 3 ist. Das bedeutet jedoch nicht, dass ein Projekt mit der Options-API als Schreibweise nicht genauso gut Pinia verwenden kann. Außerdem gibt es auch für Vue 2-Projekte einige Beispiele und Anmerkungen in der offiziellen Pinia-Dokumentation. Der größte Teil der Dokumentation richtet sich jedoch an Vue 3-Projekte.

Einbindung in die Vue-App

Das Einbinden der Stores in die Vue-App ist in den beiden Bibliotheken sehr ähnlich. Um Pinia in ein bestehendes Vue-Projekt integrieren, muss es zunächst installiert werden. Dies kann einfach mit dem entsprechenden npm- oder yarn-Befehl gemacht werden:

In der Einstiegsdatei (namentlich meistens main.js / main.ts ), in der sich die Konfiguration von Vue befindet, wird auch die Verwendung des entsprechenden Stores definiert. In einem bestehenden Projekt mit einem Vuex-Store sieht dies zum Beispiel wie folgt aus:

Da Pinia aus Vuex entstanden ist, ist es also nicht verwunderlich, dass die Einbindung nicht sehr abweicht. Daher kann an dieser Stelle einfach die Funktion createStore durch createPinia ersetzt werden.

Definieren des Stores

Das Definieren eines Pinia-Stores funktioniert ebenfalls sehr ähnlich wie das des Vuex-Stores.

Anstelle der createStore-Funktion muss die defineStore aus Pinia importiert werden. Der Variablenname sollte sich nach Namenskonvention useXY richten, damit beim Import in der Vue-Komponente ersichtlich wird, dass es sich um eine Verwendung des Stores handelt. Anders als bei Vuex muss als erster Parameter der Name des Stores definiert werden – in diesem Beispiel personalData. Dieser wird dann verwendet, um sich mit den DevTools des Browsers zu verbinden. Der Name muss pro Store in der Anwendung einzigartig sein.

Erstellen des States

Beim Erstellen des States zeigt sich sehr deutlich, wie viele Zeilen Code durch den Typsupport von Pinia eingespart werden können. Bei Vuex wird zunächst ein defaultState angelegt, der verschiedene, personenbezogene Daten beinhaltet. Verwendet das Projekt TypeScript, muss hierfür dann ein entsprechender PersonalDataState als Typisierung angelegt werden. Beim Erstellen des Stores kann dann der defaultState eingefügt werden.

Bei Pinia passiert die Typisierung der Properties automatisch anhand der Startwerte. In den State muss beim Erstellen also nur der defaultState eingefügt werden.

Definieren der Getter-Methoden

Das Erstellen der Getter in Vuex funktioniert fast genauso wie das Anlegen eines States – mit dem kleinen Unterschied, dass die Typisierung dieses Mal die Funktionen und deren Rückgabetyp beinhaltet. Die definierten Funktionen können dann wieder in die createStore-Methode unter getters eingefügt werden.

Um im Pinia-Store dieselben Getter zu definieren, können einfach Arrow-Functions unter dem Punkt getters hinterlegt werden. Die Typisierung der Funktionen wird auch hier automatisch ermittelt. Außerdem wird in sehr vielen Entwicklungsumgebungen die Autovervollständigung direkt unterstützt.

Als erstes Argument können die Funktionen den state übergeben bekommen. Es ist auch möglich, Informationen aus dem State eines anderen Pinia-Stores zu kombinieren. Hierfür kann einfach mit this auf den gesamten Store-Inhalt zugegriffen werden. In diesem Fall ist es dann leider nicht mehr möglich, dass der Rückgabetyp automatisiert bestimmt wird.

In diesem Beispiel muss also für die getNameAndCity-Funktion der Typ String explizit angegeben werden.

Das Schlüsselwort this kann außerdem verwendet werden, um auf andere Getter aus dem aktuellen Store zuzugreifen. Wenn jedoch ein Getter eines anderen Stores verwendet werden soll, muss zunächst der Store importiert werden.

Als Beispiel wird im PersonalDataStore ein Getter implementiert, der Vor- und Nachname einer Person in einem String zurückgibt. Um auf diesen zuzugreifen, wird zunächst der Store über die usePersonalDataStore-Funktion importiert. Jetzt kann anhand der Store-Variable auf dessen Getter-Funktionen zugegriffen werden.

Unterschiede in den Actions

Die Actions sind für die Business-Logik bestimmt. Damit ähneln sie den, als methods bekannten, Funktionen in Vue-Komponenten. Im Gegensatz zu den Gettern ist es möglich, asynchrone Actions zu implementieren.

In Vuex werden die Actions ähnlich wie die Getter aufgesetzt. Die Funktionen werden entsprechend definiert und erhalten eine selbst implementierte Typisierung. Innerhalb dieser Action-Methoden werden dann Mutations aufgerufen. Diese dienen dazu, State-Änderungen durchzuführen. Auch für diese Mutations müssen nach demselben Prinzip Methoden definiert und eine entsprechende Typisierung implementiert werden.

In Pinia können State-Änderungen direkt über die Action, oder auch aus der Vue-Komponente heraus, durchgeführt werden. Daher konnte der extra Aufwand, den die Mutations verursacht haben, eingespart werden. Die Implementierung der Actions in Pinia orientiert sich ebenfalls an dessen Gettern, und auch die Verwendung eines anderen Stores funktioniert gleich. Der Store muss importiert werden, um danach über das Store-Objekt auf dessen Funktionen zugreifen zu können.  

Verwendung des Stores innerhalb einer Vue-Komponente

Um den Vuex-Store nun in der Vue-Komponente zu verwenden, muss dieser zunächst importiert werden.

In den folgenden Beispielen wird die auf der Composition-API basierende lesbarerer Schreibweise des <script setup> verwendet.

Dafür muss nur das Schlüsselwort setup im <script>-Block ergänzt werden. Dadurch entfällt sehr viel Boilerplate-Code, und die Komponente ist deutlich kleiner und lesbarer. Soll zum Beispiel dynamische Referenz auf die Straße erstellt werden, kann dies mit Hilfe der computed-Funktion aus Vue umgesetzt werden. Darin kann dann aus dem Vuex-State der entsprechende Getter aufgerufen werden.

Die Einbindung des Pinia-Stores in eine Vue-Komponente ist ebenfalls sehr simpel. Der benötigte Store kann aus der entsprechenden Datei importiert werden. Der Rückgabewert der Funktion kann dann in eine addressStore-Variable gespeichert und darüber verwendet werden.

Pinia lässt es außerdem zu, dass der State aus der Komponente heraus verändert werden kann. In diesem Beispiel kann, ganz ohne Mutations, die Postleitzahl im Store-State verändert werden.

Da der Pinia-Store ein reaktives Objekt ist, kann beim Abruf der Werte aus dem State auf das .value-Keyword verzichtet werden. Das führt jedoch dazu, dass dieses nicht mehr zur Destrukturierung geeignet ist. Wird die Straße, wie in diesem Beispiel, über Destrukturierung referenziert, dann wird eine Änderung an der Straße nicht dynamisch an den Store übertragen. Für unseren addressStore bleibt der Standardwert erhalten, also die hinterlegte Baker Street.

Damit die Straße als reaktives Objekt aus dem Store referenziert werden kann, muss die Pinia-Funktion storeToRefs importiert werden. Diese erhält den entsprechenden Store als Eingabeparameter, und eine Destrukturierung kann danach wie gewohnt stattfinden.

Verwendung der Actions

In einer HTML-Komponente kann die definierte Action loadZipCode direkt über das Store-Objekt aufgerufen werden. In diesem Beispiel wird durch das Klicken auf den Button die Postleitzahl geladen.

Eine alternative Schreibweise, um den eigentlichen Funktionsaufruf zu verkürzen, ist, die Action zuerst zu mappen. Hierzu kann die mapActions-Funktionalität von Pinia genutzt werden. Diese erhält als Parameter den jeweiligen Store und dann ein String-Array mit den Funktionsnamen, die gemappt werden sollen.

Pinia vs. Vuex im Kontext der Bundling-Tools

Während Vuex hauptsächlich auf Webpack ausgelegt ist (sehen Sie auch unsere Einführung in Webpack), unterstützt Pinia standardmäßig nur das Bundling-Tool Vite. Wird in dem Projekt Webpack verwendet, muss im Pinia-Store eine Ergänzung stattfinden. Das eigentliche Bundling muss nicht verändert werden.

Damit das Hot Module Replacement (kurz HMR) von Webpack in der lokalen Entwicklung weiterhin funktioniert, muss mit einem dynamischen Import gearbeitet werden. Die dynamischen Imports stehen seit der ECMAScript Version 2020 zur Verfügung. Diese ermöglichen es, zum einen asynchron Inhalte zu laden und zum anderen diese Inhalte nur unter bestimmten Bedingungen zu laden. Um das HMR weiterhin zu gewähren, muss die accept-Methode von Webpack verwendet werden.

Diese erhält dann als Parameter die gewünschte Dependency, um sie mit dem HMR zu verbinden. Für Pinia gibt es die acceptHMRUpdate-Funktion, die diese Verbindung unterstützt.

Diese erhält als ersten Parameter den jeweiligen Store und als zweiten den ursprünglichen Import, der adaptiert werden soll (hier: import.meta.webpackHot).

Die acceptHMRUpdate-Funktion wird nun als Parameter in die accept-Methode von Webpack übergeben. Das ganze wird durch einen dynamischen Import verschachtelt, der das ganze nur ausführt, wenn auch der ursprüngliche import.meta.webpackHot ausgeführt wird. Das bedeutet, dass das ganze nur in der lokalen Entwicklung in Effekt tritt und die Produktionsumgebung nach wie vor vom HMR getrennt ist.

Vuex vs. Pinia – Lohnt sich die Migration?

Obwohl Pinia im Vergleich zu Vuex keine neuen Funktionalitäten anbietet, wird durch die obigen Beispiele schnell klar, dass sehr viel Boilerplate-Code entfällt. Das gilt nicht nur, aber insbesondere, für TypeScript-Projekte. Durch das automatische Ermitteln der Typisierung für die meisten Funktionen und Variablen kann viel Schreibarbeit eingespart werden. Aber auch in klassischen JavaScript-Projekten entfällt durch das Entfernen von Mutations-Layern einiges an Codezeilen. Die Stores bleiben insgesamt klein und übersichtlich. Für jeden Pinia-Store genügt meist eine einzelne Datei.

Obwohl sich die beiden sehr ähneln, bringt eine Migration von Vuex nach Pinia einiges an Arbeitsaufwand mit sich. Während die Definition des States und der Getter mehr oder weniger kopiert werden kann, fliesst durch den Wegfall der Mutations viel Arbeit in die Umgestaltung der Actions. Auch die entsprechenden Vue-Komponenten müssen umgestaltet werden. Abschließend kann auch das Nachziehen der Tests und das korrekte Einrichten des HMR viel Zeit einfordern.

Da Vuex nicht mehr weiterentwickelt und auch irgendwann nicht mehr gewartet werden wird, sollte eine Migration auf Pinia früher oder später auf dem Programm stehen. Die Bibliothek ist sehr gut dokumentiert, und es gibt für weitere Informationen bereits eine Vielzahl an Blog-Posts und YouTube-Tutorials, sowie eine aktive Community.

Abschließend ist festzuhalten, dass sich eine Migration von Vuex nach Pinia in puncto Lesbarkeit und somit auch Wartbarkeit definitiv lohnen wird. Da das Thema noch sehr frisch ist, besteht hier jedoch kein akuter Zeitdruck. Es empfiehlt sich – wie in allen Bereichen der IT – eine Migration durchzuführen, bevor das tatsächliche Lebensende von Vuex erreicht ist.

In diesem Sinne, happy migrating!

Lisa Messerli

Große Auswahl an günstigen Domain-Endungen – schon ab 0,08 € /Monat
Jetzt Domain-Check starten