Apprends à déclencher un formulaire manuellement avec useFetcher et useSubmit : plusieurs actions par page, champs dynamiques, validation serveur et typage précis.
avec React Router 7
Posez vos questions 24/7 à notre IA experte en React Router 7
Validez vos acquis avec des quiz personnalisés et un feedback instantané
<Form>
classique ?Le composant <Form>
de React Router 7 fait un travail remarquable :
il sérialise ton FormData
, déclenche l’action
, relance le loader
et
met à jour l’UI. Dans 90 % des cas, c’est the way to go.
Mais il existe des situations où tu vas préférer appeler la soumission toi-même :
<Form>
à chaque ligne d’un tableau.action
.Pour ces cas-là, place au duo useFetcher()
et useSubmit()
!
fetcher.submit
pour une action localeuseFetcher()
renvoie un objet avec la même API que <Form>
,
mais il n’affecte pas la navigation principale. Idéal pour un bouton
« Supprimer ».
1import { useFetcher } from "react-router"2import { z } from "zod"3import { UserDeleteSchema } from "~/routes/users+/$userSlug"45export const UserItem = ({ user }) => {6const fetcher = useFetcher() // ↙ hook local7const isSubmitting = fetcher.state === "submitting"89const handleDelete = () => {10const formData: z.infer<typeof UserDeleteSchema> = {11action: "delete-user",12slug: user.slug,13}14fetcher.submit(formData, {15method: "POST",16action: `/users/${user.slug}`,17})18}1920return (21<li className="flex justify-between items-center">22{/* …nom, avatars, etc.… */}23<button24onClick={handleDelete}25disabled={isSubmitting}26className="bg-red-600 text-white px-2 py-1 rounded"27>28{isSubmitting ? "…" : "Supprimer"}29</button>30</li>31)32}
fetcher.submit
accepte un objet littéral – Remix le transforme
en FormData
pour toi.method
et le action
cible ; aucune navigation
n’est déclenchée.fetcher.state
vaut submitting
→ parfait pour afficher un loader.useSubmit
: même puissance, navigation incluseParfois tu veux quand même naviguer (redirect, page de
confirmation…) mais sans écrire de <form>
.
useSubmit()
est ta boîte à outils :
1export default function UserButtons({ user }) {2const submit = useSubmit()34const deleteUser = () => {5submit(6{ action: "delete-user", slug: user.slug }, // payload7{ method: "POST", action: `/users/${user.slug}` }8)9}1011return (12<button onClick={deleteUser} className="btn-danger">13Supprimer14</button>15)16}
Ici la navigation principale passe en submitting
.
Côté UX c’est équivalent à un <Form>
classique, mais tu as le
contrôle programmatique sur le contenu envoyé.
Prenons un cas réel : sur algomax.fr j’utilise Google reCAPTCHA v2 pour protéger le formulaire de contact.
Le token n’est connu qu’après le clic utilisateur ; impossible de le
mettre dans un <input>
statique. Solution : construire le
FormData
à la volée !
1import { useFetcher } from "react-router"23export default function ContactForm() {4const fetcher = useFetcher()56const handleSubmit = async (e: React.FormEvent) => {7e.preventDefault()89// 1. Récupère les champs saisis10const formData = new FormData(e.currentTarget)11// 2. Appelle l’API reCAPTCHA12const token = await grecaptcha.execute(13import.meta.env.PUBLIC_RECAPTCHA_KEY,14{ action: "contact" }15)16// 3. Injecte le token17formData.set("captcha", token)18// 4. Envoie via fetcher19fetcher.submit(formData, { method: "POST", action: "/contact" })20}2122return (23<form onSubmit={handleSubmit} className="space-y-4">24{/* inputs name + email… */}25<button className="btn-primary">Envoyer</button>26</form>27)28}
Avantage : aucun champ « captcha » n’apparaît dans ton HTML, et tu
gardes la validation serveur dans l’action
.
Quand tu passes un objet littéral à submit
, TypeScript peut vérifier
que tu n’oublies rien :
1import { z } from "zod"2import { UserDeleteSchema } from "~/routes/users+/$userSlug"34export type DeleteUserPayload = z.infer<typeof UserDeleteSchema>
1const formData: DeleteUserPayload = {2action: "delete-user",3slug: user.slug, // 🛑 si tu oublies la clé => erreur compile !4}
➜ Moins de fautes de frappe, et ton backend reçoit toujours l’objet attendu.
<Form>
: parfait pour un seul submit, DX ultra-simple.useFetcher()
: plusieurs mini-actions indépendantes sans
navigation ; chaque fetcher expose son propre state
.useSubmit()
: navigation classique mais déclenchée
programmatique ; idéal pour ajouter des champs au dernier moment.infer
= payload sécurisé côté client et serveur.submitUserDelete(slug)
et la réutiliser
partout dans l’app.<Form>
: tu gardes la
même validation, le même cycle loader ⇒ action ⇒ loader
.Quelle est la principale différence entre les composants client et serveur dans React ?
Quelle technique est recommandée pour éviter les rendus inutiles dans React ?
Quel hook permet de gérer les effets de bord dans un composant React ?
Comment implémenter la gestion des erreurs pour les requêtes API dans React ?
Quelle est la meilleure pratique pour déployer une application React en production ?