Bibliotheken für Zustands-Management

Über Komponenten verteilter Zustand

Render Cycle in Pure React

Motivation für Zustandsmanagement

Zustand, der über viele Komponenten verteilt ist, macht Programme kompliziert

Gemeinsame Nutzung von State in unterschiedlichen Komponenten-Hierarchien ist schwierig

Zustandmanagement und UI-Handler werden aus React-Komponenten extrahiert

Besseres SoC

Option 1: Flux

Die klassische Idee

Option 2: MobX

Referenz

https://mobx.js.org/

Prinzipien

https://mobx.js.org/intro/concepts.html

  1. Zustand wird extern in einer Klasse gehalten
  2. Abgeleitete Darstellungen des Zustands werden automatisch berechnet, sind getter einer Klasse
  3. Aktionen, die den Zustand ändern, sind Methoden des Klasse
  4. Komponenten werden bei Bedarf automatisch neu gerendert

Beispiel-Zustand


class Store {
    @observable greetings = [];
    @observable filter = null;

    @computed get filteredGreetings() {
        return filterGreetings(this.greetings, this.filter);
    }

    @action.bound
    setFilter(filter) {
        if (this.filter === filter) {
            // reset filter when clicking again
            this.filter = null;
        } else {
            this.filter = filter;
        }
    }
}

export default new Store();
            

Komponenten reagieren


import store from './store';

ReactDOM.render(<GreetingController store={store}/>, mountNode);
            

import {observer} from 'mobx-react';

@observer
export default class GreetingController extends React.Component {
    render() {
        const {store} = this.props;
        const {filteredGreetings} = store;
        const {setFilter} = store;

        // ...
    }

    // ...
}
            

Zusammenfassung

Übung: Stelle deine Anwendung auf MobX um

Nutze die vorbereitete Implementierung

Schritte

  • mache dich mit der vorbereiteten und lauffähigen Implementierung in code/experiment/mobx vertraut
  • integriere sie in deine bestehende Lösung inklusive des Routings
  • füge in eine neue Komponente hinzu, die zählt, wie viele Grüße es insgesamt gibt und wie viele davon nach der Filterung noch angezeigt werden

Wie kann man Stores noch an Komponenten übergeben?

https://github.com/mobxjs/mobx/issues/300#issuecomment-223704275
  1. Explizit durch props
  2. Direkter Zugriff aus importierten Stores
  3. Implizit über einen Provider (wie bei Redux), https://github.com/mobxjs/mobx-react#provider-and-inject

Wie funktioniert MobX?

RuhrJS 2016 - Michel Weststrate - MAGIC MOBX BECOME A REACTIVE WIZARD IN 30 MINUTES

Option 3: Redux

Render-Zyklus in purem React

Redux extrahiert die Verantwortlichkeiten

Resultierende Redux-Architektur

Redux vs MobX

http://redux.js.org/

  • Redux ist die Mainstream-Lösung
  • Lässt sich sehr gut testen
  • Fehlersuche und Debugging erstaunlich einfach
  • Funktioniert auch in großen Anwendungen sehr gut
  • Dev-Tools erlauben Magisches

Allerdings: Lernkurve steil, gerade am Anfang wirklich viel Code

Redux ist für kleinere Anwendungen meistens Overkill

MobX erlaubt einen sehr einfachen Einstieg

Event-Handlers werden Action-Creators


export function setFilter(filter) {
    return {
        type: SET_FILTER,
        filter
    };
}
  • Action-Creators erzeugen Action-Objekte
  • Actions sind Kommando-artige Strukturen von Dingen, die die Applikation tun soll
  • Über die dispatch-Methode des Stores werden sie an alle Reducer weiter gegeben
  • Actions bestehen aus einem Typen und einer belibiegen Nutzlast (payload)

Action-Creators machen Server-Calls


export const loadGreetings = dispatch => {
    return fetch(BACKEND_URL)
        .then(response => response.json())
        .then(greetings => dispatch({
            type: SET_GREETINGS,
            greetings
        });
    );
};
  • Da der Server-Call asynchron ist, bekommen wir die dispatch Methode als Parameter, mit der wir später die Action dispatchen

