Verfolgen Sie, warum eine React-Komponente neu gerendert wird

154

Gibt es einen systematischen Ansatz zum Debuggen, was dazu führt, dass eine Komponente in React neu gerendert wird? Ich habe ein einfaches console.log () eingefügt, um zu sehen, wie oft es gerendert wird, aber ich habe Probleme herauszufinden, warum die Komponente in meinem Fall mehrmals, dh (viermal) gerendert wird. Gibt es ein Tool, das eine Zeitleiste und / oder das Rendern und die Reihenfolge aller Komponentenbäume anzeigt?

jasan
quelle
Möglicherweise können Sie die shouldComponentUpdateautomatische Komponentenaktualisierung deaktivieren und dann von dort aus Ihre Ablaufverfolgung starten. Weitere Informationen finden Sie hier: facebook.github.io/react/docs/optimizing-performance.html
Reza Sadr
Die Antwort von @jpdelatorre ist korrekt. Im Allgemeinen besteht eine der Stärken von React darin, dass Sie den Datenfluss mithilfe des Codes problemlos in der Kette zurückverfolgen können. Die React DevTools-Erweiterung kann dabei helfen. Außerdem habe ich eine Liste nützlicher Tools zum Visualisieren / Verfolgen des erneuten Renderns von React-Komponenten als Teil meines Redux-Addons-Katalogs sowie eine Reihe von Artikeln zu [React Performance Monitoring] (htt
markerikson,

Antworten:

251

Wenn Sie ein kurzes Snippet ohne externe Abhängigkeiten möchten, finde ich dies nützlich

componentDidUpdate(prevProps, prevState) {
  Object.entries(this.props).forEach(([key, val]) =>
    prevProps[key] !== val && console.log(`Prop '${key}' changed`)
  );
  if (this.state) {
    Object.entries(this.state).forEach(([key, val]) =>
      prevState[key] !== val && console.log(`State '${key}' changed`)
    );
  }
}

Hier ist ein kleiner Hook, mit dem ich Aktualisierungen von Funktionskomponenten nachverfolge

function useTraceUpdate(props) {
  const prev = useRef(props);
  useEffect(() => {
    const changedProps = Object.entries(props).reduce((ps, [k, v]) => {
      if (prev.current[k] !== v) {
        ps[k] = [prev.current[k], v];
      }
      return ps;
    }, {});
    if (Object.keys(changedProps).length > 0) {
      console.log('Changed props:', changedProps);
    }
    prev.current = props;
  });
}

// Usage
function MyComponent(props) {
  useTraceUpdate(props);
  return <div>{props.children}</div>;
}
Jacob Rask
quelle
5
@ yarden.refaeli Ich sehe keinen Grund, einen if-Block zu haben. Kurz und prägnant.
Isaac
Wenn Sie feststellen, dass ein Status aktualisiert wird und nicht klar ist, wo oder warum, können Sie die setStateMethode (in einer Klassenkomponente) überschreiben setState(...args) { super.setState(...args) }und dann in Ihrem Debugger einen Haltepunkt festlegen, den Sie dann aktivieren können um zu der Funktion zurückzukehren, die den Zustand einstellt.
Redbmk
Wie genau verwende ich die Hook-Funktion? Wo genau soll ich anrufen, useTraceUpdatenachdem ich es so definiert habe, wie Sie es geschrieben haben?
Damon
In einer Funktionskomponente können Sie sie wie function MyComponent(props) { useTraceUpdate(props); }folgt verwenden und sie wird protokolliert, wenn sich die Requisiten ändern
Jacob Rask
1
@DawsonB Sie haben wahrscheinlich keinen Status in dieser Komponente, this.stateist also undefiniert.
Jacob Rask
67

In den folgenden Fällen wird eine React-Komponente erneut gerendert.

  • Rendern der übergeordneten Komponente
  • Aufruf this.setState()innerhalb der Komponente. Dadurch werden die folgenden Komponenten Lifecycle Methoden auslösen shouldComponentUpdate> componentWillUpdate> render>componentDidUpdate
  • Änderungen an Komponenten props. Dies wird Trigger componentWillReceiveProps> shouldComponentUpdate> componentWillUpdate> render> componentDidUpdate( connectMethode der react-reduxTrigger dies , wenn es anwendbar Änderungen in dem Redux - Shop)
  • Aufruf, this.forceUpdateder ähnlich ist wiethis.setState

Sie können das Rendern Ihrer Komponente minimieren, indem Sie eine Prüfung in Ihrer Komponente implementieren shouldComponentUpdateund diese zurückgeben, falsewenn dies nicht erforderlich ist.

Eine andere Möglichkeit ist die Verwendung von React.PureComponent oder zustandslosen Komponenten. Reine und zustandslose Komponenten werden nur dann neu gerendert, wenn Änderungen an den Requisiten vorgenommen wurden.

jpdelatorre
quelle
6
Nitpick: "zustandslos" bedeutet nur jede Komponente, die keinen Status verwendet, unabhängig davon, ob sie mit Klassensyntax oder funktionaler Syntax definiert ist. Außerdem werden Funktionskomponenten immer wieder neu gerendert. Sie müssen entweder verwenden shouldComponentUpdateoder erweitern React.PureComponent, um nur das erneute Rendern bei Änderungen zu erzwingen.
markerikson
1
Sie haben Recht mit der zustandslosen / funktionalen Komponente, die immer wieder neu gerendert wird. Wird meine Antwort aktualisieren.
jpdelatorre
Können Sie klären, wann und warum Funktionskomponenten immer wieder neu gerendert werden? Ich verwende eine ganze Reihe von Funktionskomponenten in meiner App.
Jason
Selbst wenn Sie die funktionale Methode zum Erstellen Ihrer Komponente verwenden, z. B. const MyComponent = (props) => <h1>Hello {props.name}</h1>;(das ist eine zustandslose Komponente). Es wird immer wieder neu gerendert, wenn die übergeordnete Komponente erneut gerendert wird.
jpdelatorre
2
Dies ist mit Sicherheit eine gute Antwort, aber sie beantwortet nicht die eigentliche Frage: - Verfolgen, was ein erneutes Rendern ausgelöst hat. Die Antwort von Jacob R sieht vielversprechend aus, wenn es darum geht, die Antwort auf ein echtes Problem zu geben.
Sanuj
10

Die Antwort von @ jpdelatorre ist großartig, um allgemeine Gründe hervorzuheben, warum eine React-Komponente möglicherweise neu gerendert wird.

Ich wollte nur etwas tiefer in eine Instanz eintauchen: Wenn sich die Requisiten ändern . Die Fehlerbehebung, die dazu führt, dass eine React-Komponente erneut gerendert wird, ist ein häufiges Problem. Nach meiner Erfahrung müssen Sie häufig feststellen, welche Requisiten geändert werden, um dieses Problem aufzuspüren .

Reagieren Sie darauf, dass Komponenten neu gerendert werden, wenn sie neue Requisiten erhalten. Sie können neue Requisiten erhalten wie:

<MyComponent prop1={currentPosition} prop2={myVariable} />

oder wenn MyComponentmit einem Redux-Speicher verbunden ist:

function mapStateToProps (state) {
  return {
    prop3: state.data.get('savedName'),
    prop4: state.data.get('userCount')
  }
}

Immer wenn der Wert prop1, prop2, prop3, oder prop4Änderungen MyComponentwerden wieder machen. Mit 4 Requisiten ist es nicht allzu schwierig herauszufinden, welche Requisiten sich ändern, indem Sie console.log(this.props)an diesem Anfang des renderBlocks ein setzen. Bei immer komplizierteren Komponenten und immer mehr Requisiten ist diese Methode jedoch unhaltbar.

Hier ist ein nützlicher Ansatz ( zur Vereinfachung die Verwendung von lodash ), um zu bestimmen, welche Requisitenänderungen dazu führen, dass eine Komponente erneut gerendert wird:

componentWillReceiveProps (nextProps) {
  const changedProps = _.reduce(this.props, function (result, value, key) {
    return _.isEqual(value, nextProps[key])
      ? result
      : result.concat(key)
  }, [])
  console.log('changedProps: ', changedProps)
}

Das Hinzufügen dieses Snippets zu Ihrer Komponente kann dazu beitragen, den Schuldigen aufzudecken, der fragwürdige Wiederholungen verursacht, und dies hilft häufig dabei, unnötige Daten zu ermitteln, die in Komponenten geleitet werden.

Cumulo Nimbus
quelle
3
Es heißt jetzt UNSAFE_componentWillReceiveProps(nextProps)und ist veraltet. "Dieser Lebenszyklus wurde zuvor benannt componentWillReceiveProps. Dieser Name funktioniert bis Version 17 weiter." Aus der React-Dokumentation .
Emile Bergeron
1
Dasselbe können Sie mit componentDidUpdate erreichen, was wahrscheinlich ohnehin besser ist, da Sie nur herausfinden möchten, warum eine Komponente tatsächlich aktualisiert wurde.
Siehe schärfer
5

Seltsamerweise hat niemand diese Antwort gegeben, aber ich finde sie sehr nützlich, zumal die Änderungen der Requisiten fast immer tief verschachtelt sind.

Haken Fanboys:

import deep_diff from "deep-diff";
const withPropsChecker = WrappedComponent => {
  return props => {
    const prevProps = useRef(props);
    useEffect(() => {
      const diff = deep_diff.diff(prevProps.current, props);
      if (diff) {
        console.log(diff);
      }
      prevProps.current = props;
    });
    return <WrappedComponent {...props} />;
  };
};

"Old" -Schule-Fanboys:

import deep_diff from "deep-diff";
componentDidUpdate(prevProps, prevState) {
      const diff = deep_diff.diff(prevProps, this.props);
      if (diff) {
        console.log(diff);
      }
}

PS Ich bevorzuge immer noch die Verwendung von HOC (Komponente höherer Ordnung), da Sie manchmal Ihre Requisiten oben zerstört haben und Jacobs Lösung nicht gut passt

Haftungsausschluss: Keine Zugehörigkeit zum Paketinhaber. Nur zehnmal herumzuklicken, um den Unterschied in tief verschachtelten Objekten zu erkennen, ist ein Problem.

ZenVentzi
quelle
4

Es gibt jetzt einen Haken dafür auf npm:

https://www.npmjs.com/package/use-trace-update

(Offenlegung, ich habe es veröffentlicht) Update: Entwickelt basierend auf Jacob Rask's Code

Damian Green
quelle
14
Dies ist praktisch derselbe Code, den Jacob gepostet hat. Hätte ihn dort gutschreiben können.
Christian Ivicevic
2

Bei Verwendung von Haken und Funktionskomponenten kann nicht nur der Propellerwechsel zu einem erneuten Rendern führen. Was ich zu verwenden begann, ist ein eher manuelles Protokoll. Ich habe mir sehr geholfen. Vielleicht finden Sie es auch nützlich.

Ich füge diesen Teil in die Datei der Komponente ein:

const keys = {};
const checkDep = (map, key, ref, extra) => {
  if (keys[key] === undefined) {
    keys[key] = {key: key};
    return;
  }
  const stored = map.current.get(keys[key]);

  if (stored === undefined) {
    map.current.set(keys[key], ref);
  } else if (ref !== stored) {
    console.log(
      'Ref ' + keys[key].key + ' changed',
      extra ?? '',
      JSON.stringify({stored}).substring(0, 45),
      JSON.stringify({now: ref}).substring(0, 45),
    );
    map.current.set(keys[key], ref);
  }
};

Zu Beginn der Methode behalte ich eine WeakMap-Referenz:

const refs = useRef(new WeakMap());

Dann schreibe ich nach jedem "verdächtigen" Anruf (Requisiten, Haken):

const example = useExampleHook();
checkDep(refs, 'example ', example);
Miklos Jakab
quelle
1

Die obigen Antworten sind sehr hilfreich, nur für den Fall, dass jemand nach einer spezifischen Methode sucht, um die Ursache für das erneute Rendern zu erkennen, fand ich diesen Bibliotheksredux-Logger sehr hilfreich.

Was Sie tun können, ist die Bibliothek hinzuzufügen und die Unterscheidung zwischen den Status (sie befindet sich in den Dokumenten) wie folgt zu aktivieren:

const logger = createLogger({
    diff: true,
});

Und fügen Sie die Middleware im Laden hinzu.

Fügen Sie dann eine console.log()in die Renderfunktion der Komponente ein, die Sie testen möchten.

Dann können Sie Ihre App ausführen und nach Konsolenprotokollen suchen. Wo immer es ein Protokoll gibt, kurz bevor es Ihnen den Unterschied zwischen dem Status anzeigt (nextProps and this.props), können Sie entscheiden, ob dort wirklich Rendering benötigt wirdGeben Sie hier die Bildbeschreibung ein

Es ähnelt dem obigen Bild zusammen mit der Diff-Taste.

pritesh
quelle