JavaScript

Kolumne: Olis bunte Welt der IT

React Hooks – React wird funktionaler
Keine Kommentare

Nachdem React schon immer parallel Ansätze zur Erzeugung von Komponenten mit Klassen oder Funktionen bot, zeigen neueste Versionen deutlich in eine funktionale Richtung.

Wie Sie vielleicht wissen, liegen mir funktionale Ansätze besonders am Herzen. Abseits aller logischen und technischen Überlegungen mag ich diese Art der Programmierung einfach besonders – es macht Spaß, aus einfachen Blöcken graduell neue Dinge zu bauen, verlässlich, testbar und konsistent.

Natürlich ist der kontinuierliche Einzug funktionaler Elemente in die verschiedensten Bereiche der Mainstreamprogrammierung nichts Neues mehr, dieser findet seit vielen Jahren statt. Im Detail sind bestimmte Entwicklungen allerdings trotzdem erwähnenswert, und so war ich vor Kurzem positiv überrascht, von einigen neuen Features und Plänen für React zu lesen. Mit diesen Neuerungen bewegt sich React deutlich in Richtung funktionaler Ideen, wesentlich deutlicher noch als das bisher der Fall war.

Als erstes Beispiel gibt es die bereits in React 16.6 verfügbare Funktion memo. Der Name stammt bereits aus der funktionalen Ecke: „Memoisierung“ nennt man die Technik, mit der in funktionalen Sprachen Rückgabewerte für spätere Aufrufe gespeichert werden. Nehmen Sie einmal an, Sie hätten eine einfache Funktion:

const calc = x => {
  return ...; // eine schwierige Berechnung!
}

Ein kurzes Memo reicht

Ein viel gepriesener Vorteil der funktionalen Programmierung besteht in der Zustandslosigkeit von Funktionen, der Unabhängigkeit von äußeren Einflüssen, der Vermeidung sog. Nebeneffekte. Die Funktion oben ist allein abhängig von ihrem Eingabeparameter x – solange derselbe Wert für x übergeben wird, liefert diese Funktion immer dasselbe Resultat. Daher lässt sich eine solche Funktion „memoisieren“, zum Beispiel mit der Hilfsfunktion in Listing 1.

const memo = f => {
  const results = {};

  // Zu Demonstrationszwecken darf f nur einen Parameter haben!
  return x => {
    if (results[x]) {
      return results[x];
    } else {
      result = f(x);
      results[x] = result;
      return result;
    }
  };
};

const memoizedCalc = memo(calc);

// Bei diesem ersten Aufruf findet die Berechnung in calc statt
console.log(calc(10));

// Bei folgenden Aufrufen mit demselben Parameterwert wird
// nur noch der gespeicherte Rückgabewert geliefert, die
// Berechnung selbst muss nicht noch einmal ausgeführt werden.
console.log(calc(10));

Natürlich dient die Implementation im Beispiel nur der Demonstration – Sie brauchen solche Helper nicht selbst zu schreiben, da vorhandene Libraries wie Lodash oder Ramda bereits sehr leistungsfähige Implementationen aufweisen, die auch mit zusätzlichen Parametern und anderen komplexeren Fällen zurechtkommen.

Nun ist es in React so, dass Komponenten eine Funktion enthalten, die zum Rendern – also zum Darstellen – des visuellen Elements aufgerufen wird. Wenn Sie eine Komponente als Klasse erzeugen, implementieren Sie diese Logik in der Methode render. Selbst im Klassenkontext gibt es gewisse Erwartungen an diese Methode (wie in der Dokumentation beschrieben), Sie sollten etwa nicht den Status der Komponente aus der Methode heraus ändern. Das erinnert an funktionale Ideen: Die Methode render ist abhängig von state und props der Komponente, sollte aber keine Nebeneffekte auslösen. Wenn Sie sich sorgfältig an diese Vorgaben halten, können Sie nun in der Klasse Maßnahmen ergreifen, damit render nicht öfter als nötig aufgerufen wird. Traditionell (und noch immer in schwierigen Fällen) implementieren Sie zu diesem Zweck eine Methode shouldComponentUpdate, in der beliebige Logik zur Entscheidungsfindung ausgewertet werden kann. Für viele Fälle genügt auch die Verwendung von PureComponent als Basisklasse, die eine Implementation von shouldComponentUpdate enthält, in der Updates nur dann ausgelöst werden, wenn state oder props sich geändert haben.

Für funktionale Komponenten sah React vor der Version 16.6 keine Möglichkeit vor, das Rendering entsprechend zu beeinflussen. Wenn Sie meinen Beispielen gefolgt sind, wissen Sie nun aber, wozu die eingangs erwähnte neue Funktion memo verwendet wird: Sie memoisiert React-Komponenten:

const memoizedComponent = React.memo(props => <div>...</div>);

