Vuex – State Management in Vue-Applikationen

Vuex – State Management in Vue-Applikationen: Eine Einführung
Keine Kommentare

Vue.js ist eine Bibliothek zur Erzeugung grafischer Oberflächen im Web. Die Leichtgewichtigkeit der Bibliothek, gepaart mit einer relativ geringen Einstiegshürde, trägt viel zur Verbreitung von Vue.js bei. Und trotzdem ist Vue.js ein mächtiges Werkzeug, mit dem sich selbst umfangreichste Applikationen umsetzen lassen. Doch ab einer bestimmten Größe und Komplexität einer Applikation stoßen Sie mit Vue.js allein an die Grenzen des Machbaren. Der Grund hierfür liegt darin, dass sich bei großen Applikationen der State kaum verwalten lässt. Aus diesem Grund wurde mit Vuex eine Bibliothek zum State Management implementiert. In diesem Artikel lernen Sie die Grundlagen dieser Bibliothek kennen und sehen, wie Sie Vuex in eine Vue-Applikation integrieren können.

Als State werden die Daten Ihrer Applikation bezeichnet. Das können dynamische Daten sein, die vom Server geladen, oder auch Zustände der Oberfläche, die nur temporär vorgehalten werden. Jede Vue-Komponente kann ihren eigenen State verwalten. Dieser wird über die data-Eigenschaft der Komponente verwaltet. In der Regel besteht Ihre Applikation nicht nur aus einer Komponente, sondern aus einem ganzen Baum von Komponenten. Und genau hier beginnen die Schwierigkeiten. Soll eine Information in mehreren Komponenten angezeigt werden, muss sie in einer gemeinsamen Elternkomponente platziert werden. Damit steigt der Grad der Abhängigkeit zwischen den Komponenten, was die einfache Wiederverwendbarkeit reduziert. Bei tiefen Komponentenbäumen führt die Zentralisierung des States schnell zu einem einfachen Durchreichen der Informationen über längere Wege. Komponenten leiten Informationen über Props an ihre Kindkomponenten weiter, ohne dass sie selbst die Information benötigen. Mit einer zunehmenden Menge an Informationen erhöht sich auch der Wartungs- und Pflegeaufwand. Ein weiteres Problem im Zusammenhang mit dem State Ihrer Applikation entsteht, wenn Sie die Informationen an mehreren Stellen manipulieren möchten. Auch hier müssen Sie ohne weitere Hilfsmittel selbst dafür sorgen, dass der State in Ihrer Applikation zu jeder Zeit konsistent ist, was häufig dazu führt, dass Sie Events ebenso wie die Props über die Komponentenhierarchie weiterreichen.

Eine Lösung für diese Probleme bietet ein Architekturmuster, das ursprünglich von Facebook entwickelt wurde und den Namen Flux trägt. Die Flux-Architektur wird mittlerweile nicht mehr nur in React-Applikationen in Form von Redux oder anderen Bibliotheken eingesetzt, sondern hat auch Einzug in Angular in Form von @ngrx/store und Vue.js mit Vuex gehalten.

International JavaScript Conference

Effective Microservices Architecture In Node.js

by Tamar Stern (Palto Alto Networks)

React Components And How To Style Them

by Jemima Abu (Telesoftas)

JavaScript Days 2020

Wie ich eine React-Web-App baue

mit Elmar Burke (Blendle) und Hans-Christian Otto (Suora)

Architektur mit JavaScript

mit Golo Roden (the native web)

In Abbildung 1 sehen Sie die Elemente einer Flux-Architektur. Den Kern bildet der Store, hier werden die Informationen festgehalten. Je nach Implementierung kann es einen oder mehrere Stores geben. Im Fall von Vuex beschränken Sie sich auf nur einen Store, der allerdings mithilfe von Modulen untergliedert werden kann. Diesen zentralen Store können Sie überall in Ihrer Applikation direkt verwenden. Der Zugriff auf den Store erfolgt allerdings nur lesend. Der Grund hierfür ist, dass die Architektur reaktiv aufgebaut ist und eine Änderung am Store dazu führt, dass sämtliche angeschlossenen Komponenten aktualisiert werden, sobald sich eine Information ändert, sodass diese für den Benutzer sichtbar wird. Modifikationen erfolgen stets über die sogenannten Actions. Das sind einfache JavaScript-Objekte, die die Änderungen am Store beschreiben. Ein solches Action-Objekt wird auf dem Store dispatcht und anschließend vom Reducer verarbeitet. Dieser Reducer sorgt dafür, dass aus dem bisherigen State und den Informationen aus der Action der neue State Ihrer Applikation generiert wird.

