Aufsteigen mit React: Redux

Avatar of Brad Westfall
Brad Westfall am

DigitalOcean bietet Cloud-Produkte für jede Phase Ihrer Reise. Starten Sie mit 200 $ kostenlosem Guthaben!

Dieses Tutorial ist der letzte Teil einer dreiteiligen Serie von Brad Westfall. Wir werden lernen, wie man den Zustand (state) über eine gesamte Anwendung hinweg effizient und auf eine Weise verwaltet, die ohne gefährliche Komplexität skaliert werden kann. Wir sind auf unserer React-Reise schon so weit gekommen, dass es sich lohnt, hier die Ziellinie zu überqueren und das volle Potenzial dieses Entwicklungsansatzes auszuschöpfen.

Artikelserie

  1. React Router
  2. Container-Komponenten
  3. Redux (Sie sind hier!)

Redux ist ein Tool zur Verwaltung sowohl des Datenzustands als auch des UI-Zustands in JavaScript-Anwendungen. Es ist ideal für Single Page Applications (SPAs), bei denen die Verwaltung des Zustands über die Zeit komplex werden kann. Es ist auch framework-agnostisch, obwohl es mit Blick auf React geschrieben wurde, kann es sogar mit Angular oder einer jQuery-Anwendung verwendet werden.

Außerdem entstand es aus einem Experiment mit „Zeitreisen“ — Tatsache, dazu kommen wir später!

Wie in unserem vorherigen Tutorial gezeigt, lässt React Daten durch Komponenten „fließen“. Genauer gesagt spricht man von „unidirektionalem Datenfluss“ — Daten fließen in eine Richtung von Eltern zu Kindern. Bei dieser Eigenschaft ist nicht offensichtlich, wie zwei Komponenten ohne Eltern-Kind-Beziehung in React kommunizieren würden.

React empfiehlt keine direkte Kommunikation zwischen Komponenten auf diese Weise. Selbst wenn es Funktionen zur Unterstützung dieses Ansatzes hätte, wird dies von vielen als schlechte Praxis angesehen, da die direkte Kommunikation zwischen Komponenten fehleranfällig ist und zu Spaghetti-Code führt — ein alter Begriff für Code, der schwer zu verfolgen ist.

React bietet einen Vorschlag, erwartet aber, dass Sie ihn selbst implementieren. Hier ist ein Abschnitt aus den React-Dokumenten

Für die Kommunikation zwischen zwei Komponenten, die keine Eltern-Kind-Beziehung haben, können Sie Ihr eigenes globales Ereignissystem einrichten. … Das Flux-Muster ist eine der möglichen Möglichkeiten, dies zu organisieren.

Hier kommt Redux ins Spiel. Redux bietet eine Lösung, um den gesamten Anwendungszustand an einem Ort zu speichern, der als „Store“ bezeichnet wird. Komponenten „dispatchen“ dann Zustandsänderungen an den Store, nicht direkt an andere Komponenten. Die Komponenten, die über Zustandsänderungen informiert werden müssen, können den Store „abonnieren“

Der Store kann als „Mittelsmann“ für alle Zustandsänderungen in der Anwendung betrachtet werden. Wenn Redux beteiligt ist, kommunizieren Komponenten nicht direkt miteinander, sondern alle Zustandsänderungen müssen über die einzige Quelle der Wahrheit, den Store, laufen.

Das unterscheidet sich stark von anderen Strategien, bei denen Teile der Anwendung direkt miteinander kommunizieren. Manchmal wird argumentiert, dass diese Strategien fehleranfällig und verwirrend in der Argumentation sind.

Mit Redux ist klar, dass alle Komponenten ihren Zustand aus dem Store beziehen. Es ist auch klar, wohin Komponenten ihre Zustandsänderungen senden sollen – ebenfalls an den Store. Die Komponente, die die Änderung initiiert, kümmert sich nur darum, die Änderung an den Store zu dispatchen, und muss sich nicht um eine Liste anderer Komponenten kümmern, die die Zustandsänderung benötigen. Auf diese Weise erleichtert Redux die Argumentation über den Datenfluss.

Das allgemeine Konzept der Verwendung von Store(s) zur Koordination des Anwendungszustands ist ein Muster, das als Flux-Muster bekannt ist. Es ist ein Designmuster, das unidirektionale Datenflussarchitekturen wie React ergänzt. Redux ähnelt Flux, aber wie nah sind sie sich?

