Einführung in React Hooks

Einführung in React Hooks

In den letzten Wochen gibt es in der React-Community eigentlich nur ein Thema: React Hooks. Die Vorstellung dieses neuen React Features auf der React Conf Ende Oktober 2018 hat die Community in helle, überwiegend positive Aufregung versetzt. Doch warum wird dieses Feature so gehyped, was sind diese Hooks eigentlich genau und wie werden sie benutzt? Die Antworten auf alle diese Fragen finden sich im folgenden Artikel. Zuerst wird allgemein erklärt, was React Hooks überhaupt sind. Daraufhin wird auf die in der Praxis relevantesten Hooks und deren Vorteile eingegangen. Mit diesem Wissen treten wir abschließend einen Schritt zurück und betrachten das große Ganze und warum es sinnvoll ist, Hooks in einer React Applikation zu nutzen.
Um die Beispiele und Fachbegriffe nachvollziehen zu können, wird Erfahrung mit React in der Praxis vorausgesetzt.

Was sind Hooks?

React Hooks sind eine Erweiterung von React ohne Breaking Changes, d.h. sie sind zu 100% kompatibel mit bestehendem Code. Sie bieten die Möglichkeit, State und andere React-Funktionen in Functional Components zu nutzen, was bisher nur in Class Components möglich war. Dadurch ergeben sich eine Reihe an Vorteilen, die nun anhand der verschiedenen Hooks gezeigt werden.

Welche Arten von Hooks gibt es?

Hooks bieten wie bereits beschrieben Zugriff auf bestimmte React-Funktionen wie z.B. State. Daher gibt es eine Reihe an unterschiedlichen Hooks, die die entsprechenden React-Funktionen abbilden. Diese Hooks werden im folgenden vorgestellt.

State Hook

Der wohl in Zukunft in der Praxis am meisten genutzte Hook ist der State Hook. Wie der Name schon vermuten lässt bietet dieser die Möglichkeit, die Kernfunktionalität von React, den State, in Functional Components zu benutzen. Das war bisher nur Class Components vorbehalten.

import React, { useState } from 'react';

function Example() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

Die Syntax ist sehr einfach: die Funktion useState liefert ein Array zurück, das mittels einer destrukturierenden Zuweisung ("Array Destructuring") in zwei Konstanten aufgeteilt wird: während der erste Wert die Repräsentation des aktuellen States ist, ist der zweite Wert eine Funktion, mit der dieser State manipuliert werden kann. Diese Konstanten (im Beispiel count und setCount) können frei benannt werden, da es sich um ganz normale Javascript-Konstanten handelt.
Als Argument wird useState der initiale State übergeben, der von jeglichem Datentyp sein kann: es werden sowohl primitive Datentypen wie Strings, Numbers, etc. akzeptiert, als auch Arrays oder Objekte. Hier zeigt sich ein weiterer Vorteil des State Hooks: in Class Components musste der State immer ein Objekt sein.
Wird der State nun geändert, aktualisiert React automatisch den Wert der State-Konstanten (im Beispiel count) und triggert ein Rerendering der Komponente.

Effect Hook

Ein weiterer Hook nennt sich "Effect Hook". Er wird für Änderungen benutzt, die bisher in Class Components hauptsächlich in den Lifecycle-Methoden componentDidMount, componentDidUpdate und componentWillUnmount ausgeführt wurden, sogenannten "Side Effects".

import React, { useState, useEffect } from 'react';

function FriendStatus(props) {
  const [isOnline, setIsOnline] = useState(null);

  function handleStatusChange(status) {
    setIsOnline(status.isOnline);
  }

  useEffect(() => {
    // componentDidMount, componentDidUpdate
    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);

    // componentWillUnmount    
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });

  if (isOnline === null) {
    return 'Loading...';
  }
  return isOnline ? 'Online' : 'Offline';
}

Die Syntax ist wiederum sehr einfach: der Funktion useEffect wird eine Funktion übergeben, die nach jedem Mount, bzw. Update der Komponente ausgeführt wird. Diese Funktion kann optional eine weitere Funktion zurückgeben, die von React zum Cleanup verwendet wird (in Class Components: componentWillUnmount).

