The Art of State: Zustandsmanagement in React-Anwendung, Teil 2
Seite 2: Die Context API mit Reducer-Funktionen
Im ersten Artikel wurde der useReducer-Hook vorgestellt. Er lässt sich einsetzen, um die Verarbeitung komplexen Zustands aus einer Komponente herauszuziehen. Dann löst die zugehörige Komponente Aktionen (Actions) aus, die eine Reducer-Funktion fachlich verarbeitet. Diese liefert als Ergebnis der Verarbeitung neuen Zustand an die Komponente zurück.
Mit dem useReducer-Hook stellt React eine Möglichkeit zur Verfügung, wie sich komplexer Zustand verarbeiten lässt, insbesondere wenn dazu komplexe Logik erforderlich ist. Mit dem useContext-Hook bietet React die Option, Daten und Funktionen global bereitzustellen. Ein naheliegendes Architekturmuster ist es, die beiden Hooks gemeinsam für das Arbeiten mit globalem Zustand zu verwenden.
Im Form-Beispiel ließe sich die Verwaltung des Form-Zustands mit einer Reducer-Funktion implementieren. Dann wäre es beispielsweise möglich, die Logik separiert von React zu testen, da sie sich nicht mehr direkt innerhalb der Komponente befindet. Die Provider-Komponente verwendet nun den useReducer-Hook, um ihren Zustand zu verwalten und zu verändern. Sie übergibt diesen aber weiterhin mit dem Kontext an die Unterkomponenten. Für die Konsumenten ändert sich beim lesenden Zugriff nichts: Aus ihrer Sicht ist der Kontext weiterhin ein Objekt mit Werten und Funktionen.
Zum Verändern des Zustands sind nun aber Aktionen auszulösen, die die dispatch-Funktion an die Reducer-Funktion weiterleitet. Dafür gibt es nun zwei Möglichkeiten: So kann die Provider-Komponente die dispatch-Funktion unverändert an die Konsumenten weitergeben. Die Konsumenten müssen dann Aktionen auslösen:
function Form({children}) {
const [ state, dispatch ] = React.useReducer(. . .);
return <FormContext.Provder value={
{ state, dispatch }>{children}</FormContext.Provider>
}
function TextField({name}) {
const formState = React.useContext(FormContext);
// Zugriff auf Werte unver�ndert
const fieldValue = formState[name]?.value || "";
// Hier warden nun Actions ausgel�st
const onChange = e => formState.dispatch({
type: "field_change",
fieldname: name,
fieldValue: e.target.value
)};
return <input value={fieldValue} onChange={onChange} />
}
Ein möglicher Nachteil dieses Ansatz besteht darin, dass die Konsumenten nun technische Implementierungsdetails des Providers kennen (dispatch-Funktionen) und auch wissen müssen, wie die Action-Objekte aufgebaut sind, die ausgelöst werden dürfen. Eine Variation besteht daher darin, dass die Provider-Komponente fachliche Funktionen anbietet und über den Kontext zur Verfügung stellt, in denen dann die Actions erzeugt werden. Für die Konsumenten des Kontexts ist es dann transparent, wie der Provider technisch implementiert ist:
function Form({children}) {
const [ state, dispatch ] = React.useReducer(. . .);
const context = {
state,
onFieldChange(fieldname, fieldValue) => dispatch({
type: "field_change",
fieldname, fieldValue
})
}
return <FormContext.Provder value={context}>
{children}
</FormContext.Provider>
}
function TextField({name}) {
const formState = React.useContext(FormContext);
// Zugriff auf Werte unver�ndert
const fieldValue = formState[name]?.value || "";
// Hier nun "fachliche" Callback-Funktion aufrufen
const onChange = e => formState.onFieldChange
(name, e.target.value);
return <input value={fieldValue} onChange={onChange} />
}
Mit einem eigenen Hook lässt sich auch die Verwendung des Kontexts für die konsumierenden Komponenten verstecken. Die Konsumenten rufen stattdessen eine "fachliche" Funktion auf, um – im Beispiel – die Form-Daten zu erhalten, ohne wissen zu müssen, dass dahinter technisch ein Kontext steht und wie dieser heißt:
// z.B. in use-form-state.js
function useFormState() {
return React.useContext(FormContext);
}
// z.B. in TextField.js
function TextField({name}) {
const formState = useFormState();
// alles Weitere unver�ndert
. . .
}
Auf diesem Weg ist nun einerseits die Logik zur Verarbeitung des Zustands aus den Komponenten gewandert (in den Reducer, eine reguläre JavaScript-Funktion ohne Abhängigkeiten auf React). Andererseits stehen Werte und Funktionen global zur Verfügung (Context), ohne dass Konsumenten genau wissen müssen, wo diese Daten herkommen und wie deren Verarbeitung technisch implementiert ist (durch die Kapselung von dispatch im Context sowie von useContext im Custom Hook).
Die Context API hat in React eine längere Geschichte, ist aber erst seit einigen Versionen in einer stabilen Version veröffentlicht. Insbesondere seit der Einführung der Hooks API und des useReducer-Hooks gerät sie zunehmend stärker in den Blickpunkt, da die Verwendung relativ einfach ist.