Vuex ist eine konkrete Implementierung des Flux-Patterns mit einigen Abwandlungen und Anpassungen speziell an die Anforderungen von Vue.js. Das führt dazu, dass Sie die Begriffe aus Flux nicht direkt in Vuex wiederfinden, die Konzepte allerdings deutlich sichtbar sind. Der Einsatz von Vuex bedeutet nicht, dass Sie komplett auf lokalen Komponenten-State verzichten müssen. Lokaler und applikationsweiter State können parallel zueinander existieren. Im lokalen State Ihrer Komponenten halten Sie Daten vor, die nur die Komponente selbst oder ihre direkten Kindkomponenten betreffen. In den meisten Fällen handelt es sich hierbei um anzeigerelevante Daten wie beispielsweise Daten von Formularelementen, Filter- oder Sortiereingaben. Alle weiteren Daten, die an mehreren Stellen angezeigt oder modifiziert werden können oder die Sie über eine tiefere Hierarchie von Komponenten weiterreichen müssten, speichern Sie in Vuex.

You might not need… Vuex

Dan Abramov, die treibende Kraft hinter Redux, veröffentlichte einen Blogartikel mit dem Titel „You might not need Redux“. Dieser Artikel entstand, weil sich viele Entwickler beschweren, dass State-Management-Bibliotheken wie Redux oder Vuex zusätzlichen Aufwand bei der Entwicklung und Pflege einer Applikation bedeuten und die Vorteile teilweise nicht direkt sichtbar werden. Grundsätzlich stimmt es natürlich, dass der Einsatz von Vuex zunächst bedeutet, dass Sie mehr Code schreiben müssen, um das gleiche wie mit lokalem Komponenten-State zu erreichen. Deshalb und wegen der zusätzlichen Komplexität, die Sie sich mit Vuex an Bord holen, sollten Sie sich zu Beginn der Entwicklung die Frage stellen, ob Sie tatsächlich eine State-Management-Bibliothek benötigen. Für viele kleinere Applikationen sind die Boardmittel von Vue.js völlig ausreichend, also machen Sie nicht den Fehler, Vuex in Ihre Standardlösung aufzunehmen. Treffen Sie die Entscheidung bewusst und nur, wenn Ihre Problemstellung den Einsatz der Bibliothek rechtfertigt.

Die Beispielapplikation

Dieser Artikel nutzt die gleiche To-do-Applikation wie der Vorgängerartikel mit der Einführung in Vue.js (Springer, Sebastian: To-dos mit Vue – in: Entwickler Magazin 4.18). Mit dieser Applikation können Sie Aufgaben anzeigen, erstellen, modifizieren und löschen. Diese Applikation wurde mit dem Kommando vue create todo-list des Vue CLI erstellt. Die Grundlage der Anzeige bilden die zwei Komponenten TodoList und TodoListItem. Bei beiden Komponenten handelt es sich um Single File Components, die jeweils in einer separaten Datei gespeichert werden. In Listing 1 sehen Sie den verkürzten Quellcode beider Komponenten.

// TodoList.vue
<template>
  <ul>
    <TodoListItem v-for="todo in todos" v-bind:todo="todo" 
     v-bind:key="todo.id"></TodoListItem>
  </ul>
</template>

<script>
import TodoListItem from './TodoListItem.vue';

export default {
  name: 'TodoList',
  components: { TodoListItem },
  data() {
    return {
      todos: []
    };
  },
};
</script>

<style scoped></style>

// TodoListItem.vue
<template>
  <li v-bind:class="todo.done ? 'done' : 'open'">
    {{todo.title}}
    <span v-if="todo.done" v-on:click="toggleDone(todo.id)">✔</span>
    <span v-else @click="toggleState(todo.id)">✘</span>
  </li>
</template>

<script>
export default {
  props: ['todo'],
};
</script>

<style scoped></style>

Mit dem Kommando npm run serve starten Sie die Applikation lokal, sodass Sie sie über den URL http://localhost:8080 erreichen können.

Vuex – Installation und erste Schritte