Action-Creators sind die einzigen Teile einer Redux-Anwendung, die asynchrone Operationen ausführen dürfen, wie z.B. eben Server-Calls

Ein einziger Store hält den kompletten Zustand


import { Provider } from 'react-redux';
import { createStore } from 'redux';
import { rootReducer } from './reducers';

// http://redux.js.org/docs/api/createStore.html
const store = createStore(
    rootReducer // reducer
);
ReactDOM.render(
    <Provider store={store}>
        <GreetingController />
    </Provider>,
    mountNode
);
        
  • Zentraler Teil der Anwendung
  • Liefert die bereits bekannte dispatch-Methode
  • Der Store wird allen Componenten über die Wrapper-Komponenten Provider zur Verfügung gestellt

Middleware sitzt zwischen Dispatch und Store


import { applyMiddleware } from 'redux';
import thunk from 'redux-thunk';

const store = createStore(
    rootReducer, // reducer
    applyMiddleware(thunk) // middleware as enhancer
);
        

Reducers enthalten die Kern-(Business)-Logik


import {combineReducers} from 'redux';

// http://redux.js.org/docs/api/combineReducers.html
export const rootReducer = combineReducers({
    greetings, // updates greeting partial state
    filter,
    mode
});
        
  • Reducer sind pure Funtionen, die den alten Zustand und eine Action bekommen und einen neuen Zustand erzeugen
  • Oft bearbeitet ein Reducer nur einen Teil des Zustands

Teil-Reducer


const mode = (state = MODE_MASTER, action) => {
    switch (action.type) {
        case SET_MODE:
            return action.mode;
        default:
            return state;
    }
};
        
  • Initialisiert seinen Teilzustand (oft mit einem Default-Parameter)
  • Ändert niemals zustand direkt
  • Sondern liefert einen neuen Zustand (manchmal teilweise als Kopie)

Zustand mit Komponenten verbinden


import { connect } from 'react-redux';

import * as actions from './actions';

export default connect(
    state => ({
        mode: state.mode
        // ...
    }),
    actions
)(GreetingController);
            
  • der Provider gibt den Store in alle Komponenten
  • Die Connect-Funktion extrahiert daraus Zustand, der als Property an Komponenten übergeben wird
  • Action-Creators können ebenso in Komponent gegeben werden

Verwendung von Zustand und Action-Creators in verbundenen Komponenten


class GreetingController extends React.Component {
    render() {
        // state from store
        const {aggregatedGreetings, greetings, mode} = this.props;
        // action creators bound to dispatch from store
        const {setMode, saveGreeting, setFilter} = this.props;

        // ...
    }
}
            
  • Action-Creators und Zustand werden in Properties hinein gemerged
  • Diese werden mit ES6-Destructuring an den Stellen aufgelöst wo wir sie brauchen
  • Die Komponenten wird nur neu gerendert wenn sich der benutzte Zustand verändert

Selektoren


export const filterGreetings = (greetings, filter) =>
        filter ? greetings.filter(greeting => greeting.name === filter)
                : greetings;

export default connect(
    state => ({
        greetings: filterGreetings(state.greetings, state.filter),
        // ...
    }),
    // ...
)(GreetingController);        
  • Berechnen abgeleiteten Zustand
  • Oft in einer eigener Datei
  • Erlauben Wiederbenutzung und Caching (normalerweise reselect)

Übung: eine Redux Anwendung fertig stellen

Es gibt bereits eine vorgebaute Redux-Anwendung, aber wir die Filterung über das Chart muss noch implementiert werden

Schritte

  • Alles Material von code/material/6-redux in deinem Source-Ordner kopieren
  • TODOs leiten dich, die bereits in den Actions, den Reducers und dem GreetingController eingefügt sind
  • Nutzen den bereits existierenden Filer-Coder als dem vorherigen Schritt
  • Erzeuge einen neuen Action-Typ und einen Action-Creator für das setzen des Filters
  • Füge einen passenden Reducer zur Behandlung einer solchen Action hinzu und füge ihn als Teil-Reducer in combineReducers hinzu
  • Reiche den Action-Creator von GreetingController in die Chart-Komponente in der du auf das Filter-Event reagierst