Redux ist „Flux-ähnlich“

Flux ist ein Muster, kein Tool wie Redux, man kann es also nicht herunterladen. Redux ist jedoch ein Tool, das vom Flux-Muster und anderen Dingen wie Elm inspiriert wurde. Es gibt viele Anleitungen, die Redux mit Flux vergleichen. Die meisten kommen zu dem Schluss, dass Redux Flux ist oder Flux-ähnlich ist, je nachdem, wie streng man die Regeln von Flux definiert. Letztendlich spielt es keine Rolle. Facebook mag und unterstützt Redux so sehr, dass sie seinen Hauptentwickler, Dan Abramov, eingestellt haben.

Dieser Artikel geht davon aus, dass Sie mit dem Flux-Muster überhaupt nicht vertraut sind. Aber wenn Sie es sind, werden Sie einige kleine Unterschiede bemerken, insbesondere in Anbetracht der drei Leitprinzipien von Redux

1. Single Source of Truth

Redux verwendet nur einen Store für den gesamten Anwendungszustand. Da sich der gesamte Zustand an einem Ort befindet, nennt Redux dies die single source of truth (einzige Quelle der Wahrheit).

Die Datenstruktur des Stores liegt letztendlich bei Ihnen, aber für eine reale Anwendung ist es typischerweise ein tief verschachteltes Objekt.

Dieser Ansatz von Redux, nur einen Store zu verwenden, ist einer der Hauptunterschiede zum Ansatz von Flux mit mehreren Stores.

2. Zustand ist schreibgeschützt

Laut den Redux-Dokumenten gilt: „Die einzige Möglichkeit, den Zustand zu verändern, besteht darin, eine Aktion auszugeben, ein Objekt, das beschreibt, was passiert ist.“

Das bedeutet, dass die Anwendung den Zustand nicht direkt ändern kann. Stattdessen werden „Aktionen“ dispatched, um die Absicht auszudrücken, den Zustand im Store zu ändern.

Das Store-Objekt selbst hat eine sehr kleine API mit nur vier Methoden

  • store.dispatch(action)
  • store.subscribe(listener)
  • store.getState()
  • replaceReducer(nextReducer)

Wie Sie sehen, gibt es keine Methode zum Setzen des Zustands. Das Dispatchen einer Aktion ist daher die einzige Möglichkeit für den Anwendungscode, eine Zustandsänderung auszudrücken.

var action = {
  type: 'ADD_USER',
  user: {name: 'Dan'}
};

// Assuming a store object has been created already
store.dispatch(action);

Die Methode dispatch() sendet ein Objekt an Redux, das als Aktion bekannt ist. Die Aktion kann als „Payload“ beschrieben werden, die einen type und alle anderen Daten enthält, die zur Aktualisierung des Zustands verwendet werden könnten – in diesem Fall ein Benutzer. Beachten Sie, dass die Gestaltung eines Aktionsobjekts nach der type-Eigenschaft Ihnen überlassen ist.

3. Änderungen werden mit reinen Funktionen vorgenommen

Wie gerade beschrieben, erlaubt Redux der Anwendung nicht, direkte Änderungen am Zustand vorzunehmen. Stattdessen „beschreibt“ die dispatched Aktion die Zustandsänderung und die Absicht, den Zustand zu ändern. Reducer sind Funktionen, die Sie schreiben, die dispatched Aktionen verarbeiten und den Zustand tatsächlich ändern können.

Ein Reducer nimmt den aktuellen Zustand als Argument entgegen und kann den Zustand nur durch Rückgabe eines neuen Zustands ändern.

// Reducer Function
var someReducer = function(state, action) {
  ...
  return state;
}

Reducer sollten als „reine“ Funktionen geschrieben werden, ein Begriff, der eine Funktion mit den folgenden Eigenschaften beschreibt

  • Sie führt keine externen Netzwerk- oder Datenbankaufrufe durch.
  • Ihr Rückgabewert hängt ausschließlich von den Werten ihrer Parameter ab.
  • Ihre Argumente sollten als „unveränderlich“ (immutable) betrachtet werden, was bedeutet, dass sie nicht geändert werden sollten.
  • Das Aufrufen einer reinen Funktion mit denselben Argumenten gibt immer denselben Wert zurück.

