Dieses Frontend macht von diversen Technologien gebrauch (mehr in package.json), aber bei den wichtigsten davon handelt es sich vermutlich bei React.js, React Redux und der Hilfs-Library Redux Toolkit, die den Umgang zwischen beiden Technologien etwas vereinfacht. Hier wird kurz auf einige der wichtigsten Aspekte dieser Technologien eingegangen. Für weitere, tiefergehende Informationen sind allerdings auch Links zu den jeweiligen Dokumentationsseiten eingebettet.
React.js
Das Rückgrat der Darstellungslogik wird durch React.js gebildet. Diese Library erlaubt es, UI-Komponenten in einer HTML-ähnlichen Syntax auszudrücken und so nach belieben zu komponieren. Weiterhin bietet React gewisse Performancevorteile durch die Optimierung von Komponentenupdates, nämlich immer nur dann und dort wo sich etwas geändert hat.
React macht dazu gebrauch von Komponenten, dargestellt etweder durch Subklassen von React.Component
oder wie in diesem Projekt vorzüglich verwendet durch Funktionen. Diese bestehen aus HTML-Elementen oder anderen React Komponenten und werden dank JSX in einer HTML-ähnlichen Sytax direkt im Javascript Code formuliert:
const Counter = props => (
<span>
Current count: {props.count}
</span>
)
// ...somewhere
<Counter count={4} />
In diesem Beispiel haben wir eine funktionale Komponente definiert, d.h. eine Komponente die durch einen Funktionsaufruf erzeugt wird. Diese Methode wird zunehmend populärer und sind inzwischen auch in ihren Möglichkeiten mit Klassen-Komponenten gleichgezogen durch die etwas weiter unten beschriebenen Hooks.
Das Beispiel definiert eine einfache statische Counter-Komponente, die lediglich einen übergebenen Wert mit einem
kleinen, beschreibenden Text darstellt. Dabei können wir beobachten das die Komponenten wirklich nur aus HTML bestehen. Der funktionalen Komponente wird dabei ein Objekt props
übergeben, das alle 'properties' enthält. Dabei handelt es sich um die Werte die ähnlich Properties in HTML übergeben werden, hier nur der Wert von count
. Wollen wir einen Wert aus der Javascript-Welt in die JSX-Welt überführen, muss dieser in geschweifte Klammern ({}
) gesetzt werden.
Die definierte Komponente selbst kann dann ebenfalls einfach wie ein neues HTML Element verwendet werden.
Es ist weiterhin wichtig zu erwähnen, dass jede Komponente nur ein einziges Element zurückgeben darf. D.h. alle Elemente die durch unsere Funktion erzeugt werden müssen im selben äußeren Element (bsw. div
) liegen. Will man dies vermeiden kann man Fragments verwenden, ein Element das nur in der internen Logik von React existiert. Die kurzschreibweise hierfür ist durch leere HTML-Tags (<>
und </>
) möglich.
Schließlich gibt es noch ein besonderes Property, children
, welches alle genesteten Komponenten enthält. Diese können beispielsweise wie folgt innerhalb der Komponente wieder realisiert werden:
const Div = props => (
<div>
{...children}
</div>
)
// ...somewhere
<Div>Hallo, Welt!</Div>
Durch die sogenannten Hooks ist es weiterhin möglich, dass Komponenten zusätzliche Logik bei Updates und weiters dynamisches Verhalten wie Zustandsänderungen ausnutzen können. Andere Libraries (insb. React-Redux) definieren auch eigene Hooks, aber wir sehen und hier die zwei wichtigsten React Hooks an: useState
und useEffect
.
useState
Veränderlicher Zustand mit Betrachten wir zuerst das folgende Beispiel:
const Counter = props => {
const [count, setCount] = useState(0)
return (
<span>
<button onClick={() => setCount(count + 1)}>Count Up</button>
Current count: {count}
</span>
)
}
Der State Hook erzeugt eine Variable deren Wert durch eine zusätzlich erzeugte Funktion geändert werden kann. Die Variable wird mit dem Parameter der useState
übergeben wird initialisiert. Solange die Komponente existiert bleibt auch der Zustand dieser Variablen erhalten.
Diese Form der Zustandshaltung ist innerhalb der Komponente internalisiert, kann also nur von außerhalb genutzt werden wenn die Komponente ihn selbst weitergibt. Änderungen des Zustandes verursachen das neuzeichnen der Komponente. Aber da Updates auf die spezifische Komponente und ihre Kind-Knoten beschränkt bleibt ist dies trotzdem effizient.
useEffect
Auf Updates reagieren mit Der Effekt Hook wird aufgerufen wenn bestimmte Teile einer Komponente verändert werden. Betrachten wir ein Beispiel:
const Counter = props => {
useEffect(() => {
document.title = `${props.item} counter`
}, [])
}
Der Hook bekommt zwei Parameter. Zuerst eine Funktion die als Folge einer Änderung aufgerufen wird, hier eine anonyme Funktion die den Seitentitel im Browser ändert. Erzeugt diese Funktion Seiteneffekte die an späterer Stelle wieder aufgeräumt werden müssen, z.B. die Verbindung zu einer Datenbank zu trennen oder eine Datei nach dem beschreiben zu schließen, so kann man diese Aufräumfunktion von der ursprünglichen Funktion zurückgeben lassen. Sie wird dann an geeigneter Stelle wieder aufgerufen.
Als zweites wird eine Liste von Abhängigkeiten übergeben. Ändert sich einer der übergebenen Werte wird der Effekt ausgeführt. In unserem Fall ist die Liste leer, was die Funktion nur beim erstellen der Komponente ausführen lässt. Alternativ kann die Liste weggelassen werden um bei jedem Update der Komponente ausgeführt zu werden.
React Redux
Natürlich ist es anstrengend React-Zustände durch die gesamte Applikation zu reichen falls Informationen z.B. in einem benachbarten Elementbaum gebraucht werden. Die Redux Library bietet ein global Verfügbares Informationslager, das über wohldefinierte Methoden manipuliert werden kann. Durch React Redux wird dieses Lager dann auch mit dem Update-System von React-Komponenten verträglich gemacht.
Wir werden hier darüber reden wie der Redux Store in Komponenten verwendet wird, der nächste Abschnitt befasst sich dann mit dem Einrichten des Stores. Hier aber vorab einige der wichtigen Konzepte:
- Das Informationslager (Store) ist unveränderlich, bei Updates wird ein neues Objekt effizient aufgebaut um das alte zu ersetzen
- Updates werden durch Actions repräsentiert, einfache Objekte die üblicherweise nur aus einem String-Typen und einer Nutzlast bestehen
- Um ein Update auszulösen muss eine Aktion an eine Dispatch-Funktion übergeben werden
- Der Store ist ein Baum der durch kombinierte Reducer-Funktionen aufgebaut wird, jeder Reducer ist für einen spezifischen Sub-Baum des Stores zuständig
- Reducer bekommen den alten Zustand und eine Aktion gegeben und liefern einen geupdateten Zustand zurück
- Aktionen werden nacheinander abgehandelt, ggf. werden mehrere Aktionen verarbeitet bevor das nächste Update des DOM-Baumes realisiert wird
- Üblicherweise werden Aktionen durch eine einfach Funktion, einen Aktionsgenerator, erzeugt
UI Updates sind genau dann nötig, wenn sich nun durch den Store abgegriffene Properties ändern. Um mit dem Store zu interagieren liefert React Redux zwei neue Hooks:
-
useDispatch
: erzeugt einedispatch
Funktion in die Aktionen eingefüttert werden können -
useSelector
: bietet Zugriff auf den State-Baum, wendet die übergebene Funktion auf den Store an und holt ihr Result in den Kontext der Komponente
Dazu ein einfaches Beispiel:
const Counter = () => {
let dispatch = useDispatch()
let count = useSelector(selectCount)
return (
<span>
<button onClick={() => dispatch(increment(1))}>Count Up</button>
Current count: {count}
</span>
)
}
Wir können sehen wie die neuen Hooks verwendet werden. Dieses Beispiel folgt dem vorigen Counter-Beispiel, anstatt lokalem State werden aber Informationen nun aus dem Store abgegriffen. In diesem kleinen Besipiel sind die Vorteile dieser Methode nicht ganz ersichtlich, aber sobald z.B. zwischen diversen Features Informationen kommuniziert werden müssen macht sich der Store schnell bezahlt.
Reducer definieren mit Redux Toolkit
Dieses Toolkit zielt darauf ab die Verwendung von Redux zu erleichtern. Für uns bezieht sich das hauptsächlich auf Slices die hier genauer erläutert werden:
const conuterSlice = createSlice({
name: 'counter',
initialState: { count: 0 },
reducers: {
increment: (state, action) => {
state.count += action.payload
}
}
})
export const selectCount = state => state.counter.count
export const { increment } = counterSlice.actions
export default counterSlice.reducer
Wir können hier sehen das createSlice
ein Objekt mit einigen relevanten Properties erhält. Zuerst geben wir einen Namen an der vor allem in Aktionstypen anwendung findet. Dieser sollte dementsprechend eindeutig sein, um Aktionen mit selben Typen zu verhindern, dies kann in ungewünschten Reducer-Updates resultieren.
In initailState
wird ein gültiger Javascript-Wert verlangt, der als Ausgangswert dieses Sub-Baumes des Stores verwendet wird. Objekte haben dabei den Vorteil einfacher erweiterbar zu sein, dementsprechend haben wir im Beispiel ebenfalls ein Objekt angegeben auch wenn nur ein einziger Zahlenwert darin gespeichert wird.
Schließlich werden noch Reducer angegeben. Reducer in einem Slice sind durch zugehörige Aktionen ausgedrückt. Wir definieren also wie eine Aktion den Zustand anpasst und können diese dann später einfach als Aktionsgeneratoren exportieren. Normalerweise sind Reducer unveränderlich, nehmen also keine Änderungen an State selbst fest sondern geben den neuen angepassten State zurück. Redux Toolkit erlaubt dies allerdings durch die Library Immer. Soll Immer verwendet werden liefert der Reducer keinen Wert zurück, statt dessen wird wie im Beispiel das State Objekt manipuliert.
Wir können weiterhin einige Exports unter dem Slice finden. Der wichtigste ist der Default Export unten im Code, der den Reducer selbst für den Store bereit stellt.
Direkt unter dem Slice ist die Definition des Selektors der im Codebeispiel der vorigen Sektion Anwendung findet. Selektoren können allerdings auch einfach bei Bedarf durch anonyme Funktionen ersetzt werden. Wichtig ist, dass der Selektor den gesamten State erhält, nicht nur den counter
Sub-Baum.
Schließlich können wir noch die durch die Reducer festgelegten Aktionen als Aktionsgeneratoren exportieren um diese dann an anderer Stelle einfach benutzen zu können. Als einziges Argument nehmen diese die gewünschte Nutzlast der Aktion.
Project Structure
Schließlich noch ein paar Worte über die Projektstruktur:
- app: Enthält App-Globalen Code, momentan ist hier eigentlich nur der Store definiert.
- constants: Entählt diverse Konstanten, die aber vermutlich demnächst in relevantere lokale Ordner umziehen.
- features: Enthält inhaltlich abgeschlossene Features und alle Komponenten und Resourcen die zu diesen gehören. Sollten generell nur einen Slice enthalten, mehrere Slices indizieren bereits eine logische Trennung und sollten demenstprechend in relevante Features aufgeteilt werden.
- pages: Einzelne Seiten, enthalten höchstens Routing Logik (React Router). Definiert Seitenlayout und Featurepositionen.
- components: Existiert momentan nicht, wäre aber der vorgesehene Ort für kleinere, Feature-unabhängige Komponenten