Die offizielle Projektseite von Vuex mit zahlreichen Ressourcen finden Sie unter https://vuex.vuejs.org/. Im Gegensatz zu Redux, das völlig unabhängig von React entwickelt wird, geht die Entwicklung von Vue und Vuex Hand in Hand. Ein deutliches Indiz hierfür ist auch, dass Vuex innerhalb der GitHub-Organisation von Vue entwickelt wird. Vuex verfolgt einen ähnlich leichtgewichtigen Ansatz wie Vue selbst, wenn es um die Einbindung der Bibliothek geht. Grundsätzlich können Sie sich die gebaute Bibliothek herunterladen, sie in Ihre Applikation einbinden und direkt loslegen. Die sauberere und besser wartbare Lösung ist jedoch der Einsatz eines Paketmanagers wie npm oder Yarn und des JavaScript-Modulsystems. Mit dem Befehl npm install vuex installieren Sie Vuex über npm in Ihrer Applikation. Die Abhängigkeit wird in die package.json-Datei eingetragen und Sie können in regelmäßigen Abständen prüfen, ob Aktualisierungen der Bibliothek verfügbar sind.

Vuex ist als Vue-Plug-in umgesetzt, sodass die Einbindung über die use-Methode der Vue-Instanz erfolgt. Die Registrierung des Plug-ins können Sie in der zentralen main.js-Datei Ihrer Applikation vornehmen. Eine bessere Variante stellt jedoch die Initialisierung von Vuex in einer separaten Datei dar. Zu diesem Zweck erzeugen Sie eine neue Datei mit dem Namen store.js. In dieser Datei registrieren Sie Vuex und erzeugen eine neue Instanz des Vuex Stores. In Listing 2 finden Sie den Quellcode dieser Datei.

import Vue from 'vue';
import Vuex from 'vuex';

Vue.use(Vuex);

const initialState = {
  todos: [
    { id: 1, title: 'Get up', done: true },
    { id: 2, title: 'Eat', done: true },
    { id: 3, title: 'Sleep', done: false },
  ],
};

export default new Vuex.Store({
  state: initialState,
});

Der Store – die Datenhaltung in einer Vuex-Applikation

Damit die Integration von Vuex in Ihre Applikation funktionieren kann, müssen Sie nun noch den Store einbinden. Das erfolgt in der main.js-Datei. Hier erstellen Sie die Vue-Instanz, die Ihre Applikation repräsentiert. Zusätzlich zur render-Eigenschaft definieren Sie hier die Eigenschaft store mit dem Wert, den Sie aus der store.js-Datei exportieren. Wie das funktioniert, sehen Sie in Listing 3.

import Vue from 'vue';
import App from './App.vue';

import store from './store';

Vue.config.productionTip = false;

new Vue({
  store,
  render: h => h(App),
}).$mount('#app');

Über die state-Eigenschaft des Objekts, das Sie bei der Erstellung des Stores übergeben, können Sie Ihren Store initial mit Daten befüllen. Da Sie den Store bereits an der zentralen Vue-Instanz registriert haben, können Sie nun von überall in Ihrer Applikation darauf zugreifen und die Daten in Ihrer Komponente anzeigen. Zu diesem Zweck existiert das $store-Objekt in jeder Komponente. Die state-Eigenschaft bietet Ihnen lesenden Zugriff auf die Daten. Für die Anzeige erzeugen Sie in der Komponente eine computed-Property, die den Zugriff kapselt. Damit erreichen Sie, dass die Anzeige aktualisiert wird, sobald sich die Information im Store ändert.

Noch etwas einfacher wird der Zugriff auf den Store über die mapState-Funktion. Dieses Hilfskonstrukt müssen Sie zunächst aus dem Vuex-Paket importieren. Anschließend bietet Ihnen mapState verschiedene Möglichkeiten, um die Informationen des Stores verfügbar zu machen. Die einfachste Variante besteht darin, dass Sie ein Array von Zeichenketten angeben. Diese geben die Schlüssel im Store an, die exportiert werden sollen. Im Beispiel der To-do-Liste finden Sie die Daten unter this.$store.state.todos. Das bedeutet, Sie übergeben die Zeichenkette todos in einem Array an mapState. Der Rückgabewert des Funktionsaufrufs ist ein Objekt, das Sie direkt als computed-Property verwenden können. Um diese mit bestehenden computed-Properties zu verwenden, können Sie das Objekt mit dem Spread-Operator in das vorhandene Objekt integrieren.

In einigen Fällen ist ein solch einfaches Mapping zwischen Store und Komponente nicht möglich. Hier können Sie statt eines Arrays auch ein Objekt übergeben, bei dem die Schlüssel die Namen der Eigenschaften und die Werte Funktionen sind, die den passenden Wert berechnen. Die Callback-Funktionen erhalten eine Objektrepräsentation des States. Damit Sie auf den lokalen State der Komponente zugreifen können, muss diese Funktion eine normale Methode des übergebenen Objekts und keine Arrow-Funktion sein. In Listing 4 sehen Sie die angepasste Version der TodoList-Komponente mit dem Zugriff auf den Store.