Diese werden als „rein“ bezeichnet, weil sie nichts anderes tun, als einen Wert basierend auf ihren Parametern zurückzugeben. Sie haben keine Nebenwirkungen in andere Teile des Systems.

Unser erster Redux Store

Erstellen Sie zunächst einen Store mit Redux.createStore() und übergeben Sie alle Reducer als Argumente. Betrachten wir ein kleines Beispiel mit nur einem Reducer

// Note that using .push() in this way isn't the
// best approach. It's just the easiest to show
// for this example. We'll explain why in the next section.

// The Reducer Function
var userReducer = function(state, action) {
  if (state === undefined) {
    state = [];
  }
  if (action.type === 'ADD_USER') {
    state.push(action.user);
  }
  return state;
}

// Create a store by passing in the reducer
var store = Redux.createStore(userReducer);

// Dispatch our first action to express an intent to change the state
store.dispatch({
  type: 'ADD_USER',
  user: {name: 'Dan'}
});

Hier ist eine kurze Zusammenfassung dessen, was passiert:

  1. Der Store wird mit einem Reducer erstellt.
  2. Der Reducer legt fest, dass der anfängliche Zustand der Anwendung ein leeres Array ist. *
  3. Es wird ein Dispatch mit einem neuen Benutzer in der Aktion selbst durchgeführt.
  4. Der Reducer fügt den neuen Benutzer zum Zustand hinzu und gibt ihn zurück, wodurch der Store aktualisiert wird.

* Der Reducer wird in dem Beispiel tatsächlich zweimal aufgerufen — einmal, wenn der Store erstellt wird, und dann noch einmal nach dem Dispatch.

Wenn der Store erstellt wird, ruft Redux sofort die Reducer auf und verwendet deren Rückgabewerte als anfänglichen Zustand. Dieser erste Aufruf des Reducers sendet undefined für den Zustand. Der Reducer-Code antizipiert dies und gibt ein leeres Array zurück, um den anfänglichen Zustand des Stores zu starten.

Reducer werden auch jedes Mal aufgerufen, wenn Aktionen dispatched werden. Da der vom Reducer zurückgegebene Zustand unser neuer Zustand im Store wird, erwartet Redux immer, dass Reducer einen Zustand zurückgeben.

Im Beispiel erfolgt der zweite Aufruf unseres Reducers nach dem Dispatch. Denken Sie daran, eine dispatched Aktion beschreibt die Absicht, den Zustand zu ändern, und enthält oft die Daten für den neuen Zustand. Dieses Mal übergibt Redux den aktuellen Zustand (immer noch ein leeres Array) zusammen mit dem Aktionsobjekt an den Reducer. Das Aktionsobjekt, jetzt mit einer Type-Eigenschaft von 'ADD_USER', ermöglicht es dem Reducer, zu wissen, wie der Zustand geändert werden soll.

Man kann sich Reducer leicht als Trichter vorstellen, die es dem Zustand ermöglichen, sie zu durchlaufen. Das liegt daran, dass Reducer immer Zustand empfangen und zurückgeben, um den Store zu aktualisieren.

Basierend auf dem Beispiel ist unser Store nun ein Array mit einem Benutzerobjekt.

store.getState();   // => [{name: 'Dan'}]

Zustand nicht mutieren, kopieren Sie ihn

Obwohl der Reducer in unserem Beispiel technisch funktioniert, mutiert er den Zustand, was eine schlechte Praxis ist. Obwohl Reducer für die Zustandsänderung verantwortlich sind, sollten sie niemals das Argument „current state“ direkt mutieren. Deshalb sollten wir .push(), eine Mutationsmethode, nicht für das State-Argument des Reducers verwenden.

Argumente, die an den Reducer übergeben werden, sollten als unveränderlich betrachtet werden. Mit anderen Worten, sie sollten nicht direkt geändert werden. Anstelle einer direkten Mutation können wir nicht-mutierende Methoden wie .concat() verwenden, um im Grunde eine Kopie des Arrays zu erstellen, und dann werden wir die Kopie ändern und zurückgeben.

var userReducer = function(state = [], action) {
  if (action.type === 'ADD_USER') {
    var newState = state.concat([action.user]);
    return newState;
  }
  return state;
}

