51 votes

Composants erronés rendus par Preact

J'utilise Preact (à toutes fins utiles, React) pour rendre une liste d'éléments, sauvegardés dans un tableau d'état. Chaque élément a un bouton "remove" à côté de lui. Mon problème est le suivant : lorsque le bouton est cliqué, l'élément approprié est supprimé (je l'ai vérifié plusieurs fois), mais les éléments sont rendus à nouveau avec l'attribut dernier manquant, et celui qui a été enlevé est toujours là. Mon code (simplifié) :

import { h, Component } from 'preact';
import Package from './package';

export default class Packages extends Component {
  constructor(props) {
    super(props);
    let packages = [
      'a',
      'b',
      'c',
      'd',
      'e'
    ];
    this.setState({packages: packages});
  }

  render () {
    let packages = this.state.packages.map((tracking, i) => {
      return (
        <div className="package" key={i}>
          <button onClick={this.removePackage.bind(this, tracking)}>X</button>
          <Package tracking={tracking} />
        </div>
      );
    });
    return(
      <div>
        <div className="title">Packages</div>
        <div className="packages">{packages}</div>
      </div>
    );
  }

  removePackage(tracking) {
    this.setState({packages: this.state.packages.filter(e => e !== tracking)});
  }
}

Qu'est-ce qui ne va pas ? Dois-je procéder à un re-rendu actif d'une manière ou d'une autre ? S'agit-il d'un cas n+1 d'une manière ou d'une autre ?

Clarification : Mon problème n'est pas la synchronicité de l'État. Dans la liste ci-dessus, si je choisis de supprimer "c", l'état est correctement mis à jour et devient ['a','b','d','e'] mais les composants rendus sont ['a','b','c','d'] . A chaque appel à removePackage la bonne est retirée du tableau, l'état correct est affiché, mais une liste erronée est rendue. (J'ai supprimé le console.log afin de ne pas donner l'impression qu'il s'agit de mon problème).

114voto

Jason Miller Points 36

Il s'agit d'un problème classique qui n'est absolument pas pris en compte dans la documentation de Preact, et je tiens donc à m'en excuser personnellement ! Nous sommes toujours à la recherche d'aide pour écrire une meilleure documentation si quelqu'un est intéressé.

Ce qui s'est passé ici, c'est que vous utilisez l'index de votre tableau comme clé (dans votre carte au sein du rendu). Cela ne fait qu'émuler le fonctionnement par défaut d'un diff VDOM - les clés sont toujours 0-n donde n est la longueur du tableau, de sorte que l'élimination d'un élément ne fait que supprimer la dernière clé de la liste.

Explication : Les clés transcendent les rendus

Dans votre exemple, imaginez à quoi ressemblera le DOM (virtuel) lors du rendu initial, puis après avoir supprimé l'élément "b" (index 3). Ci-dessous, imaginons que votre liste ne contienne que 3 éléments ( ['a', 'b', 'c'] ) :

Voici ce que produit le rendu initial :

<div>
  <div className="title">Packages</div>
  <div className="packages">
    <div className="package" key={0}>
      <button>X</button>
      <Package tracking="a" />
    </div>
    <div className="package" key={1}>
      <button>X</button>
      <Package tracking="b" />
    </div>
    <div className="package" key={2}>
      <button>X</button>
      <Package tracking="c" />
    </div>
  </div>
</div>

Maintenant, lorsque nous cliquons sur "X" sur le deuxième élément de la liste, "b" est transmis à removePackage() qui définit state.packages a ['a', 'c'] . Cela déclenche notre rendu, qui produit le DOM (virtuel) suivant :

<div>
  <div className="title">Packages</div>
  <div className="packages">
    <div className="package" key={0}>
      <button>X</button>
      <Package tracking="a" />
    </div>
    <div className="package" key={1}>
      <button>X</button>
      <Package tracking="c" />
    </div>
  </div>
</div>

Étant donné que la bibliothèque VDOM ne connaît que la nouvelle structure que vous lui donnez à chaque rendu (et non la manière de passer de l'ancienne structure à la nouvelle), ce que les clés ont fait, c'est en fait lui dire que les éléments 0 y 1 est resté en place - nous savons que c'est incorrect, car nous voulions que l'élément à l'index 1 à supprimer.

Rappelez-vous : key a la priorité sur la sémantique de réordonnancement des diff enfants par défaut. Dans cet exemple, parce que key est toujours l'index du tableau basé sur 0, le dernier élément ( key=2 ) est simplement abandonné parce que c'est celui qui manque dans le rendu suivant.

Le correctif

Ainsi, pour corriger votre exemple, vous devriez utiliser quelque chose qui identifie le article plutôt que son compensation comme clé. Il peut s'agir de l'élément lui-même (n'importe quelle valeur est acceptable comme clé), ou d'un élément .id (préférée parce qu'elle évite de disperser les références d'objets, ce qui peut empêcher le GC) :

let packages = this.state.packages.map((tracking, i) => {
  return (
                                  // ↙️ a better key fixes it :)
    <div className="package" key={tracking}>
      <button onClick={this.removePackage.bind(this, tracking)}>X</button>
      <Package tracking={tracking} />
    </div>
  );
});

Ouf, c'était beaucoup plus long que ce que j'avais prévu.

TL,DR : ne jamais utiliser un index de tableau (index d'itération) comme key . Au mieux, il imite le comportement par défaut (réorganisation descendante des enfants), mais le plus souvent, il repousse toutes les différences sur le dernier enfant.


éditer : @tommy recommandé cet excellent lien vers la documentation de eslint-plugin-react qui l'explique mieux que je ne l'ai fait ci-dessus.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X