<template>
  <ul>
    <TodoListItem v-for="todo in todos" v-bind:todo="todo" 
     v-bind:key="todo.id"></TodoListItem>
  </ul>
</template>

<script>
import TodoListItem from './TodoListItem.vue';
import { mapState } from 'vuex';

export default {
  name: 'TodoList',
  components: { TodoListItem },
  computed: mapState(['todos']),
};
</script>

<style scoped></style>

Getter – flexibler Zugriff auf den Store

Der direkte Zugriff auf die Daten im Store stößt an seine Grenzen, wenn es um Aggregationen oder andere Berechnungen geht. Hier können Sie zwar mit den Callback-Funktionen im mapState arbeiten, was jedoch wenig sinnvoll ist, wenn diese Funktionalität in mehreren Komponenten benötigt wird. Eine ähnliche Funktionalität, allerdings auf einer globaleren Ebene, bieten die Getter-Funktionen des Stores. Hierbei handelt es sich um Funktionen, die als Argument eine Objektrepräsentation des States erhalten, daraus bestimmte Informationen extrahieren und diese als Rückgabewert zurückliefern. Die Getter-Funktionen werden über die getter-Eigenschaft des Stores bei der Erzeugung registriert. Typische Beispiele für solche Getter-Funktionen sind Sortier- und Filterfunktionen. Um beispielsweise alle bereits erledigten Aufgaben auszublenden, können Sie eine entsprechende Getter-Funktion mit dem Namen openTodos erzeugen. Den Quellcode hierfür enthält Listing 5.

export default new Vuex.Store({
  state: initialState,
  getters: {
    openTodos: state => state.todos.filter(todo => !todo.done),
  },
});

In Ihrer Komponente können Sie über this.$store.getter.openTodos auf die gefilterte Aufgabenliste zugreifen. Analog zum mapState-Helper gibt es eine ähnliche Hilfsfunktion auch für die Getter-Funktionen. Sie trägt den Namen mapGetters und muss zunächst aus dem Vuex-Paket importiert werden. Anschließend können Sie sie ähnlich wie die mapState-Funktion innerhalb der computed-Properties verwenden. Dazu übergeben Sie ein Array mit den Namen der Getter-Funktionen, die Sie verwenden möchten. Auch hier besteht wieder die Möglichkeit, das Objekt, das Sie von dieser Funktion erhalten, über den Spread-Operator in eine bestehende Datenstruktur zu integrieren.

Bei einem parametrisierten Getter, wie Sie ihn beispielsweise für eine Suche benötigen, müssen Sie etwas tiefer in die Trickkiste greifen und eine Funktion definieren, die ihrerseits wiederum eine Funktion zurückgibt. Wie eine solche Filterfunktion aussehen kann, sehen Sie in Listing 6.

export default new Vuex.Store({
  state: initialState,
  getters: {
    openTodos: state => state.todos.filter(todo => !todo.done),
    filter: state => query =>
      state.todos.filter(todo => todo.title.includes(query)),
  },
});

Mutations – schreibender Zugriff auf den Store

Wie bereits erwähnt, ist es nicht ohne weiteres möglich, schreibend auf den Vuex-Store zuzugreifen. Die schreibenden Operationen werden durch sogenannte Mutations gekapselt. Die erste Mutation, die Sie für Ihre Applikation implementieren, ist die Statusänderung Ihrer Aufgabe, worüber eine Aufgabe als erledigt markiert werden kann. Mutations sind im weitesten Sinne mit Events vergleichbar und haben ihre Entsprechung in der Flux-Architektur in einer Kombination aus Action-Objekt und Reducer-Funktion. Die für Ihren Store verfügbaren Mutations registrieren Sie wie schon den State und die Getter-Funktionen bei der Erzeugung des Store-Objekts unter dem Schlüssel mutations. Eine solche Mutation-Funktion erhält als ersten Parameter den aktuellen State, alle weiteren Parameter bestimmen Sie selbst. Im Fall des Beispiels benötigen Sie die ID der Aufgabe, deren Status geändert werden soll. Eine Mutation sorgt dafür, dass aufgrund der zur Verfügung stehenden Information der State entsprechend angepasst wird. Das reaktive System von Vuex und Vue trägt dann die Verantwortung dafür, dass die Änderungen überall in der Applikation korrekt aktualisiert werden. Damit Sie eine Mutation anstoßen können, müssen Sie diese committen. Der Store stellt Ihnen hierfür die commit-Methode zur Verfügung. Als Argumente übergeben Sie der Funktion den Namen der Mutation und optional weitere Argumente, die Ihnen in der Mutation-Funktion zur Verfügung stehen. In Listing 7 sehen Sie die Implementierung der Mutation.