Mit dieser Aktualisierung des Reducers führt das Hinzufügen eines neuen Benutzers dazu, dass eine Kopie des State-Arguments geändert und zurückgegeben wird. Wenn kein neuer Benutzer hinzugefügt wird, wird der ursprüngliche Zustand zurückgegeben, anstatt eine Kopie zu erstellen.

Es gibt unten einen ganzen Abschnitt über unveränderliche Datenstrukturen, der mehr Licht auf diese Art von Best Practices wirft.

Möglicherweise haben Sie auch bemerkt, dass der anfängliche Zustand jetzt von einem ES2015-Standardparameter stammt. Bisher haben wir in dieser Serie ES2015 vermieden, damit Sie sich auf die Hauptthemen konzentrieren können. Redux ist jedoch mit ES2015 viel schöner. Daher werden wir in diesem Artikel endlich anfangen, ES2015 zu verwenden. Aber keine Sorge, jedes Mal, wenn eine neue ES2015-Funktion verwendet wird, wird sie hervorgehoben und erklärt.

Mehrere Reducer

Das letzte Beispiel war eine schöne Einführung, aber die meisten Anwendungen benötigen einen komplexeren Zustand für die gesamte Anwendung. Da Redux nur einen Store verwendet, müssen wir verschachtelte Objekte verwenden, um den Zustand in verschiedene Abschnitte zu organisieren. Stellen wir uns vor, wir möchten, dass unser Store diesem Objekt ähnelt:

{
  userState: { ... },
  widgetState: { ... }
}

Es ist immer noch „ein Store = ein Objekt“ für die gesamte Anwendung, aber es hat verschachtelte Objekte für userState und widgetState, die alle Arten von Daten enthalten können. Das mag übermäßig simpel erscheinen, aber es ist tatsächlich nicht weit entfernt von einem echten Redux Store.

Um einen Store mit verschachtelten Objekten zu erstellen, müssen wir jeden Abschnitt mit einem Reducer definieren.

import { createStore, combineReducers } from 'redux';

// The User Reducer
const userReducer = function(state = {}, action) {
  return state;
}

// The Widget Reducer
const widgetReducer = function(state = {}, action) {
  return state;
}

// Combine Reducers
const reducers = combineReducers({
  userState: userReducer,
  widgetState: widgetReducer
});

const store = createStore(reducers);
ES2015-Hinweis! Die vier Haupt-„Variablen“ in diesem Beispiel werden nicht geändert, also definieren wir sie stattdessen als Konstanten. Wir verwenden auch ES2015-Module und Destructuring.

Die Verwendung von combineReducers() ermöglicht es uns, unseren Store in Bezug auf verschiedene logische Abschnitte zu beschreiben und Reducer jedem Abschnitt zuzuordnen. Wenn nun jeder Reducer den anfänglichen Zustand zurückgibt, gelangt dieser Zustand in den jeweiligen userState- oder widgetState-Abschnitt des Stores.

Es ist sehr wichtig zu beachten, dass jetzt jedem Reducer sein jeweiliger Unterabschnitt des gesamten Zustands übergeben wird, nicht der gesamte Zustand des Stores wie bei dem Beispiel mit einem Reducer. Dann gilt der von jedem Reducer zurückgegebene Zustand für seinen Unterabschnitt.

Welcher Reducer wird nach einem Dispatch aufgerufen?

Alle. Der Vergleich von Reducern mit Trichtern wird noch deutlicher, wenn wir bedenken, dass jedes Mal, wenn eine Aktion dispatched wird, alle Reducer aufgerufen werden und die Möglichkeit haben, ihren jeweiligen Zustand zu aktualisieren.

Ich sage „ihren“ Zustand mit Bedacht, weil das Argument „current state“ des Reducers und sein zurückgegebener „updated“ state nur den Abschnitt des Stores betreffen, der diesem Reducer zugeordnet ist. Denken Sie daran, wie im vorherigen Abschnitt erwähnt, dass jedem Reducer nur sein jeweiliger Zustand übergeben wird, nicht der gesamte Zustand.

Aktionsstrategien

Es gibt tatsächlich einige Strategien zum Erstellen und Verwalten von Aktionen und Aktionstypen. Obwohl sie sehr nützlich sind, sind sie nicht so kritisch wie einige andere Informationen in diesem Artikel. Um den Artikel kleiner zu halten, haben wir die grundlegenden Aktionsstrategien, die Sie kennen sollten, im GitHub-Repository dokumentiert, das zu dieser Serie gehört.

