Aller au contenu

Penser en React

Introduction

Pour bien débuter une application en React, il est recommandé de débuter par une maquette froide de l'application. Cela permet de visualiser l'ensemble des composants nécessaires à la réalisation de l'application.

Mockup

Mockup d'une application simple

Étape 1 : Découper en composants

Pour découper l'application en composants, il est recommandé de suivre les étapes suivantes :

  1. Identifier les composants réutilisables
  2. Découper les composants en sous-composants
  3. Définir la hiérarchie des composants

Mockup

Composants d'une application simple
  1. ProduitTableFiltrable (bleu) contient l'application au complet.
  2. BarreRecherche (vert) reçoit l'entrée de l'utilisateur.
  3. ProduitTable (jaune) affiche et filtre la liste selon l'entrée de l'utilisateur.
  4. ProduitCategorieRangee (rose) affiche l'entête par catégorie.
  5. ProduitRangee (brun) affiche une rangée pour un produit.

Ces composants peuvent être représentés en une hiérarchie de composants :

ProduitTableFiltrable
  BarreRecherche
  ProduitTable
    ProduitCategorieRangee
    ProduitRangee

Étape 2 : Construire une version statique de l'application

Passage de données par props

Les données sont passées de haut en bas dans React. Cela signifie que les données sont passées du composant parent au composant enfant par le biais de props.

Commencez pas le composant de produit le plus bas de la hiérarchie, ProduitRangee. Ce composant reçoit des données sous forme de props et les affiche.

ProduitRangee.tsx
import { Produit } from '../models/Produit.model';

interface ProduitRangeeProps {
  produit: Produit;
}

export default function ProduitRangee(props: ProduitRangeeProps) {
  const nom = props.produit.en_inventaire ? (
    props.produit.nom
  ) : (
    <span style={{ color: 'red' }}>{props.produit.nom}</span>
  );

  return (
    <tr>
      <td>{nom}</td>
      <td>{props.produit.prix}</td>
    </tr>
  );
}

Ensuite, construisez le composant ProduitCategorieRangee qui reçoit la catégorie sous forme de props et les affiche.

ProduitCategorieRangee.tsx
interface ProduitCategorieRangeeProps {
  categorie: string;
}

export default function ProduitCategorieRangee(
  props: ProduitCategorieRangeeProps
) {
  return (
    <tr>
      <th colSpan={2}>{props.categorie}</th>
    </tr>
  );
}

Enfin, construisez le composant ProduitTable qui reçoit les données sous forme de props et les affiche.

ProduitTable.tsx
import { Produit } from '../models/Produit.model';
import ProduitCategorieRangee from './ProduitCategorieRangee';
import ProduitRangee from './ProduitRangee';

interface ProduitTableProps {
  produits: Produit[];
}

export default function ProduitTable(props: ProduitTableProps) {
  const produits = props.produits;
  const rangees: JSX.Element[] = [];
  let derniereCategorie: string | null = null;

  produits.forEach((produit) => {
    if (produit.categorie !== derniereCategorie) {
      rangees.push(
        <ProduitCategorieRangee
          categorie={produit.categorie}
          key={produit.categorie}
        />
      );
    }
    rangees.push(<ProduitRangee produit={produit} key={produit.nom} />);
    derniereCategorie = produit.categorie;
  });

  return (
    <table>
      <thead>
        <tr>
          <th>Nom</th>
          <th>Prix</th>
        </tr>
      </thead>
      <tbody>{rangees}</tbody>
    </table>
  );
}

Étape 3 : Identifier l'état minimal de l'application

Il faut maintenant identifier ce qui doit être conservé dans un état (state) VS ce qui est statique ou dérivé d'un état.

Quelles sont les données de notre application?

  1. La liste des produits originale
  2. La catégorie des produits
  3. Le texte de recherche
  4. L'état de la case à cocher
  5. Les produits filtrés

Quelles sont les données qui changent au cours du temps?

  1. Le texte de recherche
  2. L'état de la case à cocher
  3. Les produits filtrés

Quelles sont les données qui peuvent être dérivées?

  1. Les produits filtrés

Donc, quelles sont les états minimaux de notre application?

  1. Le texte de recherche
  2. L'état de la case à cocher

Étape 4 : Identifier où l'état doit vivre

