Organiser les composants du projet
Objectif : Répartir formulaire, filtres, liste et récapitulatif dans des composants distincts reliés par des props typées.
Quand un tableau de bord mélange saisie, filtres et calculs dans un seul composant, chaque évolution devient difficile à localiser. Vous allez organiser ces rôles dans les fichiers déjà prévus par le projet, sans déplacer les données d’un composant à l’autre ni créer de state en double.
Vous avez déjà construit les composants fonctionnels et utilisé leurs props dans les leçons précédentes. Ici, vous allez vérifier leur répartition et suivre le trajet des données depuis App jusqu’aux composants qui les affichent ou les modifient.
Répartir les responsabilités
App coordonne la page : il conserve le tableau des dépenses et les deux filtres, calcule les données dérivées, puis transmet les valeurs nécessaires. EnteteDepenses affiche le titre ; FormulaireDepense reçoit une fonction pour ajouter une dépense ; FiltresDepenses reçoit et signale les choix ; RecapitulatifMensuel affiche un total ; ListeDepenses reçoit la liste filtrée et la transmet aux lignes.
Cette répartition suit les fichiers imposés par le projet. Les composants enfants ne conservent pas une copie des dépenses ou des filtres : ils reçoivent les valeurs à afficher et les fonctions qui permettent de demander une modification à App.
- src/main.tsx : point d’entrée React, inchangé.
- src/App.tsx : état source, calculs et assemblage de la page.
- src/composants/EnteteDepenses.tsx : titre de la page.
- src/composants/FormulaireDepense.tsx : saisie et demande d’ajout.
- src/composants/FiltresDepenses.tsx : filtres contrôlés.
- src/composants/RecapitulatifMensuel.tsx : montant récapitulatif.
- src/composants/ListeDepenses.tsx et LigneDepense.tsx : affichage des dépenses.
- src/style.css : feuille de styles unique, inchangée.
Faire circuler les données avec des props
Dans App, les valeurs calculées restent locales au parent. La catégorie et le mois sélectionnés servent à produire depensesFiltrees ; le total est calculé à partir des dépenses de ce mois, indépendamment du filtre de catégorie, afin que le récapitulatif reste mensuel.
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";
export type Depense = {
id: number;
description: string;
montant: number;
categorie: string;
date: string;
};
const categories = ["Alimentation", "Transport", "Logement", "Loisirs"];
export default function App() {
const [depenses, setDepenses] = useState<Depense[]>([
{ id: 1, description: "Courses", montant: 42.5, categorie: "Alimentation", date: "2025-05-12" },
]);
const [categorieSelectionnee, setCategorieSelectionnee] = useState("toutes");
const [moisSelectionne, setMoisSelectionne] = useState("2025-05");
function ajouterDepense(nouvelleDepense: Depense) {
setDepenses((depensesActuelles) => [...depensesActuelles, nouvelleDepense]);
}
const depensesFiltrees = depenses.filter((depense) =>
(categorieSelectionnee === "toutes" || depense.categorie === categorieSelectionnee) &&
depense.date.slice(0, 7) === moisSelectionne
);
const totalMensuel = depenses
.filter((depense) => depense.date.slice(0, 7) === moisSelectionne)
.reduce((total, depense) => total + depense.montant, 0);
return (
<div className="depenses-app">
<EnteteDepenses />
<main className="depenses-principale">
<FormulaireDepense categories={categories} onAjouter={ajouterDepense} />
<RecapitulatifMensuel total={totalMensuel} />
<FiltresDepenses
categories={categories}
categorieSelectionnee={categorieSelectionnee}
moisSelectionne={moisSelectionne}
onCategorieChange={setCategorieSelectionnee}
onMoisChange={setMoisSelectionne}
/>
<ListeDepenses depenses={depensesFiltrees} />
</main>
</div>
);
}Le tableau depenses est la source des lignes et des calculs. La fonction ajouterDepense est transmise au formulaire : celui-ci ne modifie pas directement l’état du parent, il lui communique une dépense validée. Les fonctions de changement des filtres suivent le même principe.
Typer les interfaces des composants
Chaque composant déclare les props dont il a besoin. Vous avez déjà étudié les props typées ; les interfaces ci-dessous montrent concrètement le contrat entre App et ses enfants. Le type Depense est exporté depuis App.tsx afin que les composants concernés réutilisent la même structure, sans ajouter de fichier au projet.
export default function EnteteDepenses() {
return (
<header className="depenses-entete">
<h1 className="depenses-entete__titre">Tableau de bord des dépenses</h1>
</header>
);
}type Props = { total: number };
export default function RecapitulatifMensuel({ total }: Props) {
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>
);
}import type { Depense } from "../App";
type Props = { depense: Depense };
export default function LigneDepense({ depense }: Props) {
return (
<li className="depenses-liste__element">
<span>{depense.description}</span>
<span className="depenses-categorie">{depense.categorie}</span>
<span>{depense.date}</span>
<span>{depense.montant.toFixed(2)} €</span>
</li>
);
}import type { Depense } from "../App";
import LigneDepense from "./LigneDepense";
type Props = { depenses: Depense[] };
export default function ListeDepenses({ depenses }: Props) {
return (
<section aria-label="Liste des dépenses">
<h2>Dépenses</h2>
<ul className="depenses-liste">
{depenses.map((depense) => (
<LigneDepense key={depense.id} depense={depense} />
))}
</ul>
{depenses.length === 0 && (
<p className="depenses-message--vide">Aucune dépense pour cette période.</p>
)}
</section>
);
}La clé stable depense.id est posée sur le composant répété, conformément à la leçon sur les listes. La condition d’affichage du message vide se trouve au même endroit que la liste, car c’est ListeDepenses qui connaît le nombre de lignes reçues.
Le formulaire et les filtres gardent également leurs responsabilités, avec des interfaces de props explicites. Le formulaire appelle onAjouter après validation ; les filtres affichent les valeurs transmises et appellent les fonctions de changement lorsque vous modifiez un champ.
import type { Depense } from "../App";
type PropsFormulaire = {
categories: string[];
onAjouter: (depense: Depense) => void;
};
type PropsFiltres = {
categories: string[];
categorieSelectionnee: string;
moisSelectionne: string;
onCategorieChange: (categorie: string) => void;
onMoisChange: (mois: string) => void;
};Ces déclarations illustrent les signatures attendues ; dans le projet, placez chacune dans le fichier du composant concerné et nommez son alias Props. Ne collez pas les deux types dans un même fichier : chaque composant reste dans son fichier contractuel. Les éléments de formulaire et leurs gestionnaires conservent le comportement établi dans les leçons précédentes.
À retenir
- App conserve les dépenses et les filtres, puis coordonne les composants enfants.
- Chaque composant reçoit uniquement les valeurs et fonctions nécessaires à son rôle.
- Les filtres, les dépenses visibles et le total sont transmis par props plutôt que stockés en double.
- La liste utilise l’identifiant stable de chaque dépense pour sa clé.
Dans la prochaine leçon, vous parcourrez les principaux scénarios de saisie et de filtrage pour vérifier le comportement du tableau de bord.