Unveränderliche Datenstrukturen

„Die Form des Zustands liegt bei Ihnen: Es kann ein Primitiv, ein Array, ein Objekt oder sogar eine Immutable.js-Datenstruktur sein. Der einzige wichtige Teil ist, dass Sie das Zustandsobjekt nicht mutieren sollten, sondern ein neues Objekt zurückgeben, wenn sich der Zustand ändert.“ – Redux-Dokumente

Diese Aussage sagt viel aus, und wir haben in diesem Tutorial bereits auf diesen Punkt angespielt. Wenn wir anfangen würden, die Einzelheiten sowie Vor- und Nachteile dessen zu diskutieren, was es bedeutet, unveränderlich vs. veränderlich zu sein, könnten wir einen ganzen Blogartikel damit füllen. Daher werde ich nur einige Hauptpunkte hervorheben.

Zunächst

  • Die primitiven Datentypen von JavaScript (Number, String, Boolean, Undefined und Null) sind bereits unveränderlich.
  • Objekte, Arrays und Funktionen sind veränderlich.

Es wurde gesagt, dass die Veränderbarkeit von Datenstrukturen anfällig für Bugs ist. Da unser Store aus Zustandsobjekten und Arrays bestehen wird, müssen wir eine Strategie implementieren, um den Zustand unveränderlich zu halten.

Stellen wir uns ein state-Objekt vor, bei dem wir eine Eigenschaft ändern müssen. Hier sind drei Möglichkeiten:

// Example One
state.foo = '123';

// Example Two
Object.assign(state, { foo: 123 });

// Example Three
var newState = Object.assign({}, state, { foo: 123 });

Das erste und zweite Beispiel mutieren das State-Objekt. Das zweite Beispiel mutiert, weil Object.assign() alle seine Argumente in das erste Argument zusammenführt. Aber dieser Grund ist auch, warum das dritte Beispiel den Zustand nicht mutiert.

Das dritte Beispiel führt den Inhalt von state und {foo: 123} in ein völlig neues leeres Objekt zusammen. Dies ist ein gängiger Trick, der es uns ermöglicht, im Grunde eine Kopie des Zustands zu erstellen und die Kopie zu mutieren, ohne den ursprünglichen state zu beeinflussen.

Der Objekt-Spread-Operator ist eine weitere Möglichkeit, den Zustand unveränderlich zu halten.

const newState = { ...state, foo: 123 };

Eine sehr detaillierte Erklärung, was passiert und warum dies für Redux nützlich ist, finden Sie in den Dokumenten zu diesem Thema.

Object.assign() und Spread-Operatoren sind beide ES2015.

Zusammenfassend lässt sich sagen, dass es viele Möglichkeiten gibt, Objekte und Arrays explizit unveränderlich zu halten. Viele Entwickler verwenden Bibliotheken wie seamless-immutable, Mori oder sogar Facebooks eigenes Immutable.js.

Ich wähle sehr sorgfältig aus, auf welche anderen Blogs und Artikel dieser hier verlinkt. Wenn Sie Unveränderlichkeit nicht verstehen, lesen Sie die Referenzlinks von oben. Dies ist ein sehr wichtiges Konzept, um mit Redux erfolgreich zu sein.

Anfangszustand und Zeitreisen

Wenn Sie die Dokumente lesen, bemerken Sie möglicherweise ein zweites Argument für createStore(), das für den „Anfangszustand“ vorgesehen ist. Dies mag wie eine Alternative zu Reducern erscheinen, die den anfänglichen Zustand erstellen. Dieser anfängliche Zustand sollte jedoch nur für die „Zustandshydratation“ verwendet werden.

Stellen Sie sich vor, ein Benutzer führt eine Aktualisierung Ihrer SPA durch und der Zustand des Stores wird auf die anfänglichen Zustände des Reducers zurückgesetzt. Dies ist möglicherweise nicht erwünscht.

Stellen Sie sich stattdessen vor, Sie hätten eine Strategie verwendet, um den Store beizubehalten, und Sie können ihn dann bei der Aktualisierung wieder in Redux hydratisieren. Dies ist der Grund, warum der anfängliche Zustand an createStore() gesendet wird.