Le principe de levée de l'état

L'état doit être levé au composant parent le plus proche qui a besoin d'y accéder.

Dans notre cas, le texte de recherche et l'état de la case à cocher seront utilisés par deux composants, BarreRecherche et ProduitTable. Donc, l'état doit être levé au composant parent commun, ProduitTableFiltrable.

ProduitTableFiltrable.tsx
import { Produit } from '../models/Produit.model';
import BarreRecherche from './BarreRecherche';
import ProduitTable from './ProduitTable';
import { useState } from 'react';

interface ProduitTableFiltrableProps {
  produits: Produit[];
}

export default function ProduitTableFiltrable(
  props: ProduitTableFiltrableProps
) {
  const [filtreTexte, setFiltreTexte] = useState('');
  const [filtreInventaireSeulement, setFiltreInventaireSeulement] =
    useState(false);

  const gererFiltreTexteChange = (valeur: string) => {
    setFiltreTexte(valeur);
  };

  const gererFiltreInventaireSeulementChange = (valeur: boolean) => {
    setFiltreInventaireSeulement(valeur);
  };

  return (
    <div>
      <BarreRecherche
        texte={filtreTexte}
        seulementEnInventaire={filtreInventaireSeulement}
        onTexteChange={gererFiltreTexteChange}
        onSeulementEnInventaireChange={gererFiltreInventaireSeulementChange}
      />
      <ProduitTable
        produits={props.produits}
        texte={filtreTexte}
        seulementEnInventaire={filtreInventaireSeulement}
      />
    </div>
  );
}

Étape 5 : Ajouter des interactions

Maintenant que l'état est levé au composant parent commun, nous pouvons ajouter des interactions à notre application.

Passage de données par props

Les fonctions de rappel sont passées de haut en bas dans React. Cela signifie que les fonctions de rappel sont passées du composant parent au composant enfant par le biais de props.

Ajoutez une fonction de rappel pour gérer le changement de texte de recherche et de la case à cocher dans BarreRecherche.

BarreRecherche.tsx
interface BarreRechercheProps {
  texte: string;
  seulementEnInventaire: boolean;
  onTexteChange: (texte: string) => void;
  onSeulementEnInventaireChange: (seulementEnInventaire: boolean) => void;
}

export default function BarreRecherche(props: BarreRechercheProps) {
  function handleTexteChange(e: React.ChangeEvent<HTMLInputElement>) {
    props.onTexteChange(e.target.value);
  }

  function handleSeulementEnInventaireChange(
    e: React.ChangeEvent<HTMLInputElement>
  ) {
    props.onSeulementEnInventaireChange(e.target.checked);
  }

  return (
    <form>
      <input
        type="text"
        placeholder="Recherche..."
        value={props.texte}
        onChange={handleTexteChange}
      />
      <label>
        <input
          type="checkbox"
          checked={props.seulementEnInventaire}
          onChange={handleSeulementEnInventaireChange}
        />{' '}
        Seulement les produits en inventaire
      </label>
    </form>
  );
}
ProduitTable.tsx
import { Produit } from '../models/Produit.model';
import ProduitCategorieRangee from './ProduitCategorieRangee';
import ProduitRangee from './ProduitRangee';

interface ProduitTableProps {
  produits: Produit[];
  texte: string;
  seulementEnInventaire: boolean;
}

export default function ProduitTable(props: ProduitTableProps) {
  const produits = props.produits;
  const rangees: JSX.Element[] = [];
  let derniereCategorie: string | null = null;

  produits.forEach((produit) => {
    if (props.texte !== '' && !produit.nom.includes(props.texte)) {
      return;
    }

    if (props.seulementEnInventaire && !produit.en_inventaire) {
      return;
    }

    if (produit.categorie !== derniereCategorie) {
      rangees.push(
        <ProduitCategorieRangee
          categorie={produit.categorie}
          key={produit.categorie}
        />
      );
    }
    rangees.push(<ProduitRangee produit={produit} key={produit.nom} />);
    derniereCategorie = produit.categorie;
  });

  return (
    <table>
      <thead>
        <tr>
          <th>Nom</th>
          <th>Prix</th>
        </tr>
      </thead>
      <tbody>{rangees}</tbody>
    </table>
  );
}