Créer un composant de formulaire amélioré

Apprends à structurer des formulaires React Router 7 robustes : erreurs par champ, UX sans perte de saisie, accessibilité native, et composants réutilisables sans état local.

5 min read
Déverrouillez votre potentiel

avec React Router 7

Vous en avez marre de...

❌ perdre du temps à chercher des informations éparpillées
❌ ne pas avoir de retour sur votre progression
Assistant IA spécialisé

Posez vos questions 24/7 à notre IA experte en React Router 7

Quiz interactifs

Validez vos acquis avec des quiz personnalisés et un feedback instantané

9 modules
45 leçons
Accès à vie
299.00
-50%

Du <Form> basique au champ haut-de-gamme

Le composant <Form> de React Router 7 nous offre déjà un cycle loader ⇒ action ⇒ loader transparent ; pourtant l’expérience reste minimale :
• aucun retour d’erreur par champ,
• impossible de conserver la saisie si le serveur répond 400,
• pas d’indication de chargement.

Dans cette leçon tu vas apprendre à :

  1. structurer une réponse d’action riche (succès + erreurs),
  2. consommer cette réponse côté client avec useActionData,
  3. créer un composant Field réutilisable qui gère label, input, accessibilité et messages,
  4. indiquer l’état submitting avec useNavigation.

On reste sans dépendance externe. Zod & Conform arriveront dans la prochaine leçon pour automatiser toute la validation !


1. Définir un type de réponse uniforme

Premier objectif : faire parler l’action sans rediriger.
Créons un type UserActionResponse qui rassemble le statut, un slug de redirection éventuel et un objet errors mappé sur chaque champ.

app/server/users.server.ts
1
type UserActionResponse = {
2
success: boolean
3
slug: string // redirection facultative
4
errors: {
5
firstName?: string[]
6
lastName?: string[]
7
}
8
}

Construire la réponse dans addUser

app/server/users.server.ts
1
export async function addUser({ firstName, lastName }: {
2
firstName: string
3
lastName: string
4
}): Promise<UserActionResponse> {
5
6
const res: UserActionResponse = {
7
success: true,
8
slug: '',
9
errors: { firstName: [], lastName: [] }
10
}
11
12
// validation manuelle – côté serveur
13
if (!firstName) {
14
res.errors.firstName!.push('Le prénom est requis')
15
res.success = false
16
}
17
if (!lastName) {
18
res.errors.lastName!.push('Le nom est requis')
19
res.success = false
20
}
21
22
if (!res.success) return res // on stoppe net
23
24
/* …vérif unicité, insertion DB… */
25
res.slug = firstName.toLowerCase()
26
return res
27
}

2. Renvoyer proprement la réponse dans l’action

Dans la route dynamique users.$userSlug.tsx :

app/routes/users+/$userSlug.tsx {29-47}
1
import { data, redirect } from "react-router"
2
3
/* … */
4
5
export async function action({ request, params }) {
6
const fd = await request.formData()
7
const firstName = fd.get('firstName') as string
8
const lastName = fd.get('lastName') as string
9
const isNew = params.userSlug === 'new'
10
11
if (isNew) {
12
const created = await addUser({ firstName, lastName })
13
14
if (!created.success) {
15
// ↩︎ Renvoi JSON + statut 400
16
return data(created, { status: 400 })
17
}
18
19
// ↪︎ Tout est ok → redirection
20
return redirect(
21
href('/users/:userSlug', { userSlug: created.slug })
22
)
23
}
24
25
/* …mise à jour existant… */
26
}

data() fournit le bon header application/json sans que tu t’en soucies.


3. Consommer la réponse côté client

a/ Récupérer les erreurs

app/routes/users+/$userSlug.tsx {60-69}
1
import { useActionData } from "react-router"
2
3
export default function UserForm() {
4
const actionData = useActionData<typeof action>() // tipé gratis
5
/* … */
6
}

actionData vaut undefined si la requête précédente était un succès.

b/ Désactiver le bouton pendant l’envoi

app/routes/users+/$userSlug.tsx {71-76}
1
import { useNavigation } from "react-router"
2
3
const navigation = useNavigation()
4
const isSubmitting = navigation.state === 'submitting'

4. Construire un champ réutilisable

Nous voulons un composant unique pour :
• afficher le label,
• binder l’input,
• lister les erreurs,
• gérer l’accessibilité (aria-invalid, aria-describedby).