Dies wirft jedoch ein interessantes Konzept auf. Wenn es so billig und einfach ist, alten Zustand wiederherzustellen, könnte man sich das Äquivalent einer „Zeitreise“ im Zustand in seiner App vorstellen. Dies kann nützlich für das Debuggen oder sogar für Rückgängig-/Wiederherstellen-Funktionen sein. All Ihren Zustand in einem Store zu haben, ist aus diesen und vielen Gründen sehr sinnvoll! Dies ist nur ein Grund, warum uns unveränderlicher Zustand hilft.

In einem Interview wurde Dan Abramov gefragt „Warum hast du Redux entwickelt?“

Ich wollte kein Flux-Framework erstellen. Als React Europe zum ersten Mal angekündigt wurde, schlug ich einen Vortrag über „Hot Reloading und Zeitreisen“ vor, aber um ehrlich zu sein, hatte ich keine Ahnung, wie man Zeitreisen implementiert.

Redux mit React

Wie bereits erwähnt, ist Redux framework-agnostisch. Das Verständnis der Kernkonzepte von Redux ist wichtig, bevor Sie überhaupt darüber nachdenken, wie es mit React funktioniert. Aber jetzt sind wir bereit, eine Container-Komponente aus dem letzten Artikel zu nehmen und Redux darauf anzuwenden.

Zuerst die Originalkomponente ohne Redux

import React from 'react';
import axios from 'axios';
import UserList from '../views/list-user';

const UserListContainer = React.createClass({
  getInitialState: function() {
    return {
      users: []
    };
  },

  componentDidMount: function() {
    axios.get('/path/to/user-api').then(response => {
      this.setState({users: response.data});
    });
  },

  render: function() {
    return <UserList users={this.state.users} />;
  }
});

export default UserListContainer;
ES2015-Hinweis! Dieses Beispiel wurde leicht von Original konvertiert. Es verwendet ES2015-Module und Pfeilfunktionen.

Sicher, es führt seine Ajax-Anfrage durch und aktualisiert seinen eigenen lokalen Zustand. Aber wenn andere Bereiche der Anwendung aufgrund der neu abgerufenen Benutzerliste geändert werden müssen, reicht diese Strategie nicht aus.

Mit der Redux-Strategie können wir eine Aktion dispatchen, wenn die Ajax-Anfrage zurückkehrt, anstatt this.setState() auszuführen. Dann können diese Komponente und andere die Zustandsänderung abonnieren. Aber das bringt uns tatsächlich zu der Frage, wie wir store.subscribe() einrichten, um den Zustand der Komponente zu aktualisieren?

Ich nehme an, ich könnte mehrere Beispiele für die manuelle Verdrahtung von Komponenten mit dem Redux Store liefern. Sie können sich wahrscheinlich sogar vorstellen, wie das mit Ihrem eigenen Ansatz aussehen könnte. Aber letztendlich würde ich am Ende dieser Beispiele erklären, dass es einen besseren Weg gibt, und die manuellen Beispiele vergessen. Ich würde dann das offizielle React/Redux-Bindungsmodul namens react-redux vorstellen. Also springen wir gleich dorthin.

Verbinden mit react-redux

Um es klarzustellen: react, redux und react-redux sind drei separate Module auf npm. Das Modul react-redux ermöglicht es uns, React-Komponenten auf bequemere Weise mit Redux zu „verbinden“.

So sieht es aus:

import React from 'react';
import { connect } from 'react-redux';
import store from '../path/to/store';
import axios from 'axios';
import UserList from '../views/list-user';

const UserListContainer = React.createClass({
  componentDidMount: function() {
    axios.get('/path/to/user-api').then(response => {
      store.dispatch({
        type: 'USER_LIST_SUCCESS',
        users: response.data
      });
    });
  },

  render: function() {
    return <UserList users={this.props.users} />;
  }
});

const mapStateToProps = function(store) {
  return {
    users: store.userState.users
  };
}

export default connect(mapStateToProps)(UserListContainer);

