Garder les interactions propres à leur composant
Objectif : Implémenter la saisie d’une dépense avec un état local et des mises à jour immuables.
Quand vous saisissez une dépense, les champs doivent réagir immédiatement sans que chaque frappe modifie directement l’ensemble des données du tableau de bord. En séparant le brouillon du formulaire des dépenses enregistrées, vous gardez une interaction simple et vous préparez le partage des données dans la prochaine leçon.
La leçon précédente a fixé la source de vérité de chaque donnée : les dépenses seront partagées, tandis que les valeurs temporaires de saisie appartiennent au formulaire. Ici, vous implémentez cette frontière : le formulaire détient son brouillon et appelle une fonction fournie par son parent quand la saisie est valide. Le contexte qui portera ensuite les dépenses n’est pas encore nécessaire pour comprendre cette responsabilité.
Détenir le brouillon dans le formulaire
Chaque champ contrôlé lit sa valeur dans un objet local et le met à jour à chaque changement. Le formulaire ne crée pas lui-même une dépense enregistrée : il transmet au parent les données saisies, sans identifiant. Cette séparation laisse au propriétaire des dépenses la responsabilité de leur ajout et de leur persistance.
import { useState, type FormEvent } from "react";
import type { Categorie, Depense } from "../types/depense";
type BrouillonDepense = {
description: string;
montant: string;
categorie: string;
date: string;
};
type FormulaireDepenseProps = {
categories: Categorie[];
onAjouter: (depense: Omit<Depense, "id">) => void;
};
const brouillonInitial: BrouillonDepense = {
description: "",
montant: "",
categorie: "",
date: "",
};
export default function FormulaireDepense({
categories,
onAjouter,
}: FormulaireDepenseProps) {
const [brouillon, setBrouillon] = useState<BrouillonDepense>(brouillonInitial);
function mettreAJourChamp(
champ: keyof BrouillonDepense,
valeur: string,
) {
setBrouillon((precedent) => ({ ...precedent, [champ]: valeur }));
}
function ajouterDepense(evenement: FormEvent<HTMLFormElement>) {
evenement.preventDefault();
const montant = Number(brouillon.montant);
if (
brouillon.description.trim() === "" ||
!Number.isFinite(montant) ||
montant <= 0 ||
brouillon.categorie === "" ||
brouillon.date === ""
) {
return;
}
onAjouter({
description: brouillon.description.trim(),
montant,
categorie: brouillon.categorie,
date: brouillon.date,
});
setBrouillon(brouillonInitial);
}
return (
<section id="saisie" className="atelier-depense" aria-labelledby="titre-saisie">
<h2 id="titre-saisie">Ajouter une dépense</h2>
<form className="atelier-depense__formulaire" onSubmit={ajouterDepense}>
<label htmlFor="description">Description</label>
<input
id="description"
name="description"
required
value={brouillon.description}
onChange={(evenement) => mettreAJourChamp("description", evenement.target.value)}
/>
<label htmlFor="montant">Montant</label>
<input
id="montant"
name="montant"
type="number"
min="0.01"
step="0.01"
required
value={brouillon.montant}
onChange={(evenement) => mettreAJourChamp("montant", evenement.target.value)}
/>
<label htmlFor="categorie">Catégorie</label>
<select
id="categorie"
name="categorie"
required
value={brouillon.categorie}
onChange={(evenement) => mettreAJourChamp("categorie", evenement.target.value)}
>
<option value="">Choisissez une catégorie</option>
{categories.map((categorie) => (
<option key={categorie.id} value={categorie.nom}>{categorie.nom}</option>
))}
</select>
<label htmlFor="date">Date</label>
<input
id="date"
name="date"
type="date"
required
value={brouillon.date}
onChange={(evenement) => mettreAJourChamp("date", evenement.target.value)}
/>
<button type="submit">Ajouter la dépense</button>
</form>
</section>
);
}Le type BrouillonDepense conserve le montant comme texte pendant la saisie, car un champ numérique peut temporairement contenir une valeur incomplète. La conversion en nombre intervient au moment de la validation. L’attribut required et les contraintes du champ apportent un premier contrôle natif ; le gestionnaire vérifie aussi que le montant converti est fini et strictement positif avant d’appeler le parent.
Mettre à jour sans modifier l’état précédent
Dans mettreAJourChamp, la fonction passée à setBrouillon reçoit la valeur précédente et construit un nouvel objet avec l’opérateur de copie. Vous ne modifiez pas l’objet déjà stocké. Cette forme fonctionnelle garantit que la mise à jour s’appuie sur la valeur la plus récente, même si plusieurs mises à jour sont planifiées rapprochées.
Le tableau de dépenses ne figure volontairement pas dans ce composant. Lorsque le parent ajoute la dépense, il lui attribue l’identifiant attendu par le type métier et décide comment appeler la couche de persistance déjà préparée dans le service. Le formulaire, lui, ne connaît que son contrat d’entrée : une liste de catégories et une fonction onAjouter.
Vérifier le flux de saisie
Pour vérifier le comportement, rendez le formulaire avec au moins une catégorie et fournissez une fonction onAjouter qui reçoit l’objet transmis. Une saisie valide appelle cette fonction une fois, puis vide les champs. Une saisie invalide ne l’appelle pas ; les contraintes HTML bloquent notamment la soumission des champs requis non renseignés. Dans le tableau final, l’ajout sera raccordé aux données partagées par le contexte.
À retenir
- Le formulaire garde localement les valeurs temporaires de ses champs.
- Le parent reste responsable de l’ajout des dépenses enregistrées.
- Une mise à jour fonctionnelle construit l’état suivant à partir de l’état précédent.
- Copiez les tableaux et les objets au lieu de les modifier directement.
- Convertissez et validez les valeurs au moment de la soumission.
Dans la prochaine leçon, vous partagerez les préférences avec un contexte ciblé, sans élargir inutilement la portée des données communes.