Mit React Level aufsteigen: Container Komponenten

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 das zweite einer dreiteiligen Serie über React von Brad Westfall. Diese Serie dreht sich darum, über grundlegende React-Fähigkeiten hinauszugehen und größere Dinge zu bauen, wie z. B. ganze Single Page Applications (SPAs). Dieser Artikel knüpft dort an, wo der letzte Artikel über React Router aufgehört hat.

Artikelserie

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

Im ersten Artikel haben wir Routen und Ansichten erstellt. In diesem Tutorial werden wir ein neues Konzept untersuchen, bei dem Komponenten keine Ansichten erstellen, sondern die ermöglichen, die es tun. Es gibt Code zum Mitverfolgen auf GitHub, wenn Sie direkt in den Code eintauchen möchten.

Wir werden auch Daten in unsere Anwendung einführen. Wenn Sie mit irgendeiner Art von Komponenten-Design oder MVC-Mustern vertraut sind, wissen Sie wahrscheinlich, dass es im Allgemeinen als schlechte Praxis gilt, Ihre Ansichten mit dem Anwendungsverhalten zu vermischen. Anders ausgedrückt: Während Ansichten Daten erhalten müssen, um sie zu rendern, sollten sie nicht wissen, woher die Daten stammen, wie sie sich ändern oder wie sie erstellt werden.

Daten mit Ajax abrufen

Als Beispiel für eine schlechte Praxis erweitern wir unsere UserList-Komponente aus dem vorherigen Tutorial, um das eigene Abrufen von Daten zu handhaben

// This is an example of tightly coupled view and data which we do not recommend

var UserList = React.createClass({
  getInitialState: function() {
    return {
      users: []
    }
  },

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

  render: function() {
    return (
      <ul className="user-list">
        {this.state.users.map(function(user) {
          return (
            <li key={user.id}>
              <Link to="{'/users/' + user.id}">{user.name}</Link>
            </li>
          );
        })}
      </ul>
    );
  }
});

Wenn Sie eine detailliertere/anfängerfreundlichere Erklärung benötigen, was diese Komponente tut, siehe diese Erklärung.

Warum ist das Beispiel weniger als ideal? Erstens haben wir die Regel gebrochen, "Verhalten" mit "wie die Ansicht gerendert wird" zu vermischen – die beiden Dinge, die getrennt bleiben sollten.

Um es klarzustellen: Es ist nichts falsch daran, getInitialState zur Initialisierung des Komponentenstatus zu verwenden, und es ist nichts falsch daran, einen Ajax-Aufruf von componentDidMount auszuführen (obwohl wir den eigentlichen Aufruf wahrscheinlich in andere Funktionen auslagern sollten). Das Problem ist, dass wir diese Dinge *gemeinsam* in derselben Komponente tun, in der die Ansicht gespeichert ist. Diese enge Kopplung macht die Anwendung starrer und WET. Was ist, wenn Sie eine Benutzerliste auch woanders abrufen müssen? Die Aktion des Abrufens von Benutzern ist an diese Ansicht gebunden, daher ist sie nicht wiederverwendbar.

Das zweite Problem ist, dass wir jQuery für den Ajax-Aufruf verwenden. Sicher, jQuery hat viele nette Funktionen, aber die meisten davon befassen sich mit DOM-Rendering und React hat seinen eigenen Weg, dies zu tun. Was jQuerys Nicht-DOM-Funktionen wie Ajax betrifft, werden Sie wahrscheinlich viele Alternativen finden, die auf eine einzelne Funktion fokussierter sind.

Eine dieser Alternativen ist Axios, ein Promise-basiertes Ajax-Tool, das (in Bezug auf die API) sehr ähnlich zu jQuerys Promise-basierten Ajax-Funktionen ist. Wie ähnlich sind sie?

// jQuery
$.get('/path/to/user-api').then(function(response) { ... });

// Axios
axios.get('/path/to/user-api').then(function(response) { ... });