Es gibt viele neue Dinge:

  1. Wir haben die Funktion connect aus react-redux importiert.
  2. Dieser Code ist möglicherweise leichter von unten nach oben zu verfolgen, beginnend mit der Verbindung. Die Funktion connect() benötigt tatsächlich zwei Argumente, aber wir zeigen nur eines für mapStateToProps().

    Es mag seltsam aussehen, die zusätzlichen Klammern für connect()() zu sehen. Dies sind tatsächlich zwei Funktionsaufrufe. Der erste, connect(), gibt eine weitere Funktion zurück. Ich nehme an, wir hätten dieser Funktion einen Namen zuweisen und sie dann aufrufen können, aber warum das tun, wenn wir sie einfach sofort mit dem zweiten Satz von Klammern aufrufen können? Außerdem würden wir diesen zweiten Funktionsnamen nach dem Aufruf sowieso aus keinem Grund benötigen. Die zweite Funktion erfordert jedoch, dass Sie eine React-Komponente übergeben. In diesem Fall ist es unsere Container-Komponente.

    Ich verstehe, wenn Sie denken: „Warum es komplizierter machen, als es sein muss?“, aber dies ist tatsächlich ein gängiges „funktionales Programmierparadigma“, daher ist es gut, es zu lernen.

  3. Das erste Argument für connect() ist eine Funktion, die ein Objekt zurückgeben sollte. Die Eigenschaften des Objekts werden zu „Props“ in der Komponente. Sie können sehen, dass ihre Werte aus dem Zustand stammen. Jetzt hoffe ich, dass der Funktionsname „mapStateToProps“ mehr Sinn ergibt. Beachten Sie auch, dass mapStateToProps() ein Argument erhält, das der gesamte Redux Store ist. Die Hauptidee von mapStateToProps() besteht darin, zu isolieren, welche Teile des Gesamtzustands diese Komponente als Props benötigt.
  4. Aus den in #3 genannten Gründen benötigen wir getInitialState() nicht mehr. Beachten Sie auch, dass wir auf this.props.users statt auf this.state.users verweisen, da das users-Array jetzt ein Prop und kein lokaler Komponentenstatus ist.
  5. Die Ajax-Rückgabe dispatched jetzt eine Aktion, anstatt den lokalen Komponentenstatus zu aktualisieren. Der Kürze halber verwenden wir keine Action Creator oder Action Type Konstanten.

Das Codebeispiel macht eine Annahme über die Funktionsweise des Benutzer-Reducers, die möglicherweise nicht offensichtlich ist. Beachten Sie, dass der Store die Eigenschaft userState hat. Aber woher kam dieser Name?

const mapStateToProps = function(store) {
  return {
    users: store.userState.users
  };
}

Dieser Name stammte aus der Kombination unserer Reducer

const reducers = combineReducers({
  userState: userReducer,
  widgetState: widgetReducer
});

Was ist mit der Eigenschaft .users von userState? Woher kam das?

Obwohl wir kein tatsächliches Reducer für das Beispiel gezeigt haben (weil es sich in einer anderen Datei befinden würde), bestimmt der Reducer die Untereigenschaften seines jeweiligen Zustands. Um sicherzustellen, dass .users eine Eigenschaft von userState ist, könnte der Reducer für diese Beispiele so aussehen:

const initialUserState = {
  users: []
}

const userReducer = function(state = initialUserState, action) {
  switch(action.type) {
  case 'USER_LIST_SUCCESS':
    return Object.assign({}, state, { users: action.users });
  }
  return state;
}

Ajax-Lebenszyklus-Dispatches

In unserem Ajax-Beispiel haben wir nur eine Aktion dispatched. Sie wurde absichtlich 'USER_LIST_SUCCESS' genannt, weil wir möglicherweise auch 'USER_LIST_REQUEST' dispatchen möchten, bevor Ajax beginnt, und 'USER_LIST_FAILED' bei einem Ajax-Fehler. Lesen Sie unbedingt die Dokumente zu asynchronen Aktionen.

Dispatchen von Ereignissen

Im vorherigen Artikel haben wir gesehen, dass Ereignisse von Container- zu Präsentationskomponenten weitergegeben werden sollten. Es stellt sich heraus, dass react-redux auch dabei hilft, wenn ein Ereignis einfach eine Aktion dispatchen muss.

...

const mapDispatchToProps = function(dispatch, ownProps) {
  return {
    toggleActive: function() {
      dispatch({ ... });
    }
  }
}

export default connect(
  mapStateToProps,
  mapDispatchToProps
)(UserListContainer);

In der Präsentationskomponente können wir onClick={this.props.toggleActive} genau wie zuvor tun, aber dieses Mal mussten wir das Ereignis nicht selbst schreiben.

Auslassung der Container-Komponente

