Comment soumettre plusieurs formulaires en parallèle
Intègre plusieurs formulaires indépendants sur une même page avec useFetcher() pour éviter les scrolls, gérer les états localement et offrir une UX fluide et réactive.
Pourquoi intégrer plusieurs formulaires dans une même page ?
Sur une seule et même page, tu peux vouloir : - éditer un utilisateur
- supprimer un utilisateur
- créer un compte
Le tout, sans changer d'onglet ni remonter en haut du viewport à chaque clic.
Le composant <Form> de React Router 7 gère très bien un
formulaire unique, mais dès que tu enchaînes les actions simultanées
tu te heurtes à deux limites :
- le navigateur recalcule le scroll après chaque POST
useNavigation()expose l’état global : un seulsubmittingpour toute la page.
C’est là qu’entre en scène le hook useFetcher() :
il te donne un mini-client HTTP local à chaque formulaire.
Mettre en place un fetcher par ligne de tableau
1. Crée ton action serveur
1export async function action({ request, params }: ActionFunctionArgs) {2const fd = await request.formData()3const slug = params.userSlug45if (fd.get('action') === 'delete-user') {6await deleteUser({ slug }) // 🚮 suppression BDD7return redirect('/users') // on reste sur /users8}910/* … autres mutations … */11}
2. Instancie le fetcher côté client
1import { useFetcher, href } from "react-router"23export function UserItem({ user }: { user: User }) {4const fetcher = useFetcher<typeof action>()56const isSubmitting = fetcher.state === "submitting"78return (9<li className={`flex gap-2 ${isSubmitting ? "opacity-50" : ""}`}>10{/* lien classique vers la page de détail */}11<a className="flex-1" href={href('/users/:userSlug',{userSlug:user.slug})}>12{user.firstName} {user.lastName}13</a>1415{/* le (mini) formulaire isolé */}16<fetcher.Form17method="POST"18action={href('/users/:userSlug',{userSlug:user.slug})}19className="flex gap-2"20>21<input type="hidden" name="action" value="delete-user" />22<button type="submit" disabled={isSubmitting}23className="bg-red-500 text-white px-2 py-1 rounded">24Supprimer25</button>26</fetcher.Form>2728{/* affiche les éventuelles erreurs */}29{fetcher.data?.result?.error && (30<p className="text-xs text-red-600">{fetcher.data.result.error}</p>31)}32</li>33)34}
Points importants :
- chaque appel à
useFetcher()retourne son proprestate→ aucun risque que le bouton de l’utilisateur A devienne gris pendant que B est en cours de suppression ; - on évite le scroll automatique grâce à
(non-nécessaire dans l’exemple, mais bon réflexe).1<fetcher.Form preventScrollReset />
Optimistic UI : masquer la ligne instantanément
La promesse d’un fetcher étant locale, tu peux donner l’illusion d’une suppression immédiate :
1const isDeleting = fetcher.state === "submitting"2if (isDeleting) return null // 🪄 effacé côté UI
Si le serveur renvoie finalement une erreur, React Router rejouera le
loader parent, et la ligne réapparaîtra avec le message d’échec —
parfait pour conserver la source de vérité côté back-end.
Afficher un toast d’erreur depuis fetcher.data
Le serveur t’envoie un JSON :
1{ "result": { "error": "L'utilisateur n'existe pas." } }
Coté client :
1if (fetcher.state === "idle" && fetcher.data?.result?.error) {2toast.error(fetcher.data.result.error)3}
Pas besoin de remonter l’information dans un store global : le fetcher porte sa propre réponse.
Comparaison navigation globale VS fetcher local
| useNavigation() | useFetcher() | |
|---|---|---|
| State | Unique pour toute la page | 1 par formulaire |
| Scroll | remonte en haut (sauf preventScrollReset) | Aucun changement |
| Redirection | suit le header Location | idem |
| Optimistic UI | complexe (doit filtrer par form) | trivial (state==="submitting") |
Pattern avancé : déclencher sans balise <form>
Tu peux déclencher un POST depuis n’importe quel handler :
1const fetcher = useFetcher()23function handleBulkDelete(ids: string[]) {4fetcher.submit(5{ action: "bulk-delete", ids },6{ method: "POST", action: "/users" }7)8}
Pratique pour un bouton « Supprimer la sélection » qui ne s’affiche qu’au survol de la table.
Points clés à retenir
useFetcher()fournit un mini-formulaire isolé, idéal pour les listes.- Le
stateest local : plusieurs mutations tournent en parallèle sans se bloquer. - Ajoute
preventScrollResetsi tu veux maintenir la position de l’utilisateur. fetcher.datareflète la dernière réponse HTTP (succès ou erreur) — parfait pour un toast ou un message inline.- Combine-le avec Zod + Conform pour valider et typer les erreurs (cf. leçon « Validation serveur »).
Comprendre les concepts fondamentaux
Quelle est la principale différence entre les composants client et serveur dans React ?
Optimisation des performances
Quelle technique est recommandée pour éviter les rendus inutiles dans React ?
Architecture des données
Quel hook permet de gérer les effets de bord dans un composant React ?