Tracez pourquoi un composant React est re-rendu

156

Existe-t-il une approche systématique pour déboguer ce qui provoque le re-rendu d'un composant dans React? J'ai mis un simple console.log () pour voir combien de fois il est rendu, mais j'ai du mal à comprendre ce qui cause le rendu du composant plusieurs fois, c'est-à-dire (4 fois) dans mon cas. Existe-t-il un outil qui affiche une chronologie et / ou tous les rendus et l'ordre de l'arborescence des composants?

Jason
la source
Vous pourriez peut-être utiliser shouldComponentUpdatepour désactiver la mise à jour automatique des composants, puis démarrer votre trace à partir de là. Plus d'informations peuvent être trouvées ici: facebook.github.io/react/docs/optimizing-performance.html
Reza Sadr
La réponse de @jpdelatorre est correcte. En général, l'un des points forts de React est que vous pouvez facilement retracer le flux de données dans la chaîne en regardant le code. L' extension React DevTools peut vous aider. De plus, j'ai une liste d' outils utiles pour visualiser / suivre le rendu des composants React dans le cadre de mon catalogue d'addons Redux , et un certain nombre d'articles sur [React performance monitoring] (htt
markerikson

Réponses:

254

Si vous voulez un court extrait sans aucune dépendance externe, je trouve cela utile

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`)
    );
  }
}

Voici un petit crochet que j'utilise pour suivre les mises à jour des composants de fonction

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
la source
5
@ yarden.refaeli Je ne vois aucune raison d'avoir un bloc if. Court et concis.
Isaac
Parallèlement à cela, si vous trouvez qu'un élément d'état est en cours de mise à jour et qu'il n'est pas évident de savoir où ni pourquoi, vous pouvez remplacer la setStateméthode (dans un composant de classe) avec setState(...args) { super.setState(...args) }, puis définir un point d'arrêt dans votre débogueur que vous pourrez alors pour remonter à la fonction définissant l'état.
redbmk
Comment utiliser exactement la fonction hook? Où suis-je censé appeler exactement useTraceUpdateaprès l'avoir défini tel que vous l'avez écrit?
damon
Dans un composant de fonction, vous pouvez l'utiliser comme ceci function MyComponent(props) { useTraceUpdate(props); }et il sera journalisé chaque fois que les accessoires changent
Jacob Rask
1
@DawsonB vous n'avez probablement aucun état dans ce composant, il this.staten'est donc pas défini.
Jacob Rask
67

Voici quelques exemples de rendu d'un composant React.

  • Rerender du composant parent
  • Appel this.setState()dans le composant. Cela déclenchera les méthodes du cycle de vie des composants suivants shouldComponentUpdate> componentWillUpdate> render>componentDidUpdate
  • Changements dans les composants props. Ce déclencheur de volonté componentWillReceiveProps> shouldComponentUpdate> componentWillUpdate> render> componentDidUpdate( connectméthode de react-reduxdéclenchement quand il y a des changements applicables dans le magasin Redux)
  • appel this.forceUpdatequi est similaire àthis.setState

Vous pouvez minimiser le rendu de votre composant en implémentant une vérification dans votre shouldComponentUpdateet en retournant falsesi ce n'est pas nécessaire.

Une autre façon consiste à utiliser React.PureComponent des composants ou sans état. Les composants purs et sans état ne sont restitués que lorsque des modifications sont apportées à leurs accessoires.

jpdelatorre
la source
6
Nitpick: "sans état" signifie simplement tout composant qui n'utilise pas l'état, qu'il soit défini avec la syntaxe de classe ou la syntaxe fonctionnelle. De plus, les composants fonctionnels sont toujours rendus. Vous devez utiliser shouldComponentUpdateou étendre React.PureComponentpour appliquer uniquement le re-rendu en cas de modification.
markerikson
1
Vous avez raison sur le composant sans état / fonctionnel qui est toujours restitué. Mettra à jour ma réponse.
jpdelatorre
pouvez-vous clarifier, quand et pourquoi les composants fonctionnels sont toujours restitués? J'utilise pas mal de composants fonctionnels dans mon application.
jasan
Donc, même si vous utilisez la manière fonctionnelle de créer votre composant par exemple const MyComponent = (props) => <h1>Hello {props.name}</h1>;(c'est un composant sans état). Il sera de nouveau rendu à chaque fois que le composant parent sera de nouveau rendu.
jpdelatorre
2
C'est une excellente réponse à coup sûr, mais elle ne répond pas à la vraie question, - Comment retracer ce qui a déclenché un nouveau rendu. La réponse de Jacob R semble prometteuse en donnant la réponse à un problème réel.
Sanuj
10

La réponse de @ jpdelatorre est excellente pour mettre en évidence les raisons générales pour lesquelles un composant React pourrait être rendu à nouveau.

Je voulais juste me plonger un peu plus dans un cas: quand les accessoires changent . Le dépannage de ce qui provoque le rendu d'un composant React est un problème courant, et d'après mon expérience, la plupart du temps, le suivi de ce problème implique de déterminer quels accessoires changent .

Les composants React sont rendus à nouveau chaque fois qu'ils reçoivent de nouveaux accessoires. Ils peuvent recevoir de nouveaux accessoires comme:

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

ou si MyComponentest connecté à un magasin redux:

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

Chaque fois que la valeur de prop1, prop2, prop3ou les prop4changements MyComponentseront réengendrer. Avec 4 accessoires, il n'est pas trop difficile de savoir quels accessoires changent en plaçant un console.log(this.props)à ce début du renderbloc. Cependant, avec des composants plus compliqués et de plus en plus d'accessoires, cette méthode est intenable.

Voici une approche utile (en utilisant lodash pour plus de commodité) pour déterminer quels changements d'accessoires entraînent le nouveau rendu d'un composant:

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

L'ajout de cet extrait de code à votre composant peut aider à révéler le coupable à l'origine de ré-rendus douteux, et cela permet souvent de faire la lumière sur les données inutiles acheminées vers les composants.

Cumulo Nimbus
la source
3
Il est maintenant appelé UNSAFE_componentWillReceiveProps(nextProps)et obsolète. "Ce cycle de vie a été précédemment nommé componentWillReceiveProps. Ce nom continuera à fonctionner jusqu'à la version 17." À partir de la documentation React .
Emile Bergeron
1
Vous pouvez obtenir la même chose avec componentDidUpdate, ce qui est sans doute mieux de toute façon, car vous souhaitez uniquement savoir ce qui a provoqué la mise à jour d'un composant.
voir plus net
5

Etrange personne n'a donné cette réponse mais je la trouve très utile, d'autant plus que les changements d'accessoires sont presque toujours profondément imbriqués.

Crochets 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} />;
  };
};

"Vieux" fanboys d'école:

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

PS Je préfère toujours utiliser HOC (composant d'ordre supérieur) car parfois vous avez déstructuré vos accessoires en haut et la solution de Jacob ne convient pas bien

Clause de non-responsabilité: aucune affiliation avec le propriétaire du package. Il suffit de cliquer des dizaines de fois pour essayer de repérer la différence entre les objets profondément imbriqués.

ZenVentzi
la source
4

Il y a maintenant un crochet pour cela disponible sur npm:

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

(Divulgation, je l'ai publié) Mise à jour: Développé sur la base du code de Jacob Rask

Damian Green
la source
14
C'est pratiquement le même code que Jacob a publié. J'aurais pu le créditer là-bas.
Christian Ivicevic
2

L'utilisation de crochets et de composants fonctionnels, et pas seulement d'un changement d'accessoire, peut provoquer un réacheminement. Ce que j'ai commencé à utiliser, c'est un journal plutôt manuel. J'ai beaucoup aidé. Vous pourriez aussi le trouver utile.

Je colle cette partie dans le fichier du composant:

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);
  }
};

Au début de la méthode je garde une référence WeakMap:

const refs = useRef(new WeakMap());

Puis après chaque appel "suspect" (accessoires, hooks) j'écris:

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

Les réponses ci-dessus sont très utiles, juste au cas où quelqu'un chercherait une méthode spécifique pour détecter la cause du rerender, alors j'ai trouvé cette bibliothèque redux-logger très utile.

Ce que vous pouvez faire est d'ajouter la bibliothèque et d'activer la différence entre les états (il est là dans la documentation) comme:

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

Et ajoutez le middleware dans le magasin.

Mettez ensuite un console.log()dans la fonction de rendu du composant que vous souhaitez tester.

Ensuite, vous pouvez exécuter votre application et vérifier les journaux de la console, partout où il y a un journal juste avant, il vous montrera la différence entre l'état (nextProps and this.props)et vous pourrez décider si le rendu est vraiment nécessaire là-basentrez la description de l'image ici

Il sera similaire à l'image ci-dessus avec la touche diff.

pritesh
la source