En savoir un peu plus sur le cycle de vie d'un composant React

Publié le 03 décembre 2016
#react# react lecon

Aller ! Aujourd'hui, on va avancer notre superbe application

Attention, cet article n'est pas à jour.

Mais tout d'abord, voyons comment réaliser les différentes tâches laissées en suspens lors de la précédente leçon.

Vous pourrez retrouver les sources de la leçon précédente à l'adresse suivante : github lecon_2

1. Rappel de l'organisation du projet

src
|___assets
| |
| |___logo.svg
|
|___components
| |
| |___establishments
| | |
| | |___establishments.js
| | |
| | |___fixtures.js
| |
| |___App.js
|
|___css
| |
| |___App.css
| |
| |___index.css
|
|___App.test.js
|
|___index.js

On n'oublie pas de lancer l'application afin de voir les changements dans notre navigateur.

$ cd HappyDrink
$ npm start

2. Proposition de correction

Rappel de ce que nous voulons faire :

  • créer un bouton like et un bouton dislike pour chacun des établissements
  • incrémenter un compteur de like et de dislike lors d'un clic sur le bouton correspondant
  • afficher le compteur associé à chacun des boutons
  • créer un bouton favori (une icône étoile par exemple) qui a un fond transparent s'il n'est pas en favori et jaune s'il l'est

Le bouton de "like" et de "dislike"

Ici nous allons utiliser deux variables : isLiked et isDisliked. Comme elles vont être amenées à changer dans le temps, nous allons les utiliser avec le state :

// Fichier : ./src/components/establishments/Establishments.js
import React, { Component } from "react";

class Establishment extends Component {
  constructor(props) {
    super(props);
    // On initialise nos deux variables
    this.state = {
      isLiked: false,
      isDisliked: false
    };
  }

  // fonction appelée lors du clic sur l'élément "like"
  like = () => {
    this.setState({
      isLiked: !this.state.isLiked,
      isDisliked: this.state.isDisliked
        ? !this.state.isDisliked
        : this.state.isDisliked
    });
  };

  // fonction appelée lors du clic sur l'élément "dislike"
  dislike = () => {
    this.setState({
      isDisliked: !this.state.isDisliked,
      isLiked: this.state.isLiked ? !this.state.isLiked : this.state.isLiked
    });
  };

  render() {
    // On définit les éléments "upIcon(like) et downIcon(dislike)"
    // on utilise ici Font-Answome (on inclura le fichier css dans ./public.index.html)
    let upIcon = <i className="fa fa-thumbs-up" aria-hidden="true"></i>;
    let downIcon = <i className="fa fa-thumbs-down" aria-hidden="true"></i>;

    // Si l'on n'a pas encore "liké"
    if (!this.state.isLiked)
      upIcon = <i className="fa fa-thumbs-o-up" aria-hidden="true"></i>;

    // Si l'on a pas encore "disliké"
    if (!this.state.isDisliked)
      downIcon = <i className="fa fa-thumbs-o-down" aria-hidden="true"></i>;

    return (
      <div className="establishment">
        <div className="establishment-description">
          <h3>{this.props.establishment.name}</h3>

          {this.props.establishment.description}
        </div>
        <div className="establishmentLikeDislike">
          {/* Au clic sur le bouton on appelle la fonction */}
          <button onClick={this.like}>{upIcon} </button>
          <button onClick={this.dislike}>{downIcon}</button>
        </div>
      </div>
    );
  }
}

export default Establishment;

on met à jour notre css :

/* Fichier : ./src/css/App.css [extrait]*/
/*...*/
.establishment {
  display: flex;
  align-items: center;
  background-color: gray;
  padding: 10px;
  margin: 5px 30px;
  border-radius: 8px;
}
.establishment-description {
  flex: 0.8;
}
.establishmentLikeDislike {
  flex: 0.2;
  display: flex;
  flex-direction: column;
  align-items: center;
}
.establishmentLikeDislike button {
  background: none;
  border: none;
  color: white;
}

Et le fichier index.html afin d'utiliser Font-Answome :

<!-- Fichier : ./public/index.html -->
<!DOCTYPE html>
<html lang="fr">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico" />
    <link
      href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css"
      rel="stylesheet"
      integrity="sha384-wvfXpqpZZVQGK6TAh5PVlGOfQNHSoD2xbE+QkPxCAFlNEevoEH3Sl0sibVcOQVnN"
      crossorigin="anonymous"
    />
    <title>React App</title>
  </head>
  <body>
    <div id="root"></div>
  </body>
</html>

Le compteur

À partir d'ici, il va être facile de réaliser le compteur de "like" et "dislike" :

