Cours JavaScript et React · Module 9: Hooks React modernes
useFormStatus et useActionState - les hooks de formulaire de React 19
Dans cette leçon6
Dans notre voyage cosmique à travers les galaxies de React, les formulaires sont l'un des modules de communication les plus importants du vaisseau. Chaque rapport de mission, chaque commande pour le système de navigation et chaque message vers la base passe par un formulaire. Jusqu'ici, cela voulait dire un onSubmit écrit à la main, e.preventDefault(), un état de chargement séparé et un autre pour les erreurs. React 19 introduit les actions de formulaire et deux hooks, useFormStatus et useActionState, qui prennent en charge la plus grande partie de ce travail.
Server Actions - un nouveau paradigme
Avant d'explorer les nouveaux hooks, nous devons comprendre le concept des Server Actions. Ce sont des fonctions marquées de la directive 'use server' qui s'exécutent sur le serveur, mais qu'on peut appeler directement depuis un formulaire dans un composant client. La directive ne fonctionne que dans un framework avec les React Server Components, par exemple Next.js avec l'App Router, car il faut bien que quelqu'un exécute cette fonction côté serveur. Un fichier d'actions ressemble à ceci :
1// actions.js
2'use server';
3
4export async function submitMissionReport(formData) {
5 const missionName = formData.get('missionName');
6 const status = formData.get('status');
7
8 // Ceci s'exécute sur le SERVEUR
9 await database.missions.create({
10 name: missionName,
11 status: status,
12 timestamp: new Date()
13 });
14
15 return { success: true, message: 'Rapport enregistré !' };
16}La fonction est exportée, car le composant l'importe comme n'importe quel module. Le framework n'envoie pas son code au navigateur, seulement une référence par laquelle le formulaire appelle le serveur. Tu passes une Server Action directement à l'attribut action du formulaire :
1// MissionForm.jsx
2import { submitMissionReport } from './actions';
3
4function MissionForm() {
5 return (
6 <form action={submitMissionReport}>
7 <input name="missionName" placeholder="Nom de la mission" />
8 <select name="status">
9 <option value="active">Active</option>
10 <option value="completed">Terminée</option>
11 </select>
12 <button type="submit">Envoyer le rapport</button>
13 </form>
14 );
15}Tu n'écris ni endpoint d'API ni gestionnaire onSubmit : React rassemble les champs du formulaire dans un objet FormData et le passe à l'action. Important : une action de formulaire peut aussi être une simple fonction asynchrone définie dans un composant client, sans 'use server'. C'est ainsi que fonctionnent les exemples suivants et l'éditeur de cette leçon, car useFormStatus et useActionState acceptent les deux types d'actions.
useFormStatus - l'état du formulaire dans les composants enfants
useFormStatus est un hook du paquet react-dom qui renvoie des informations sur l'état d'envoi du formulaire. Règle clé : il doit être utilisé dans un composant enfant d'un élément <form>, et non dans le composant qui rend ce formulaire.
1import { useFormStatus } from 'react-dom';
2
3// Composant du bouton d'envoi - À L'INTÉRIEUR du formulaire
4function SubmitButton() {
5 const { pending, data, method, action } = useFormStatus();
6
7 return (
8 <button
9 type="submit"
10 disabled={pending}
11 style={{
12 padding: '10px 20px',
13 background: pending ? '#333' : '#00d4ff',
14 color: pending ? '#666' : '#0a0e17',
15 border: 'none',
16 borderRadius: '6px',
17 cursor: pending ? 'wait' : 'pointer'
18 }}
19 >
20 {pending ? 'Envoi du rapport...' : 'Envoyer le rapport de mission'}
21 </button>
22 );
23}
24
25// pending - true lorsque le formulaire est en cours d'envoi
26// data - objet FormData avec les données du formulaire envoyé
27// method - la méthode du formulaire : 'get' ou 'post'
28// action - la fonction passée à l'attribut actionLe bouton sait de lui-même que le formulaire est en cours d'envoi, donc le parent n'a pas besoin de lui passer d'état par les props. Quand rien n'est envoyé, pending vaut false et les autres champs valent null.
Exemple pratique avec useFormStatus
Le hook n'est pas utile que dans un bouton. Chaque composant à l'intérieur d'un formulaire peut demander son état, donc les champs se désactivent tout seuls et une couche affiche un message de transmission. Le formulaire MissionReportForm ne leur passe aucune prop d'état :
1import { useFormStatus } from 'react-dom';
2
3function MissionInput({ name, label, placeholder }) {
4 const { pending } = useFormStatus();
5 return (
6 <div style={{ marginBottom: '12px' }}>
7 <label style={{ color: '#8899aa', display: 'block', marginBottom: '4px' }}>
8 {label}
9 </label>
10 <input
11 name={name}
12 placeholder={placeholder}
13 disabled={pending}
14 style={{
15 width: '100%',
16 padding: '8px 12px',
17 background: pending ? '#1a1a2e' : '#0f1729',
18 color: '#e0e1dd',
19 border: '1px solid #2a3a5c',
20 borderRadius: '6px',
21 opacity: pending ? 0.5 : 1
22 }}
23 />
24 </div>
25 );
26}
27
28function LoadingOverlay() {
29 const { pending } = useFormStatus();
30 if (!pending) return null;
31 return (
32 <div style={{
33 position: 'absolute', top: 0, left: 0, right: 0, bottom: 0,
34 background: 'rgba(10, 14, 23, 0.7)',
35 display: 'flex', alignItems: 'center', justifyContent: 'center',
36 borderRadius: '8px'
37 }}>
38 <p style={{ color: '#00d4ff' }}>Transmission en cours...</p>
39 </div>
40 );
41}
42
43function MissionReportForm({ submitAction }) {
44 return (
45 <form action={submitAction} style={{ position: 'relative' }}>
46 <LoadingOverlay />
47 <MissionInput name="mission" label="Nom de la mission" placeholder="Ex. Alpha Centauri" />
48 <MissionInput name="coordinates" label="Coordonnées" placeholder="Ex. 14h 29m 43s" />
49 <SubmitButton />
50 </form>
51 );
52}Le SubmitButton de l'exemple précédent fonctionne ici sans changement. Si tu appelais useFormStatus dans MissionReportForm lui-même, pending vaudrait toujours false, car le hook lit le formulaire parent et non celui que le composant rend lui-même.
useActionState - gestion complète de l'état d'une action
useActionState (anciennement useFormState du paquet react-dom) est un hook du paquet react qui relie une action, serveur ou ordinaire, à la gestion d'état. Il renvoie l'état actuel, une action encapsulée et un indicateur isPending :
1import { useActionState } from 'react';
2
3// Action qui renvoie le nouvel état : ici une simple fonction asynchrone dans le navigateur
4async function createMission(previousState, formData) {
5 const name = formData.get('name');
6 const destination = formData.get('destination');
7
8 if (!name || name.length < 3) {
9 return {
10 error: 'Le nom de la mission doit comporter au moins 3 caractères',
11 success: false
12 };
13 }
14
15 // Simulation d'un enregistrement en base de données
16 await new Promise(resolve => setTimeout(resolve, 1000));
17
18 return {
19 success: true,
20 message: 'Mission créée avec succès !',
21 missionId: Math.random().toString(36).slice(2, 11)
22 };
23}
24
25function CreateMissionForm() {
26 const [state, formAction, isPending] = useActionState(
27 createMission,
28 { error: null, success: false } // État initial
29 );
30
31 return (
32 <form action={formAction}>
33 <h2 style={{ color: '#00d4ff' }}>Nouvelle mission spatiale</h2>
34
35 <input name="name" placeholder="Nom de la mission" />
36 <input name="destination" placeholder="Destination" />
37
38 <button type="submit" disabled={isPending}>
39 {isPending ? 'Création de la mission...' : 'Créer la mission'}
40 </button>
41
42 {state.error && (
43 <p style={{ color: '#ff6b6b' }}>{state.error}</p>
44 )}
45
46 {state.success && (
47 <p style={{ color: '#00ff88' }}>{state.message} (ID: {state.missionId})</p>
48 )}
49 </form>
50 );
51}L'action reçoit l'état précédent comme premier argument et les données du formulaire comme second. Ce qu'elle renvoie devient le nouveau state, et isPending reste à true jusqu'à la fin de l'action. React met les envois suivants en file d'attente, donc chaque action reçoit l'état renvoyé par la précédente.
Paramètres de useActionState
Le hook useActionState accepte trois arguments :
- action - une fonction
(previousState, formData) => newState, généralement asynchrone - initialState - l'état initial (n'importe quelle valeur, le plus souvent un objet)
- permalink (facultatif) - l'adresse de la page qui contient le formulaire, utile pour l'amélioration progressive
Il renvoie un tableau de trois éléments :
- state - l'état actuel (résultat de la dernière action ou initialState)
- formAction - l'action encapsulée à passer à
<form action> - isPending - un booléen : l'action est-elle en cours d'exécution
Amélioration progressive
L'un des plus grands avantages des actions de formulaire est l'amélioration progressive : un formulaire peut fonctionner avant le chargement de JavaScript. Une seule condition : l'action doit être une fonction serveur ('use server') et la page doit être rendue sur le serveur par un framework. Le navigateur envoie alors le formulaire comme un simple formulaire HTML, et le serveur exécute l'action. D'abord l'action :
1// search-actions.js
2'use server';
3
4export async function searchGalaxy(previousState, formData) {
5 const query = formData.get('query');
6 const results = await findStars(query);
7 return { results, query };
8}Un composant client la passe à useActionState, et le troisième argument est le permalink, l'adresse de la page avec le formulaire :
1// SearchForm.jsx
2'use client';
3import { useActionState } from 'react';
4import { searchGalaxy } from './search-actions';
5
6function SearchForm() {
7 const [state, searchAction, isPending] = useActionState(
8 searchGalaxy,
9 { results: [], query: '' },
10 '/recherche'
11 );
12
13 return (
14 <form action={searchAction}>
15 <input
16 name="query"
17 defaultValue={state.query}
18 placeholder="Rechercher des étoiles, planètes, nébuleuses..."
19 />
20 <button type="submit" disabled={isPending}>
21 {isPending ? 'Analyse...' : 'Scanner la galaxie'}
22 </button>
23
24 {state.results.length > 0 && (
25 <ul>
26 {state.results.map(r => (
27 <li key={r.id}>{r.name} - {r.type}</li>
28 ))}
29 </ul>
30 )}
31 </form>
32 );
33}Si l'utilisateur envoie le formulaire avant l'hydratation, le navigateur va à l'adresse /recherche et le serveur renvoie la page avec le résultat. Une fois JavaScript chargé, React prend le formulaire en main et l'envoie sans recharger la page. Avec une action écrite comme simple fonction dans un composant client, comme dans les exemples précédents, le formulaire ne fonctionne qu'après le chargement de JavaScript.
Validation de formulaire avec useActionState
useActionState est parfait pour la validation côté serveur avec un retour immédiat :
1async function validateAndSubmit(prevState, formData) {
2 const errors = {};
3
4 const name = formData.get('crewName');
5 const role = formData.get('role');
6 const experience = parseInt(formData.get('experience'));
7
8 if (!name || name.length < 2) errors.crewName = 'Le nom doit comporter au moins 2 caractères';
9 if (!role) errors.role = 'Sélectionne un rôle';
10 if (isNaN(experience) || experience < 0) errors.experience = 'Expérience invalide';
11
12 if (Object.keys(errors).length > 0) {
13 return { errors, success: false, values: { name, role, experience } };
14 }
15
16 // saveCrewMember est ta fonction d'enregistrement, par exemple une requête vers la base
17 await saveCrewMember({ name, role, experience });
18 return { errors: {}, success: true, message: 'Membre d’équipage ajouté !' };
19}
20
21function CrewRegistrationForm() {
22 const [state, formAction, isPending] = useActionState(
23 validateAndSubmit,
24 { errors: {}, success: false }
25 );
26
27 return (
28 <form action={formAction}>
29 <input name="crewName" defaultValue={state.values?.name || ''} />
30 {state.errors?.crewName && <span style={{color:'red'}}>{state.errors.crewName}</span>}
31
32 <select name="role" defaultValue={state.values?.role || ''}>
33 <option value="">Sélectionne un rôle</option>
34 <option value="pilot">Pilote</option>
35 <option value="engineer">Ingénieur</option>
36 <option value="scientist">Scientifique</option>
37 </select>
38 {state.errors?.role && <span style={{color:'red'}}>{state.errors.role}</span>}
39
40 <input name="experience" type="number" defaultValue={state.values?.experience || ''} />
41 {state.errors?.experience && <span style={{color:'red'}}>{state.errors.experience}</span>}
42
43 <button type="submit" disabled={isPending}>
44 {isPending ? 'Enregistrement...' : 'Ajouter à l’équipage'}
45 </button>
46
47 {state.success && <p style={{color:'green'}}>{state.message}</p>}
48 </form>
49 );
50}Remarque les values dans la réponse avec les erreurs. À la fin de l'action, React réinitialise un formulaire aux champs non contrôlés, et chaque champ revient à son defaultValue. Comme defaultValue lit les valeurs dans l'état, après une erreur l'utilisateur voit toujours ce qu'il a saisi, et après un enregistrement réussi il retrouve un formulaire vide.
Résumé
Les nouveaux hooks de formulaire de React 19 représentent un changement fondamental dans la gestion des formulaires :
useFormStatus- fournit des informations sur l'état d'envoi du formulaire dans les composants enfantsuseActionState- relie une action à la gestion d'état, à la validation et à l'amélioration progressive- Server Actions - fonctions serveur appelées directement depuis les formulaires (dans un framework avec les React Server Components)
- Amélioration progressive - un formulaire avec une action serveur fonctionne aussi avant le chargement de JavaScript
Ces hooks éliminent une grande partie du code répétitif des formulaires : tu n'as plus besoin d'un onSubmit manuel, d'un useState pour chaque champ ni d'un état de chargement séparé. Retiens : une action de formulaire est un ordre pour le centre de contrôle, et useActionState et useFormStatus indiquent où en est son exécution.
Code de cette leçon : App.jsx
1import { useActionState } from 'react';
2import { useFormStatus } from 'react-dom';
3
4// Action du formulaire. Dans Next.js, ce pourrait être une fonction serveur avec la directive 'use server',
5// ici c’est une simple fonction asynchrone, donc elle s’exécute dans le navigateur.
6async function submitMissionReport(previousState, formData) {
7 // Simulation du délai de la liaison avec la base
8 await new Promise(resolve => setTimeout(resolve, 1500));
9 const name = formData.get('missionName');
10 const dest = formData.get('destination');
11 const priority = formData.get('priority');
12 const values = { missionName: name, destination: dest, priority };
13
14 // En cas d’erreur, on renvoie les valeurs saisies : React réinitialise le formulaire après l’action
15 if (!name || name.length < 3) {
16 return { ...previousState, error: 'Le nom de la mission doit comporter au moins 3 caractères', success: false, values };
17 }
18 if (!dest) {
19 return { ...previousState, error: 'Indique la destination de la mission', success: false, values };
20 }
21
22 const entry = {
23 missionId: 'MSN-' + String(previousState.history.length + 1).padStart(3, '0'),
24 message: `Mission "${name}" vers ${dest} (priorité : ${priority}) créée !`
25 };
26 // L’historique grandit d’une action à l’autre, car l’action reçoit l’état précédent
27 return { error: null, success: true, values: {}, ...entry, history: [entry, ...previousState.history] };
28}
29
30// Le bouton lit l’état du formulaire avec useFormStatus, sans aucune prop
31function SubmitButton() {
32 const { pending } = useFormStatus();
33 return (
34 <button
35 type="submit"
36 disabled={pending}
37 style={{
38 width: '100%', padding: '12px', marginTop: '12px',
39 background: pending ? '#1a2a3c' : 'linear-gradient(135deg, #00d4ff, #0088cc)',
40 color: pending ? '#556677' : '#0a0e17',
41 border: 'none', borderRadius: '8px', cursor: pending ? 'wait' : 'pointer',
42 fontSize: '14px', fontWeight: 'bold', transition: 'all 0.3s',
43 }}
44 >
45 {pending ? 'Transmission en cours...' : 'Envoyer le rapport de mission'}
46 </button>
47 );
48}
49
50// Le champ se désactive tout seul pendant l’envoi du formulaire
51function MissionInput({ name, label, placeholder, type = 'text', defaultValue }) {
52 const { pending } = useFormStatus();
53 const fieldStyle = {
54 width: '100%', padding: '10px 12px', background: '#0f1729',
55 color: '#e0e1dd', border: '1px solid #2a3a5c', borderRadius: '6px',
56 opacity: pending ? 0.5 : 1, boxSizing: 'border-box',
57 };
58 return (
59 <div style={{ marginBottom: '12px' }}>
60 <label htmlFor={name} style={{ color: '#8892b0', fontSize: '12px', display: 'block', marginBottom: '4px' }}>
61 {label}
62 </label>
63 {type === 'select' ? (
64 <select id={name} name={name} defaultValue={defaultValue || 'medium'} disabled={pending} style={fieldStyle}>
65 <option value="high">Haute</option>
66 <option value="medium">Moyenne</option>
67 <option value="low">Basse</option>
68 </select>
69 ) : (
70 <input id={name} name={name} placeholder={placeholder} defaultValue={defaultValue || ''} disabled={pending} style={fieldStyle} />
71 )}
72 </div>
73 );
74}
75
76// Couche visible seulement pendant l’envoi
77function LoadingOverlay() {
78 const { pending } = useFormStatus();
79 if (!pending) return null;
80 return (
81 <div style={styles.overlay}>
82 <div style={styles.spinner} />
83 <p style={{ color: '#00d4ff', marginTop: '10px', fontSize: '13px' }}>Envoi vers la base...</p>
84 </div>
85 );
86}
87
88function MissionHistory({ missions }) {
89 if (missions.length === 0) return null;
90 return (
91 <div style={styles.history}>
92 <h3 style={{ color: '#00d4ff', fontSize: '14px', margin: '0 0 10px' }}>Historique des missions</h3>
93 {missions.map((m) => (
94 <div key={m.missionId} style={styles.historyItem}>
95 <span style={styles.missionId}>{m.missionId}</span>
96 <span>{m.message}</span>
97 </div>
98 ))}
99 </div>
100 );
101}
102
103function App() {
104 const [state, formAction] = useActionState(submitMissionReport, {
105 error: null, success: false, values: {}, history: []
106 });
107
108 return (
109 <div style={styles.container}>
110 <header style={styles.header}>
111 <h1 style={styles.title}>Centre de Commandement des Missions</h1>
112 <p style={styles.subtitle}>useFormStatus et useActionState - les hooks de formulaire de React 19</p>
113 </header>
114
115 <div style={styles.grid}>
116 <div style={styles.formPanel}>
117 <h2 style={{ color: '#00d4ff', fontSize: '16px', margin: '0 0 16px' }}>
118 Nouvelle mission spatiale
119 </h2>
120
121 <form action={formAction}>
122 <LoadingOverlay />
123 <MissionInput name="missionName" label="Nom de la mission" placeholder="Ex. Exploration de Proxima b" defaultValue={state.values.missionName} />
124 <MissionInput name="destination" label="Destination" placeholder="Ex. Alpha Centauri" defaultValue={state.values.destination} />
125 <MissionInput name="priority" label="Priorité" type="select" defaultValue={state.values.priority} />
126 <SubmitButton />
127 </form>
128
129 {state.error && (
130 <div role="alert" style={styles.error}>
131 <span style={{ marginRight: '8px' }}>!</span> {state.error}
132 </div>
133 )}
134 {state.success && (
135 <div style={styles.success}>
136 <span style={{ marginRight: '8px' }}>OK</span> {state.message}
137 <div style={{ fontSize: '11px', color: '#00aa66', marginTop: '4px' }}>
138 ID: {state.missionId}
139 </div>
140 </div>
141 )}
142 </div>
143
144 <MissionHistory missions={state.history} />
145 </div>
146
147 <div style={styles.info}>
148 <h3 style={{ color: '#ffd700', fontSize: '13px', margin: '0 0 8px' }}>Comment ça marche :</h3>
149 <ul style={{ margin: 0, paddingLeft: '20px', fontSize: '12px', color: '#8892b0' }}>
150 <li>useActionState relie une action à l’état et renvoie [state, formAction, isPending]</li>
151 <li>useFormStatus dans le bouton, les champs et la couche lit l’état d’envoi du formulaire</li>
152 <li>L’action reçoit l’état précédent et le FormData avec les champs du formulaire</li>
153 <li>Après l’action, React réinitialise les champs, donc après une erreur ils reviennent par defaultValue</li>
154 </ul>
155 </div>
156 </div>
157 );
158}
159
160const styles = {
161 container: { fontFamily: 'system-ui', background: '#0a0e17', color: '#e0e1dd', minHeight: '100vh', padding: '20px' },
162 header: { textAlign: 'center', marginBottom: '20px' },
163 title: { fontSize: '22px', color: '#00d4ff', margin: '0 0 4px' },
164 subtitle: { fontSize: '12px', color: '#8892b0', margin: 0 },
165 grid: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '16px', maxWidth: '800px', margin: '0 auto' },
166 formPanel: { background: 'rgba(0,212,255,0.05)', borderRadius: '10px', padding: '20px', border: '1px solid rgba(0,212,255,0.15)', position: 'relative', overflow: 'hidden' },
167 overlay: { position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, background: 'rgba(10,14,23,0.85)', display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', zIndex: 10, borderRadius: '10px' },
168 spinner: { width: '30px', height: '30px', border: '3px solid #1a3a5c', borderTopColor: '#00d4ff', borderRadius: '50%', animation: 'spin 1s linear infinite' },
169 error: { marginTop: '12px', padding: '10px 14px', background: 'rgba(255,107,107,0.1)', border: '1px solid rgba(255,107,107,0.3)', borderRadius: '6px', color: '#ff6b6b', fontSize: '13px' },
170 success: { marginTop: '12px', padding: '10px 14px', background: 'rgba(0,255,136,0.08)', border: '1px solid rgba(0,255,136,0.3)', borderRadius: '6px', color: '#00ff88', fontSize: '13px' },
171 history: { background: 'rgba(0,212,255,0.05)', borderRadius: '10px', padding: '16px', border: '1px solid rgba(0,212,255,0.15)' },
172 historyItem: { display: 'flex', gap: '8px', padding: '8px 10px', background: 'rgba(0,0,0,0.3)', borderRadius: '6px', marginBottom: '6px', fontSize: '12px', alignItems: 'center' },
173 missionId: { padding: '2px 8px', background: 'rgba(0,212,255,0.2)', borderRadius: '4px', color: '#00d4ff', fontSize: '10px', fontWeight: 'bold', whiteSpace: 'nowrap' },
174 info: { maxWidth: '800px', margin: '20px auto 0', padding: '14px', background: 'rgba(255,215,0,0.05)', border: '1px solid rgba(255,215,0,0.15)', borderRadius: '8px' },
175};
176
177export default App;Tu as repéré une erreur dans cette leçon ?
Teste tes connaissances
Réponds aux questions de cette leçon. Choisis une réponse pour voir tout de suite si elle est correcte.
1. Où le hook useFormStatus doit-il être utilisé pour lire correctement l'état du formulaire ?
2. Quelles sont les trois valeurs renvoyées par le hook useActionState ?
Voici 2 des 3 questions de cette leçon. Réponds aux autres dans le jeu.
Tâches pratiques dans le jeu
- Éditeur de code
Le fichier App.jsx contient un formulaire de rapport de mission et une action asynchrone sendReport, qui vérifie le nom de la mission et répond après une demi-seconde, comme la base sur Terre. Complète les trous : ___BLANK1___ est la méthode de FormData qui lit la valeur d'un champ d'après son nom, ___BLANK2___ est le hook de React 19 qui relie l'action à l'état du formulaire et renvoie [state, formAction, isPending], ___BLANK3___ est l'attribut de <form> auquel tu passes formAction, et ___BLANK4___ est la valeur qui désactive le bouton pendant l'envoi. Un nom de moins de 3 caractères doit afficher un message d'erreur, un nom valide une confirmation avec le nom de la mission. Envoie le formulaire dans l'aperçu et vérifie les deux cas.
- Clique dans l'ordre
Reconstitue la syntaxe correcte pour appeler useActionState :
- Clique dans l'ordre
Reconstitue la syntaxe correcte d'un formulaire avec une Server Action :
- Éditeur de code
Le fichier App.jsx contient un formulaire d'inscription d'un membre d'équipage (champs name, role et experience) et une action registerCrewMember, qui vérifie les données « côté serveur » et renvoie un objet errors avec un message pour chaque champ invalide. Complète les trous : ___BLANK1___ est la condition vraie quand le prénom values.name a moins de 2 caractères, ___BLANK2___ est l'objet avec les valeurs saisies que l'action renvoie avec les erreurs, et ___BLANK3___ est la clé de state.errors qui contient le message du champ rôle. React vide les champs du formulaire à la fin de l'action, donc defaultValue les restaure depuis l'état. Des données invalides doivent afficher un message séparé à côté de chaque champ invalide et garder les valeurs saisies, des données valides un message de bienvenue avec le prénom.