export const TOGGLE_DONE = 'TOGGLE_DONE';

export default new Vuex.Store({
  state: initialState,
  getters: {},
  mutations: {
    [TOGGLE_DONE](state, id) {
      const todo = state.todos.find(todo => todo.id === id);
      Vue.set(todo, 'done', !todo.done);
    },
  },
});

Was auf den ersten Blick etwas gewöhnungsbedürftig erscheint, ist, dass die Mutation keinen gewöhnlichen Methodennamen aufweist, sondern durch eine computed-Property repräsentiert wird. Der Grund hierfür ist, dass Sie die Konstante, die für den Namen steht, exportieren und in Ihrer Applikation verwenden können.

Eine Mutation wird in der Regel durch eine Benutzerinteraktion ausgelöst und so ist es auch hier der Fall. In der To-do-Liste hat ein Benutzer die Möglichkeit, eine Aufgabe mit einem Klick auf ein Haken- beziehungsweise Kreuzsymbol als erledigt oder offen zu markieren. Die Handler-Funktionen für die Klick-Events der Komponenten finden sich in der methods-Eigenschaft der Komponente. Im einfachsten Fall implementieren Sie wie gewohnt eine solche Handler-Funktion. Innerhalb dieser Funktion rufen Sie die commit-Methode des Stores auf und sorgen damit dafür, dass die Mutation committet wird. Als erstes Argument übergeben Sie der commit-Methode die Konstante, die für die Methode steht. Das zweite Argument ist die ID der Aufgabe, deren Status geändert werden soll.

Auch für das Committen von Mutations existiert ein Shortcut analog zu mapGetters. Mit dem mapMutations-Helper aus dem Vuex-Paket können Sie die Mutations direkt auf die methods der Komponente mappen. Hierfür übergeben Sie der mapMutations-Funktion ein Array mit den Namen der entsprechenden Mutations. Auf diese können Sie dann zugreifen wie auf gewöhnliche Methoden der Komponente. Nutzen Sie Konstanten als Benennung für die Mutations, müssen Sie diese auch im Template verwenden. Alternativ dazu können Sie auch Aliases für die Mutations vergeben. Hierfür übergeben Sie statt dem Array ein Objekt. Die Schlüssel des Objekts sind die Alias-Namen und die Werte die Namen der Mutations. In Listing 8 finden Sie den angepassten Quellcode der TodoListItem-Komponente, in der die Mutation auf diese Art gemappt ist.

<template>
  <li v-bind:class="todo.done ? 'done' : 'open'">
    {{todo.title}}
    <span v-if="todo.done" v-on:click="toggleDone(todo.id)">✔</span>
    <span v-else @click="toggleDone(todo.id)">✘</span>
    </li>
</template>

<script>
import { TOGGLE_DONE } from '../store.js';
import { mapMutations } from 'vuex';

export default {
  props: ['todo'],
  methods: mapMutations({ toggleDone: TOGGLE_DONE }),
};
</script>

<style scoped></style>

Ein wichtiges Merkmal von Mutations ist, dass sie synchron sind. Das bedeutet, dass sie den State direkt modifizieren und keine asynchronen Operationen wie Timeouts oder Serverkommunikation aufweisen dürfen. Solche asynchronen Operationen können von Vuex also nicht durch Mutations überwacht werden. Eine klassische Single Page Application lebt jedoch von der asynchronen Kommunikation mit dem Server, und so ist es auch in Vuex erforderlich, solche Operationen durchführen zu können. Andere Bibliotheken wie beispielsweise Redux benötigen für asynchrone Operationen zusätzliche Hilfsbibliotheken. Vuex hat diese Hilfskonstrukte in Form von Actions direkt integriert.

Actions – Umgang mit asynchronen Operationen in Vuex

Actions werden ähnlich integriert wie Mutations, mit dem Unterschied, dass eine Action nicht den State der Applikation modifiziert, sondern ihrerseits Actions committet, die den State modifiziert. Vor dem Commit können beliebige asynchrone Aktionen erfolgen.

