Webtechnologien Wintersemester 2024

React State Management

Quelle: Andrés Gómez

React Router

  • deklarative Zuordnung von Pfaden und Komponenten
  • Extraktion von URL-Parametern
  • Einfache Möglichkeit, URLs in SPAs zu nutzen um Application-State zu transportieren
  • Übersichtliche API:
    • Router, Routes, Route ,Link (und ein paar andere)
    • useParams, useLocation, useNavigation
npm install react-router-dom
  • Für dieses Problem, eine App anhand der URL in den richtigen Zustand zu versetzen, hat sich React Router in den vergangenen Jahren als de facto Standard etabliert.

Application State

  • Eben haben wir uns angesehen, wie der State, der implizit oder explizit in URLs enthalten ist, verarbeitet werden kann, um eine App in den richtigen Zustand zu bringen.
  • Jetzt würde ich gerne eine andere Form von State ansehen, und zwar: App State
  • Mit zunehmender Größe einer Anwendung wachsen meist auch deren Komplexität und die Daten, die mit der Anwendung verwaltet werden sollen. Also anders gesagt: Der App State wird schwieriger und unübersichtlicher zu verwalten. Wann gebe ich wie welcher Komponente welche Props hinein? Wie wirken sich diese Props auf den State meiner Komponente aus und was passiert wenn ich den State in einer Komponente modifiziere?
  • Um dieses Problem zu lösen, gibt es einige externe Tools für globales State Management, die sich im Ökosystem von React gebildet haben. Das populärste ist wohl Redux.

Prop Drilling

Übergabe von State als Props an Komponenten, die diesen nur weiterreichen, ohne ihn selbst zu verwenden.

Redux

  • A Predictable State Container for JS Apps
  • Zentraler Speicher für State
  • Unidirektionaler Datenfluss
npm install redux react-redux
  • Redux bezeichnet sich selber als vorhersehbaren State Container. Um zu verstehen, was das bdeutet, müssen wir uns das Grundprinzip von Redux anschauen: Unidirektionaler Datenfluss
  • Das kennen wir schon von React:
    • Eine Aktion (bspw. ausgelöst durch einen Button-Klick) ändert den State einer Komponente, die State-Änderung löst ein Rerendering aus und erlaubt es dann weitere Aktionen auszuführen
    • genau das macht auch Redux, nur dass die Änderung nach ganz oben, an eine globale Instanz gegeben wird und die erzeugte State-Änderung an alle daran interessierten Komponenten weitergegeben wird, egal wo sie sich im Baum befinden.
  • Wir installieren daher 2 Pakete: redux und react-redux
  • Theoretisch wäre auch die Verwendung von Redux alleine möglich, allerdings müssten wir uns dann selbst darum kümmern zu schauen, wann Komponenten neu gerendert werden und darum, wie Daten aus einer Komponente in den State Container rein und wieder raus kommen. Da wir das nicht wollen, weil sich andere das bereits gemacht haben, nutzen wir eben zusätzlich react-redux, was uns eine schöne API gibt, um mit Redux zu arbeiten.

Store, Actions und Reducer