Nun ist dies keine schwarze Magie, aber der Ansatz besticht sofort durch seine Kürze und Einfachheit! Das kürzeste Äquivalent in Klassenform würde von PureComponent ableiten:

class CleverComponent extends PureComponent {
  render() {
    return <div>...</div>;
  }
}

Nicht nur ist der minimale Code deutlich länger als die funktionale Version, er ist auch wesentlich weniger verständlich. Es war womöglich die Intention des Autors, dasselbe effiziente Rendering zu erzielen wie im funktionalen Beispiel – das weiß der Leser des Codes aber nur, wenn er die Klasse PureComponent genau kennt und sich erinnert, wodurch diese sich von der Basisklasse Component unterscheidet. Im Vergleich ist die Intention bei der Verwendung einer Funktion memo sofort deutlich, oder zumindest mit einer zielgerichteten Google-Suche schnell zu verstehen.

Die neuen Hooks

Nun gibt es für die Zukunft von React weitere Pläne, die noch deutlicher funktionale Ideen in den Vordergrund stellen. Es geht dabei um die sogenannten Hooks, mit deren Hilfe funktionale Komponenten gleichwertig zu Klassenkomponenten implementiert werden können.

Bisher war es so, dass zur funktionalen Erweiterung einer React-Komponente das Konzept der higherOrderComponent verwendet wurde. Eine Higher-Order Component ist eine Funktion, die eine andere Komponente kapselt. Aus der React-Dokumentation:

const EnhancedComponent = higherOrderComponent(WrappedComponent);

Dieser Ansatz wurde verbreitet eingesetzt, um fertige Komponenten an zusätzliche Systeme anzubinden. Ein Beispiel ist etwa die Funktion connect aus der Library Redux: Um einen Redux-Store für eine Komponente verfügbar zu machen, rufen Sie connect als Higher-Order Component auf.

const reduxConnectedComponent = connect(
  mapStateToProps,
  mapDispatchToProps
)(MyComponent);

Mit der Library Recompose gibt es eine Sammlung von flexibel verwendbaren Higher-Order Components, mit deren Hilfe funktionale React-Komponenten alles konnten, was die Klassensyntax auch unterstützt. Dazu zählt der Umgang mit state, die Reaktion auf Anderungen an props und vieles mehr. Ich habe selbst komplexe Komponenten auf Basis von Recompose gebaut. Ich möchte hier aus Platzgründen kein vollständiges Beispiel zeigen, aber die Struktur einer solchen Komponente konnte etwa so aussehen wie in Listing 2.

const Debounce = compose(
  onlyUpdateForPropTypes,
  setPropTypes({
    // ...
  }),
  defaultProps({
    // ...
  }),
  withState('viewValue', 'setViewValue', ({ value }) => value),
  withPropsOnChange(
    ['debounceWait', 'onChange'],
    ({ debounceWait, onChange }) => ({
      onChangeFunc: _.debounce(debounceWait)(onChange)
    })
  ),
  withPropsOnChange(['extract'], ({ extract }) => ({
    extractFunc: // ...
  })),
  withHandlers({
    childChange: props => e => {
      // ...
    }
  }),
  lifecycle({
    componentWillReceiveProps(np) {
      // ...
    }
  })
)(({ children, valueField, changeEvent, viewValue, childChange }) => {
  // render here
});

Dies ist zweifellos ein relativ komplexes Beispiel und vielleicht finden Sie die Syntax nicht auf Anhieb verständlich. Daran kann man sich aber gewöhnen und ich selbst finde die Syntax gut lesbar. Die äußere Funktion compose deutet auf einen funktionalen Ansatz hin, der mehrere Funktionen verkettet und zu einer neuen Funktion zusammenbaut. Mir wird also schnell klar, dass die Funktionen, die an compose übergeben werden, alle gemeinsam für diese Komponente zur Anwendung kommen: onlyUpdateForPropTypes, setPropTypes, defaultProps, withState usw., insgesamt acht Funktionen.

IT Security Summit 2019

Sichere Logins sind doch ganz einfach!

mit Arne Blankerts (thePHP.cc)

Hands-on workshop – Hansel & Gretel do TLS

mit Marcus Bointon (Synchromedia Limited)

Jede dieser Funktionen stellt eine Higher-Order Component dar, und darin liegt das Problem mit diesem Ansatz: Es wird eine zwiebelartige Struktur erzeugt, wie sie ein tief verschachtelter Funktionsaufruf liefern würde.

onlyUpdateForPropTypes()(
  setPropTypes(...)(
    defaultProps(...)(
      withState(...)(
        withPropsOnChange(...)(
          withHandlers(...) (
            lifecycle(...)(
              // render function here
            )))))))