app/components/Field.tsx
1
export type ListOfErrors = Array<string | null | undefined> | null | undefined
2
3
export function Field({
4
labelProps,
5
inputProps,
6
errors
7
}: {
8
labelProps: React.LabelHTMLAttributes<HTMLLabelElement>
9
inputProps: React.InputHTMLAttributes<HTMLInputElement>
10
errors?: ListOfErrors
11
}) {
12
return (
13
<div className="flex flex-col gap-1">
14
<label {...labelProps} />
15
<input
16
{...inputProps}
17
aria-invalid={Boolean(errors?.length)}
18
aria-describedby={errors?.length ? `${inputProps.id}-error` : undefined}
19
className="px-3 py-2 border rounded-md focus:ring-2 focus:ring-blue-500"
20
/>
21
{errors?.length ? (
22
<ul id={`${inputProps.id}-error`} className="text-red-600 text-sm">
23
{errors.map(e => <li key={e}>{e}</li>)}
24
</ul>
25
) : null}
26
</div>
27
)
28
}

⚡️ Avantage : tu ne réécriras plus jamais <label> ni règles ARIA.


5. Brancher le formulaire sur Field

app/routes/users+/$userSlug.tsx {85-107}
1
<Form method="POST" className="space-y-3">
2
<Field
3
labelProps={{ children: "Prénom", htmlFor: "firstName" }}
4
inputProps={{
5
id: "firstName",
6
name: "firstName",
7
type: "text",
8
defaultValue: user?.firstName ?? ""
9
}}
10
errors={actionData?.errors.firstName}
11
/>
12
13
<Field
14
labelProps={{ children: "Nom", htmlFor: "lastName" }}
15
inputProps={{
16
id: "lastName",
17
name: "lastName",
18
type: "text",
19
defaultValue: user?.lastName ?? ""
20
}}
21
errors={actionData?.errors.lastName}
22
/>
23
24
<button
25
type="submit"
26
disabled={isSubmitting}
27
className="px-4 py-2 rounded-md bg-blue-600 text-white
28
disabled:opacity-50 transition-colors"
29
>
30
{isSubmitting ? "En cours…" : isNew ? "Ajouter" : "Modifier"}
31
</button>
32
</Form>

Résultat :

  1. l’utilisateur conserve sa saisie,
  2. chaque champ signale son erreur,
  3. le bouton passe en grisé pendant 250 ms (delay simulé côté serveur).

6. Pourquoi cette approche améliore la DX/UX ?

AvantAprès
Redirection 302 sur erreur ⇒ perte du formulaireRetour JSON 400 ⇒ pas de reload
Une erreur par requête (early return)Plusieurs erreurs agrégées
Input géré à la main, pas accessibleField DRY et ARIA-ready
Pas d’indicateur de chargementuseNavigation désactive le bouton

Tu viens d’écrire 0 ligne de state local tout en offrant une UX proche de Stripe Checkout.


Points clés à retenir

  • Action → JSON → useActionData : le triplet magique pour les messages
    d’erreur granularisés.
  • Field.tsx concentre le pattern label + input + errors et se réutilise partout.
  • useNavigation().state === "submitting" suffit pour gérer le loader bouton.
  • Tout est encore vanilla ; Zod et Conform automatiseront la validation et le focus dans la prochaine leçon.

Exercices rapides

  1. Ajoute la suppression d’un utilisateur
    Crée un bouton “Supprimer” qui envoie _method=delete et affiche un toast de confirmation si action renvoie { deleted: true }.

  2. Affichage inline
    Modifie Field pour entourer l’input de border-red-600 lorsque errors?.length > 0.

  3. Accessibilité avancée
    Utilise l’API useEffect pour placer automatiquement le focus sur le premier champ en erreur (pense au ref sur inputProps.id).

Prends le temps de tester ; la suite sera encore plus puissante avec Zod et Conform.

Premium
Quiz interactif
Testez vos connaissances et validez votre compréhension du module avec notre quiz interactif.
1

Comprendre les concepts fondamentaux

Quelle est la principale différence entre les composants client et serveur dans React ?

Les composants client s'exécutent uniquement dans le navigateur
Les composants serveur peuvent utiliser useState
Les composants client sont plus rapides
Il n'y a aucune différence significative
2

Optimisation des performances

Quelle technique est recommandée pour éviter les rendus inutiles dans React ?

Utiliser React.memo pour les composants fonctionnels
Ajouter plus d'états locaux
Éviter d'utiliser les props
Toujours utiliser les class components
3

Architecture des données

Quel hook permet de gérer les effets de bord dans un composant React ?

useEffect
useState
useMemo
useContext
4

Gestion des erreurs

Comment implémenter la gestion des erreurs pour les requêtes API dans React ?

Utiliser try/catch avec async/await
Ignorer les erreurs
Toujours afficher un message d'erreur
Rediriger l'utilisateur
5

Déploiement et CI/CD

Quelle est la meilleure pratique pour déployer une application React en production ?

Utiliser un service CI/CD comme GitHub Actions
Copier les fichiers manuellement via FTP
Envoyer le code source complet
Ne jamais mettre à jour l'application

Débloquez ce quiz et tous les autres contenus premium en achetant ce cours