Vergleich von Funktions-Aufrufen in Class und Functional Components

Im Beispiel zeigt sich der Vorteil eines Effect Hooks gegenüber der Implementierung in einer Class Component: inhaltlich zusammengehörige Logik (im Beispiel Subscribe/Unsubscribe eines Listeners) bleibt zusammen an einer Stelle (innerhalb des Hooks), während dieselbe Logik in Class Components auf die drei Lifecycle-Methoden verteilt ist. Außerdem ist die gesamte Logik nun sehr leicht wiederverwendbar, mehr dazu im Kapitel "Custom Hook".

Context Hook

Der Context Hook vereinfacht die Verwendung der React Context API ( https://reactjs.org/docs/context.html ).

import React, { useContext } from 'react';

const UserContext = React.createContext();

function App (){
  var user = {
    name: 'Niklas Wolf',
    gender: 'male'
  }
  return (
    <UserContext.Provider value={user}>
      <div>
        <NameDisplay />
      </div>
    </UserContext.Provider>
  );
}

function NameDisplay() {
  const user = useContext(UserContext);
  return (
    <div>My name is: {user.name}</div>
  );
}

Um den aktuellen Wert des Context zu verwenden, genügt ein Aufruf des Hooks useContext. Als Argument erhält die Funktion das komplette Context-Objekt. Sobald sich nun der Wert des Context (im nächstgelegenen Provider) ändert, triggert React automatisch ein Rerendering der Komponenten, die den Context Hook verwenden und aktualisiert den Wert der jeweiligen Variablen.

Custom Hook

Custom Hooks sind, wie der Name vermuten lässt, keine von React zur Verfügung gestellten Hooks, sondern selbst entwickelte Hooks. Sie stellen eine tolle Möglichkeit dar, Logik wiederverwendbar zu machen und sich eine Bibliothek an Funktionalitäten aufzubauen (sowohl öffentlich als auch privat).

import React, { useState, useEffect } from 'react';

function useFriendStatus(friendID) {
  const [isOnline, setIsOnline] = useState(null);

  function handleStatusChange(status) {
    setIsOnline(status.isOnline);
  }

  useEffect(() => {
    ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
    };
  });

  return isOnline;
}

Die Konvention hier ist, dass Custom Hooks ebenso wie die Standard Hooks mit dem Keyword "use" beginnen. Ausgehend vom Beispiel des Effect Hooks wird hier die Abfrage des Online-Status eines Freundes in einen Custom Hook gekapselt. Wie zu sehen ist, verwendet der Custom Hook useFriendStatus eine Kombination aus State und Effect Hook, um den Online-Status eines Freundes zurückzuliefern.

Dieser Custom Hook kann nun in mehreren voneinander unabhängigen Komponenten verwendet werden, ohne Code-Duplizierungen zu erzeugen.

function FriendStatus(props) {
  const isOnline = useFriendStatus(props.friend.id);

  if (isOnline === null) {
    return 'Loading...';
  }
  return isOnline ? 'Online' : 'Offline';
}

function FriendListItem(props) {
  const isOnline = useFriendStatus(props.friend.id);

  return (
    <li style={{ color: isOnline ? 'green' : 'black' }}>
      {props.friend.name}
    </li>
  );
}

Desweiteren ist diese Funktionalität durch die Kapselung unabhängig von den Komponenten testbar geworden, was das Testen der Gesamt-Applikation deutlich vereinfacht und weniger komplex macht.

Weitere Hooks

Es gibt weitere Hooks, die Variationen der oben beschriebenen Hooks sind oder nicht so häufig gebrauchte Funktionalitäten abdecken.