Die resultierende Komponente ist tief geschachtelt, da sie mehrfach – acht Mal im Beispiel! – gekapselt wird. Natürlich muss man sich fragen, ob die Performance von diesem Ansatz nicht beeinträchtigt werden könnte, aber davon abgesehen ist solch eine Struktur etwa beim Debuggen höchst unangenehm! React-Anwendungen enthalten aufgrund der Komponentenstrukturen grundsätzlich Hierarchien, die eine beeindruckende Tiefe erreichen können. Wenn allerdings vielfach einzelne Komponenten als Resultat von Kapselung zahlreiche Ebenen zur Hierarchie hinzufügen, wird eine Anwendung schnell unübersichtlich.

Das neue Konzept der Hooks bietet einen anderen Ansatz, funktionale Komponenten vollwertig zu machen, ohne dabei tiefe Kapselung zu erzeugen. Hier ist ein einfaches Beispiel aus der Dokumentation, in dem state in einer funktionalen Komponente verwendet wird:

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

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

Die Funktion useState ist hier der Hook. Die Funktion erzeugt einen state für die Komponente und gibt zwei Werte zurück, den state selbst und eine Funktion, mit deren Hilfe der state modifiziert werden kann. Der Begriff Hook deutet darauf hin, dass die entsprechenden Funktionen sich in das React-System „einhängen“. Natürlich ist die interne Vorgehensweise nicht immer gleich, aber es ist leicht vorstellbar, dass manche Hooks an den Lifecycle-Mechanismen von React teilnehmen und somit das Verhalten von Komponenten beeinflussen.

Ein Beispiel dafür ist der Hook useEffect. Wie oben erwähnt, sollte die render-Funktion selbst keine Nebeneffekte auslösen. Daher verwenden Sie bei der Implementation von Klassenkomponenten in React andere Lifecycle-Methoden, um Nebeneffekte auszulösen. Besonders die Methoden componentDidMount und componentDidUpdate werden so oft zum Laden von Daten oder ähnlichen Zwecken eingesetzt. Das war wiederum bisher in funktionalen Komponenten unmöglich, kann nun aber mit dem Hook useEffect erreicht werden:

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

  useEffect(() => {
    // Dieser Code löst einen Nebeneffekt aus, läuft aber nicht
    // im Kontext der render-Funktion
  });

  return (...);
}

In der aktuellen Preview-Version unterstützt React neun verschiedene Hooks. Der Autor der vorhin beschriebenen Library Recompose war aktiv in die Entwicklung dieses neuen Konzepts involviert und ist nun überzeugt, dass Hooks alle Anwendungsfälle von Recompose abdecken. Es ist offensichtlich, dass die strukturellen Probleme, die aus exzessiver Verwendung von Higher-Order Components entstehen, durch Hooks vermieden werden können – hier wird nicht gekapselt!

Um die syntaktische Einfachheit zu erreichen, gibt es bei der Verwendung von Hooks einige Regeln, an die man sich tunlich halten sollte. Dazu zählt offensichtlich, dass Hooks nur in funktionalen Komponenten verwendet werden dürfen, aber auch, dass Hooks nur aus der obersten Ebene einer Komponentenfunktion aufgerufen werden sollen. Natürlich lassen sich Lint-Regeln bauen, die dem Entwickler entsprechende Disziplin nahelegen. Es ist allerdings abzuwarten, inwiefern die Einhaltung der Vorgaben in „echten“ Projekten zum Problem wird.

Aktuell sind Hooks in einer Vorabversion des nächsten React-Releases verfügbar, und Dokumentation ist bereits online. Facebook betont, dass die Idee von Klassenkomponenten mit dieser neuen Entwicklung nicht hinfällig ist – es muss also nichts neu geschrieben werden. Allerdings wird auch erwähnt, dass für zukünftige Erweiterungen in Facebook-Apps anscheinend Hooks favorisiert werden sollen – ein beeindruckender Schritt für Anhänger der funktionalen Programmierung! Ich empfehle Ihnen, das Konzept besser kennenzulernen, den Beispielen auf der React-Website zu folgen und mit der neuen Version zu experimentieren.

Windows Developer

Windows DeveloperDieser Artikel ist im Windows Developer erschienen. Windows Developer informiert umfassend und herstellerneutral über neue Trends und Möglichkeiten der Software- und Systementwicklung rund um Microsoft-Technologien.

Natürlich können Sie den Windows Developer über den entwickler.kiosk auch digital im Browser oder auf Ihren Android- und iOS-Devices lesen. In unserem Shop ist der Windows Developer ferner im Abonnement oder als Einzelheft erhältlich.

Unsere Redaktion empfiehlt:

Relevante Beiträge

Hinterlasse einen Kommentar

Hinterlasse den ersten Kommentar!

avatar
400
  Subscribe  
Benachrichtige mich zu:
X
- Gib Deinen Standort ein -
- or -