---
title: "Comment soumettre plusieurs formulaires en parallèle"
description: "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."
url: https://algomax.fr/formations/react-router-7/maitrise-les-formulaires-dans-react-router-7/comment-soumettre-plusieurs-formulaires-en-parallele
dateModified: 2025-05-21T20:05:52.376Z
---

# 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](https://reactrouter.com) 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 seul `submitting` pour 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

```ts app/routes/users.$userSlug.tsx
export async function action({ request, params }: ActionFunctionArgs) {
  const fd   = await request.formData()
  const slug = params.userSlug

  if (fd.get('action') === 'delete-user') {
    await deleteUser({ slug })            // 🚮  suppression BDD
    return redirect('/users')             // on reste sur /users
  }

  /* … autres mutations … */
}
```

### 2. Instancie le fetcher côté client

```tsx app/components/UserItem.tsx
import { useFetcher, href } from "react-router"

export function UserItem({ user }: { user: User }) {
  const fetcher = useFetcher<typeof action>()

  const isSubmitting = fetcher.state === "submitting"

  return (
    <li className={`flex gap-2 ${isSubmitting ? "opacity-50" : ""}`}>
      {/* lien classique vers la page de détail */}
      <a className="flex-1" href={href('/users/:userSlug',{userSlug:user.slug})}>
        {user.firstName} {user.lastName}
      </a>

      {/* le (mini) formulaire isolé */}
      <fetcher.Form
        method="POST"
        action={href('/users/:userSlug',{userSlug:user.slug})}
        className="flex gap-2"
      >
        <input type="hidden" name="action" value="delete-user" />
        <button type="submit" disabled={isSubmitting}
                className="bg-red-500 text-white px-2 py-1 rounded">
          Supprimer
        </button>
      </fetcher.Form>

      {/* affiche les éventuelles erreurs */}
      {fetcher.data?.result?.error && (
        <p className="text-xs text-red-600">{fetcher.data.result.error}</p>
      )}
    </li>
  )
}
```

Points importants :

* chaque appel à `useFetcher()` retourne **son propre `state`**
  → 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 à
  ```tsx
  <fetcher.Form preventScrollReset />
  ```
  (non-nécessaire dans l’exemple, mais bon réflexe).

***

## Optimistic UI : masquer la ligne instantanément

La promesse d’un fetcher étant locale, tu peux donner l’illusion d’une
suppression immédiate :

```tsx app/components/UserItem.tsx {3,11-13}
const isDeleting = fetcher.state === "submitting"
if (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 :

```json
{ "result": { "error": "L'utilisateur n'existe pas." } }
```

Coté client :

```tsx app/components/UserItem.tsx {22-25}
if (fetcher.state === "idle" && fetcher.data?.result?.error) {
  toast.error(fetcher.data.result.error)
}
```

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** :

```tsx app/routes/users._usersLayout.tsx
const fetcher = useFetcher()

function handleBulkDelete(ids: string[]) {
  fetcher.submit(
    { action: "bulk-delete", ids },
    { method: "POST", action: "/users" }
  )
}
```

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 `state` est local : plusieurs mutations tournent en parallèle sans se bloquer.
* Ajoute `preventScrollReset` si tu veux maintenir la position de l’utilisateur.
* `fetcher.data` reflè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 »).