Für die restlichen Beispiele verwenden wir weiterhin Axios. Andere ähnliche Tools sind got, fetch und SuperAgent.

Props und State

Bevor wir uns mit Container- und Presentational Components befassen, müssen wir etwas über Props und State klären.

Props und State sind in gewisser Weise verwandt, da sie beide Daten für React-Komponenten "modellieren". Beide können von übergeordneten zu untergeordneten Komponenten weitergegeben werden. Die Props und der State einer übergeordneten Komponente werden jedoch für ihre untergeordnete Komponente einfach zu Props.

Nehmen wir zum Beispiel an, ComponentA übergibt einige seiner Props und seines States an seine untergeordnete Komponente, ComponentB. Die render-Methode von ComponentA könnte so aussehen

// ComponentA
render: function() {
  return <ComponentB foo={this.state.foo} bar={this.props.bar} />
}

Obwohl foo "State" in der übergeordneten Komponente ist, wird es zu einem "Prop" in der untergeordneten Komponente ComponentB. Das Attribut für bar wird ebenfalls zu einem Prop in der untergeordneten Komponente, da alle von der übergeordneten zur untergeordneten Komponente übergebenen Daten in der untergeordneten Komponente zu Props werden. Dieses Beispiel zeigt, wie eine Methode von ComponentB als Props auf foo und bar zugreifen kann

// ComponentB
componentDidMount: function() {
  console.log(this.props.foo);
  console.log(this.props.bar);
}

Im Beispiel *Fetching Data with Ajax* werden Daten, die von Ajax empfangen werden, als State der Komponente gesetzt. Das Beispiel hat keine untergeordneten Komponenten, aber Sie können sich vorstellen, dass, wenn es welche hätte, der State als Props von übergeordnet zu untergeordnet "fließen" würde.

Um den State besser zu verstehen, siehe die React-Dokumentation. Von nun an wird dieses Tutorial die sich im Laufe der Zeit ändernden Daten als "State" bezeichnen.

Zeit zum Aufteilen

Im Beispiel *Fetching Data with Ajax* haben wir ein Problem geschaffen. Unsere UserList-Komponente funktioniert, aber sie versucht, zu viele Dinge zu tun. Um das Problem zu lösen, zerlegen wir die UserList in zwei Komponenten, die jeweils eine andere Rolle erfüllen. Die beiden Komponententypen werden konzeptionell als Container-Komponenten und Presentational-Komponenten, auch bekannt als "smarte" und "dumme" Komponenten, bezeichnet.

Kurz gesagt: Container-Komponenten beziehen die Daten und befassen sich mit dem State. Der State wird dann an Presentational-Komponenten als Props übergeben und dann in Ansichten gerendert.

Die Begriffe "smarte" vs. "dumme" Komponenten verschwinden in der Community. Ich beziehe mich nur auf sie, falls Sie in älteren Artikeln darüber lesen, damit Sie wissen, dass sie dasselbe Konzept wie Container vs. Presentational sind.

Presentational-Komponenten

Sie wissen es vielleicht nicht, aber Sie haben in dieser Tutorial-Serie bereits Presentational Components gesehen. Stellen Sie sich einfach vor, wie die UserList-Komponente aussah, bevor sie ihren eigenen State verwaltete

var UserList = React.createClass({
  render: function() {
    return (
      <ul className="user-list">
        {this.props.users.map(function(user) {
          return (
            <li key={user.id}>
              <Link to="{'/users/' + user.id}">{user.name}</Link>
            </li>
          );
        })}
      </ul>
    );
  }
});

Es ist nicht *genau* dasselbe wie vorher, aber es *ist* eine Presentational Component. Der große Unterschied zwischen ihr und dem Original ist, dass diese über Benutzerdaten iteriert, um Listenelemente zu erstellen, und die Benutzerdaten über Props erhält.