Redux Data Flow

  • Store speichert alle Daten und ermöglicht Zugriff (getState) und Änderung (dispatch)
  • Action Nachricht über eine Änderung mit type und ggf. payload
  • Reducer berechnet aus aktuellem State und einer Action den neuen State (newState = reducer(currentState, action))
  • Es gibt in Redux drei wichtige Begriffe: Store, Actions und Reducer. Gehen wir sie nacheinander durch.
  • Alle Daten in Redux befinden sich in einem sogenannten Store, der sich um die Verwaltung des globalen States kümmert. Theoretisch kann eine Anwendung auch mehrere Stores haben, in Redux ist das jedoch, um Komplexität zu reduzieren eher unüblich und so beschränken sich React-Anwendungen, die Redux einsetzen, meist auch auf lediglich einen einzigen Store als Single Source of Truth, also als die einzige wahre Quelle für alle Daten. Der Store stellt Methoden bereit, um die sich in ihm befindlichen Daten zu verändern (dispatch), zu lesen (getState), und auf Änderungen zu reagieren (subscribe).
  • Die einzige Möglichkeit um Daten in einem Store zu verändern, ist dabei das Auslösen („dispatchen) von Actions. Sie bestehen aus einem simplen JavaScript-Objekt, das eine bestimmte Form haben muss, und zwar muss es einen type haben, und zusätzlich kann es Daten wie payload, meta und error haben.
  • type lässt sich wie ein Funktionsame betrachten und payload wie die Parameter.
  • Die Payload stellt sozusagen den Inhalt einer Action dar und kann alles beinhalten, was serialisierbar ist, also Boolean, String, Number und Arrays und Objekte, solange sie keine Funktioen enthalten, also quasi alles, was sich in JSON ausdrücken ließe.
  • Wird eine Action durch die vom Store bereitgestellte dispatch-Methode ausgelöst, wird der zum Zeitpunkt des Aufrufs aktuelle State zusammen mit der ausgelösten Action an die Reducer übergeben.
  • Ein Reducer ist eine pure Function, und dient dazu, aus dem aktuellen State und der jeweiligen Action mit ihren type- und payload-Eigenschaften einen neuen State zu erzeugen.
  • Eine pure Function erzeugt stets dieselbe Ausgabe bei gleichen Eingabeparametern, egal wie oft diese aufgerufen wird. Dieses Verhalten ist es, das sie vorhersehbar und dadurch auch gleichzeitig einfach testbar macht.

  • Prinzipien
    • Single Source of Truth
    • State is read-only

Beispiel-Action

const remove = (id) => {
   const action = {
      type: 'REMOVE',
      payload: id,
   };
   return action;
}

const onClick = {
   const action = remove(4);
   store.dispatch(action);
}

<button onClick={onClick}>Remove</button>
  • Für unser Favourites-Beispiel könnte eine Action nach Klick auf dem Herz-Button so aussehen
  • als kurzer Einschub: Wer schon mit Redux in Berührung gekommen ist, ist vielleicht auf die Begriffe Action und Action Creator gestoßen.
    • Action: serialisierbares Object, das eine State-Änderung beschreibt
    • Action Creator: Funktion, die eine Action zurück gibt (quasi eine Action Factory). Werde meist verwendet, um Logik zu kapseln

Beispiel-Reducer

const initialState = [];

const favouritesReducer = (state = initialState, action) => {
  switch (action.type) {
    case 'ADD': {
      return [...state, action.id];
    }
    case 'REMOVE': {
      return state.filter(id => id !== action.id);
    }
    default: {
      return state;
    }
  }
};
  • Der Reducer bekommt den kompletten aktuellen State und die Action und erzeugt basierend darauf den neuen State
  • Dieser muss immutable sein, es darf also icht einfach der initiale State verändert werden
  • Reducer müssen synchron sein.
  • In der Praxis würde man die Types in eine eigene Datei auslagern und nur mit den Variablen-Namen arbeiten, um Tippfehler zu vermeiden.

Beispiel-Store

import { createStore } from 'redux';
const store = createStore(favouritesReducer);
  • der Store stellt dann die dispatch-Methode aus dem ersten Beispiel bereit.

React + Redux

  1. State bereitstellen
<Provider store={store}> <App /> </Provider>
  1. Komponenten an State anbinden
// App-State zu Props umformen
const mapStateToProps = state => ({ isLoggedIn: state.user.isLoggedIn });

// State-Änderungen zu Props umformen
const mapDispatchToProps = dispatch =>
  ({ login: () => dispatch({ type: 'LOGIN_USER' }) });

connect(mapStateToProps, mapDispatchToProps)(Komponente)

Redux Hooks

  • Lesen: useSelector (statt mapStateToProps)
  • Schreiben: useDispatch (statt mapDispatchToProps)
import { useSelector, useDispatch } from 'react-redux';

const isLoggedIn = useSelector(state => state.user.isLoggedIn);

const dispatch = useDispatch();
const login = () => dispatch({ type: 'LOGIN_USER' });
  • der Store stellt dann die dispatch-Methode aus dem ersten Beispiel bereit.

Async Actions

npm install redux-thunk
import { applyMiddleware, createStore } from 'redux';
import thunk from 'redux-thunk';
const store = createStore(reducer, applyMiddleware(thunk));

Redux Thunk Data Flow

  • Reducer imm synchron, aber viele Aktionen sind asynchron (fetch -> pending, suscess, error)
  • Enhancer legt sich um dispatch und kann Veränderungen vornehmen

react-query

  • Server-Kommunikation und Caching

React Context & Reducer mit Hooks