State und Lifecycle-Methods

Kommen wir zu dem, was die Arbeit mit React erstmal wirklich effizient macht: State und die sogenannten Lifecycle-Methods.

Wie im vorangegangenen Kapitel bereits angesprochen, können Komponenten einen eigenen Zustand, den State, halten, verwalten und verändern. Dabei gilt der Grundsatz: Ändert sich der State einer Komponente, löst dies immer auch ein Rerendering der Komponente aus! Dieses Verhalten kann durch die Verwendung von PureComponent auch explizit unterbunden werden, was in einigen Fällen sinnvoll ist. Aber der Grundsatz bleibt unverändert: Eine State-Änderung löst ein Rerendering einer Komponente und ihrer Kind-Komponenten aus, außer diese sind selbst wiederum als PureComponent implementiert oder von einem React.memo()-Aufruf umschlossen.

Das ist insofern hilfreich, als wir nicht mehr manuell ReactDOM.render() aufrufen müssen, wann immer wir meinen, dass sich etwas an unserem Interface geändert hat, sondern die Komponenten dies stattdessen selbst entscheiden können.

Neben dem State an sich gibt es auch eine Handvoll sogenannter Lifecycle-Methoden. Dies sind Methoden, die optional in einer Klassen-Komponente definiert werden können und von React bei bestimmten Anlässen aufgerufen werden. Beispielsweise wenn eine Komponente erstmals gemountet wird, die Komponente neue Props empfangen oder sich der State innerhalb der Komponente geändert hat.

Erstmals seit React 16.8.0 können mit der Einführung von Hooks auch Function Components einen eigenen State verwalten und auf bestimmte Lifecycle-Events reagieren. Dieses Kapitel handelt primär von Klassen-Komponenten, in denen State und Lifecycle-Events als Methoden der Klassen-Komponenten implementiert werden. Da Hooks ein sehr umfassendes und neues Konzept ist, habe ich ihnen ein eigenes Kapitel gewidmet, in dem uns die meisten Themen, die in diesem Kapitel behandelt werden noch einmal speziell für Function Components beschrieben erneut begegnen werden.

Eine erste stateful Component

Der State innerhalb einer Klassen-Komponente ist verfügbar über die Instanz-Eigenschaft this.state und ist somit innerhalb einer Komponente gekapselt. Weder Eltern- noch Kind-Komponenten können ohne Weiteres auf den State einer anderen Komponente zugreifen.

Um in einer Komponente einen initialen Zustand zu definieren gibt es zwei einfache Wege; einen dritten, von der Funktionalität her etwas erweiterten Weg, lernen wir später noch mit der Lifecycle-Methode getDerivedStateFromProps() kennen.

Initialer State kann definiert werden, indem die Instanz-Eigenschaft this.state gesetzt wird. Dies ist entweder im Constructor einer Klassen-Komponente möglich:

class MyComponent extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      counter: props.counter,
    };
  }
  render() {
    // ...
  }
}

… oder indem der State als ES2017 Class Property definiert wird, was deutlich kürzer ist, jedoch momentan noch das Babel-Plugin @babel/plugin-proposal-class-properties (vor Babel 7: babel-plugin-transform-class-properties) benötigt:

class MyComponent extends React.Component {
  state = {
    counter: this.props.counter,
  };
  render() {
    // ...
  }
}

Create React App unterstützt die Class Property Syntax standardmäßig und da viele React-Projekte heute vollständig oder zumindest zu gewissen Teilen auf dem CRA-Setup oder Varianten davon basieren, kommt diese Syntax heute in den meisten Projekten zum Einsatz und kann genutzt werden. Sollte dies einmal in einem Projekt nicht der Fall sein, empfehle ich dringend die Installation und Nutzung des Babel-Plugins, da es wirklich viele unnötige Zeilen Code bei der täglichen Arbeit mit React einspart, während es gleichzeitig in nur wenigen Minuten eingerichtet ist.

Ist der State erst einmal definiert, können wir innerhalb der Klassen-Komponente mittels this.state lesend auf ihn zugreifen. Lesend ist hier ein entscheidendes Stichwort. Denn auch wenn es prinzipiell möglich, ist den State direkt über this.state zu verändern, sollte dies aus verschiedenen Gründen vermieden werden.

Den State verändern mit this.setState()

Um State zu verändern, stellt React eine eigene Methode innerhalb einer Klassen-Komponente bereit:

this.setState(updatedState);

Wann immer der State innerhalb einer Komponente verändert werden soll, sollte dafür this.setState() verwendet werden. Der Aufruf von this.setState() führt dann dazu, dass React entsprechende Lifecycle-Methoden (wie bspw. componentDidUpdate()) ausführt und eine Komponente neu rendert! Würden wir den State stattdessen direkt verändern, also bspw. this.state.counter = 1; schreiben, hätte dies vorerst keinerlei Auswirkungen auf unsere Komponente und alles würde aussehen wie bisher, da der Render-Prozess nicht ausgelöst werden würde. React hat dann keine Kenntnis von der Änderung am State!