Presentational Components sind "dumm" in dem Sinne, dass sie keine Ahnung haben, wie die erhaltenen Props zustande gekommen sind. Sie haben keine Ahnung vom State.

Presentational Components sollten die Prop-Daten niemals selbst ändern. Tatsächlich sollte jede Komponente, die Props erhält, diese Daten als unveränderlich und im Besitz der übergeordneten Komponente betrachten. Während die Presentational Component die Bedeutung der Daten im Prop nicht ändern sollte, kann sie die Daten für die Ansicht formatieren (z. B. einen Unix-Zeitstempel in etwas menschlich Lesbareres umwandeln).

In React werden Ereignisse direkt an die Ansicht mit Attributen wie onClick angehängt. Man könnte sich jedoch fragen, wie Ereignisse funktionieren, da Presentational Components Props nicht ändern sollen. Dafür haben wir unten einen ganzen Abschnitt über Ereignisse.

Iterationen

Beim Erstellen von DOM-Knoten in einer Schleife muss das key-Attribut eindeutig sein (relativ zu seinen Geschwisterelementen). Beachten Sie, dass dies nur für den obersten DOM-Knoten gilt – in diesem Fall das <li>.

Auch wenn das verschachtelte return für Sie seltsam aussieht, sollten Sie einen anderen Ansatz in Betracht ziehen, der dasselbe tut, indem er die Erstellung eines Listenelements in eine eigene Funktion aufteilt

var UserList = React.createClass({
  render: function() {
    return (
      <ul className="user-list">
        {this.props.users.map(this.createListItem)}
      </ul>
    );
  },

  createListItem: function(user) {
    return (
      <li key={user.id}>
        <Link to="{'/users/' + user.id}">{user.name}</Link>
      </li>
    );
  }
});

Container-Komponenten

Container-Komponenten sind fast immer die Eltern von Presentational Components. Sie dienen gewissermaßen als Vermittler zwischen Presentational Components und dem Rest der Anwendung. Sie werden auch als "smarte" Komponenten bezeichnet, da sie sich der Anwendung als Ganzes bewusst sind.

Da Container- und Presentational-Komponenten unterschiedliche Namen haben müssen, nennen wir diese hier UserListContainer, um Verwechslungen zu vermeiden.

var React = require('react');
var axios = require('axios');
var UserList = require('../views/list-user');

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

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

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

module.exports = UserListContainer;

Zur Kürze wurden in diesen Beispielen require()- und module.exports-Anweisungen weggelassen. In diesem Fall ist es jedoch wichtig zu zeigen, dass Container-Komponenten ihre jeweiligen Presentational-Komponenten als direkte Abhängigkeit einbinden. Zur Vollständigkeit zeigt dieses Beispiel alle erforderlichen Abhängigkeiten an.

Container-Komponenten können wie jede andere React-Komponente erstellt werden. Sie haben auch eine render-Methode wie jede andere Komponente, nur dass sie nichts zum Rendern selbst erstellen. Stattdessen geben sie das Ergebnis der Presentational Component zurück.

Ein kurzer Hinweis zu ES6 Arrow Functions: Sie bemerken vielleicht den klassischen Trick var _this = this, der für das obige Beispiel benötigt wird. ES6 Arrow Functions haben neben einer kürzeren Syntax weitere Vorteile, die die Notwendigkeit dieses Tricks beseitigen. Um Ihnen die Konzentration auf das Erlernen von React zu ermöglichen, vermeidet dieses Tutorial die ES6-Syntax zugunsten der älteren ES5-Syntax. Die GitHub-Anleitung für diese Serie verwendet jedoch intensiv ES6 und enthält einige Erklärungen in ihren README-Dateien.

Ereignisse

Bisher haben wir gezeigt, wie State von Container- zu Presentational Components übergeben werden kann, aber was ist mit Verhalten? Ereignisse fallen in die Kategorie Verhalten und sie müssen oft Daten mutieren. Ereignisse in React werden auf der Ansichtsebene angehängt. Zur Trennung der Zuständigkeiten kann dies ein Problem in unseren Presentational Components darstellen, wenn wir Ereignisfunktionen erstellen, wo sich die Ansicht befindet.

