Comment soumettre un formulaire dans une fonction
Apprends à déclencher un formulaire manuellement avec useFetcher et useSubmit : plusieurs actions par page, champs dynamiques, validation serveur et typage précis.
Pourquoi sortir du <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 :
- Tu as plusieurs actions indépendantes dans la même page (supprimer un élément, liker, archiver…) et tu veux un indicateur d’envoi pour chacune.
- Tu dois injecter une valeur dynamique au dernier moment (ex. jeton reCAPTCHA, timestamp, signature HMAC).
- Tu souhaites factoriser la logique dans une fonction réutilisable
au lieu de répéter un
<Form>à chaque ligne d’un tableau. - Tu veux bénéficier d’un typage TypeScript strict sur le payload
envoyé à l’
action.
Pour ces cas-là, place au duo useFetcher() et useSubmit() !
Utiliser fetcher.submit pour une action locale
useFetcher() 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}
Ce qu’il faut retenir
fetcher.submitaccepte un objet littéral – Remix le transforme enFormDatapour toi.- Tu précises la
methodet leactioncible ; aucune navigation n’est déclenchée. fetcher.statevautsubmitting→ parfait pour afficher un loader.
useSubmit : même puissance, navigation incluse
Parfois 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é.
Ajouter un champ « fantôme » avant l’envoi
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.
Super-pouvoirs TypeScript : typer le payload
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.
Résumé des points clés
<Form>: parfait pour un seul submit, DX ultra-simple.useFetcher(): plusieurs mini-actions indépendantes sans navigation ; chaque fetcher expose son proprestate.useSubmit(): navigation classique mais déclenchée programmatique ; idéal pour ajouter des champs au dernier moment.- Typage Zod +
infer= payload sécurisé côté client et serveur. - Tu peux factoriser
submitUserDelete(slug)et la réutiliser partout dans l’app. - Les API sont exactement les mêmes que
<Form>: tu gardes la même validation, le même cycleloader ⇒ action ⇒ loader.
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 ?