Die erste Action, die Sie in Ihre Applikation integrieren, sorgt dafür, dass die Aufgaben nicht mehr lokal im Quellcode vorgehalten werden müssen, sondern von einem Server bezogen werden können. Damit das funktionieren kann, müssen Sie noch etwas Vorarbeit leisten. Die einfachste Variante, um ein funktionierendes Backend für Ihre Applikation zu erhalten, führt über den JSON-Server – ein npm-Paket, das Ihnen einen eigenständigen Server zur Verfügung stellt, der eine JSON-Struktur ausliefert. Dieser unterstützt neben lesenden auch schreibende Zugriffe und erfüllt somit alle Anforderungen, die unsere Applikation an ein Backend stellt. Das JSON-Server-Paket können Sie global oder lokal installieren. Für das Beispiel reicht eine lokale Installation als DevDependency aus. Hierfür führen Sie das Kommando npm install -D json-server auf der Kommandozeile aus. Im nächsten Schritt erzeugen Sie eine Datei mit dem Namen data.json in dem Projektverzeichnis, das die Aufgaben im JSON-Format enthält. Im letzten Schritt bearbeiten Sie die package.json-Datei Ihres Projekts und fügen einen neuen Eintrag in der scripts-Sektion ein, um den Server zu starten. Zusätzlich zu diesem Skript müssen Sie außerdem einen Proxy konfigurieren, damit Ihre Applikation im Entwicklungsmodus problemlos auf den Server zugreifen kann. Das erreichen Sie, indem Sie eine Datei mit dem Namen vue.config.js mit einer entsprechenden Konfiguration erzeugen. Die Ausschnitte aus der vue.config.js sowie der package.json-Datei finden Sie in Listing 9.

// package.json
{
  "scripts": {
    "server": "json-server --watch data.json"
  }
}

// vue.config.js
module.exports = {
  devServer: {
    proxy: 'http://localhost:3000',
  },
};

Nachdem Sie den JSON-Server mit dem Kommando npm run server sowie Ihre Applikation mit npm run serve gestartet haben, können Sie die To-do-Liste vom Server befüllen lassen. Zunächst benötigen Sie eine Action, die dafür sorgt, dass die Daten vom Server geladen werden. Actions registrieren Sie wie alle übrigen Elemente von Vuex beim Erstellen des Stores. Unter dem Schlüssel actions implementieren Sie die Action GET_TODOS. Als erstes Argument der Action-Methode erhalten Sie ein Context-Objekt. Dieses stellt Ihnen die commit-Methode zur Verfügung, mit der Sie Mutations committen können. Innerhalb der Action-Methode nutzen Sie das fetch-API des Browsers, um die Daten vom Server zu laden. Da diese Schnittstelle mit Promises arbeitet, können Sie die Action auch als async-Methode implementieren und mit dem await-Schlüsselwort arbeiten. Im Erfolgsfall committen Sie die GET_TODOS_SUCCESS-Mutation mit den Daten, die Sie vom Server erhalten haben. Diese Mutation sorgt dafür, dass die Daten des Servers in den State Ihrer Applikation geschrieben werden. Das reaktive System von Vue übernimmt dann die übrige Arbeit und aktualisiert die Anzeige, sodass Sie im Browser die Liste der Aufgaben sehen können. In Listing 10 finden Sie den Quellcode der Action und der zugehörigen Mutation.

export default new Vuex.Store({
  state: initialState,
  getters: {},
  mutations: {
    [TOGGLE_DONE](state, id) {},
    [GET_TODOS_SUCCESS](state, todos) {
      Vue.set(state, 'todos', todos);
    },
  },
  actions: {
    async [GET_TODOS]({ commit }) {
      const response = await fetch('/todos');
      commit(GET_TODOS_SUCCESS, await response.json());
    },
  },
});

Noch wird die Action jedoch nicht aktiviert, sodass die bisherigen Modifikationen an Ihrer Applikation noch keine Wirkung zeigen. Die Action muss ähnlich wie eine Mutation committet werden. Allerdings spricht man bei Actions hier von dispatchen. Aus einer Komponente heraus können Sie mit einem Aufruf von this.$store.dispatch(GET_TODOS) die Action auslösen und die Aufgaben vom Server laden. Diesen Aufruf platzieren Sie am besten in den mounted-Hook der Komponente. Mit dem mapActions-Helper können Sie den Quellcode Ihrer Komponente noch etwas aufräumen. Diese Funktion funktioniert ähnlich wie die mapActions-Funktion. Sie können entweder ein Array von Action-Namen übergeben oder mit einem Objekt mit Aliasnamen und Action-Namen arbeiten. Listing 11 zeigt Ihnen den Quellcode der TodoList-Komponente und der Verbindung des mounted-Hooks mit der Action.