Um dies zu verdeutlichen, fügen wir zunächst ein Ereignis zu unserer Presentational Component hinzu (ein <button>, den Sie anklicken können), um Probleme direkt zu identifizieren.

// Presentational Component
var UserList = React.createClass({
  render: function() {
    return (
      <ul className="user-list">
        {this.props.users.map(function(user) {

          return (
            <li key={user.id}>
              <Link to="{'/users/' + user.id}">{user.name}</Link>
              <button onClick={this.toggleActive}>Toggle Active</button>
            </li>
          );

        })}
      </ul>
    );
  },

  toggleActive: function() {
    // We shouldn't be changing state in presentational components :(
  }
});

Dies würde technisch funktionieren, ist aber keine gute Idee. Wahrscheinlich muss das Ereignis Daten ändern, und Daten, die sich ändern, sollten als State gespeichert werden – etwas, das die Presentational Component nicht kennen sollte.

In unserem Beispiel wäre die State-Änderung die "Aktivität" des Benutzers, aber Sie können beliebige Funktionen erfinden, die Sie an onClick binden möchten.

Eine bessere Lösung ist, Funktionalität von der Container Component in die Presentational Component als Prop zu übergeben, so:

// Container Component
var UserListContainer = React.createClass({
  ...
  render: function() {
    return (<UserList users={this.state.users} toggleActive={this.toggleActive} />);
  },

  toggleActive: function() {
    // We should change state in container components :)
  }
});

// Presentational Component
var UserList = React.createClass({
  render: function() {
    return (
      <ul className="user-list">
      {this.props.users.map(function(user) {

        return (
          <li key={user.id}>
            <Link to="{'/users/' + user.id}">{user.name}</Link>
            <button onClick={this.props.toggleActive}>Toggle Active</button>
          </li>
        );

      })}
      </ul>
    );
  }
});

Das onClick-Attribut muss dort sein, wo sich die Ansicht befindet – bei der Presentational Component. Die Funktion, die sie aufruft, wurde jedoch in die übergeordnete Container Component verschoben. Das ist besser, weil die Container Component sich um den State kümmert.

Wenn die übergeordnete Funktion den State ändert, führt die State-Änderung zu einem erneuten Rendern der übergeordneten Funktion, was wiederum die untergeordnete Komponente aktualisiert. Dies geschieht in React automatisch.

Hier ist eine Demo, die zeigt, wie das Ereignis auf der Container Component den State ändern kann, was die Presentational Component automatisch aktualisiert.

Siehe den Pen React Container Component Demo von Brad Westfall (@bradwestfall) auf CodePen.

Beachten Sie, wie dieses Beispiel mit unveränderlichen Daten umgeht und die Methode .bind() verwendet.

Container-Komponenten mit dem Router verwenden

Der Router sollte nicht mehr direkt UserList verwenden. Stattdessen wird er direkt UserListContainer verwenden, der wiederum UserList verwendet. Letztendlich gibt UserListContainer das Ergebnis von UserList zurück, sodass der Router immer noch das erhält, was er benötigt.

Datenfluss und der Spread-Operator

In React wird das Konzept, dass Props von übergeordneten Komponenten an untergeordnete Komponenten weitergegeben werden, als Flow bezeichnet. Die bisherigen Beispiele zeigten nur einfache Eltern-Kind-Beziehungen, aber in einer realen Anwendung könnte es viele verschachtelte Komponenten geben. Stellen Sie sich vor, Daten fließen von übergeordneten Komponenten auf hoher Ebene über State und Props durch viele untergeordnete Komponenten. Dies ist ein grundlegendes Konzept in React und wichtig zu beachten, wenn wir uns dem nächsten Tutorial über Redux zuwenden.

