Accueil / Articles

Formulaires asynchrones dans React 19 avec use, useActionState et useOptimistic

Comment use(), useActionState, useFormStatus et useOptimistic remplacent le chargement manuel ainsi que les indicateurs d’erreur dans React 19, avec les pièges que chacun de ces hooks cache discrètement.

Éditeur : REACTAPP.TOP4 min de lecture978 mots

Sources: React 19’s New Hooks: A Practical Guide

Au préalable de React 19, presque toutes les requêtes ou formulaires nécessitaient la même structure de base : useState pour les données, useEffect pour les charger, ainsi que des indicateurs pour l’état de chargement, les erreurs et les résultats. React 19 ajoute quatre API qui intégrent ces fonctionnalités directement dans le framework : use(), useActionState, useFormStatus et useOptimistic. Voici ce que chacune d’elles fait, comment elles s’articulent entre elles, ainsi que les contraintes qui peuvent facilement être négligées. Pour une vue d’ensemble plus complète des mises à niveau, consultez React 19 upgrade triage.

Lecture des promesses pendant le rendu avec use()

use() n’est pas un crochet au sens traditionnel. Contrairement aux crochets, il peut être appelé à l’intérieur de conditions, de boucles ou après un retour prématuré. Il lit la valeur d’une Promise ou d’un Context pendant le rendu et coopère avec Suspense pour afficher un état en attente.

import { use, Suspense } from "react";

UserCard reçoit une promise et la déballonne à l’aide de use ; tant qu’elle n’est pas résolue, le plus proche bordure Suspense dans Page affiche son contenu de remplacement.

function UserCard({ userPromise }) {
  const user = use(userPromise);
  return <h2>{user.name}</h2>;
}function Page({ userPromise }) {
  return (
    <Suspense fallback={<p>Loading user…</p>}>
      <UserCard userPromise={userPromise} />
    </Suspense>
  );
}

Le composant parent crée la promise, le composant enfant la consomme, et Suspense gère l’état de chargement. Une contrainte est importante : la promise doit être stable. Une promise créée pendant le rendu d’un composant client est nouvelle à chaque rendu et provoque une suspension répétée ; il faut donc la créer dans un composant serveur, un chargeur mémorisé ou une API de données du framework.

Gestion des soumissions avec useActionState

useActionState remplace les états isSubmitting, error et result gérés manuellement. Vous passez une action ainsi qu’un état initial ; vous obtenez l’état actuel, une action encapsulée pour le formulaire, et un indicateur d’attente. L’action reçoit l’état précédent ainsi que le FormData, et ce qu’elle renvoie devient l’état suivant.

import { useActionState } from "react";

Ici, submitFeedback valide le message, le sauvegarde, puis renvoie soit un indicateur d’erreur soit un indicateur de succès, que le formulaire affiche directement.

async function submitFeedback(prevState, formData) {
  const message = formData.get("message");
  if (message.length < 5) {
    return { error: "Message is too short." };
  }
  await saveFeedback(message);
  return { success: true };
}function FeedbackForm() {
  const [state, formAction, isPending] = useActionState(submitFeedback, {});  return (
    <form action={formAction}>
      <textarea name="message" required />
      <button disabled={isPending}>
        {isPending ? "Sending…" : "Send feedback"}
      </button>
      {state.error && <p className="error">{state.error}</p>}
      {state.success && <p>Thanks for the feedback!</p>}
    </form>
  );
}

Puisque l’action est affectée à l’attribut natif action du formulaire, cela permet une amélioration progressive. Notez que soumettre avant l’hydratation ne fonctionne que lorsque l’action est une fonction serveur ; une simple fonction client nécessite que JavaScript soit chargé. De plus, formData.get renvoie null pour un champ manquant, il faut donc le gérer dans du code réel.

Lecture de l’état du formulaire depuis n’importe quel enfant avec useFormStatus

Un bouton de soumission situé profondément dans un formulaire a souvent besoin de savoir si une soumission est en cours, et transmettre isPending à chaque niveau est fastidieux. useFormStatus, importé de react-dom, lit l’état du parent <form> le plus proche.

import { useFormStatus } from "react-dom";

SubmitButton ne prend aucune propriété ; il récupère l’état en attente depuis le formulaire qui l’entoure, ce qui en fait un composant idéal pour des boutons partagés.

function SubmitButton() {
  const { pending } = useFormStatus();
  return <button disabled={pending}>{pending ? "Saving…" : "Save"}</button>;
}function SettingsForm({ action }) {
  return (
    <form action={action}>
      <input name="username" />
      <SubmitButton />
    </form>
  );
}

Le hook doit être appelé dans un composant rendu à l’intérieur de <form>. L’appel dans le composant qui rend lui-même le formulaire ne renvoie aucun état en attente.

Affichage précoce des résultats attendus avec useOptimistic

useOptimistic affiche immédiatement le résultat anticipé d’une action pendant que la requête réelle est en cours.

import { useOptimistic, useRef } from "react";

Dans TodoList, le hook dérive optimisticTodos à partir de la propriété todos, en y ajoutant des entrées marquées pending: true. L’action du formulaire ajoute l’élément optimiste, réinitialise le formulaire via un ref, puis attend l’action réelle du serveur. Les éléments en attente s’affichent avec une opacité réduite.

function TodoList({ todos, addTodoAction }) {
  const [optimisticTodos, addOptimisticTodo] = useOptimistic(
    todos,
    (state, newTodo) => [...state, { ...newTodo, pending: true }]
  );
  const formRef = useRef(null);  async function handleSubmit(formData) {
    const title = formData.get("title");
    addOptimisticTodo({ id: Date.now(), title });
    formRef.current?.reset();
    await addTodoAction(formData);
  }  return (
    <div>
      <form ref={formRef} action={handleSubmit}>
        <input name="title" required />
        <button type="submit">Add</button>
      </form>
      <ul>
        {optimisticTodos.map((todo) => (
          <li key={todo.id} style={{ opacity: todo.pending ? 0.5 : 1 }}>
            {todo.title}
          </li>
        ))}
      </ul>
    </div>
  );
}

Le modèle mental est précis : l’état optimiste n’existe que tant que l’action est en cours d’exécution. Lorsqu’elle se termine, React le supprime et affiche ce que todos contient actuellement. En cas de succès, le composant parent doit fournir une version mise à jour de todos (par exemple via une révalidation), sinon l’élément disparaît ; en cas d’échec, il disparaît automatiquement, ce qui constitue le mécanisme de rollback. Les messages d’erreur doivent néanmoins être gérés. L’article sur les modes d’échec de useOptimistic rollback aborde les cas limites.

Bonus : ref en tant que prop ordinaire

React 19 permet également aux composants fonctionnels de recevoir ref en tant que prop ordinaire, ce qui rend forwardRef inutile dans la plupart des cas. Cela élimine beaucoup de couches supplémentaires dans les bibliothèques de composants.

Points clés

  • use() déballle les promesses et le contexte lors du rendu avec Suspense, mais la promesse doit être stable.
  • useActionState regroupe l’état, le résultat et un indicateur d’action en attente dans un seul hook basé sur les formulaires.
  • useFormStatus lit l’état du formulaire contenant celui-ci à partir d’un descendant, éliminant ainsi le besoin de parcours profond des propriétés.
  • useOptimistic affiche les résultats attendus pendant une action et s’appuie sur des propriétés fraîches pour les conserver.
  • Au préalable de reconstruire une combinaison de useState et useEffect, vérifiez si l’un de ces cas s’applique.

Lectures complémentaires