export default {
  name: 'TodoList',
  components: { TodoListItem },
  data() {
    return {
      showOnlyOpen: false,
    };
  },
  methods: {
    toggleFilter() {
      this.showOnlyOpen = !this.showOnlyOpen;
    },
    getTodos() {
      return this.showOnlyOpen ? this.openTodos : this.todos;
    },
    ...mapActions({ fetchTodos: GET_TODOS }),
  },
  computed: {
    ...mapState(['todos']),
    ...mapGetters(['openTodos']),
  },
  mounted() {
    this.fetchTodos();
  },
};

Actions können nicht nur lesende, sondern auch schreibende Operationen auslösen. In unserer To-do-Liste sorgen wir durch eine weitere Action im nächsten Schritt dafür, dass die Statusänderung an einer Aufgabe auf dem Server gespeichert wird. Die Action setzen Sie zwischen den Click Handler der TodoListItem-Komponente und die TOGGLE_DONE-Mutation. Um die erforderlichen Anpassungen möglichst gering zu halten, folgen Sie der Namenskonvention, die Sie schon beim Lesen der Datensätze verwendet haben. Danach lautet der Name der Action TOGGLE_DONE, und die zugehörige Mutation, die nach dem erfolgreichen Speichern ausgelöst wird, heißt TOGGLE_DONE_SUCCESS. Die TOGGLE_DONE-Action erzeugt zunächst eine Kopie des Datensatzes und ändert den Statuswert. Anschließend werden die Daten über das fetch-API mit der PUT-Methode zum Server gesendet. Achten Sie hierbei darauf, den korrekten Content Type zu setzen und die Daten als JSON-String zu übermitteln. Der Server antwortet mit dem modifizierten Datensatz, den Sie decodieren und als TOGGLE_DONE_SUCCESS-Mutation committen können. An der ehemaligen TOGGLE_DONE-Mutation müssen Sie noch eine Anpassung vornehmen, damit der Status nicht wieder zurückgesetzt wird. Außerdem arbeiten Sie an dieser Stelle nicht mehr mit der ID des Datensatzes, sondern mit dem kompletten Datensatz. Diese Anpassung müssen Sie außerdem noch in der TodoListItem-Komponente vornehmen. Listing 12 enthält den Code der Komponente sowie die Anpassungen am Store.

// store.js
export const TOGGLE_DONE = 'TOGGLE_DONE';
export const TOGGLE_DONE_SUCCESS = 'TOGGLE_DONE_SUCCESS';
export const GET_TODOS = 'GET_TODOS';
export const GET_TODOS_SUCCESS = 'GET_TODOS_SUCCESS';

export default new Vuex.Store({
  state: initialState,
  getters: {},
  mutations: {
    [TOGGLE_DONE_SUCCESS](state, modifiedTodo) {
      const todo = state.todos.find(todo => todo.id === modifiedTodo.id);
      Vue.set(todo, 'done', modifiedTodo.done);
    },
    [GET_TODOS_SUCCESS](state, todos) {},
  },
  actions: {
    async [GET_TODOS]({ commit }) {},
    async [TOGGLE_DONE]({ commit }, todo) {
      const clone = { ...todo, done: !todo.done };
      const result = await fetch(`/todos/${todo.id}`, {
        method: 'PUT',
        headers: { 'content-type': 'application/json' },
        body: JSON.stringify(clone),
      });
      commit(TOGGLE_DONE_SUCCESS, await result.json());
    },
  },
});

// TodoListItem.vue
<template>
  <li v-bind:class="todo.done ? 'done' : 'open'">
    {{todo.title}}
    <span v-if="todo.done" v-on:click="toggleDone(todo)">✔</span>
    <span v-else @click="toggleDone(todo)">✘</span>
  </li>
</template>

<script>
import { TOGGLE_DONE } from '../store.js';
import { mapActions } from 'vuex';

export default {
  props: ['todo'],
  methods: mapActions({ toggleDone: TOGGLE_DONE }),
};
</script>

<style scoped></style>

Ein wichtiger Punkt, der in diesem Beispiel viel zu kurz gekommen ist, ist die Fehlerbehandlung. Innerhalb der Action fangen Sie den Fehler bei der Kommunikation ab und committen statt der Mutation TOGGLE_DONE_SUCCESS eine TOGGLE_DONE_FAILURE-Mutation, die einen Fehler-State setzen kann, auf den die Komponenten reagieren können. Bei allen Operationen, die potenziell fehlschlagen können, sollten Sie solche Fehler-Mutations vorsehen.

Modules – Modularisierung einer Vuex-Applikation