ES6 hat einen neuen Spread-Operator, der sehr nützlich ist. React hat eine ähnliche Syntax zu JSX übernommen. Dies hilft React erheblich bei der Datenfluss über Props. Die GitHub-Anleitung für dieses Tutorial verwendet ihn ebenfalls, lesen Sie daher unbedingt die Anleitungsdokumentation zu dieser Funktion.

Stateless Functional Components

Ab React 0.14 (veröffentlicht Ende 2015) gibt es eine neue Funktion, um zustandslose (Presentational) Komponenten noch einfacher zu erstellen. Die neue Funktion heißt Stateless Functional Components.

Sie haben wahrscheinlich schon bemerkt, dass, wenn Sie Ihre Container- und Presentational-Komponenten trennen, viele Ihrer Presentational-Komponenten nur eine Render-Methode haben. In diesen Fällen erlaubt React jetzt, die Komponente als einzelne Funktion zu schreiben.

// The older, more verbose way
var Component = React.createClass({

  render: function() {
    return (
      <div>{this.props.foo}</div>
    );
  }

});

// The newer "Stateless Functional Component" way
var Component = function(props) {
  return (
    <div>{props.foo}</div>
  );
};

Sie sehen deutlich, dass der neue Weg kompakter ist. Denken Sie aber daran, dass dies nur eine Option für Komponenten ist, die nur eine render-Methode benötigen.

Mit dem neuen Stateless Functional-Ansatz akzeptiert die Funktion ein Argument für props. Das bedeutet, sie muss nicht this verwenden, um auf Props zuzugreifen.

Hier ist ein sehr gutes Egghead.io-Video zu Stateless Functional Components.

MVC

Wie Sie wahrscheinlich bisher gesehen haben, ähnelt React nicht dem traditionellen MVC. Oft wird React als "nur die Ansichtsschicht" bezeichnet. Das Problem, das ich mit dieser Aussage habe, ist, dass es für React-Anfänger zu leicht ist zu glauben, dass React in ihre vertraute traditionelle MVC-Welt passen *sollte*, als ob es für die Verwendung mit traditionellen Controllern und Modellen von Drittanbietern gedacht wäre.

Obwohl es *stimmt*, dass React keine "traditionellen Controller" hat, bietet es auf seine eigene besondere Weise eine Möglichkeit, Ansichten und Verhalten zu trennen. Ich glaube, dass Container-Komponenten denselben grundlegenden Zweck erfüllen wie ein Controller im traditionellen MVC.

Was die Modelle betrifft, so habe ich Leute gesehen, die Backbone-Modelle mit React verwenden, und ich bin sicher, sie haben alle möglichen Meinungen darüber, ob das für sie gut funktioniert hat. Aber ich bin nicht überzeugt, dass traditionelle Modelle der richtige Weg in
React sind. React möchte Daten so fließen lassen, dass sie nicht gut zu der Funktionsweise traditioneller Modelle passen. Das von Facebook entwickelte Flux-Designmuster ist eine Möglichkeit, Reacts inhärente Fähigkeit zum Datenfluss zu nutzen. Im nächsten Tutorial behandeln wir Redux, eine sehr beliebte Flux-Implementierung und was ich als Alternative zu traditionellen Modellen ansehe.

Zusammenfassung

Container-Komponenten sind eher ein Konzept als eine exakte Lösung. Die Beispiele in diesem Tutorial sind nur eine Möglichkeit, sie umzusetzen. Das Konzept ist jedoch so gut etabliert, dass es sogar Richtlinie von Facebook ist, sie in ihren Teams zu verwenden – auch wenn sie vielleicht andere Terminologien verwenden.

Dieses Tutorial wurde stark von anderen Artikeln zu diesem Thema beeinflusst. Schauen Sie sich unbedingt die offiziellen GitHub-Anleitungen zu diesem Tutorial an, um noch mehr Informationen und ein funktionierendes Beispiel für Container-Komponenten zu erhalten.


Artikelserie

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