// Fichier : ./src/components/establishments/Establishments.js [Extrait]
//...
  constructor(props) {
    super(props);

    // On ajoute les deux variables à notre "state"
    this.state = {
      isLiked: false,
      isDisliked: false,
      likeCounter: 0,
      dislikeCounter: 0
    };
  }

  like = () => {
    // On met à jour le compteur à chaque clic sur le composant "like"
    let likeCounter = this.state.likeCounter;
    let dislikeCounter = this.state.dislikeCounter;

    likeCounter += !this.state.isLiked ? 1 : -1;
    dislikeCounter += this.state.isDisliked ? -1 : 0;

    this.setState({
      isLiked: !this.state.isLiked,
      isDisliked: this.state.isDisliked
        ? !this.state.isDisliked
        : this.state.isDisliked,
      likeCounter: likeCounter,
      dislikeCounter: dislikeCounter
    });
  };

  dislike = () => {
    // On met à jour le compteur à chaque clic sur le composant "dislike"
    let likeCounter = this.state.likeCounter;
    let dislikeCounter = this.state.dislikeCounter;

    dislikeCounter += !this.state.isDisliked ? 1 : -1;
    likeCounter += this.state.isLiked ? -1 : 0;

    this.setState({
      isDisliked: !this.state.isDisliked,
      isLiked: this.state.isLiked ? !this.state.isLiked : this.state.isLiked,
      likeCounter: likeCounter,
      dislikeCounter: dislikeCounter
    });
  };

  render() {
    //...

    return (
      <div className="establishment">
        <div className="establishment-description">
          <h3>{this.props.establishment.name}</h3>

          {this.props.establishment.description}
        </div>
        <div className="establishmentLikeDislike">
          {/* On affiche les compteurs */}
          <button onClick={this.like}>{upIcon} </button>{" "}
          <span>{this.state.likeCounter}</span>
          <button onClick={this.dislike}>{downIcon}</button>{" "}
          <span>{this.state.dislikeCounter}</span>
        </div>
      </div>
    );
  }
  //...

Bon pour l'instant, les compteurs ne servent à rien. Mais pour les prochaines leçons nous allons utiliser une API afin de récupérer les "like" et "dislike" des autres utilisateurs. Cela deviendra ainsi de vrais compteurs.

Favori

Nous allons réutiliser une icône Font-Answome et ajouter une variable booléenne à notre state pour faire le boulot :

// Fichier : ./src/components/establishments/Establishments.js [Extrait]
//...
  constructor(props) {
    super(props);

    this.state = {
      isLiked: false,
      isDisliked: false,
      likeCounter: 0,
      dislikeCounter: 0,
      favori: false
    };
  }
  //...
  favori = () => {
    this.setState({
      favori: !this.state.favori
    });
  };
  //...
  render() {
    //...
    let starIcon = <i className="fa fa-star-o" aria-hidden="true"></i>;
    //...
    if (this.state.favori) {
      starIcon = <i className="fa fa-star favoriIcon" aria-hidden="true"></i>;
    }

    return (
      <div className="establishment">
        <div className="establishment-favori">
          <button onClick={this.favori}>{starIcon}</button>
        </div>
        <div className="establishment-description">
          <h3>{this.props.establishment.name}</h3>

          {this.props.establishment.description}
        </div>
        <div className="establishmentLikeDislike">
          <button onClick={this.like}>{upIcon} </button>{" "}
          <span>{this.state.likeCounter}</span>
          <button onClick={this.dislike}>{downIcon}</button>{" "}
          <span>{this.state.dislikeCounter}</span>
        </div>
      </div>
    );
  }

Et on met à jour le css :

/** Fichier : ./src/css/App.css [extrait]**/
/**...**/
.establishment-favori {
  flex: 0.1;
}
.establishment-description {
  flex: 0.7;
}
.establishmentLikeDislike {
  flex: 0.2;
  display: flex;
  flex-direction: column;
  align-items: center;
}
.establishment button {
  background: none;
  border: none;
  color: white;
}

.favoriIcon {
  color: yellow;
}

3. Le cycle de vie d'un composant React

Initialisation

Jetons un coup d'oeil aux fonctions, de notre component, qui sont appelées par React à l'initialisation de celui-ci :

__constructor()
|
|__componentWillMount()
|
|__render()
|
|__componentDidMount()
|
|__componentWillUnmount()

Avec componentWillMount, vous pourrez réaliser des opérations avant que votre component ne soit rendu.