Werfen Sie einen Blick auf den Code, der für die Erzeugung des Stores verantwortlich ist, fällt schnell auf, dass der Code mit zunehmendem Funktionsumfang der Applikation immer unübersichtlicher wird. Die Lösung für größere Applikationen liegt hier in der Modularisierung des Stores. Mit den Vuex Modules lässt sich der Store in mehrere Sektionen unterteilen, die jeweils über eigene Getter, Actions und Mutations verfügen. Für Ihre To-do-Liste bedeutet das, dass Sie sämtliche Inhalte des Stores in ein Modul auslagern und es in den Store einbinden. Ein solches Modul ist ein Objekt mit den Schlüsseln state, getters, mutations und actions. Eingebunden wird das Modul bei der Erstellung des Stores über den modules-Schlüssel, dem Sie ein Objekt zuweisen. Listing 13 zeigt Ihnen den erforderlichen Quellcode.

// store.js
import Vue from 'vue';
import Vuex from 'vuex';

import todo from './todo';

Vue.use(Vuex);

export default new Vuex.Store({
  modules: {
    todo,
  },
});

// todo.js
import Vue from 'vue';

const initialState = { todos: [] };

export const TOGGLE_DONE = 'TOGGLE_DONE';
export const TOGGLE_DONE_SUCCESS = 'TOGGLE_DONE_SUCCESS';
export const GET_TODOS = 'GET_TODOS';
export const GET_TODOS_SUCCESS = 'GET_TODOS_SUCCESS';

export default {
  namespaced: true,
  state: initialState,
  getters: {},
  mutations: {},
  actions: {},
};

Im Modul wurde zusätzlich die Eigenschaft namespaced eingefügt. Das sorgt dafür, dass die einzelnen Sektionen des Stores nicht automatisch zusammengefügt werden, sondern jeweils in einem separaten Namespace liegen, was für eine bessere Trennung sorgt. Durch das Namespacing ändert sich der Zugriff auf die Eigenschaften des Stores ein wenig. Statt TOGGLE_DONE direkt zu verwenden, müssen Sie den Namespace voranstellen, also auf todo/TOGGLE_DONE zugreifen. Damit Sie das nicht in allen Komponenten Ihrer Applikation anpassen müssen, können Sie mit der createNamespacedHelpers-Funktion eine Version der Hilfsfunktionen generieren, die automatisch auf den korrekten Namespace zugreift. Der createNamespacedHelpers-Funktion übergeben Sie den Namen des Namespace, den Sie verwenden möchten, und erhalten ein Objekt zurück. Dieses Objekt verfügt unter anderem über die Methoden mapActions und mapMutations. Diese können Sie dann ohne weitere Modifikationen in Ihren Komponenten nutzen. Greifen Sie aus einer Komponente auf verschiedene Namespaces zu, können Sie entweder mehrere Helper-Funktionen erzeugen und zusammenführen, oder Sie greifen explizit auf den jeweiligen Namespace zu.

Zusammenfassung

Im Vergleich zu anderen State-Management-Bibliotheken ist Vuex eine leichtgewichtige und verhältnismäßig einfach zu erlernende Lösung. Trotz dieser Einfachheit skaliert Vuex auch gut für umfangreiche Applikationen. Vor allem die Möglichkeit, den Store durch Module und Namespaces zu unterteilen, hilft bei der Entwicklung größerer Applikationen. Gliedern Sie die einzelnen fachlichen Aspekte Ihrer Applikation in separate Verzeichnisse, lassen sich die einzelnen Module gut entkoppeln und auch parallel von unterschiedlichen Entwicklungsteams weiterentwickeln, ohne dass Konflikte entstehen.

Ein weiterer Vorteil von Vuex ist die nahezu nahtlose Integration in Vue. Das reaktive System von Vue sorgt dafür, dass Änderungen am State der Applikation in der Ansicht aktualisiert werden, ohne dass Sie selbst dafür sorgen müssen.

Entwickler Magazin

Entwickler Magazin abonnierenDieser Artikel ist im Entwickler Magazin erschienen.

Natürlich können Sie das Entwickler Magazin über den entwickler.kiosk auch digital im Browser oder auf Ihren Android- und iOS-Devices lesen. In unserem Shop ist das Entwickler Magazin ferner im Abonnement oder als Einzelheft erhältlich.

Unsere Redaktion empfiehlt:

Relevante Beiträge

Hinterlasse einen Kommentar

Hinterlasse den ersten Kommentar!

avatar
400
  Subscribe  
Benachrichtige mich zu:
X
- Gib Deinen Standort ein -
- or -