Die Methode ist von der Funktionsweise her allerdings etwas komplexer, als es auf den ersten Moment aussehen mag. Und so wird nicht einfach nur der alte State durch den neuen State ersetzt und ein Rerendering ausgelöst; es passieren auch noch allerhand andere Dinge. Der Reihe nach.

Zuerst einmal kann die Funktion zwei verschiedene Arten von Argumenten entgegennehmen. Das ist einerseits ein Objekt mit neuen oder aktualisierten State-Eigenschaften, sowie andererseits eine Updater-Funktion, die wiederum ein Objekt zurückgibt oder null, falls nichts geändert werden soll. Bestehende gleichnamige Eigenschaften innerhalb des State-Objekts werden dabei überschrieben, alle anderen bleiben unangetastet! Möchten wir eine Eigenschaft im State zurücksetzen, müssen wir diese dazu also explizit auf null oder undefined setzen. Der übergebene State wird also immer mit dem bestehenden State zusammengefügt, niemals ersetzt!

Nehmen wir nochmal unseren oben definierten State mit einer counter Eigenschaft, deren initialer Wert für dieses Beispiel erst einmal 0 ist. Nun verändern wir den State und möchten diesem zusätzlich eine date-Eigenschaft mit dem aktuellen Datum hinzufügen. Übergeben als Objekt wäre unser Aufruf:

this.setState({
  date: new Date(),
});

Nutzen wir stattdessen eine Updater-Funktion, wäre unser Aufruf:

this.setState(() => {
  return {
    date: new Date(),
  };
});

Oder kurz:

this.setState(() => ({
  date: new Date(),
});

Unsere Komponente besitzt anschließend den neuen State:

{
  counter: 0,
  date: new Date(),
}

Um sicherzustellen, dass stets auf den aktuellen State zugegriffen wird, sollte eine Updater-Funktion verwendet werden, die den jeweils aktuellen State als ersten Parameter übergeben bekommt. Ein beliebter Fehler, der vielen Entwicklern bei der Arbeit mit React schon passiert ist, ist es direkt nach einem setState()-Aufruf auf this.state zuzugreifen und sich zu wundern, dass der State noch immer der alte ist.

React „sammelt“ aufeinanderfolgende setState()-Aufrufe und führt diese nicht unmittelbar aus, um unnötig häufiges und überflüssiges Rerendering von Komponenten zu vermeiden. Schnell aufeinanderfolgende setState()-Aufrufe werden später in gesammelter Form als Batch-Prozess ausgeführt. Das ist wichtig zu wissen, da wir nicht unmittelbar nach einem setState()-Aufruf mittels this.state auf den neu gesetzten State zugreifen können.

Stellen wir uns eine Situation vor, in der unser counter-State dreimal in schneller Abfolge erhöht werden soll. Intuitiv würde man nun vermutlich folgenden Code schreiben:

this.setState({ counter: this.state.counter + 1 });
this.setState({ counter: this.state.counter + 1 });
this.setState({ counter: this.state.counter + 1 });

Was denkst du, wie ist der neue State wenn der initiale State 0 war? 3? Falsch. Er ist 1! Hier kommt der angesprochene Batching-Mechanismus von React zum Zug. Um ein sich zu schnell aktualisierendes User Interface zu vermeiden, wartet React hier erst einmal ab. Am Ende kann man den obigen Code simpel ausgedrückt vom Funktionsprinzip her in etwa gleichsetzen mit:

this.state = Object.assign(
  this.state,
  { counter: this.state.counter + 1 },
  { counter: this.state.counter + 1 },
  { counter: this.state.counter + 1 }
);

Die counter-Property überschreibt sich hier während eines Batch-Updates also immer wieder selbst, nimmt aber stets this.state.counter als Basiswert für die Erhöhung um 1. Nachdem alle State-Updates ausgeführt wurden, ruft React dann erneut die render()-Methode der Komponente auf.

Bei der Verwendung einer Updater-Funktion wird dieser Funktion dabei der jeweils aktuellste State als Parameter übergeben und wir haben Zugriff auf den zum Zeitpunkt des Funktionsaufrufs gültigen State:

this.setState((state) => ({ counter: state.counter + 1 });
this.setState((state) => ({ counter: state.counter + 1 });
this.setState((state) => ({ counter: state.counter + 1 });

In diesem Fall nutzen wir eine solche Updater-Funktion und aktualisieren jeweils den aktuellsten Wert. Der Wert von this.state.counter wäre hier also wie erwartet 3, da wir über den übergebenen state-Parameter mit jedem Aufruf auf den aktuellen State zugreifen. Grundsätzlich ist es allerdings zu empfehlen, sich die benötigten Werte erst einmal zu erzeugen und anschließend gesammelt in einem einzigen setState()-Aufruf zu übergeben. So ist sichergestellt, dass es keine unnötigen zwischenzeitlichen render()-Aufrufe mit im nächsten Moment direkt wieder veraltetem State gibt.

Sollte es doch einmal nötig werden, unmittelbar nach einem setState()-Aufruf auf den eben neu gesetzten State zuzugreifen, bietet React die Möglichkeit, als optionalen zweiten Parameter eine Callback-Funktion zu übergeben. Diese wird aufgerufen nachdem der State aktualisiert wurde, so dass in dieser mittels this.state sicher auf den dann neu gesetzten State zugegriffen werden kann.

this.setState(
  {
    time: new Date().toLocaleTimeString(),
  },
  () => {
    console.log('Neue Zeit:', this.state.time);
  }
);

Lifecycle-Methoden

Lifecycle-Methoden können als Methoden einer Klassen-Komponente implementiert werden und werden durch React zu unterschiedlichen Zeitpunkten während eines Komponenten-Lebenszyklus (daher der Name) ausgeführt.

Der Lifecycle einer Komponente beginnt in dem Moment, in der diese instanziiert bzw. gemounted wird, also sich innerhalb der render()-Methode einer Eltern-Komponente befindet und tatsächlich auch Teil des zurückgegebenen Komponenten-Baumes ist. Der Lifecycle endet, wenn die Komponente aus dem Baum der zu rendernden Elemente entfernt wird. Währenddessen gibt es noch Lifecycle-Methoden die auf Updates und auf Fehler reagieren. Oder eben darauf, dass sie nun entfernt („unmounted“) werden.

Überblick über die Lifecycle-Methoden

Im Folgenden die Liste der Lifecycle-Methoden in der Reihenfolge, wann und in welcher Phase diese durch React aufgerufen werden, sofern diese in einer Komponente definiert wurden:

Mount-Phase

Die folgenden Methoden werden einmalig aufgerufen, wenn eine Komponenten erstmals gerendert wird, also, vereinfacht gesagt, erstmals zum DOM hinzugefügt wird:

  • constructor(props)

  • static getDerivedStateFromProps(nextProps, prevState)

  • componentWillMount(nextProps, nextState) (deprecated in React 17)

  • render()

  • componentDidMount()

Update-Phase

Die folgenden Methoden werden aufgerufen, wenn Komponenten entweder durch die Hereingabe neuer Props von außen oder durch die Veränderung des eigenen States ein Update erhalten oder oder explizit die von React bereitgestellte forceUpdate()-Methode aufgerufen wird:

  • componentWillReceiveProps(nextProps) (deprecated in React 17)

  • static getDerivedStateFromProps(nextProps, prevState)

  • shouldComponentUpdate(nextProps, nextState)

  • componentWillUpdate(nextProps, nextState) (deprecated in React 17)

  • render()

  • getSnapshotBeforeUpdate(prevProps, prevState)

  • componentDidUpdate(prevProps, prevState, snapshot)

Unmount-Phase

Hier gibt es nur eine Methode. Diese wird aufgerufen sobald die Komponente aus dem DOM entfernt wird. Dies ist nützlich, um bspw. Event-Listener oder setTimeout()/setInterval()-Aufrufe, die beim Mounting der Komponente hinzugefügt wurden, wieder zu entfernen:

  • componentWillUnmount()

Fehlerbehandlung

Zuletzt gibt es noch eine Methode, die in React 16 neu hinzukam und immer dann aufgerufen wird, wenn während des Renderings, in einer der Lifecycle-Methoden oder im Constructor einer Kind-Komponente ein Fehler geworfen wird:

  • componentDidCatch()

Komponenten, die eine componentDidCatch()-Methode implementieren werden auch als Error Boundary bezeichnet und dienen dazu, eine Alternative zum fehlerhaften Elementen-Baum darzustellen. Dies kann eine High-Level-Komponente sein (bezogen auf ihre Position innerhalb der Komponenten-Hierarchie), die grundsätzliche eine Fehler-Seite anzeigt und den Nutzer auffordert, die Anwendung neu zu laden, sollte ein Fehler auftreten. Es kann aber auch eine Low-Level-Komponente sein, die nur einen kurzen Fehlertext neben einem Button ausgibt, sollte die Aktion, die der Button ausgelöst hat, einen Fehler geworfen haben.

Lifecycle-Methoden in der Praxis

Werfen wir einmal einen Blick darauf, wie sich die Lifecycle-Methoden in einer einfachen Komponente verhalten. Zu diesem Zweck implementieren wir beispielhalber eine Komponente, die sekündlich ihren eigenen State verändert und jeweils die aktuelle Zeit ausgibt. Dazu wird beim Mounting der Komponente, also in der componentDidMount()-Methode, ein Interval gestartet, welches den State der Komponente aktualisiert, wodurch ein Rerendering ausgelöst und wieder die aktuelle Zeit angezeigt wird:

import React from 'react';
import ReactDOM from 'react-dom';

class Clock extends React.Component {
  state = {
    date: new Date(),
  };

  componentDidMount() {
    this.intervalId = setInterval(() => {
      this.setState(() => ({
        date: new Date(),
      }));
    }, 1000);
  }

  componentWillUnmount() {
    clearTimeout(this.intervalId);
  }

  render() {
    return <div>{this.state.date.toLocaleTimeString()}</div>;
  }
}

ReactDOM.render(<Clock />, document.getElementById('root'));

Hier sehen wir die Lifecycle-Methoden componentDidMount() und componentWillUnmount() im Einsatz. Wir definieren einen Default-State mit einer Eigenschaft date, die eine Instanz des Date-Objekts hält. Beim Mounting der Komponente (componentDidMount()) wird dann via setInterval() das Intervall gestartet und dessen Intervall-ID in der Instanz-Eigenschaft this.intervalId gespeichert. Da das Intervall sekündlich die setState()-Methode aufruft, verursacht die Komponente auch regelmäßig ein Rerendering, d.h. die render()-Methode wird erneut aufgerufen und zeigt wieder die aktuelle Zeit an.

Da die Intervall-Funktion grundsätzlich unabhängig von der React-Komponente ist und abgesehen davon, dass sie die setState()-Methode der Komponente aufruft, keinerlei Verbindung zu ihr hat, kümmert sich React auch nicht automatisch darum, dass der Intervall-Aufruf der Funktion gestoppt wird, wenn wir die Komponente nicht mehr weiter benötigen. Dafür müssen wir selber sorgen und genau zu diesem Zweck hält React für uns die nächste Lifecycle-Methode bereit: componentWillUnmount().

Diese Methode wird, unmittelbar bevor React die Komponente aus dem DOM entfernt, aufgerufen und kann dazu benutzt werden um bspw. noch laufende XHRs abzubrechen, Event Listener zu entfernen oder eben ein laufendes Funktionsintervall zu beenden. Genau das tun wir hier: bevor die Komponente entfernt wird, rufen wir clearTimeout() auf und übergeben der Funktion die Intervall ID, die wir zuvor in der entsprechenden Instanz-Eigenschaft gespeichert haben.

Sollten wir dies einmal vergessen, werden wir im Development-Modus von React aber spätestens beim Aufruf von this.setState() in einer bereits entfernten Komponente mit einer Warnung erinnert:

Warning: Can't call setState (or forceUpdate) on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in the componentWillUnmount method. in Clock

Anders als in einigen vorherigen Beispielen rufen wir hier die ReactDOM.render()-Methode nur ein einziges Mal auf. Die Komponente kümmert sich ab dann „um sich selbst“ und löst einen Render-Vorgang aus, sobald sich ihr State aktualisiert hat. Dies ist die übliche Vorgehensweise bei der Entwicklung von Anwendungen, die auf React basieren. Ein einziger ReactDOM.render()-Aufruf und ab dort verwaltet sich die App sozusagen von alleine, erlaubt Interaktion mit dem Benutzer, reagiert auf Zustandsänderungen und rendert regelmäßig das Interface neu.

Das Zusammenspiel von State und Props

Wir haben jetzt Beispiele gesehen für Komponenten, die Props verarbeiten und für Komponenten, die stateful sind, also ihren eigenen lokalen State verwalten. Doch es gibt noch eine ganze Menge mehr zu entdecken. Erst die Kombination mehrerer verschiedener Komponenten macht React zu dem mächtigen Werkzeug in der User Interface Entwicklung, das es ist. Eine Komponente kann dabei einen eigenen State haben und diesen gleichzeitig an Kind-Komponenten über deren Props weitergeben. So ist nicht nur die strikte Trennung von Business-Logik und Darstellung/Layout möglich, sondern es erlaubt uns auch, wunderbar aufgabenbasierte Komponenten zu entwickeln, die jeweils nur einen kleinen Teil der Applikation abbilden.

Bei der Trennung von Business- und Layout-Komponenten ist im React Jargon meist die Rede von Smart (Business-Logik) und Dumb (Layout) Components. Smart Components sollten dabei möglichst wenig bis gar nicht mit der Darstellung des User Interfaces betraut werden, während Dumb Components frei von jeglicher Business-Logik oder Seiteneffekten sein sollten, sich also tatsächlich auf die reine Darstellung von statischen Werten konzentrieren.

Schauen wir uns also das Zusammenspiel mehrerer Komponenten in einem weiteren Beispiel an:

const ShowDate = ({ date }) => <div>Heute ist {date}</div>;

const ShowTime = ({ time }) => <div>Es ist {time} Uhr</div>;

class DateTime extends React.Component {
  state = {
    date: new Date(),
  };

  componentDidMount() {
    this.intervalId = setInterval(() => {
      this.setState(() => ({
        date: new Date(),
      }));
    });
  }

  componentWillUnmount() {
    clearInterval(this.intervalId);
  }

  render() {
    return (
      <div>
        <ShowDate date={this.state.date.toLocaleDateString()} />
        <ShowTime time={this.state.date.toLocaleTimeString()} />
      </div>
    );
  }
}

ReactDOM.render(<DateTime />, document.getElementById('root'));

Zugegeben: Das Beispiel ist sehr konstruiert, demonstriert aber leicht verständlich das Zusammenspiel mehrerer Komponenten. Die DateTime-Komponente ist in diesem Sinne unsere Logik-Komponente: Sie kümmert sich darum die Zeit zu "besorgen" und zu aktualisieren, überlässt dann aber den Darstellungs-Komponenten deren Ausgabe, indem sie das Datum (ShowDate) bzw. die Zeit (ShowTime) über die Props übergeben bekommt. Die Darstellungs-Komponenten selbst sind dabei als simple Function Components implementiert, da eine Klassen-Komponente hier einfach nicht notwendig ist und nur unnötigen Overhead erzeugen würde.

Die Rolle der Lifecycle-Methoden im Zusammenspiel der Komponenten

Eingangs habe ich neben den bisher in den Beispielen verwendeten componentDidMount() und componentWillMount() noch einige weitere Lifecycle-Methoden erwähnt. Auch diese werden, sofern in einer Klassen-Komponente implementiert, zu den verschiedenen Anlässen von React berücksichtigt.

Zu diesem Zweck wollen wir eine Übungskomponente erstellen, welche die verschiedenen Lifecycle-Methoden als Debug-Nachricht in der Browser-Konsole ausgibt. Genau genommen sind es zwei Komponenten, von denen eine als Eltern-Komponente, die andere als Kind-Komponente dient, die von ihrer Eltern-Komponente Props hineingereicht bekommt (und in diesem Fall einfach ignoriert).

import React from 'react';
import ReactDOM from 'react-dom';

const log = (method, component) => {
  console.log(`[${component}]`, method);
};

class ParentComponent extends React.Component {
  state = {};

  constructor(props) {
    super(props);
    log('constructor', 'parent');
  }

  static getDerivedStateFromProps() {
    log('getDerivedStateFromProps', 'parent');
    return null;
  }

  componentDidMount() {
    log('componentDidMount', 'parent');
    this.intervalId = setTimeout(() => {
      log('state update', 'parent');
      this.setState(() => ({
        time: new Date().toLocaleTimeString(),
      }));
    }, 2000);
  }

  shouldComponentUpdate() {
    log('shouldComponentUpdate', 'parent');
    return true;
  }

  getSnapshotBeforeUpdate() {
    log('getSnapshotBeforeUpdate', 'parent');
    return null;
  }

  componentDidUpdate() {
    log('componentDidUpdate', 'parent');
  }

  componentWillUnmount() {
    log('componentWillUnmount', 'parent');
    clearInterval(this.intervalId);
  }

  render() {
    log('render', 'parent');
    return <ChildComponent time={this.state.time} />;
  }
}

class ChildComponent extends React.Component {
  state = {};

  constructor(props) {
    super(props);
    log('constructor', 'child');
  }

  static getDerivedStateFromProps() {
    log('getDerivedStateFromProps', 'child');
    return null;
  }

  componentDidMount() {
    log('componentDidMount', 'child');
  }

  shouldComponentUpdate() {
    log('shouldComponentUpdate', 'child');
    return true;
  }

  getSnapshotBeforeUpdate() {
    log('getSnapshotBeforeUpdate', 'child');
    return null;
  }

  componentDidUpdate() {
    log('componentDidUpdate', 'child');
  }

  componentWillUnmount() {
    log('componentWillUnmount', 'child');
  }

  render() {
    log('render', 'child');
    return <div>{this.props.time}</div>;
  }
}

ReactDOM.render(<ParentComponent />, document.getElementById('root'));

Diese beiden Komponenten führen zuverlässig (d.h. reproduzierbar) zur folgenden Ausgabe:

[parent] constructor
[parent] getDerivedStateFromProps
[parent] render
[child] constructor
[child] getDerivedStateFromProps
[child] render
[child] componentDidMount
[parent] componentDidMount
[parent] state update
[parent] shouldComponentUpdate
[parent] render
[child] getDerivedStateFromProps
[child] shouldComponentUpdate
[child] render
[child] getSnapshotBeforeUpdate
[parent] getSnapshotBeforeUpdate
[child] componentDidUpdate
[parent] componentDidUpdate
[parent] componentWillUnmount
[child] componentWillUnmount

Oh wow. Hier passiert also eine ganze Menge. Gehen wir also Zeile für Zeile durch und starten mit der Mount-Phase.

constructor(props)

Als Erstes wird stets der constructor der ParentComponent-Komponente aufgerufen. React geht hier von „außen nach innen“ vor. Also Komponenten, die weiter oben in der Komponenten-Hierarchie stehen, werden zuerst instanziiert, anschließend wird deren render()-Methode aufgerufen. Dies ist notwendig, da React ansonsten nicht wüsste, welche Kind-Komponenten überhaupt verarbeitet und berücksichtigt werden sollen, da React nur solche Kind-Komponenten berücksichtigt und für diese die Lifecycle-Methoden ausführt, die auch tatsächlich von der render()-Methode ihrer jeweiligen Eltern-Komponente zurückgegeben werden.

Der Constructor bekommt als Parameter die Props der Komponente übergeben und sollte diese über super(props) an seine Elternklasse (in der Regel ist das die React.Component oder React.PureComponent Klasse) weitergeben, da this.props ansonsten im Constuctor undefined ist, was zu unerwünschtem Verhalten und Bugs führen kann.

In vielen Fällen ist der Constructor heute gar nicht mehr notwendig, wenn mit dem Babel-Plugin „Class Properties“ gearbeitet wird und sowohl State als auch Instanz-Methoden als Klassen-Eigenschaft implementiert werden. Ist dies nicht der Fall, ist der Constructor der Ort, um einen initialen State zu setzen (bspw. this.state = { }) und Instanz-Methoden mittels .bind() an die jeweilige Klassen-Instanz zu binden (bspw. this.handleClick = this.handleClick.bind(this)). Letzteres ist notwendig, da Instanz-Methoden bei der Verwendung als Event-Listener innerhalb von JSX sonst ihren Bezug zur Komponente verlieren würden und this nicht auf die Instanz der Komponente verweisen würde.

static getDerivedStateFromProps(nextProps, prevState)

Auf den Constructor folgt die statische getDerivedStateFromProps()-Methode. Da dies eine statische Methode ist (und als solche wie auch in unserem Beispiel oben mit dem Keyword static ausgezeichnet werden muss), hat sie keinerlei Zugriff auf die Instanz der Komponente mittels this. Die Methode dient dazu, basierend auf den in die Komponente hineingereichten Props und ggf. dem letzten State, den nächsten State der Komponente zu berechnen. Dieser wird als Objekt aus der Methode zurückgegeben. Sind keine Änderungen am State notwendig, soll null zurückgegeben werden. Dabei ist das Verhalten identisch zu this.setState(), und es wird nur der Teil des States aktualisiert, der sich auch im zurückgegebenen Objekt befindet. Diese Eigenschaften werden mit dem letzten State zu einem neuen State zusammengeführt.

Die Methode ist innerhalb der Community kontrovers diskutiert worden, da sie die nun als deprecated gekennzeichnete Lifecycle-Methode componentWillReceiveProps() ablöst, jedoch anders als diese keinen Zugriff auf die Instanz der Komponente erlaubt. Die React-Entwickler haben diesen Schritt damit begründet dass diese beim überarbeiteten asynchronen Rendering von Komponenten zu unerwünschtem Verhalten führen kann und haben die Methode (ebenso wie componentWillMount() und componentWillUpdate()) als „unsicher“ erkärt. Mit dem Begriff ist dabei explizit nicht die Sicherheit (im Sinne von Sicherheitslücken) gemeint sondern lediglich die Tatsache, dass Komponenten, die diese Lifecycle-Methoden verwenden, in React 17 Bugs und anderen unerwünschten Effekte hervorrufen können.

Die getDerivedStateFromProps()-Methode sollte außerdem frei von Side Effects bleiben (also bspw. keine XHRequests veranlassen) sondern lediglich den neuen State der Komponenten-Instanz auf Basis der jeweiligen aktuellen Props berechnen bzw. ableiten (= derive). Anders als der Constructor wird die Methode nicht nur beim Mounting (also dem „Einhängen“ in den DOM) der Komponente aufgerufen, sondern auch wenn die Komponente erneut Props empfängt. Diese müssen sich dabei nicht zwangsweise inhaltlich geändert haben.

render()

Ist die Instanz einer Komponente erstellt und ihr State abgeleitet, ruft React bereits die render()-Methode auf. Die render()-Methode beschreibt unser User Interface und somit auch, welche Kind-Komponenten gerendert werden sollen. In unserem obigen Beispiel haben wir nur eine Kind-Komponente: die ChildComponent-Komponente.

Und so geht hier das Spiel erneut los: constructor(), getDerivedStateFromProps() und anschließend wird auch die render()-Methode der Kind-Komponente aufgerufen. Also das exakt selbe Verhalten wie auch schon bei der ParentComponent Komponente. Die Kind-Komponente aus dem Beispiel hat hier keine weiteren eigenen Kind-Komponenten. Hätte sie diese, würden auch hier die obigen Lifecycle-Methoden ausgeführt werden, bis React bei diesem Ast irgendwann auf eine Komponente trifft, die keinerlei eigenen React-Komponenten mehr zurückgibt, sondern DOM-Elemente wie div, p, section, span, etc. (natürlich auch Kombinationen dieser) oder null oder eben ein Array, der wiederum ebenfalls keine Komponenten enthält.

componentDidMount()

Ist eine solche Komponente erreicht, ist die componentDidMount()-Methode an der Reihe. Diese Methode wird aufgerufen, sobald eine Komponente und all ihre Kind-Komponenten gerendert wurden. Ab diesem Moment kann auf die DOM-Node der Komponente zugegriffen werden falls notwendig. Die Methode ist außerdem der richtige Ort um bspw. Timeouts oder Intervalle zu starten oder Netzwerk-Requests bspw. via XHR/Fetch zu veranlassen.

Die Methode wird „von innen nach außen“ aufgerufen. Also erst sind die Kind-Komponenten an der Reihe sobald diese gerendert wurden, dann kommen die Eltern-Komponenten dran. So können wir in der obigen Log-Ausgabe auch gut sehen, dass dort erst einmal die componentDidMount()-Methode der ChildComponent aufgerufen wird, erst danach die der ParentComponent.

In unserem Beispiel starten wir in der ParentComponent einmalig einen setTimeout() Aufruf, der nach 2000 Millisekunden den State der Komponente ändert, um diejenigen Lifecycle-Methods zu demonstrieren, die beim Update einer Komponente aufgerufen werden. Die Mount-Phase ist damit abgeschlossen und alle weiteren Änderungen am State der gemounteten Komponenten führen dazu, dass React die Lifecycle-Methods aus der Update-Phase aufruft. Dies ist hier eben nach 2000 Millisekunden der Fall, wenn die ParentComponent ihren eigenen State mittels this.setState() modifiziert.

shouldComponentUpdate(nextProps, nextState)

Findet ein Update einer Komponente statt, das ist immer der Fall wenn die Komponente ihren State verändert oder von außen Props hereingereicht bekommt, wird shouldComponentUpdate() aufgerufen. Doch Vorsicht, hier gibt es einen Unterschied je nachdem ob sich die Props geändert haben oder der State: bekommt eine Komponente neue Props von außen, wird zuvor getDerivedStateFromProps() aufgerufen.

Die shouldComponentUpdate()-Methode dient als „Hilfe“, mit der React mitgeteilt werden kann, ob ein kostspieliges Re-Rendering überhaupt nötig ist. Die Methode bekommt die nächsten Props und den nächsten State als Parameter übergeben und kann auf deren Basis die Entscheidung treffen, ob ein Rendering ausgeführt werden soll. Die Methode muss dabei entweder true zurückgeben, damit wird ein Re-Rendering ausgelöst wird oder false, wodurch der Aufruf von sowohl componentDidUpdate() , getSnapshotBeforeUpdate() als auch render() in dieser Komponente unterbunden wird.

In komplexen Anwendungen ist es oftmals der Fall, dass der Update-Zyklus nur ausgelöst wird, weil sich in einem anderen Teil der Anwendung, in einer Eltern-Komponente etwas geändert hat, diese Änderung aber für Kind-Komponenten irrelevant ist. Die shouldComponentUpdate()-Methode ist dann sehr hilfreich, wenn es um die Optimierung der Rendering-Performance geht, da so unnötige Re-Renderings verhindert werden.

Würden wir in unserer obigen ParentComponent aus der shouldComponentUpdate()-Methode prinzipiell false zurückgeben, wäre unsere Log-Ausgabe um einiges kürzer: die Zeilen 14-18 würden fehlen. Die Komponente würde nicht neu gerendert werden, ein Aufruf der render()-Methode fände nicht statt, damit würde auch die ChildComponent nicht neu gerendert werden und folglich auch deren Update-Methoden nicht aufgerufen werden.

Im Code-Beispiel geben wir aber true zurück. Dadurch wird folglich auch die render()-Methode der ParentComponent aufgerufen. Diese rendert wiederum erneut die ChildComponent, die den aktualisierten State der ParentComponent in ihren Props übergeben bekommt und schon befinden wir uns im Update-Zyklus der ChildComponent.

Hier wird wie schon beim Mount-Zyklus getDerivedStateFromProps() aufgerufen um einen neuen State basierend auf den neuen Props abzuleiten. Anschließend wird auch hier shouldComponentUpdate() aufgerufen. Hier könnten wir bspw. prüfen, ob sich die für diese Komponente relevanten Props überhaupt geändert haben und könnten dann, falls sie das nicht getan haben, ein Rerendering der Komponente sparen, indem wir ganz einfach false zurückgeben. Tun wir nicht, also folgt als nächstes der obligatorische Aufruf der render()-Funktion. Direkt darauf folgend wird nun die nächste Lifecycle-Methode aufgerufen.

getSnapshotBeforeUpdate(prevProps, prevState)

Diese Methode ist noch recht neu und wurde in React 16.3.0 zusammen mit getDerivedStateFromProps() neu eingeführt, um den Anforderungen an das neue asynchrone Rendering in React gerecht zu werden. Sie bekommt die letzten Props und den letzten State übergeben und hat letztmals Zugriff auf den aktuellen Zustand des HTML Dokuments, genauer gesagt dem DOM, bevor React die möglichen Änderungen aus dem letzten render()-Aufruf anwendet.

Dies kann nützlich sein, wenn man sich bspw. die aktuelle Scrollposition in einer langen Tabelle oder Liste merken möchte um bei einem Update der Liste wieder zur entsprechenden Zeile springen zu können. Die Methode kann hier einen beliebigen Wert oder null zurückgeben. Das, was getSnapshotBeforeUpdate() zurückgibt, wird dann als dritter Parameter an die nächste Lifecycle-Methode, componentDidUpdate() übergeben.

Aus meiner Erfahrung wird diese Methode nur sehr selten verwendet, wahrscheinlich am seltensten von allen Lifecycle-Methoden, da bei der Arbeit mit React auch nur sehr selten direkt auf DOM-Elemente zugegriffen werden muss. Viele Dinge, bei denen man bei einer imperativen Vorgehensweise auf die DOM API zurückgegriffen hätte, lassen sich hier direkt im abstrakten Komponenten-Baum, also über JSX lösen.

componentDidUpdate(prevProps, prevState, snapshot)

Als letzte Methode aus dem Update-Zyklus ist dann componentDidUpdate() an der Reihe. Diese wird aufgerufen, nachdem getDerivedStateFromProps() den neuen State abgeleitet hat, nachdem shouldComponentUpdate(), sofern implementiert, true zurückgegeben hat und nachdem getSnapshotBeforeUpdate() einen Snapshot des letzten Stands des DOM erstellt hat.

Die Methode bekommt als Parameter die letzten Props und den letzten State übergeben, d.h. die jeweiligen Props und den jeweiligen State bevor die Komponente aktualisiert wurde, und, sollte die Komponente eine getSnapshotBeforeUpdate()-Methode besitzen, deren Rückgabewert als dritten Parameter.

Ähnlich wie bereits componentDidMount() wird auch componentDidUpdate() „von innen nach außen“ aufgelöst, es werden also erst die componentDidMount()-Methoden der Kind-Komponente(n) aufgerufen, danach die der Eltern-Komponente(n). Diese Methode ist der ideale Ort um Side Effects auszulösen, also bspw. XHRs zu starten, wenn sich gewisse Eigenschaften der Komponente geändert haben. Dies lässt sich durch einen simplem Vergleich der aktuellen Props mit den als Parameter übergebenen letzten Props bzw. dem aktuellen State mit den letzten State festgestellt werden.

Auch ist es in dieser Komponente sicher auf den aktuellen DOM zuzugreifen, sollte dies doch einmal notwendig sein. Zu dem Zeitpunkt, zu dem diese Methode aufgerufen wird, hat React die notwendigen Änderungen, die sich aus dem möglicherweise geänderten JSX aus der render()-Methode ergeben, bereits durchgeführt und in den DOM übertragen.

Und damit ist neben dem Mount-Zyklus auch der Update-Zyklus abgeschlossen. Während der Mount-Zyklus immer nur einmal durchlaufen wird, nämlich dann wenn eine Komponente erstmals gerendert wird, kann der Update-Zyklus, solange die Komponente gemountet ist, beliebig häufig angestoßen werden und wird immer durchlaufen, sobald eine Komponente ihren State ändert oder neue Props bezieht.

componentWillUnmount()

Zugegeben, im obigen Log habe ich etwas geschummelt, denn die componentWillUnmount()-Methode wird immer dann (und nur dann) ausgeführt, wenn eine Komponente komplett aus dem DOM entfernt wird. Das wird sie im dazugehörigen Code-Beispiel allerdings nie. Eine Komponente gilt dann als unmounted, wenn sie über den Aufruf von ReactDOM.unmountComponentAtNode() explizit entfernt wird (das gilt insbesondere für Mount-Nodes) oder wenn sie implizit nicht mehr aus der render()-Methode einer Eltern-Komponente zurückgegeben wird.

Immer dann wird die componentWillUnmount()-Methode einer Komponente aufgerufen; natürlich wie bei allen Methoden, abgesehen von render(), nur dann wenn sie auch implementiert wurde. Diese Lifecycle-Methode ist essentiell, wenn es um das „Aufräumen“ geht. Hier können und sollten all die Funktionen aufgerufen werden, die benötigt sind damit die Komponente keine „Spuren hinterlässt“. Das können noch ausstehende Timeouts (setTimeout) oder fortlaufende Intervalle (setInterval) sein aber auch DOM-Modifikationen, die außerhalb des eigenen Komponenten-JSX vorgenommen wurden, noch laufende Netzwerkverbindungen bzw. XHR/Fetch-Aufrufe oder auch eigene Event Listener, die mittels der DOM API Methode Element.addEventListener() hinzugefügt wurden.

Event Listener. Gutes Stichwort. Darum kümmern wir uns im nächsten Kapitel, denn in den meisten Fällen ist der Einsatz von addEventListener() in React nicht mehr nötig, da React ein eigenes Event-System mitbringt, um für bessere Übersicht zu sorgen.

Diagramm der Lifecycle-Methoden

Last updated