Hook Beschreibung
useReducer Variation des State Hooks, hier kann der State über einen Reducer verwaltet werden (ähnlich wie bei Redux). Sinnvoll vor allem bei komplexeren State-Objekten, oder wenn der neue State abhängig vom vorherigen ist.
useMemo Gibt einen memoisierten Wert zur Performance-Optimierung zurück. Sinnvoll für das Caching von Ergebnissen von performance-intensiven Funktionen. Läuft während des Renderings, also nicht geeignet für Side Effects.
useCallback Variation von useMemo. Gibt ein memoisiertes Callback zurück.
useRef Gibt ein veränderliches (mutable) Objekt zurück, das während des gesamten Komponenten-Lifecycles bestehen bleibt. Kann in Functional Components ähnlich verwendet werden wie ein Attribut in einer Class Component.
useDebugValue Kann benutzt werden, um in den DEV-Tools ein eigenes Label für einen Custom Hook anzuzeigen.

Die komplette Liste an verfügbaren Hooks und deren Dokumentation findet man unter https://reactjs.org/docs/hooks-reference.html.

Warum sind React Hooks ein tolles Feature?

Mit dem Wissen, wie die einzelnen Hooks funktionieren und welche Vorteile sie bieten, erkennt man nun die Vorteile, die sich auf einem höheren Level für die gesamte React Applikation un deren Architektur ergeben.

Wiederverwendbarkeit von "stateful" Logik

Bisher war es nur möglich "stateful" Logik (also React-Code, der State verwendet) wiederzuverwenden, indem man eine Class Component verwendet. Durch React-Hooks ist es nun möglich, sehr viel kleinere Teile des Codes zu kapseln und wiederzuverwenden. Durch eine eigene "Hook-Bibliothek" mit eigenen Custom Hooks für bestimmte Funktionalitäten lassen sich oft benutzte Frontend-Features einmal programmieren, testen und beliebig weiterverwenden. Dieses Prinzip ist hierbei nicht nur auf den State Hook begrenzt: über einen eigenen Effect Hook lässt sich beispielsweise eine simple Komponente erstellen, die den Dokumenten-Titel ändert. So lässt sich sehr schnell eine eigene kleine Bibliothek aufbauen, mit der das Erstellen von Frontends noch einmal deutlich schneller von der Hand gehen sollte.

Code wird weniger komplex und lesbarer

Vor allem in den Lifecycle-Methoden componentDidMount, componentDidUpdate und componentWillUnmount ist es bisher oft der Fall, dass eine semantische zusammengehörige Funktionalität über mehrere dieser Methoden verteilt ist. Hier ein einfaches Beispiel: Um den Titel einer Seite abhängig vom State der Applikation zu ändern, ist es notwendig, die Änderung sowohl in componentDidMount als auch in componentDidUpdate vorzunehmen (duplizierter Code!).

constructor(props) {
  super(props);
  this.state = {
    count: 0
  };
}

componentDidMount() {
  document.title = `You clicked ${this.state.count} times`;
}

componentDidUpdate() {
  document.title = `You clicked ${this.state.count} times`;
}

In diesem Beispiel kann diese Code-Duplizierungen durch die Nutzung eines Effect Hooks vermieden werden. Damit wird der Code weniger komplex, besser lesbar und so auch wartbarer.

const [count, setCount] = useState(0);

useEffect(() => {
  document.title = `You clicked ${count} times`;
});

Reduzierung der "Wrapper Hell"

Durch Hooks werden Komponenten unabhängiger, außerdem kann Logik aus den Komponenten extrahiert werden. Dadurch ist es nicht mehr so oft nötig, Wrapper Components (auch Smart oder Container Components genannt) zu verwenden. Das trägt dazu bei, das die Baumstruktur, die durch die Verschachtelung von React-Komponenten entsteht, kleiner wird und die sogenannte "Wrapper Hell" einem längst nicht mehr so viele Sorgenfalten auf die Stirn treibt, wie bisher.

Das klingt alles super, aber ab wann kann man React Hooks nutzen?

Das Schöne ist: man kann Hooks bereits jetzt nutzen! Die erste stabile Implementierung dieses Features ist in der React-Version 16.8.0 enthalten, die vor kurzem veröffentlicht wurde. Wir haben React Hooks bereits in einem ersten Projekt produktiv im Einsatz und sind begeistert!

markiert mit Social Network