Manchmal muss eine Container-Komponente nur den Store abonnieren und benötigt keine Methoden wie componentDidMount(), um Ajax-Anfragen zu starten. Sie benötigt möglicherweise nur eine render()-Methode, um den Zustand an die Präsentationskomponente weiterzugeben. In diesem Fall können wir eine Container-Komponente auf diese Weise erstellen:

import React from 'react';
import { connect } from 'react-redux';
import UserList from '../views/list-user';

const mapStateToProps = function(store) {
  return {
    users: store.userState.users
  };
}

export default connect(mapStateToProps)(UserList);

Ja, Leute, das ist die ganze Datei für unsere neue Container-Komponente. Aber warten Sie, wo ist die Container-Komponente? Und warum verwenden wir hier kein React.createClass()?

Es stellt sich heraus, dass connect() eine Container-Komponente für uns erstellt. Beachten Sie, dass wir dieses Mal die Präsentationskomponente direkt übergeben, anstatt unsere eigene Container-Komponente zu erstellen, die wir übergeben. Wenn Sie wirklich darüber nachdenken, was Container-Komponenten tun, erinnern Sie sich, dass sie existieren, damit sich die Präsentationskomponente nur auf die Ansicht und nicht auf den Zustand konzentrieren kann. Sie geben den Zustand auch als Props an die untergeordnete Ansicht weiter. Und genau das tut connect() — es übergibt den Zustand (über Props) an unsere Präsentationskomponente und gibt tatsächlich eine React-Komponente zurück, die die Präsentationskomponente umschließt. Im Wesentlichen ist dieser Wrapper eine Container-Komponente.

Heißt das also, dass die Beispiele von zuvor eigentlich zwei Container-Komponenten sind, die eine Präsentationskomponente umschließen? Sicher, Sie können es so sehen. Aber das ist kein Problem, es ist nur notwendig, wenn unsere Container-Komponente mehr React-Methoden als render() benötigt.

Stellen Sie sich vor, die beiden Container-Komponenten dienen unterschiedlichen, aber verwandten Rollen

Hmm, vielleicht sieht das React-Logo deshalb wie ein Atom aus!

Provider

Damit dieser react-redux-Code funktioniert, müssen Sie Ihrer App über eine <Provider />-Komponente mitteilen, wie react-redux verwendet werden soll. Diese Komponente umschließt Ihre gesamte React-Anwendung. Wenn Sie React Router verwenden, würde es so aussehen:

import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import store from './store';
import router from './router';

ReactDOM.render(
  <Provider store={store}>{router}</Provider>,
  document.getElementById('root')
);

Der an Provider angehängte store verbindet React und Redux wirklich über react-redux. Diese Datei ist ein Beispiel dafür, wie Ihr Haupteinstiegspunkt aussehen könnte.

Redux mit React Router

Es ist nicht erforderlich, aber es gibt ein weiteres npm-Projekt namens react-router-redux. Da Routen technisch gesehen Teil des UI-Zustands sind und React Router nichts über Redux weiß, hilft dieses Projekt, die beiden zu verknüpfen.

Sehen Sie, was ich dort gemacht habe? Wir haben den Kreis geschlossen und sind zurück beim ersten Artikel!

Abschlussprojekt

Die finale Projektanleitung für diese Serie ermöglicht es Ihnen, eine kleine „Users and Widgets“ Single Page App zu erstellen.

Final Preview

Wie bei den anderen Artikeln dieser Serie enthält jeder eine Anleitung, die noch mehr Dokumentation darüber enthält, wie die Anleitung auf GitHub funktioniert.

Zusammenfassung

Ich hoffe wirklich, dass Ihnen diese Serie genauso viel Spaß gemacht hat wie mir beim Schreiben. Mir ist klar, dass wir viele Themen zu React nicht behandelt haben (Formulare zum Beispiel), aber ich habe versucht, der Prämisse treu zu bleiben, dass ich neuen Benutzern von React ein Gefühl dafür vermitteln wollte, wie man über die Grundlagen hinauskommt und wie es sich anfühlt, eine Single Page Application zu erstellen.

Obwohl viele geholfen haben, geht ein besonderer Dank an Lynn Fisher für die erstaunlichen Grafiken, die sie für die Tutorials bereitgestellt hat!


Artikelserie

  1. React Router
  2. Container-Komponenten
  3. Redux (Sie sind hier!)