Leçon 12 / 15

Calculer le total mensuel sans dupliquer l’état

20 min React — composants et état Gratuit

Objectif : Calculer le total du mois sélectionné à partir des dépenses filtrées et l’afficher dans le tableau de bord sans stocker ce total dans un état séparé.

Quand vous changez le mois, le total affiché doit suivre les dépenses de ce mois. Stocker ce total dans un état distinct vous obligerait à le synchroniser chaque fois que les dépenses ou le mois changent.

Vous avez déjà calculé la liste correspondant aux filtres de catégorie et de mois dans la leçon précédente. Vous allez maintenant additionner les montants de cette liste et transmettre le résultat au composant du récapitulatif.

Additionner les dépenses avec reduce

La méthode `reduce` parcourt un tableau et construit une seule valeur à partir de ses éléments. Ici, elle reçoit un accumulateur — le total en cours — et la dépense parcourue ; elle renvoie leur somme. La valeur initiale `0` permet aussi d’obtenir zéro si le tableau ne contient aucune dépense.

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

Dans votre tableau de bord, `depensesFiltrees` contient déjà les dépenses du mois et de la catégorie sélectionnés. Le calcul additionne donc précisément les montants de cette liste. Si aucune dépense ne correspond, le résultat est `0`, sans cas particulier à ajouter.

Afficher le total dans le récapitulatif

Le composant `App` calcule le total à partir de la liste filtrée, puis le transmet à `RecapitulatifMensuel` par une prop. Le composant enfant se limite à présenter cette valeur : il ne recalcule pas le total et ne possède pas de copie de celui-ci dans son propre état.

src/App.tsx — calcul et transmission du totalTexte
import { useState } from "react";
import EnteteDepenses from "./composants/EnteteDepenses";
import FormulaireDepense from "./composants/FormulaireDepense";
import FiltresDepenses from "./composants/FiltresDepenses";
import RecapitulatifMensuel from "./composants/RecapitulatifMensuel";
import ListeDepenses from "./composants/ListeDepenses";

interface Depense {
  id: string;
  description: string;
  montant: number;
  categorie: string;
  date: string;
}

const categories = ["Logement", "Alimentation", "Transport", "Loisirs"];

const depensesInitiales: Depense[] = [
  {
    id: "depense-1",
    description: "Courses",
    montant: 42.5,
    categorie: "Alimentation",
    date: "2025-03-12",
  },
  {
    id: "depense-2",
    description: "Abonnement de transport",
    montant: 35,
    categorie: "Transport",
    date: "2025-03-05",
  },
  {
    id: "depense-3",
    description: "Livre",
    montant: 18,
    categorie: "Loisirs",
    date: "2025-02-20",
  },
];

export default function App() {
  const [depenses, setDepenses] = useState<Depense[]>(depensesInitiales);
  const [categorieSelectionnee, setCategorieSelectionnee] = useState("toutes");
  const [moisSelectionne, setMoisSelectionne] = useState("2025-03");

  const depensesFiltrees = depenses.filter((depense) => {
    const correspondCategorie =
      categorieSelectionnee === "toutes" ||
      depense.categorie === categorieSelectionnee;
    const correspondMois = depense.date.slice(0, 7) === moisSelectionne;
    return correspondCategorie && correspondMois;
  });

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

  return (
    <div className="depenses-app">
      <EnteteDepenses />
      <main className="depenses-principale">
        <FormulaireDepense categories={categories} onAjouter={setDepenses} />
        <RecapitulatifMensuel total={totalMensuel} />
        <FiltresDepenses
          categories={categories}
          categorieSelectionnee={categorieSelectionnee}
          moisSelectionne={moisSelectionne}
          onCategorieChange={setCategorieSelectionnee}
          onMoisChange={setMoisSelectionne}
        />
        <ListeDepenses depenses={depensesFiltrees} />
      </main>
    </div>
  );
}

Ce fichier prolonge le tableau de bord avec les états et les composants utilisés jusque-là ; les autres composants conservent leurs props et leur comportement existants. Le point important est que `totalMensuel` est recalculé pendant le rendu depuis `depensesFiltrees` : ce n’est pas une valeur mémorisée séparément avec `useState`.

src/composants/RecapitulatifMensuel.tsxTexte
interface RecapitulatifMensuelProps {
  total: number;
}

export default function RecapitulatifMensuel({
  total,
}: RecapitulatifMensuelProps) {
  return (
    <section
      className="depenses-recapitulatif"
      aria-label="Récapitulatif mensuel"
    >
      <h2>Total du mois</h2>
      <p className="depenses-recapitulatif__montant">
        {total.toFixed(2)} €
      </p>
    </section>
  );
}

La méthode `toFixed(2)` formate le nombre pour afficher deux décimales, par exemple `77.50 €`. Elle concerne uniquement la présentation : la valeur utilisée dans le calcul demeure un nombre. Aucun style en ligne n’est ajouté ; le montant conserve la classe prévue par le contrat et les règles de présentation restent dans `src/style.css`.

Avec les données d’exemple et le mois `2025-03`, le récapitulatif additionne les dépenses de 42,50 € et 35,00 €. Si vous choisissez une catégorie, seule la dépense correspondante contribue au total, puisque le calcul part de la même liste que celle transmise à l’affichage des dépenses.

À retenir

  • `reduce` additionne les montants et renvoie un seul nombre.
  • L’accumulateur initialisé à `0` donne un résultat correct même si la liste filtrée est vide.
  • Le total est calculé depuis `depensesFiltrees`, la même source que la liste affichée.
  • Transmettez ce total au récapitulatif par props ; ne le dupliquez pas dans un état React.

Dans la prochaine leçon, vous organiserez les composants du tableau de bord selon leurs responsabilités et examinerez les échanges de props entre eux.

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.