The Art of State: Zustandsmanagement in React-Anwendung, Teil 2
Seite 3: Externes State-Management mit Redux
Redux ist eine externe Bibliothek, die explizit das Arbeiten mit globalem Zustand adressiert. Sie ist in dem Bereich die wohl prominenteste und am meisten eingesetzte Lösung zur Zustandsverwaltung in React-Anwendungen. Die Kern-Idee von Redux ist, dass der globale Zustand vollständig aus der Anwendung heraus in eine oder mehrere Reducer-Funktionen wandert. Eine Reducer-Funktion in Redux verhält sich genau wie die Reducer-Funktionen, die im Zusammenhang mit useReducer behandelt wurden, und teilt auch deren Eigenschaften: Sie ist bibliothekenunabhängig, verarbeitet Aktionen und liefert neuen Zustand zurück.
Allerdings gehören Reducer-Funktionen, die useReducer erzeugt, zu einer Komponente. Das bedeutet, dass auch der mit der Reducer-Funktion verwaltete Zustand beim Entfernen der Komponente beseitigt wird. In Redux stehen die Reducer-Funktionen, und somit der Anwendungszustand, vollständig außerhalb der Komponentenhierarchie. Er ist demnach unberührt vom Lebenszyklus einzelner Komponenten. (Es ist allerdings auch in Redux-Anwendungen üblich, nur globalen Zustand in Redux zu speichern und lokalen Zustand mit useState zu verwalten.)
Das zentrale Element von Redux ist der Store. Er hält den globalen Zustand der Anwendung. Möchte diese den Zustand verändern, löst sie – genau wie bei useReducer – eine Aktion aus. Redux verteilt die Aktion nun aber nicht an eine Reducer-Funktion, sondern an alle bekannten Reducer-Funktionen. So ist es möglich und üblich, dass nicht ein Reducer, sondern gleich mehrere eine Aktion verarbeiten. Beispielsweise könnte eine LOGOUT-Aktion für mehrere Reducer interessant sein, damit diese jeweils ihren Zustand beim Abmelden der Benutzer wieder zurücksetzen können.
Auch in Redux sind Actions einfache JavaScript-Objekte, die eine type-Property zur Identifikation enthalten. Außerdem lassen sich je nach Fachlichkeit beliebige weitere Properties verwenden, die anwendungsfallspezifische Nutzdaten (Payload) enthalten. Per Konvention werden diese Daten meist unter einer payload-Property zusammengefasst. Eine Aktion zum Setzen der Sortierreihenfolge der Blog-Posts könnte wie folgt aussehen:
type:
{
type: "blogOrderChange",
payload: {
orderBy: "date",
direction: "descending"
}
}
Eine Anwendung erzeugt diese Action-Objekte entweder direkt innerhalb einer Komponente oder verwendet dafĂĽr eine eigene Funktion, einen Action Creator. Dabei handelt es sich um eine einfache Factory-Funktion, die Action-Objekte erzeugt.
function blogOrderChange(orderBy, direction) {
return {
type: "blogOrderChange",
payload: { orderBy, direction }
}
}
Ob Entwickler Action-Creator-Funktionen verwenden oder die Action-Objekte direkt in einer Komponente erzeugen, ist in vielen Fällen Geschmackssache. Eine Action-Creator-Funktion könnte sicherstellen, dass nur konsistente Actions erzeugt werden (z. B. dass keine null/undefined-Werte gesetzt werden, wo das nicht erlaubt ist), und ist möglicherweise leichter les- und schreibbar im Anwendungscode. Verpflichtend wird eine Action-Creator-Funktion, wenn asynchroner Code ausgeführt werden soll, da das die einzige Stelle ist, an der in Redux asynchroner Code geschrieben werden darf.
Eine Komponente kann dann mit der dispatch-Funktion Aktionen auslösen. Redux stellt die dispatch-Funktion über den useDispatch-Hook zur Verfügung:
function OrderByButton() {
const dispatch = useDispatch();
const handleClick = () => dispatch(
blogOrderChange("date", "desc")
);
return <button onClick={handleClick}>Order By Date</button>;
}
Redux leitet diese Aktion an alle bekannten Reducer-Funktionen weiter. Eine Reducer-Funktion implementiert dabei einen fachlich abgeschlossenen Teil der Anwendung. Die Funktionen kennen sich untereinander nicht, können also nur ihren Teil des Zustands einsehen und verändern, nicht aber den anderer Reducer. Ein Reducer ist also für einen Teilzustand des globalen Zustands verantwortlich. Ein solcher Teil heißt bei Redux auch Slice.
Eine Komponente wählt aus dem globalen Zustand die Daten aus, die sie zur Darstellung benötigt. Dazu stellt Redux den useSelector-Hook zur Verfügung, dem eine Selector-Funktion übergeben wird. Diese Funktion wählt die für eine Komponente relevanten Daten aus dem Zustand aus und liefert sie an die Komponente zurück. Im folgenden Beispiel wählt die Komponente den Namen des aktiven Themes sowie den Namen des angemeldeten Benutzers (oder null) aus:
function UserProfile() {
const themeName = useSelector(state => state.theme.name);
const userName = useSelector(state = >
state.auth.logged_in ? state.auth.user.name : null
);
if (!userName) {
return <div className={themeName}>Not logged in</div>
}
return <div className={themeName}>Hello, {userName}</div>;
}
Redux ruft die Selector-Funktionen der gerenderten Komponenten auf, nachdem eine Änderung des globalen Zustands erfolgt ist, also in der Regel nach dem eine oder mehrere Aktionen verarbeitet wurden. Wenn mindestens eine der Selector-Funktionen einer Komponente einen anderen Wert als beim vorherigen Rendern zurückliefert, veranlasst Redux, dass die Komponente neu gerendert wird und somit die aktualisierten Werten aus dem State zur Darstellung verwenden kann.
Häufig verwendete Selektoren lassen sich in einen eigenen Hook verschieben (ähnlich wie useFormContext im Context-Beispiel):
function useThemeName() {
return useSelector(state => state.state.theme.name);
}
Komponenten, die den Theme-Namen wissen müssen, brauchen nun nicht mehr zu wissen, wo der Name herkommt, sondern haben eine "fachliche" Funktion für den Zugriff darauf. Durch diese Kapselung lässt sich eine Migration von Context auf Redux vereinfachen.
Da die Selektoren bei allen Änderungen des globalen Zustands ausgeführt werden, sollten diese entsprechend schnell sein. Einfache Selektoren, wie die oben gezeigten, erfüllen diese Anforderung sicherlich. Bei komplexeren Selektoren, die mehr Logik enthalten (die zum Beispiel direkt Daten Sortieren oder Filtern), lässt sich die Bibliothek re-select verwenden. Sie stammt ebenfalls aus dem Redux-Umfeld, ist allerdings nicht Bestandteil von Redux. re-select kann Selektor-Funktionen erzeugen, die nur ausgeführt werden, wenn sich ein oder mehrere Eingangsparameter verändert haben. Die Eingangsparameter werden ebenfalls bei allen Änderungen im globalen Zustand ermittelt und mit den vorherigen verglichen. Nur wenn sich einer der Parameter verändert hat, kommt es zur Ausführung der eigentlichen Selektor-Funktion, sodass nur dann die (potenziell teure) Verarbeitungslogik (sortieren, filtern ...) ausgeführt wird, wenn es wirklich notwendig ist.