Remonter l’état partagé
Objectif : Placer la liste des dépenses dans un composant parent afin de la partager entre le formulaire et la liste.
Le formulaire ajoute des dépenses et la liste les affiche. Si chacun de ces composants conserve sa propre copie des dépenses, ils peuvent rapidement présenter des données différentes. Vous allez placer cet état dans `App`, qui pourra le transmettre aux deux composants et recevoir les nouvelles dépenses du formulaire.
Dans la leçon précédente, vous avez déjà transmis une dépense validée à son parent et ajouté cette dépense avec une mise à jour fonctionnelle. Ici, vous formalisez ce flux : le parent détient la liste, la transmet aux composants qui en ont besoin, et fournit au formulaire un moyen de demander une mise à jour.
Détenir l’état partagé dans App
Une donnée doit être conservée par le composant le plus proche qui en a besoin et qui peut la partager. Le formulaire a besoin de signaler une nouvelle dépense ; la liste a besoin de recevoir toutes les dépenses à afficher. Comme `App` contient ces deux composants, l’état des dépenses y est placé.
import { useState } from "react";
import FormulaireDepense from "./composants/FormulaireDepense";
import ListeDepenses from "./composants/ListeDepenses";
import type { Depense } from "./types";
export default function App() {
const [depenses, setDepenses] = useState<Depense[]>([]);
function ajouterDepense(nouvelleDepense: Depense) {
setDepenses((depensesActuelles) => [...depensesActuelles, nouvelleDepense]);
}
return (
<div className="depenses">
<header className="depenses__entete">
<h1>Tableau de bord des dépenses</h1>
<p>Suivez vos dépenses personnelles, vos catégories et votre budget mensuel.</p>
</header>
<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>
<div className="depenses__filtres">
<label htmlFor="filtre-mois">Mois</label>
<input id="filtre-mois" name="mois" type="month" />
<label htmlFor="filtre-categorie">Catégorie</label>
<select id="filtre-categorie" name="categorie">
<option value="">Toutes les catégories</option>
</select>
</div>
</section>
<section className="depenses__panneau" aria-labelledby="titre-liste">
<h2 className="depenses__titre" id="titre-liste">Dépenses</h2>
<div className="depenses__etat" role="status" aria-live="polite"></div>
<ListeDepenses depenses={depenses} />
</section>
<section className="depenses__panneau" aria-labelledby="titre-recapitulatif">
<h2 className="depenses__titre" id="titre-recapitulatif">Récapitulatif mensuel</h2>
<div className="depenses__recapitulatif">
<p>Mois sélectionné : <span></span></p>
<p>Total des dépenses : <strong>0,00 €</strong></p>
<p>Nombre de dépenses : <strong>0</strong></p>
</div>
</section>
</main>
</div>
);
}`depenses` est désormais l’état canonique de la liste dans `App`. La fonction `ajouterDepense` applique une mise à jour fonctionnelle : elle construit un nouveau tableau à partir de l’ancien et de la dépense reçue, sans modifier le tableau existant. Le formulaire reçoit cette fonction sous la prop `onAjouter` ; la liste reçoit les données sous la prop `depenses`.
Déclarer les props des composants
Une prop est une valeur transmise par un composant parent à un composant enfant. Dans `ListeDepenses`, les dépenses sont uniquement lues et affichées. Le composant ne crée pas une seconde liste dans son propre état : il utilise directement la prop reçue.
import type { Depense } from "../types";
interface ListeDepensesProps {
depenses: Depense[];
}
export default function ListeDepenses({ depenses }: ListeDepensesProps) {
return (
<ul className="depenses__liste">
{depenses.map((depense) => (
<li className="depenses__ligne" key={depense.id}>
<span>{depense.libelle}</span>
<span>{depense.categorie}</span>
<time dateTime={depense.date}>{depense.date}</time>
<span>{depense.montant.toFixed(2)} €</span>
</li>
))}
</ul>
);
}Le formulaire reçoit une prop fonctionnelle. Après sa validation, il appelle `onAjouter` avec l’objet qu’il vient de construire ; il ne modifie pas lui-même la liste. Le type de la fonction indique explicitement qu’elle reçoit une dépense et ne renvoie pas de valeur.
import type { Depense } from "../types";
interface FormulaireDepenseProps {
onAjouter: (depense: Depense) => void;
}
export default function FormulaireDepense({ onAjouter }: FormulaireDepenseProps) {
// Conservez ici les champs contrôlés et la validation déjà construits.
// Après validation, transmettez l’objet créé ainsi :
// onAjouter(nouvelleDepense);
return null;
}Suivre le trajet d’une dépense
Lorsqu’une personne valide le formulaire, `FormulaireDepense` appelle `onAjouter`. Cette fonction, définie dans `App`, met à jour l’état ; React réaffiche alors `App` et transmet la nouvelle liste à `ListeDepenses`. Les données descendent par les props, tandis que la demande de modification remonte par l’appel de la fonction fournie par le parent.
Ce flux ne crée pas de copie locale des dépenses dans les enfants. Le formulaire conserve seulement les valeurs temporaires de ses champs, tandis que `App` détient la collection partagée. Les filtres et le récapitulatif restent inchangés dans cette étape ; leur calcul à partir des dépenses sera abordé dans la leçon suivante.
À retenir
- Placez l’état partagé dans le parent commun des composants concernés, ici `App`.
- Transmettez les données aux enfants avec des props typées.
- Transmettez une fonction de rappel au formulaire pour demander une mise à jour au parent.
- Laissez la liste afficher la prop reçue, sans conserver une copie locale des dépenses.
Dans la prochaine leçon, vous calculerez la liste filtrée et le récapitulatif à partir des dépenses et des filtres.