Retour au blog

React 19 : la nouvelle ère

React 19 n'est pas juste une mise à jour. C'est un changement dans notre façon de penser les formulaires, le state async et les interactions serveur. useActionState, useFormStatus, useOptimistic et use() changent tout.

4 février 202615 min de lecture
ReactReact 19HooksuseActionStateuseOptimistic
React 19 : la nouvelle ère
Le Grand Effondrement - 4 appels useState deviennent 1 useActionState
Le Grand Effondrement - 4 appels useState deviennent 1 useActionState

React 19 n'est pas juste une mise à jour. C'est un changement dans notre façon de penser les formulaires, le state async, et les interactions serveur.

Pendant des années, on a jonglé entre useState, useEffect, et des librairies tierces pour gérer les formulaires. Des états de chargement ici, des états d'erreur là, des mises à jour optimistes ailleurs. Ça marchait, mais c'était le bordel.

React 19 dit : "Et si on gérait tout ça pour vous ?"

Voyons ce qui est nouveau.


Le problème que React 19 résout

Voici un formulaire typique en React 18 :

function NewsletterForm() {
  const [email, setEmail] = useState('');
  const [isSubmitting, setIsSubmitting] = useState(false);
  const [error, setError] = useState(null);
  const [success, setSuccess] = useState(false);

  async function handleSubmit(e) {
    e.preventDefault();
    setIsSubmitting(true);
    setError(null);

    try {
      await subscribeToNewsletter(email);
      setSuccess(true);
      setEmail('');
    } catch (err) {
      setError(err.message);
    } finally {
      setIsSubmitting(false);
    }
  }

  return (
    <form onSubmit={handleSubmit}>
      <input
        value={email}
        onChange={e => setEmail(e.target.value)}
        disabled={isSubmitting}
      />
      <button disabled={isSubmitting}>
        {isSubmitting ? 'Inscription...' : "S'inscrire"}
      </button>
      {error && <p className="error">{error}</p>}
      {success && <p className="success">C'est bon !</p>}
    </form>
  );
}

Quatre appels useState. Des transitions de state manuelles. La gestion d'erreur éparpillée dans la fonction.

Maintenant voici le même formulaire en React 19 :

function NewsletterForm() {
  const [state, formAction, isPending] = useActionState(
    async (prevState, formData) => {
      const email = formData.get('email');
      try {
        await subscribeToNewsletter(email);
        return { success: true, error: null };
      } catch (err) {
        return { success: false, error: err.message };
      }
    },
    { success: false, error: null }
  );

  return (
    <form action={formAction}>
      <input name="email" disabled={isPending} />
      <button disabled={isPending}>
        {isPending ? 'Inscription...' : "S'inscrire"}
      </button>
      {state.error && <p className="error">{state.error}</p>}
      {state.success && <p className="success">C'est bon !</p>}
    </form>
  );
}

Un hook. État pending automatique. FormData géré nativement. C'est la méthode React 19.


useActionState : la révolution des formulaires

useActionState est la pièce centrale de la gestion des formulaires en React 19. Il wrappe une fonction async et te donne :

  • L'état actuel (ta valeur de retour)
  • Une action de formulaire à passer à <form action={...}>
  • Un booléen pending
const [state, formAction, isPending] = useActionState(actionFn, initialState);

Comment fonctionne la fonction action

Ton action reçoit deux arguments :

async function myAction(previousState, formData) {
  // previousState: la dernière valeur que tu as retournée
  // formData: FormData natif du formulaire

  const name = formData.get('name');

  // Fais des trucs async...

  return newState; // Ça devient le prochain previousState
}

Exemple concret : liste de todos

function TodoApp() {
  const [todos, addTodo, isPending] = useActionState(
    async (currentTodos, formData) => {
      const text = formData.get('todo');
      const newTodo = await createTodoOnServer(text);
      return [...currentTodos, newTodo];
    },
    []
  );

  return (
    <>
      <form action={addTodo}>
        <input name="todo" placeholder="Qu'est-ce qui doit être fait ?" />
        <button disabled={isPending}>
          {isPending ? 'Ajout...' : 'Ajouter'}
        </button>
      </form>
      <ul>
        {todos.map(todo => <li key={todo.id}>{todo.text}</li>)}
      </ul>
    </>
  );
}

Pas de useState pour les todos. Pas de useState pour le loading. Pas de gestion manuelle du formulaire. L'action gère tout.


useFormStatus : composants de formulaire imbriqués

Tu construis un design system. Tu as un composant <SubmitButton> utilisé dans plusieurs formulaires. Comment sait-il si son formulaire parent est en train de soumettre ?

Avant React 19 : prop drilling ou context.

React 19 : useFormStatus.

import { useFormStatus } from 'react-dom';

function SubmitButton({ children }) {
  const { pending } = useFormStatus();

  return (
    <button disabled={pending}>
      {pending ? 'Envoi...' : children}
    </button>
  );
}

Maintenant utilise-le dans n'importe quel formulaire :

function ContactForm() {
  const [state, formAction] = useActionState(submitContact, null);

  return (
    <form action={formAction}>
      <input name="email" placeholder="Email" />
      <textarea name="message" placeholder="Message" />
      <SubmitButton>Envoyer</SubmitButton>
    </form>
  );
}

Le bouton sait automatiquement quand son formulaire parent est en train de soumettre. Pas besoin de props.

Ce que retourne useFormStatus

const { pending, data, method, action } = useFormStatus();
  • pending : booléen, le formulaire est-il en train de soumettre ?
  • data : objet FormData en cours de soumission
  • method : méthode HTTP (généralement "post")
  • action : la fonction action

Important : ça doit être un enfant

useFormStatus ne fonctionne que dans les composants rendus à l'intérieur d'un <form>. Ça ne marchera pas :

// ❌ Faux : même composant que le formulaire
function Form() {
  const { pending } = useFormStatus(); // Toujours false !
  return <form>...</form>;
}

// ✅ Correct : composant enfant
function Form() {
  return (
    <form>
      <SubmitButton /> {/* useFormStatus marche ici */}
    </form>
  );
}

useOptimistic : feedback instantané

Les utilisateurs détestent attendre. Quand ils cliquent sur "Like", ils veulent voir le cœur se remplir immédiatement. Pas après la réponse du serveur.

C'est l'UI optimiste. Et React 19 rend ça trivial.

function LikeButton({ postId, initialLikes }) {
  const [likes, setLikes] = useState(initialLikes);
  const [optimisticLikes, addOptimisticLike] = useOptimistic(
    likes,
    (current, increment) => current + increment
  );

  async function handleLike() {
    addOptimisticLike(1); // Mise à jour UI instantanée

    try {
      const newLikes = await likePostOnServer(postId);
      setLikes(newLikes); // Confirme avec les vraies données
    } catch (error) {
      // useOptimistic revient automatiquement en arrière en cas d'erreur
    }
  }

  return (
    <button onClick={handleLike}>
      ❤️ {optimisticLikes}
    </button>
  );
}

Clique sur le bouton :

  1. optimisticLikes se met à jour immédiatement (+1)
  2. La requête part vers le serveur
  3. Si succès : likes se met à jour avec la vraie valeur
  4. Si erreur : optimisticLikes revient automatiquement en arrière

Exemple concret : todo avec ajout optimiste

function TodoList() {
  const [todos, setTodos] = useState([]);
  const [optimisticTodos, addOptimisticTodo] = useOptimistic(
    todos,
    (current, newTodo) => [...current, { ...newTodo, pending: true }]
  );

  async function addTodo(formData) {
    const text = formData.get('text');
    const tempTodo = { id: crypto.randomUUID(), text, pending: true };

    addOptimisticTodo(tempTodo);

    const savedTodo = await saveTodoToServer(text);
    setTodos(current => [...current, savedTodo]);
  }

  return (
    <>
      <form action={addTodo}>
        <input name="text" />
        <button>Ajouter</button>
      </form>
      <ul>
        {optimisticTodos.map(todo => (
          <li key={todo.id} style={{ opacity: todo.pending ? 0.5 : 1 }}>
            {todo.text}
          </li>
        ))}
      </ul>
    </>
  );
}

Les nouveaux todos apparaissent instantanément (grisés), puis deviennent solides une fois confirmés.


use() : lire des promises et du context

use() est bizarre. Ce n'est pas un hook (pas de règles "use" prefix), mais on l'appelle comme un hook.

Il fait deux choses :

1. Lire des promises (avec Suspense)

function UserProfile({ userPromise }) {
  const user = use(userPromise); // Suspend jusqu'à résolution

  return <h1>{user.name}</h1>;
}

// Le parent wrappe dans Suspense
function App() {
  const userPromise = fetchUser(userId);

  return (
    <Suspense fallback={<Loading />}>
      <UserProfile userPromise={userPromise} />
    </Suspense>
  );
}

Pas de useEffect. Pas d'état de chargement. Juste lire la promise, et React gère le reste.

2. Lire le context conditionnellement

Contrairement à useContext, tu peux appeler use() dans des conditions :

function Component({ showTheme }) {
  if (showTheme) {
    const theme = use(ThemeContext); // ✅ Ça marche !
    return <div className={theme}>Thémé</div>;
  }
  return <div>Pas de thème</div>;
}

Avec useContext, ça serait une erreur. Les hooks ne peuvent pas être appelés conditionnellement. Mais use() peut.


Form actions : l'upgrade de <form>

React 19 ajoute le support natif de l'attribut action sur les formulaires :

<form action={myServerAction}>
  <input name="email" />
  <button>Envoyer</button>
</form>

Quand le formulaire est soumis :

  1. React appelle ton action avec FormData
  2. Le formulaire reste interactif (pas de rechargement de page)
  3. En cas de succès, React reset automatiquement les inputs non contrôlés

Ça marche aussi avec les Server Actions (dans des frameworks comme Next.js) :

// actions.js
'use server';

export async function subscribe(formData) {
  const email = formData.get('email');
  await db.subscribers.create({ email });
}

// component.jsx
import { subscribe } from './actions';

function Newsletter() {
  return (
    <form action={subscribe}>
      <input name="email" />
      <button>S'inscrire</button>
    </form>
  );
}

L'action s'exécute sur le serveur. Pas besoin de route API.


Le changement de modèle mental

React 19 veut que tu penses différemment :

Avant (React 18)Après (React 19)
useState pour les champsformData.get() dans les actions
useState pour le loadingisPending des hooks
useState pour les erreursRetourner l'état d'erreur de l'action
useEffect pour les soumissionsAttribut action du formulaire
Mises à jour optimistes manuellesuseOptimistic
Context pour le statut du formulaireuseFormStatus

Le pattern : laisse React gérer le cycle de vie, toi tu gères la logique.


La cheat sheet

Hook/APICe qu'il faitUtilise quand
useActionStateWrappe une action async, track state + pendingSoumissions de formulaire, toute action async
useFormStatusAccède au statut du formulaire parentComposants de formulaire imbriqués (boutons, inputs)
useOptimisticMises à jour UI instantanées qui reviennent en arrière si erreurLikes, ajouts, toute mise à jour "optimiste"
use()Lit des promises ou du context (conditionnellement)Data fetching avec Suspense, context conditionnel
<form action>Gestion de formulaire async nativeToute soumission de formulaire

Faut-il upgrader ?

React 19 est prêt pour la production. Mais voici mon avis :

Upgrade maintenant si :

  • Tu commences un nouveau projet
  • Tu utilises déjà Next.js App Router
  • Les formulaires sont un point de douleur dans ton app

Attends si :

  • Ton app marche bien et les formulaires ne sont pas complexes
  • Tu dépends fortement de librairies qui n'ont pas encore été mises à jour
  • Tu n'as pas le temps d'apprendre les nouveaux patterns

Les nouveaux hooks sont optionnels. Ton code React 18 marche toujours. Mais une fois que tu auras essayé useActionState, tu ne voudras plus revenir en arrière.


Conclusion de la série

On a couvert tous les hooks React :

  1. Hooks quotidiens → useState, useEffect, useContext
  2. Hooks sous-utilisés → useRef, useMemo, useCallback
  3. Débloqueurs de patterns → useReducer, useLayoutEffect, useImperativeHandle
  4. Hooks oubliés → useId, useDebugValue, useSyncExternalStore, useDeferredValue, useTransition
  5. Hooks React 19 → useActionState, useFormStatus, useOptimistic, use()

C'est la boîte à outils complète. Tu en sais maintenant plus sur les hooks React que 90% des développeurs.

La question n'est plus "quel hook dois-je apprendre ?" C'est "quel hook résout mon problème ?"

Bon code.


Des questions ? Trouvé un bug dans mon code ? Contacte-moi sur LinkedIn ou découvre plus sur mon blog.