4. Événements et formulaires
Sections
Réglages de lecture
Aucune voix pour cette langue dans ce navigateur.
Plus de voix
Code et termes techniques en anglais
Désactivé : la voix de la page lit tout, sans pause entre deux voix.
Aucune voix pour cette langue dans ce navigateur.
Plus de voix
D’autres voix : Microsoft Edge propose des voix naturelles en ligne, et Windows (Paramètres › Heure et langue › Voix), macOS et Android en installent d’autres.
Code : src/l04, src/TuningBook.tsx, l’extrait d’erreur errors/l04_events.tsx, et les solutions dans src/solutions.
Les gestionnaires sont des props
Section intitulée « Les gestionnaires sont des props »En Blazor, @onclick="Raise" attache une méthode, et un composant enfant expose un paramètre EventCallback<T> que son parent remplit. En React, les deux sont la même chose : une prop dont la valeur est une fonction. Sur un élément HTML, React connaît les noms, onClick, onChange, onSubmit, onKeyDown ; sur un composant, le nom est le tien, et la convention est on suivi de ce qui s’est passé.
import type { MouseEvent } from 'react';
// Un enfant signale ce qui s'est passé par une prop fonction ; le parent décide quoi en faireinterface FretButtonProps { string: number; fret: number; onSelect: (string: number, fret: number) => void;}
export function FretButton({ string, fret, onSelect }: FretButtonProps) { function handleClick(event: MouseEvent<HTMLButtonElement>) { event.stopPropagation(); // le gestionnaire de la rangée ne voit pas ce clic onSelect(string, fret); } return ( <button type="button" onClick={handleClick}> {fret} </button> );}
export function StringRow({ string, onSelect }: { string: number; onSelect: FretButtonProps['onSelect'] }) { return ( // Un clic entre les boutons atteint la rangée ; un clic sur un bouton s'arrête là <div role="group" aria-label={`String ${string}`} onClick={() => console.log(`row ${string} clicked`)}> {[0, 1, 2, 3].map((fret) => ( <FretButton key={fret} string={string} fret={fret} onSelect={onSelect} /> ))} </div> );}Passe la fonction, onClick={handleClick}, pas son résultat : onClick={handleClick()} l’appellerait pendant le rendu, comme le signale Responding to events. Une fonction fléchée, onClick={() => setCapo(capo + 1)}, crée un gestionnaire qui appelle quelque chose avec des arguments.
FretButton ne sait pas ce que signifie sélectionner une case : il signale la corde et la case, et le parent décide. FretButtonProps['onSelect'] réutilise le type de cette prop, un type d’accès indexé. Le test utilise une fonction mock de Vitest comme onSelect, clique sur la case 3, puis sur la rangée elle-même :
test('the button calls onSelect, and the click stops at the button', async () => { const onSelect = vi.fn((string: number, fret: number) => console.log(`selected string ${string}, fret ${fret}`)); render(<StringRow string={6} onSelect={onSelect} />); const row = screen.getByRole('group', { name: 'String 6' }); await userEvent.click(within(row).getByRole('button', { name: '3' })); await userEvent.click(row); expect(onSelect).toHaveBeenCalledExactlyOnceWith(6, 3);});✓ the button calls onSelect, and the click stops at the button selected string 6, fret 3 row 6 clickedLe premier clic a atteint le bouton et s’y est arrêté ; le second, sur la rangée, n’a atteint que la rangée. Les événements se propagent de l’élément où ils se produisent jusqu’à ses ancêtres, comme dans le DOM et comme les événements routés à propagation (bubbling) de WPF, et stopPropagation met fin au trajet. user-event simule les actions d’un utilisateur, un appui et un relâchement du pointeur pour un clic, au lieu d’envoyer un seul événement.
Les types d’événements
Section intitulée « Les types d’événements »Un gestionnaire reçoit un événement React, un SyntheticEvent qui enveloppe l’événement du navigateur, disponible dans nativeEvent, et suit l’interface standard Event. @types/react donne à chaque sorte son type, avec l’élément en paramètre de type :
| Événement | Type dans @types/react 19.3 |
Blazor |
|---|---|---|
onClick |
MouseEvent<HTMLButtonElement> |
MouseEventArgs |
onChange sur un input, un select ou un textarea |
ChangeEvent<HTMLInputElement> |
ChangeEventArgs |
onSubmit |
SubmitEvent<HTMLFormElement> |
OnValidSubmit d’EditForm |
onKeyDown |
KeyboardEvent<HTMLDivElement> |
KeyboardEventArgs |
Deux propriétés contiennent un élément. currentTarget est l’élément dont le gestionnaire s’exécute, et son type est le paramètre de type. target est l’élément où l’événement a commencé, qui peut être n’importe quel descendant, donc son type n’est que EventTarget. Lis currentTarget. Dans @types/react 19.3, FormEvent et FormEventHandler, que beaucoup de projets utilisent pour onSubmit et onChange, sont marqués @deprecated : leur commentaire dit qu’aucun événement de ce nom n’existe, et suggère ChangeEvent, InputEvent ou SubmitEvent.
errors/l04_events.tsx fait les erreurs habituelles :
import { useState, type ChangeEvent } from 'react';
export function CapoPicker() { const [capo, setCapo] = useState(0);
function handleInput(event: ChangeEvent<HTMLInputElement>) { setCapo(event.currentTarget.valueAsNumber); }
return ( <form onSubmit={() => setCapo(0)}> <input type="number" value={capo} onChange={setCapo} /> <select value={capo} onChange={handleInput}> <option value={0}>No capo</option> <option value={2}>Fret 2</option> </select> <button type="button" onClick={(event) => setCapo(event.currentTarget.value)}> Reset </button> <button type="button" onClick={(event) => console.log(event.target.value)}> Log </button> </form> );}errors/l04_events.tsx:13:41 - error TS2322: Type 'Dispatch<SetStateAction<number>>' is not assignable to type 'ChangeEventHandler<HTMLInputElement, HTMLInputElement>'. Types of parameters 'value' and 'event' are incompatible. Type 'ChangeEvent<HTMLInputElement, HTMLInputElement>' is not assignable to type 'SetStateAction<number>'.
13 <input type="number" value={capo} onChange={setCapo} /> ~~~~~~~~
node_modules/@types/react/index.d.ts:3411:9 - The expected type comes from property 'onChange' which is declared here on type 'DetailedHTMLProps<InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>' 3411 onChange?: ChangeEventHandler<T, HTMLInputElement> | undefined; ~~~~~~~~
errors/l04_events.tsx:14:28 - error TS2322: Type '(event: ChangeEvent<HTMLInputElement, Element>) => void' is not assignable to type 'ChangeEventHandler<HTMLSelectElement, HTMLSelectElement>'. Types of parameters 'event' and 'event' are incompatible. Type 'ChangeEvent<HTMLSelectElement, HTMLSelectElement>' is not assignable to type 'ChangeEvent<HTMLInputElement, Element>'. Type 'HTMLSelectElement' is missing the following properties from type 'HTMLInputElement': accept, align, alt, capture, and 38 more.
14 <select value={capo} onChange={handleInput}> ~~~~~~~~
node_modules/@types/react/index.d.ts:3578:9 - The expected type comes from property 'onChange' which is declared here on type 'DetailedHTMLProps<SelectHTMLAttributes<HTMLSelectElement>, HTMLSelectElement>' 3578 onChange?: ChangeEventHandler<T, HTMLSelectElement> | undefined; ~~~~~~~~
errors/l04_events.tsx:18:57 - error TS2345: Argument of type 'string' is not assignable to parameter of type 'SetStateAction<number>'.
18 <button type="button" onClick={(event) => setCapo(event.currentTarget.value)}> ~~~~~~~~~~~~~~~~~~~~~~~~~
errors/l04_events.tsx:21:74 - error TS2339: Property 'value' does not exist on type 'EventTarget'.
21 <button type="button" onClick={(event) => console.log(event.target.value)}> ~~~~~
Found 4 errors in the same file, starting at: errors/l04_events.tsx:13
exit 1- Une fonction set n’est pas un gestionnaire de changement :
onChangepasse un événement, etsetCapoattend un nombre. Le@bindde Blazor fait cette conversion ; React n’a pas de binding, et c’est le gestionnaire qui lit la valeur. - Un gestionnaire pour un input ne convient pas à un select : le type du paramètre dit quel élément il attend, et les paramètres de fonction sont vérifiés comme dans la leçon 4 de TypeScript.
valueest toujours une chaîne dans le DOM, même sur un bouton ou un<input type="number">. Convertis-la avecNumber(…), ou lisvalueAsNumbersur un champ numérique.event.targetest unEventTarget, qui n’a pas devalue;currentTargetest typé.onSubmit={() => setCapo(0)}compile : un gestionnaire peut ignorer son événement, comme tout callback peut ignorer ses arguments.
Les champs contrôlés
Section intitulée « Les champs contrôlés »React n’a ni @bind ni {Binding Mode=TwoWay}. Un champ est contrôlé quand sa value vient de l’état et que son onChange réécrit l’état : React rend la valeur, l’utilisateur tape, onChange reçoit le nouveau texte, la fonction set le stocke, et React le rend à nouveau. L’état est l’unique source de vérité, et le composant peut vérifier, transformer ou refuser chaque changement. TuningForm.tsx :
// Un formulaire contrôlé : la valeur de chaque champ vient de l'état, et chaque frappe passe par onChangeexport function TuningForm({ onAdd }: { onAdd: (tuning: NewTuning) => void }) { const [name, setName] = useState(''); const [notesText, setNotesText] = useState('E A D G B E'); const [capo, setCapo] = useState(0); const [submitted, setSubmitted] = useState(false);
// La validation est calculée à partir de l'état pendant le rendu, pas stockée à côté const parsed = parseNotes(notesText); const nameError = name.trim() === '' ? 'Give the tuning a name.' : undefined; const notesError = parsed.ok ? undefined : parsed.error;
function handleNotesChange(event: ChangeEvent<HTMLInputElement>) { setNotesText(event.currentTarget.value); }
function handleSubmit(event: SubmitEvent<HTMLFormElement>) { event.preventDefault(); // pas de rechargement de la page : React gère l'envoi setSubmitted(true); if (nameError || !parsed.ok) return; onAdd({ name: name.trim(), notes: parsed.notes, capo }); setName(''); setSubmitted(false); }
return ( <form onSubmit={handleSubmit} noValidate> <label> Name <input value={name} onChange={(event) => setName(event.currentTarget.value)} aria-invalid={submitted && !!nameError} /> </label> {submitted && nameError && <p role="alert">{nameError}</p>} <label> Notes <input value={notesText} onChange={handleNotesChange} aria-invalid={!!notesError} /> </label> {notesError && <p role="alert">{notesError}</p>} <label> Capo{' '} <select value={capo} onChange={(event) => setCapo(Number(event.currentTarget.value))}> {[0, 1, 2, 3, 4, 5].map((fret) => ( <option key={fret} value={fret}> {fret} </option> ))} </select> </label> <button type="submit">Add tuning</button> </form> );}Les pièces, et d’où vient chacune :
- Un gestionnaire en ligne,
(event) => setName(event.currentTarget.value), n’a pas besoin de type : TypeScript infèreeventdepuis la proponChange, comme il infère le paramètre d’une lambda depuis un délégué en C#.handleNotesChangeest une fonction nommée, donc elle déclare le type. onChangese déclenche à chaque frappe, comme l’événementinputdu navigateur, et non quand le champ perd le focus, comme l’événementchangedu navigateur. La documentation le dit.preventDefaultarrête l’envoi propre au navigateur, qui enverrait le formulaire à l’URL courante et rechargerait la page.noValidatedésactive les messages de validation du navigateur, puisque le composant affiche les siens.- La validation est calculée, comme la leçon 3 le recommande pour tout ce qui est dérivé :
parsed,nameErroretnotesErrorviennent de l’état à chaque rendu. Il n’y a pas d’état « erreurs » à garder synchronisé. Le seul état en plus estsubmitted, un fait que les champs ne contiennent pas : si l’utilisateur a tenté d’envoyer, pour qu’un nom vide ne soit pas signalé avant qu’il ait tapé quoi que ce soit. role="alert"fait annoncer un message par les lecteurs d’écran quand il apparaît, etaria-invalidmarque le champ.- Un
selectest contrôlé de la même façon, et sa valeur est une chaîne, d’oùNumber(…).
parseNotes est une simple fonction qui renvoie une union discriminée, { ok: true; notes } | { ok: false; error }, comme celles que construit la leçon 3 de TypeScript ; une fois parsed.ok vérifié, parsed.notes existe. La garder hors du composant permet de la tester seule :
✓ parseNotes "E A D G B E" { ok: true, notes: [ 'E', 'A', 'D', 'G', 'B', 'E' ] } "d a d g a d" { ok: true, notes: [ 'D', 'A', 'D', 'G', 'A', 'D' ] } "Eb Ab Db Gb Bb Eb" { ok: true, notes: [ 'D#', 'G#', 'C#', 'F#', 'A#', 'D#' ] } "E A D G B" { ok: false, error: 'A guitar tuning has 6 notes, not 5.' } "E A D G H E" { ok: false, error: '"H" is not a note.' }Le test du composant tape dans le formulaire comme le ferait un utilisateur :
✓ the form validates as you type, and on submit typed "D A D G": [ 'A guitar tuning has 6 notes, not 4.' ] typed " A D": [] submitted without a name: [ 'Give the tuning a name.' ] onAdd calls: 0 submitted: [] [[{"name":"DADGAD","notes":["D","A","D","G","A","D"],"capo":2}]]L’erreur sur les notes apparaît pendant la saisie et disparaît avec la sixième note ; l’erreur sur le nom attend un envoi. Après un envoi valide, onAdd a reçu un accordage, et le champ du nom est de nouveau vide.
L’EditForm de Blazor en fait davantage à ta place, avec des annotations de données sur un modèle et des composants ValidationMessage. React lui-même s’arrête aux champs contrôlés et aux événements ; des bibliothèques comme React Hook Form ajoutent des schémas et la gestion des erreurs, et React 19 ajoute les Actions de formulaire, que couvre la leçon 12.
Les deux avertissements des champs contrôlés
Section intitulée « Les deux avertissements des champs contrôlés »Warnings.tsx fait les deux erreurs que React signale en développement :
// Une value sans onChange : React rend le champ en lecture seule, et avertitexport function ReadOnlyCapo() { return <input aria-label="Capo" type="number" value={2} />;}
// undefined, puis un nombre : le champ commence non contrôlé, puis devient contrôléexport function LateCapo() { const [capo, setCapo] = useState<number>(); return <input aria-label="Capo" type="number" value={capo} onChange={(event) => setCapo(event.currentTarget.valueAsNumber)} />;}✓ value without onChange: typing changes nothing console.error: You provided a `value` prop to a form field without an `onChange` handler. This will render a read-only field. If the field should be mutable use `defaultValue`. Otherwise, set either `onChange` or `readOnly`. value after typing 5: 2✓ undefined, then a number: React warns on the first keystroke console.error: A component is changing an uncontrolled input to be controlled. This is likely caused by the value changing from undefined to a defined value, which should not happen. Decide between using a controlled or uncontrolled input element for the lifetime of the component. More info: https://react.dev/link/controlled-components value after typing 3: 3Avec une value et sans onChange, l’utilisateur tape et le champ affiche toujours 2 : React remet la valeur de l’état. value={undefined} signifie « pas de valeur », un champ non contrôlé, donc un état qui commence à undefined fait passer le champ de non contrôlé à contrôlé à la première frappe. tsc accepte les deux composants ; commence l’état avec une valeur, useState(0) ou useState(''), ou utilise defaultValue pour un champ que React ne contrôle pas. Le TuningNotes du cours, dans la leçon 2, était exprès un tel champ non contrôlé.
L’état qui appartient au parent
Section intitulée « L’état qui appartient au parent »TuningForm ne stocke pas les accordages : il appelle onAdd. La liste des accordages appartient à TuningBook, le parent commun le plus proche du formulaire qui ajoute un accordage et de la liste qui les affiche. React appelle cela faire remonter l’état :
// Les leçons 2 à 4 ensemble : le carnet possède la liste, le formulaire signale un nouvel accordage, la liste le rendexport function TuningBook() { const [tunings, setTunings] = useState<readonly Tuning[]>([standard, dropD]);
function handleAdd(tuning: NewTuning) { const id = `${tuning.name.toLowerCase().replaceAll(/\W+/g, '-')}-${tunings.length + 1}`; setTunings([...tunings, { id, name: tuning.capo ? `${tuning.name}, capo ${tuning.capo}` : tuning.name, notes: tuning.notes }]); }
return ( <> <TuningForm onAdd={handleAdd} /> <TuningList tunings={tunings} /> <Capo /> </> );}flowchart TB book["TuningBook<br/>état : tunings"] form["TuningForm<br/>état : les champs en cours de saisie"] list["TuningList<br/>pas d'état"] book -- "props : onAdd" --> form form -. "onAdd(tuning), quand l'utilisateur envoie" .-> book book -- "props : tunings" --> listLes données descendent sous forme de props, et les événements remontent sous forme d’appels à des props fonctions. L’id est construit quand l’accordage est ajouté, une seule fois, et ne change jamais, comme la leçon 2 le demande d’une clé. Le test ajoute un accordage par le formulaire et lit la liste :
✓ a tuning added with the form appears in the list [ 'Standard', 'Drop D', 'Open D' ]TuningBook est ce que App.tsx rend sous le compteur, donc npm run dev montre les trois leçons ensemble.
Dans GuitarAlchemist/ga
Section intitulée « Dans GuitarAlchemist/ga »Le ga-react-components de GA a deux composants qui communiquent dans l’autre sens, de l’enfant vers le parent, par un effet. NotesSelector.tsx signale ses notes après chaque rendu où elles, ou le callback, ont changé :
const NotesSelector: React.FC<NoteSelectorProps> = ({ onNotesChange }) => { const [textNotes, setTextNotes] = useState(''); const [toggledNotes, setToggledNotes] = useState<string[]>([]); const [useTextInput, setUseTextInput] = useState(true);
useEffect(() => { const notes = useTextInput ? textNotes.split(' ').filter(note => allNotes.includes(note)) : toggledNotes; onNotesChange(notes); }, [textNotes, toggledNotes, useTextInput, onNotesChange]);ScaleSelector.tsx le rend avec un gestionnaire qu’il crée à chaque rendu, et qui stocke les notes, sous forme d’un nouveau tableau, et la gamme :
const handleNotesChange = (notes: string[]) => { setSelectedNotes(notes); setScale(calculateScale(notes)); }; // … <NotesSelector onNotesChange={handleNotesChange} />Les effets sont le sujet de la leçon 5 ; ce qui compte ici, c’est que React exécute un effet après un rendu dans lequel l’une de ses dépendances diffère du rendu précédent, comparée avec Object.is. Mets les deux ensemble :
flowchart LR render["ScaleSelector rend<br/>un nouveau handleNotesChange"] --> child["NotesSelector rend<br/>onNotesChange a changé"] child --> effect["son effet s'exécute<br/>onNotesChange(nouveau tableau)"] effect --> set["setSelectedNotes(nouveau tableau)<br/>une nouvelle valeur pour Object.is"] set --> rendersrc/l04/NotesSelector.tsx réduit les deux composants à leur état, leur effet et leur gestionnaire, sans Material UI, et ajoute un compteur qui lève une exception après 60 rendus pour que le test se termine :
✓ GA's two selectors render each other in a loop console.error: Maximum update depth exceeded. This can happen when a component calls setState inside useEffect, but useEffect either doesn't have a dependency array, or one of the dependencies changes on every render.React détecte la boucle et dit ce qui la cause : « one of the dependencies changes on every render ». Dans la réduction, la boucle commence dès que ScaleSelector est monté. Dans GA, ScaleSelector est exporté depuis components/index.ts, mais aucune application de GA à ce commit ne le rend : ga-client a son propre ScaleSelector. Le bug est latent, et apparaîtra le jour où quelqu’un utilisera le composant exporté. Je n’ai pas monté le composant de GA lui-même, avec Material UI, pour le confirmer (à vérifier). La correction n’a pas besoin de useCallback pour stabiliser le gestionnaire ; elle supprime l’effet, comme le montre l’exercice 3. La page de React You might not need an effect décrit exactement ce cas.
À retenir
Section intitulée « À retenir »- Un gestionnaire est une prop fonction : sur un élément, React la nomme ; sur un composant, c’est toi,
onSomething. - Passe la fonction, pas un appel. Type un gestionnaire nommé avec le type d’événement et son élément ; un gestionnaire en ligne est inféré.
- Lis
currentTarget, pastarget;valueest toujours une chaîne.FormEventest déprécié dans@types/react19.3. - Les événements remontent ;
stopPropagationles arrête,preventDefaultannule l’action par défaut du navigateur, comme l’envoi d’un formulaire. - Un champ contrôlé prend
valuedans l’état et le réécrit dansonChange; ne lui donne pasundefined, ni unevaluesansonChange. - Calcule la validation pendant le rendu. Garde l’état dans le parent commun le plus proche, et fais remonter les événements par des props fonctions.
- Un enfant qui prévient son parent depuis un effet peut boucler ; préviens-le plutôt depuis le gestionnaire d’événement.
Exercices
Section intitulée « Exercices »- Écris un composant
StringOrderqui reçoit six notes et les affiche, avec une case à cocher intitulée « High string first » qui inverse l’ordre. De quelle propriété de l’événement une case à cocher a-t-elle besoin, et quelle méthode de tableau laisse les notes intactes ?
Solution
src/solutions/l04_ex1/StringOrder.tsx :
// Exercice 1 : une case à cocher contrôlée lit event.currentTarget.checked, pas valueexport function StringOrder({ notes }: { notes: readonly string[] }) { const [highFirst, setHighFirst] = useState(false);
function handleChange(event: ChangeEvent<HTMLInputElement>) { setHighFirst(event.currentTarget.checked); }
const shown = highFirst ? notes.toReversed() : notes; return ( <section> <label> <input type="checkbox" checked={highFirst} onChange={handleChange} /> High string first </label> <p>{shown.join(' ')}</p> </section> );}✓ the checkbox reverses the order D A D G A E E A G D A DUne case à cocher est contrôlée par checked, pas par value, et son onChange lit currentTarget.checked ; sa value est la chaîne envoyée avec un formulaire, "on" par défaut. toReversed renvoie un nouveau tableau ; reverse inverserait les props sur place, ce qu’un composant ne doit pas faire, et tsc le refuse sur un readonly string[]. L’ordre inversé est calculé, pas stocké.
- Écris un
FretSelector, qui peut prendre le focus au clavier, où les flèches droite et gauche déplacent la case sélectionnée entre 0 et un maximum, et où Home revient à 0. Les autres touches, comme Tab, doivent continuer de fonctionner, et les flèches ne doivent pas faire défiler la page. Expose-le aux technologies d’assistance comme un curseur (slider).
Solution
src/solutions/l04_ex2/FretSelector.tsx :
// Exercice 2 : les flèches déplacent la case sélectionnée, Home revient à la corde à videexport function FretSelector({ frets = 12 }: { frets?: number }) { const [fret, setFret] = useState(0);
function handleKeyDown(event: KeyboardEvent<HTMLDivElement>) { const moves: Record<string, number> = { ArrowRight: fret + 1, ArrowLeft: fret - 1, Home: 0 }; if (!(event.key in moves)) return; // les autres touches gardent leur comportement habituel event.preventDefault(); // les flèches ne font pas défiler la page setFret(Math.min(frets, Math.max(0, moves[event.key]))); }
return ( <div role="slider" tabIndex={0} aria-label="Fret" aria-valuemin={0} aria-valuemax={frets} aria-valuenow={fret} onKeyDown={handleKeyDown}> Fret {fret} </div> );}Le test utilise un maximum de 2, et appuie sur Left, puis trois fois sur Right, puis sur Home :
✓ arrows, bounds and Home {ArrowLeft} Fret 0 {ArrowRight}{ArrowRight}{ArrowRight} Fret 2 {Home} Fret 0event.key nomme la touche, "ArrowRight" ou "Home". preventDefault n’est appelé que pour les touches que gère le composant, donc Tab déplace toujours le focus. tabIndex={0} rend le div focusable, et le rôle slider avec ses attributs aria-value* dit à un lecteur d’écran ce qu’il est et où il en est. Un <input type="range"> donnerait tout cela gratuitement, et reste le meilleur choix quand son apparence convient ; l’exercice porte sur les événements clavier.
- Réécris la paire
NotesSelectoretScaleSelectorde GA, avec le champ texte seulement, pour que les notes parviennent au parent sans effet et que le numéro de gamme ne soit pas de l’état. Montre que taper « C E G » fait un commit de l’arbre par frappe.
Solution
src/solutions/l04_ex3/ScaleSelector.tsx :
// Exercice 3 : l'enfant prévient le parent dans le gestionnaire d'événement, quand l'utilisateur tape, sans effetexport function NotesSelector({ onNotesChange }: { onNotesChange: (notes: string[]) => void }) { const [textNotes, setTextNotes] = useState('');
return ( <input aria-label="Notes" value={textNotes} onChange={(event) => { const text = event.currentTarget.value; setTextNotes(text); onNotesChange(text.split(' ').filter((note) => allNotes.includes(note))); }} /> );}
// Le parent ne garde que les notes, et calcule le numéro de gamme à partir d'ellesexport function ScaleSelector() { const [selectedNotes, setSelectedNotes] = useState<string[]>([]); const scale = selectedNotes.reduce((bits, note) => bits | (1 << allNotes.indexOf(note)), 0);
return ( <section> <NotesSelector onNotesChange={setSelectedNotes} /> <p> {selectedNotes.join(' ') || 'no notes'}: scale {scale} </p> </section> );}Le test compte les commits avec le Profiler de React, dont onRender s’exécute chaque fois que React fait le commit de l’arbre qu’il contient :
test('one commit per keystroke, and the scale follows the notes', async () => { let commits = 0; render( // Profiler appelle onRender chaque fois que React fait le commit de l'arbre qu'il contient <Profiler id="scale" onRender={() => commits++}> <ScaleSelector /> </Profiler>, ); await userEvent.type(screen.getByLabelText('Notes'), 'C E G'); console.log(screen.getByRole('paragraph').textContent, `after ${commits} commits`); expect(commits).toBe(6);});✓ one commit per keystroke, and the scale follows the notes C E G: scale 145 after 6 commitsSix commits : le montage, et un pour chacun des cinq caractères de « C E G ». Chaque frappe appelle deux fonctions set, celle de l’enfant et celle du parent, dans le même gestionnaire, et React les regroupe en un seul rendu, comme l’a montré la leçon 3. Rien ne s’exécute après le rendu, donc rien ne peut boucler. 145 vaut 1 + 16 + 128, les bits de C, E et G. Le parent passe directement setSelectedNotes : une fonction set garde la même identité d’un rendu à l’autre, et correspond au type (notes: string[]) => void.
- React : Responding to events, Reacting to input with state, Sharing state between components, You might not need an effect,
<input>, Common components: the React event object,<Profiler> @types/reactsur DefinitelyTyped- Testing Library :
user-event, requêtes par rôle ; Vitest : fonctions mock - MDN : rôle ARIA
slider, rôlealert - Microsoft : Gestion des événements Blazor ASP.NET Core, liaison de données, formulaires