Ein Jahr React Hooks-API – eine Bilanz

Seite 5: Hooks außerhalb von Core-React

Inhaltsverzeichnis

Viele Bibliotheken aus dem React-Ökosystem bieten mittlerweile eine Hooks-API an, wie der React Router, Redux und die GraphQL Clients Apollo und Relay. Sie ersetzen häufig bekannte Muster wie Higher-Order-Components (HOCs) oder Render-Properties durch Hooks, die sowohl in der Verwendung als auch in der Implementierung einfacher sein sollen.

Redux verwendet beispielsweise die Hooks-API als Ersatz für die bisherige Higher-Order-Component, um eine Komponente mit Redux zu verknüpfen (connect-Funktion). Anstatt der connect-Funktion können Entwickler nun den useSelector-Hook verwenden. Er bekommt den globalen Zustand der Anwendung übergeben und wählt daraus die notwendigen Informationen für die Komponente aus. Die folgende E-Mail-Komponente verwendet den Hook, um den Betreff und Text einer E-Mail aus dem globalen Zustand auszuwählen:

import { useSelector } from "react-redux";

function Email() {
  // state ist der globale Redux State
  const subject = useSelector(state => state.subject);
  const body = useSelector(state => state.body);

  return <div><h1>{subject}<h1><p>{body}</p>/</h1></div>;

Da der Hook die ausgewählten Werte – im Gegensatz zur connect-Funktion – per Referenzvergleich auf Änderungen überprüft, kommt es zu einer getrennten Auswahl der beiden Werte aus dem globalen Zustand. Ändert sich mindestens einer davon, rendert die Komponente erneut.

Unabhängig davon hat der Hook noch weitergehende Auswirkungen. Bislang waren verbundene Redux-Komponenten frei von der Redux-API. Sowohl der benötigte Zustand aus dem Store als auch die benötigten Action Creators gelangten über die Higher-Order-Komponente per Property zur Komponente. Aus Sicht der Komponente hatte sie es nur mit regulären React Properties zu tun. Nun haben Komponenten, die Redux verwenden, eine direkte Abhängigkeit auf die Redux-API, was unter anderem Konsequenzen für das Testen der Komponente hat. Ob das nun gut oder schlecht, gravierend oder irrelevant ist, müssen Entwickler in ihrer jeweiligen Anwendung selbst entscheiden. Diese Entscheidungen gehen über die Ebene der reinen technischen API hinaus und betreffen Fragen der Anwendungsarchitektur.

Damit zusammenhängend diskutieren Teile der React-Community übrigens, ob die Trennung in Container- und Presentation-Components wegfallen sollte, da es technisch nun einfach ist, überall Zustand (oder Redux oder GraphQL) zu verwenden. Auch hier betrifft die Fragestellung neben der rein technischen auch die Architekturebene der Anwendung. Reine Presentation-Components sind weiterhin einfacher zu testen und besser wiederzuverwenden.

Viele Bibliotheken (z.B. der Router oder der Apollo-GraphQL-Client) verwenden das Render-Properties-Pattern, um Informationen an eine beliebige (unbekannte) Kind-Komponente zu übergeben. Dabei wird einer Komponente eine Funktion übergeben, die die Komponente zu definierten Zeiten mit beliebigen Parametern aufruft. Die Funktion liefert eine Komponente zurück, die die umschließenden Komponente rendern soll.

Die React Context-API nutzt das Muster ebenfalls. Wenn eine Komponente einen Kontext konsumieren möchte, verwendet sie dessen Consumer-Komponente, die als Kind-Element eine Funktion erwartet. Die Funktion übergibt der Consumer-Komponente den aktuellen Wert des Context. Sie kann den Context dann verwenden und eine Komponente zurückliefern. Das folgende Beispiel zeigt das Verhalten. Abhängig davon, ob ein currentUser im LoginContext gesetzt ist oder nicht, erfolgt die Ausgabe unterschiedlicher Nachrichten.

function Welcome() {
  return <LoginContext.Consumer>
           { ({currentUser}) => currentUser ? 
              <h1>Welcome, {currentUser}</h1> : 
              <h1>Please login</h1>
           }
         </LoginContext.Consumer>
}

Schon bei einem einfachen Anwendungsfall ist der entstandene Code relativ schwer zu lesen. Noch komplizierter wird es, wenn mehrere Render-Properties ineinander geschachtelt sind, zum Beispiel, weil eine Komponente mehrere Kontexte konsumieren will.

Mit dem useContext-Hook können Entwickler auf das Render-Properties-Pattern in diesem Fall verzichten. Das verbessert die Lesbarkeit:

function Welcome() {
  const { currentUser } = React.useContext(LoginContext);

  return currentUser ? 
      <h1>Welcome, {currentUser}</h1> : <h1>Please login</h1>;
}

Die auf Hooks-basierte Router-API tauscht das render-Property der Route-Komponente gegen Hooks, in Apollo GraphQL ersetzen sie die Query-, Mutation- und Subscription-Komponenten. Das macht den Code kürzer und lesbarer.