Vous pouvez par exemple initialiser le state (car toute modification du state dans cette fonction n'implique pas le rechargement du component). Cependant React conseille de réaliser cette opération dans le constructeur, comme nous l'avons fait depuis le début.

componentDidMount est quant à elle, appelée après le rendu du component. C'est ici que nous réaliserons les futurs appels API.

Et componentWillUnmount est appelée quand votre component est "démonté" du DOM.

Voici un exemple d'implémentation :

// Fichier : ./src/components/App.js [Extrait]
//...
class App extends Component {
  constructor(props) {
    super(props);

    this.state = {
      pseudo: "Inconnu"
    };
  }

  componentWillMount() {
    console.log("componentWillMount");
  }

  componentDidMount() {
    console.log("componentDidMount");
  }

  render() {
    console.log("render");
    //...
  }
  //...
}
//...

Vous devriez avoir un aperçu dans la console de votre navigateur préféré, similaire à celui-ci :

componentWillMount
render
componentDidMount

Changement d'état

À chaque changement de notre state, React appelle les fonctions suivantes :

|
|__componentWillReceiveProps(nextProps)
|
|__shouldComponentUpdate(nextProps, nextState)
|
|__componentWillUpdate(nextProps, nextState)
|
|__render
|
|__componentDidUpdate(prevProps, prevState)

componentWillReceiveProps(nextProps) est appelée lorsqu'un component, qui est déjà initialisé, reçoit de nouvelles props. Dans cette fonction, on peut comparer les nouvelles props (ici contenues dans l'objet nextProps), avec les anciennes (this.props).

La fonction shouldComponentUpdate(nextProps, nextState) est appelée à chaque changement du state. On utilise cette fonction afin de faire savoir à React si, oui ou non, le component est affecté par le changement en retournant true ou false. Si l'on retourne false, les méthodes componentWillUpdate(), render(), et componentDidUpdate() ne seront pas appelées.

componentWillUpdate(nextProps, nextState) est appelée par React juste avant que les nouvelles props et le nouveau state ne soit rendues.

La fonction componentDidUpdate(prevProps, prevState) est, quant à elle, exécutée après le rendu du nouveau state/ des nouvelles props. On utilisera cette fonction pour réaliser des actions sur le DOM ou des appelles API.

Toutes ces fonctions ne sont pas appelées à l'initialisation du component.

Voyons un petit exemple :

// Fichier : ./src/components/establishments/Establishments.js [Extrait]
//...
class Establishment extends Component {
  //...
  componentWillReceiveProps(nextProps) {
    console.log("componentWillReceiveProps");
    console.log("    this.props : ", this.props);
    console.log("    nextProps  : ", nextProps);
  }

  shouldComponentUpdate(nextProps, nextState) {
    console.log("shouldComponentUpdate");
    console.log("    this.props : ", this.props);
    console.log("    nextProps  : ", nextProps);
    console.log("    this.state : ", this.state);
    console.log("    nextState  : ", nextState);

    return true;
  }

  componentWillUpdate(nextProps, nextState) {
    console.log("componentWillUpdate");
    console.log("    this.props : ", this.props);
    console.log("    nextProps  : ", nextProps);
    console.log("    this.state : ", this.state);
    console.log("    nextState  : ", nextState);
  }

  componentDidUpdate(prevProps, prevState) {
    console.log("componentDidUpdate");
    console.log("    prevProps : ", prevProps);
    console.log("    prevState : ", prevState);
  }
  //...
}
//...

Et l'affichage console correspondant (lors du clic sur l'élément "favori" par exemple):

shouldComponentUpdate
this.props : Object {establishment: Object}
nextProps : Object {establishment: Object}
this.state : Object {isLiked: false, isDisliked: false, likeCounter: 0, dislikeCounter: 0, favori: false}
nextState : Object {isLiked: false, isDisliked: false, likeCounter: 0, dislikeCounter: 0, favori: true}
componentWillUpdate
this.props : Object {establishment: Object}
nextProps : Object {establishment: Object}
this.state : Object {isLiked: false, isDisliked: false, likeCounter: 0, dislikeCounter: 0, favori: false}
nextState : Object {isLiked: false, isDisliked: false, likeCounter: 0, dislikeCounter: 0, favori: true}
componentDidUpdate
prevProps : Object {establishment: Object}
prevState : Object {isLiked: false, isDisliked: false, likeCounter: 0, dislikeCounter: 0, favori: false}

4. Récapitulatif

Regardons un peu l'avancement de notre projet HappyDrink :

  • lister les bars.
  • filtrer la liste.
  • mettre en favori un bar.
  • visualiser l'happy-hour de celui-ci.
  • liker/disliker ce bar.

On a bien avancé cette fois-ci !

Vous pourrez retrouver les sources de cette leçon à l'adresse suivante : github lecon_3

5. Pour la suite

Aller, je vous promets Redux c'est pour bientôt !

En attendant, amusons-nous un peu à utiliser le cycle de vie de notre composant App.

Je vous propose la chose suivante :

  • utiliser Jquery pour faire un appel API :
$ npm install --save jquery

et

const $ = require('jquery');

pour utiliser "$" dans un component

  • faire un appel API sur cette adresse : "https://jsonplaceholder.typicode.com/posts/1"
  • afficher le champ "body" de ce que répond l'API
gkueny

À propos de l'auteur - gkueny @ZEBet

Développeur depuis maintenant 6 ans, j'ai une grande affinité avec le mobile et les tests bien fait. Pas full-stack mais touche à tout, je suis également à l'aise sur du Symfony / php.