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
React mit Magento 2

Erweiterung eines bestehenden Projekts mit ReactJS

Einleitung

Als Frontend-Entwickler stößt man heutzutage sehr oft auf die gehypte Javascript Library React. Die extreme Popularität der "Javascript-Bibliothek zum Erstellen von Benutzeroberflächen" (https://reactjs.org/) ist ungebrochen und wächst mit der immer größer werdenden Verbreitung von Web-Apps stetig. So setzt auch das vor kurzem veröffentlichte Magento PWA-Studio zur Erstellung einer Progressive Web App für Magento2 komplett auf die Bibliothek. Allerdings wird React mittlerweile sehr häufig nur noch mit Single-Page-Applikationen (SPA) in Verbindung gebracht, was aber einen großen Vorteil komplett unterschlägt: React wurde von Grund auf dahin konzipiert und optimiert, es als Erweiterung für bestehende Projekte zu verwenden. Hier zeigt sich auch, warum React als Bibliothek und nicht als Framework bezeichnet wird.

Im Entwickler-Alltag ist es oft eben nicht der Fall, dass man ein komplett neues Frontend (als SPA) erstellen soll, sondern ein bestehendes Projekt weiterentwickelt. Hier ist es oft schwierig, kostet zu viele Ressourcen oder ist komplett unnötig die gesamte Applikation zu einer SPA umzubauen. Gleichzeitig möchte man als Entwickler aber die Vorteile, die eine neue Technologie wie z.B. React bietet, nutzen.

Dieser Artikel soll also zeigen, wie React sehr einfach in ein bereits bestehendes Frontend integriert oder einzelne Komponenten ausgetauscht werden können. Im Folgenden werden einige grundlegende React-Kenntnisse vorausgesetzt.

Demo

Das im Folgenden beschriebene Beispiel-Setup und -Projekt ist als Demo-Code unter https://github.com/mothership-gmbh/react-demo verfügbar.

Setup/Installation

In diesem Beispiel wird ein möglichst einfaches Setup ohne Module-Bundler wie Webpack o.ä. gezeigt. Es wird einzig und allein eine Kompilierung von JSX zu Javascript aufgesetzt, hier über den Task Runner Gulp, der in vielen Projekten schon vorhanden sein könnte und entsprechend nur noch erweitert werden müsste.

Voraussetzungen:

Nachdem beides installiert ist, kann im Root-Verzeichnis folgende package.json erzeugt werden.

package.json

{
  "name": "mothership-react-demo",
  "version": "1.0.0",
  "description": "Wir integrieren React in eine bestehende App",
  "main": "index.js",
  "author": "Mothership GmbH",
  "devDependencies": {
    "@babel/core": "^7.1.2",
    "@babel/plugin-transform-react-jsx": "^7.0.0",
    "gulp": "^4.0.0",
    "gulp-babel": "^8.0.0-beta.2"
  }
}

Nach dem Ausführen des Befehls npm install ist das Grundsetup schon fertig. Nun fehlt nur noch die Konfiguration von Gulp, damit die JSX-Syntax in valides Javascript kompiliert wird. Dazu legen wir die Datei gulpfile.js mit folgendem Inhalt an.

gulpfile.js

var gulp = require('gulp');
var babel = require('gulp-babel');

// JSX-Task um JSX in Javascript zu kompilieren
gulp.task('jsx', () => {
    return gulp.src('web/js/react/src/**/*.js')
        .pipe(babel({
            plugins: ['@babel/plugin-transform-react-jsx']
        }))
        .pipe(gulp.dest('web/js/react/dist'));
});

// Watch-Task um Änderungen am JSX automatisch zu kompilieren
gulp.task('watch', function () {
    gulp.watch('web/js/react/src/**/*.js', {ignoreInitial: false}, gulp.series('jsx'));
});

// Default Task von Gulp
gulp.task('default', gulp.parallel(['watch']));

Die Dateistruktur sieht in diesem Beispiel also wie folgt aus: Alle React-spezifischen Dateien befinden sich im Ordner js/react, um eine möglichst einfache Abgrenzung von evtl. bereits existierendem Javascript-Code zu schaffen. Die JSX-Quelldateien werden in web/js/react/src abgelegt, die fertig kompilierten Dateien sind in web/js/react/dist zu finden.

app
├── gulpfile.js
├── package-lock.json
├── package.json
└── web
    ├── css
        ├── mothership.css
        └── styles.css
    ├── index.html
    └── js
        └── react
            ├── dist
            │   └── modal.js
            ├── src
            │   └── modal.js
            └── vendor
                ├── react-dom.js
                └── react.js

Hiermit ist das Setup vollendet. Um die React-Komponenten in valides Javascript zu kompilieren, kann der Befehl gulp jsx ausgeführt werden. Um während der Entwicklung nicht bei jeder Änderung diesen Befehl erneut manuell ausführen zu müssen, kann stattdessen gulp watch verwendet werden. Hier wird ein Watcher gestartet, der bei Änderungen an den Quelldateien automatisch den JSX-Task ausführt.

React Komponente entwickeln und in bestehender Seite verwenden

Um das Beispiel möglichst einfach zu halten, soll eine simple Modal-Komponente entwickelt werden, die sich durch den Klick eines Buttons aufrufen lässt.

Zuerst muss auf der Seite, die die React-Komponente enthalten soll (hier: index.html), die React-Bibliothek eingebunden werden. Dazu braucht es zwei Dateien, react.js und react-dom.js. Diese können z.B. über ein CDN geladen werden, im Beispiel werden sie lokal bereitgestellt. Zu beachten ist, dass diese beiden Dateien vor dem Javascript der späteren Komponente eingebunden werden.
Im Folgenden wird nun die React-Komponente erstellt und in die Seite integriert. Dazu braucht es ein Container-Element im DOM, hier mit der ID react-modal.

index.html

<html>
  <head>...</head>
  <body>
    ...
    <!-- Container-Element für die React-Komponente -->
    <div id="react-modal"></div>
    ...
    <script type="text/javascript" src="js/react/vendor/react.js"></script>
    <script type="text/javascript" src="js/react/vendor/react-dom.js"></script>
    <!-- Einbinden der Modal-Komponente -->
    <script type="text/javascript" src="js/react/dist/modal.js"></script>
  </body>
</html>

Die React-Komponente ist sehr simpel gehalten. Da das Modal zuerst einmal ausgeblendet sein soll, wird der initiale State isOpen auf false gesetzt. Über die Funktion toggleOpen kann dieser Status gewechselt werden.
Gerendert wird das Modal so, dass sowohl bei Klick auf den Button "Modal öffnen" als auch bei Klick auf den Hintergrund (bei geöffnetem Modal) toggleOpen aufgerufen wird, sodass sich das Modal entsprechend öffnet, bzw. schließt.
Zuletzt wird die Komponente in den zuvor beschriebenen DOM-Container mit der ID react-modal eingefügt.

js/react/dist/modal.js

'use strict';

class Modal extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
          isOpen: false
        };
        this.toggleOpen = this.toggleOpen.bind(this);
    }

    toggleOpen() {
        this.setState({
            isOpen: !this.state.isOpen
        });
    }

    render() {
      var className = 'modal';
      className += this.state.isOpen ? ' open' : '';
        return <div>
            <button className='button-open' onClick={this.toggleOpen}>Modal öffnen</button>
            <div className={className}>
              <div className='content'>
                <button className='close-button' onClick={this.toggleOpen}>X</button>
                <div>
                  <h2>Ich bin das Modal</h2>
                  <div>Man kann mich über das X schließen, aber auch über einen Klick außerhalb</div>
                  <iframe src="https://giphy.com/embed/xT77XWum9yH7zNkFW0"
                          width="480"
                          height="270"
                          frameBorder="0"
                          className='giphy-embed'
                          allowFullScreen>
                  </iframe>
                  <p>
                      <a href="https://giphy.com/gifs/9jumpin-wow-nice-well-done-xT77XWum9yH7zNkFW0">via GIPHY</a>
                  </p>
                </div>
              </div>
              <button className='background' onClick={this.toggleOpen}/>
            </div>
          </div>
    }
}

const e = React.createElement;
const domContainer = document.querySelector('#react-modal');
ReactDOM.render(e(Modal), domContainer);

Fazit

Obwohl React meist in einem Atemzug mit Single-Page-Applikationen genannt wird, eignet es sich auch hervorragend, um ein bereits bestehendes Frontend mit einigen dynamischen Elementen modular zu erweitern. So müssen Frontend-Entwickler nicht auf den Einsatz dieser modernen Technologie verzichten, ohne einen kompletten Relaunch/Rewrite des Frontends nötig zu machen. React-Neulinge haben des Weiteren so die Möglichkeit, schon jetzt mit wenig Aufwand Erfahrung mit der Bibliothek zu sammeln, um so für zukünftige Aufgaben (z.B. die Umsetzung einer Magento2 PWA-Storefront) gewappnet zu sein.

markiert mit Social Network