Ajouter une dépense sans modifier l’existant
Objectif : Insérer une dépense validée dans la liste en produisant un nouvel état immuable.
Quand vous ajoutez une dépense, la liste ne doit pas disparaître ni être modifiée directement. Vous devez conserver les éléments déjà chargés, créer une nouvelle dépense à partir des valeurs validées et produire un nouvel état React. Vous allez aussi vider les champs après l’ajout réussi, sans effacer le formulaire si la validation échoue.
Construire une dépense à partir des valeurs validées
La leçon précédente a associé les erreurs de validation aux champs et empêche la soumission lorsque les valeurs sont incorrectes. Une fois ces vérifications passées, convertissez le montant en nombre et créez un objet conforme au modèle Depense défini au début du cours. Dans cet exemple, l’identifiant est créé au moment de l’ajout avec crypto.randomUUID() : la nouvelle dépense possède ainsi une clé stable pour son affichage.
Le formulaire transmet la dépense créée à son parent par la fonction onAjouter. Le parent détient déjà la liste chargée depuis le service ; il est donc l’endroit où effectuer la mise à jour. Le passage de cette fonction permet de relier les deux composants, mais la remontée d’état et le flux de données seront étudiés en détail dans la prochaine leçon.
Mettre à jour la liste sans modifier le tableau existant
Dans App, utilisez la forme fonctionnelle de setDepenses : React fournit à la fonction la valeur précédente, même si plusieurs mises à jour sont rapprochées. L’expression [...precedentes, nouvelleDepense] crée un nouveau tableau qui reprend les dépenses existantes, puis ajoute la nouvelle. N’utilisez ni push ni une modification directe de l’état : ces opérations altéreraient le tableau conservé par React.
import { useState, type FormEvent } from "react";
import type { Categorie, Depense } from "../types";
interface FormulaireDepenseProps {
categories: Categorie[];
onAjouter: (depense: Depense) => void;
}
interface ValeursFormulaire {
libelle: string;
montant: string;
categorie: Categorie | "";
date: string;
}
interface ErreursFormulaire {
libelle?: string;
montant?: string;
categorie?: string;
date?: string;
}
const valeursInitiales: ValeursFormulaire = {
libelle: "",
montant: "",
categorie: "",
date: "",
};
export default function FormulaireDepense({
categories,
onAjouter,
}: FormulaireDepenseProps) {
const [valeurs, setValeurs] = useState<ValeursFormulaire>(valeursInitiales);
const [erreurs, setErreurs] = useState<ErreursFormulaire>({});
function handleSubmit(evenement: FormEvent<HTMLFormElement>) {
evenement.preventDefault();
const montant = Number(valeurs.montant);
const prochainesErreurs: ErreursFormulaire = {};
if (!valeurs.libelle.trim()) prochainesErreurs.libelle = "Saisissez un libellé.";
if (!Number.isFinite(montant) || montant <= 0) {
prochainesErreurs.montant = "Saisissez un montant supérieur à zéro.";
}
if (!valeurs.categorie) prochainesErreurs.categorie = "Choisissez une catégorie.";
if (!valeurs.date || Number.isNaN(Date.parse(valeurs.date))) {
prochainesErreurs.date = "Saisissez une date valide.";
}
setErreurs(prochainesErreurs);
if (Object.keys(prochainesErreurs).length > 0 || !valeurs.categorie) return;
onAjouter({
id: crypto.randomUUID(),
libelle: valeurs.libelle.trim(),
montant,
categorie: valeurs.categorie,
date: valeurs.date,
});
setValeurs(valeursInitiales);
}
return (
<form className="depenses__formulaire" onSubmit={handleSubmit}>
<label className="depenses__champ" htmlFor="libelle">Libellé</label>
<input id="libelle" name="libelle" type="text" value={valeurs.libelle}
onChange={(e) => setValeurs({ ...valeurs, libelle: e.target.value })}
aria-invalid={Boolean(erreurs.libelle)} aria-describedby="erreur-libelle" />
<p className="depenses__erreur" id="erreur-libelle" role="alert">{erreurs.libelle}</p>
<label className="depenses__champ" htmlFor="montant">Montant</label>
<input id="montant" name="montant" type="number" min="0.01" step="0.01"
value={valeurs.montant}
onChange={(e) => setValeurs({ ...valeurs, montant: e.target.value })}
aria-invalid={Boolean(erreurs.montant)} aria-describedby="erreur-montant" />
<p className="depenses__erreur" id="erreur-montant" role="alert">{erreurs.montant}</p>
<label className="depenses__champ" htmlFor="categorie">Catégorie</label>
<select id="categorie" name="categorie" value={valeurs.categorie}
onChange={(e) => setValeurs({ ...valeurs, categorie: e.target.value as Categorie | "" })}
aria-invalid={Boolean(erreurs.categorie)} aria-describedby="erreur-categorie">
<option value="">Choisir une catégorie</option>
{categories.map((categorie) => <option key={categorie} value={categorie}>{categorie}</option>)}
</select>
<p className="depenses__erreur" id="erreur-categorie" role="alert">{erreurs.categorie}</p>
<label className="depenses__champ" htmlFor="date">Date</label>
<input id="date" name="date" type="date" value={valeurs.date}
onChange={(e) => setValeurs({ ...valeurs, date: e.target.value })}
aria-invalid={Boolean(erreurs.date)} aria-describedby="erreur-date" />
<p className="depenses__erreur" id="erreur-date" role="alert">{erreurs.date}</p>
<button type="submit">Ajouter</button>
</form>
);
}Après l’appel à onAjouter, les valeurs du formulaire reviennent à leur état initial. Cette réinitialisation est placée après la validation et la création : une saisie refusée reste visible et peut être corrigée. Le tableau vide de erreurs est également enregistré en cas de soumission valide, ce qui retire les anciens messages.
import { useState } from "react";
import type { Depense } from "./types";
import FormulaireDepense from "./composants/FormulaireDepense";
// Dans App, conservez l'état depenses déjà utilisé pour les données chargées.
const [depenses, setDepenses] = useState<Depense[]>([]);
function ajouterDepense(nouvelleDepense: Depense) {
setDepenses((precedentes) => [...precedentes, nouvelleDepense]);
}
// Dans le JSX d'App, remplacez le formulaire statique par :
<FormulaireDepense categories={categories} onAjouter={ajouterDepense} />Vérifier le résultat
Avec une dépense chargée et une nouvelle dépense saisie, la fonction de mise à jour conserve le premier élément et ajoute le second dans un tableau distinct. Le composant de liste reçoit alors le nouvel état lors du rendu suivant ; il n’est pas nécessaire de maintenir une autre copie de la liste dans le formulaire. Testez aussi un montant nul ou une catégorie manquante : aucune dépense ne doit être ajoutée et les valeurs saisies restent présentes.
À retenir
Dans la prochaine leçon, vous organiserez explicitement l’état partagé et les fonctions de mise à jour entre les composants.