Leçon 11 / 15

Calculer les filtres et le récapitulatif

20 min React — données, effets et formulaires Gratuit

Objectif : Dériver les dépenses affichées et le récapitulatif mensuel à partir des dépenses et des filtres, sans dupliquer l’état.

Une liste de dépenses peut être filtrée par mois et par catégorie, tandis que le récapitulatif doit présenter les chiffres correspondant au mois choisi. Si vous stockez ces résultats séparément, ils peuvent diverger des données d’origine après un ajout ou un changement de filtre. Vous allez les calculer à partir des dépenses et des filtres déjà détenus par `App`.

Le modèle `Depense` et le flux des données par props ont été établis dans les leçons précédentes. Ici, l’état des filtres est détenu par `App`, comme l’état des dépenses : les composants de filtres signalent les changements au parent, qui transmet ensuite les valeurs aux composants concernés.

Conserver les critères dans App

Les critères sont de simples chaînes : le mois suit le format `AAAA-MM` d’un champ `type="month"`, et la catégorie correspond à une valeur de catégorie ou à une chaîne vide pour désactiver ce critère. Conservez-les dans `App` avec `useState`. Cette leçon suppose que vos dépenses sont déjà chargées et que votre formulaire d’ajout transmet une dépense à `onAjouter`, conformément à l’état du projet.

Texte
import { useState } from "react";
import type { ChangeEvent } from "react";
import type { Depense } from "./types";
import { Entete } from "./composants/Entete";
import { FormulaireDepense } from "./composants/FormulaireDepense";
import { FiltresDepenses } from "./composants/FiltresDepenses";
import { ListeDepenses } from "./composants/ListeDepenses";
import { RecapitulatifMensuel } from "./composants/RecapitulatifMensuel";

export default function App() {
  const [depenses, setDepenses] = useState<Depense[]>([]);
  const [mois, setMois] = useState("");
  const [categorie, setCategorie] = useState("");

  function modifierMois(event: ChangeEvent<HTMLInputElement>) {
    setMois(event.currentTarget.value);
  }

  function modifierCategorie(event: ChangeEvent<HTMLSelectElement>) {
    setCategorie(event.currentTarget.value);
  }

  function ajouterDepense(nouvelleDepense: Depense) {
    setDepenses((precedentes) => [...precedentes, nouvelleDepense]);
  }

  const depensesFiltrees = depenses.filter((depense) => {
    const correspondAuMois = mois === "" || depense.date.startsWith(mois);
    const correspondALaCategorie =
      categorie === "" || depense.categorie === categorie;

    return correspondAuMois && correspondALaCategorie;
  });

  const depensesDuMois = mois === ""
    ? depenses
    : depenses.filter((depense) => depense.date.startsWith(mois));

  const totalMensuel = depensesDuMois.reduce(
    (total, depense) => total + depense.montant,
    0,
  );

  return (
    <div className="depenses">
      <Entete />
      <main className="depenses__contenu">
        <section className="depenses__panneau" aria-labelledby="titre-saisie">
          <h2 className="depenses__titre" id="titre-saisie">Ajouter une dépense</h2>
          <FormulaireDepense onAjouter={ajouterDepense} />
        </section>
        <section className="depenses__panneau" aria-labelledby="titre-filtres">
          <h2 className="depenses__titre" id="titre-filtres">Filtres</h2>
          <FiltresDepenses
            mois={mois}
            categorie={categorie}
            onMoisChange={modifierMois}
            onCategorieChange={modifierCategorie}
          />
        </section>
        <section className="depenses__panneau" aria-labelledby="titre-liste">
          <h2 className="depenses__titre" id="titre-liste">Dépenses</h2>
          <ListeDepenses depenses={depensesFiltrees} />
        </section>
        <section className="depenses__panneau" aria-labelledby="titre-recapitulatif">
          <h2 className="depenses__titre" id="titre-recapitulatif">Récapitulatif mensuel</h2>
          <RecapitulatifMensuel
            mois={mois}
            total={totalMensuel}
            nombre={depensesDuMois.length}
          />
        </section>
      </main>
    </div>
  );
}

Ce fichier montre le calcul et le branchement des valeurs dérivées ; conservez dans votre fichier réel le chargement asynchrone et les états de chargement et d’erreur déjà mis en place. Vérifiez également les noms des props existantes : gardez ceux déjà définis dans vos composants et adaptez uniquement les appels présentés si votre contrat local emploie d’autres noms. Les sélecteurs doivent être contrôlés par `mois` et `categorie`, et leurs événements doivent appeler les deux fonctions de mise à jour.

Dériver la liste filtrée

La constante `depensesFiltrees` est recalculée pendant le rendu à partir de `depenses`, `mois` et `categorie`. Une date au format ISO, par exemple `2025-03-08`, commence par `2025-03` : `startsWith` suffit donc pour comparer son année et son mois. Les deux critères sont combinés avec `&&`, ce qui signifie qu’une dépense apparaît si elle correspond aux deux critères actifs.

Quand `mois` ou `categorie` est vide, le test correspondant est vrai pour toutes les dépenses. Si les deux valeurs sont vides, la liste affichée contient donc toutes les dépenses. Le résultat est passé à `ListeDepenses` sans modifier la liste canonique détenue par `App`.

Calculer le récapitulatif du mois

Le récapitulatif mensuel utilise `depensesDuMois`, qui filtre uniquement sur le mois sélectionné. Il ne reprend pas le filtre de catégorie : le total représente les dépenses du mois entier, même si la liste est réduite à une catégorie. Si aucun mois n’est sélectionné, le code considère toutes les dépenses comme période de référence ; vous pouvez afficher cette situation dans le composant de récapitulatif en présentant « Tous les mois ».

`reduce` additionne les montants et utilise `0` comme valeur initiale : le total reste donc défini même si le mois ne contient aucune dépense. Le nombre du récapitulatif provient de la longueur de la même liste mensuelle. Le total et le nombre évoluent automatiquement après un ajout ou un changement de mois, sans état React séparé.

À retenir

  • Conservez les dépenses et les critères de filtre dans `App` ; transmettez les valeurs et les rappels aux composants concernés.
  • Calculez la liste affichée avec `filter`, en combinant les critères de mois et de catégorie.
  • Calculez le récapitulatif à partir des dépenses du mois, indépendamment du filtre de catégorie.
  • Utilisez `reduce` pour le total et la longueur de la liste filtrée pour le nombre ; ne stockez pas ces résultats dans un état séparé.

Dans la prochaine leçon, vous extrairez le chargement des dépenses dans un hook personnalisé afin de réutiliser cette logique sans déplacer les responsabilités des composants.

Envie d'aller plus loin ?

Les cours Enovatic sont gratuits et le resteront. Si vous cherchez un accompagnement encadré, nos formations professionnelles sont animées par les mêmes formateurs.