React 19Next.jsServer ActionsTypeScriptApp Router

React 19 dans Next.js : useOptimistic, useActionState et use() expliqués

Découvrez les nouveaux hooks React 19 (useOptimistic, useActionState, use) et comment les utiliser avec Next.js pour des UI rapides et réactives.

AMAlexis Mouchon9 min de lecture

React 19 a introduit trois hooks qui changent la manière d'écrire des interfaces modernes : useOptimistic, useActionState et use(). Couplés aux Server Actions de Next.js, ils permettent de construire des formulaires rapides, des UI réactives et de consommer des promesses côté client sans bibliothèque tierce. Dans cet article, je vous montre comment les utiliser concrètement, avec des exemples typés en TypeScript.

Pourquoi ces hooks changent la donne

Avant React 19, gérer un formulaire avec feedback utilisateur relevait souvent de la gymnastique : un useState pour l'état de chargement, un autre pour les erreurs, un useTransition pour ne pas bloquer l'UI, et parfois une librairie comme react-hook-form ou SWR pour lisser l'expérience. React 19 simplifie radicalement ce schéma en intégrant nativement le support des actions — des fonctions asynchrones qui peuvent s'exécuter côté serveur ou client, et dont l'état est géré par React lui-même.

Dans Next.js 15 et 16, ces hooks se marient parfaitement avec les Server Actions déjà présentes dans l'App Router. Le résultat : moins de code, moins d'états à gérer manuellement, et une UX qui paraît instantanée même quand le serveur met une seconde à répondre.

useActionState : gérer l'état d'une Server Action

useActionState remplace l'ancien useFormState et sert à lier un formulaire à une Server Action tout en récupérant l'état retourné par celle-ci. Il gère automatiquement le statut pending, conserve le dernier état retourné et expose une nouvelle action à passer au formulaire.

// app/newsletter/NewsletterForm.tsx
"use client";

import { useActionState } from "react";
import { subscribeAction } from "./actions";

type FormState = {
  success: boolean;
  message: string;
};

const initialState: FormState = { success: false, message: "" };

export function NewsletterForm() {
  const [state, formAction, isPending] = useActionState(
    subscribeAction,
    initialState
  );

  return (
    <form action={formAction} className="flex flex-col gap-2">
      <input
        type="email"
        name="email"
        required
        className="rounded border px-3 py-2"
      />
      <button
        type="submit"
        disabled={isPending}
        className="rounded bg-black px-4 py-2 text-white disabled:opacity-50"
      >
        {isPending ? "Envoi..." : "S'abonner"}
      </button>
      {state.message && (
        <p className={state.success ? "text-green-600" : "text-red-600"}>
          {state.message}
        </p>
      )}
    </form>
  );
}

Côté serveur, l'action reçoit l'état précédent et le FormData :

// app/newsletter/actions.ts
"use server";

type FormState = { success: boolean; message: string };

export async function subscribeAction(
  _prev: FormState,
  formData: FormData
): Promise<FormState> {
  const email = formData.get("email");
  if (typeof email !== "string" || !email.includes("@")) {
    return { success: false, message: "Email invalide." };
  }

  // ... logique d'abonnement (DB, API, etc.)
  return { success: true, message: "Inscription confirmée !" };
}

L'indicateur isPending est particulièrement utile : plus besoin d'un useState séparé pour désactiver le bouton pendant la soumission.

useOptimistic : des UI instantanées malgré le réseau

useOptimistic permet d'afficher immédiatement un état "optimiste" pendant qu'une action s'exécute en arrière-plan. Si l'action réussit, React remplace l'état optimiste par la vraie valeur ; si elle échoue, React revient automatiquement à l'état précédent. C'est exactement ce que font Twitter ou Linear quand vous likez un tweet : la vue se met à jour avant même que le serveur ne confirme.

// app/todos/TodoList.tsx
"use client";

import { useOptimistic, useRef } from "react";
import { addTodoAction } from "./actions";

type Todo = { id: string; text: string; pending?: boolean };

export function TodoList({ todos }: { todos: Todo[] }) {
  const formRef = useRef<HTMLFormElement>(null);

  const [optimisticTodos, addOptimisticTodo] = useOptimistic<Todo[], string>(
    todos,
    (current, newText) => [
      ...current,
      { id: crypto.randomUUID(), text: newText, pending: true },
    ]
  );

  async function handleSubmit(formData: FormData) {
    const text = formData.get("text") as string;
    addOptimisticTodo(text);
    formRef.current?.reset();
    await addTodoAction(text);
  }

  return (
    <div>
      <form ref={formRef} action={handleSubmit} className="flex gap-2">
        <input name="text" required className="rounded border px-3 py-2" />
        <button className="rounded bg-blue-600 px-4 py-2 text-white">
          Ajouter
        </button>
      </form>

      <ul className="mt-4 space-y-1">
        {optimisticTodos.map((todo) => (
          <li
            key={todo.id}
            className={todo.pending ? "opacity-50 italic" : ""}
          >
            {todo.text}
          </li>
        ))}
      </ul>
    </div>
  );
}

L'utilisateur voit son todo apparaître instantanément (en italique pour marquer le caractère temporaire), et dès que l'action serveur termine, la liste est remplacée par les vraies données de la base.

Quand l'utiliser — et quand l'éviter

useOptimistic est idéal pour les actions à forte probabilité de succès et sans effet critique : likes, ajout à un panier, réorganisation d'une liste. En revanche, pour un paiement ou une action irréversible, mieux vaut attendre la confirmation serveur : afficher un état "validé" avant que l'argent ne soit débité crée de la confusion.

use() : consommer une promesse ou un contexte côté client

Le hook use() est sans doute le plus polyvalent des trois. Il peut lire la valeur d'une promesse ou d'un contexte, et fonctionne avec Suspense : tant que la promesse n'est pas résolue, le composant suspend son rendu et laisse le fallback s'afficher.

// app/products/[id]/ProductPanel.tsx
"use client";

import { use } from "react";

type Product = { id: string; name: string; price: number };

export function ProductPanel({
  productPromise,
}: {
  productPromise: Promise<Product>;
}) {
  const product = use(productPromise);

  return (
    <div className="rounded border p-4">
      <h2 className="text-lg font-semibold">{product.name}</h2>
      <p className="text-gray-600">{product.price.toFixed(2)} €</p>
    </div>
  );
}

Le composant parent (un Server Component) peut alors transmettre la promesse sans l'attendre :

// app/products/[id]/page.tsx
import { Suspense } from "react";
import { ProductPanel } from "./ProductPanel";
import { fetchProduct } from "@/lib/api";

export default function Page({ params }: { params: { id: string } }) {
  const productPromise = fetchProduct(params.id);

  return (
    <Suspense fallback={<p>Chargement du produit...</p>}>
      <ProductPanel productPromise={productPromise} />
    </Suspense>
  );
}

Ce pattern permet de démarrer la requête côté serveur tout en la consommant côté client — ce qui était impossible avant React 19 sans passer par un data fetching lib. C'est également la clé du streaming : la page répond immédiatement, et les blocs se remplissent au fil des données.

Contrairement à useEffect ou à useState, use() peut être appelé conditionnellement (dans une boucle ou après un if), ce qui ouvre la porte à des patterns jusqu'ici interdits par les règles des hooks.

Combiner les trois hooks dans un cas réel

Imaginons une app de commentaires où chaque utilisateur peut poster, et où la liste se met à jour en temps réel. On combine :

Le résultat tient en moins de 80 lignes et ne nécessite ni useState, ni useEffect, ni librairie externe. Pour les projets Next.js modernes, c'est un énorme gain de lisibilité et de maintenabilité — surtout quand plusieurs développeurs interviennent sur la codebase.

Pièges à éviter

Quelques points d'attention quand vous intégrez ces hooks dans vos projets :

Conclusion

useActionState, useOptimistic et use() forment un trio cohérent qui simplifie la gestion d'état, accélère l'UX et réduit la surface de code à maintenir. Associés aux Server Actions de Next.js et aux React Server Components, ils permettent de construire des applications web modernes avec un code élégant et typé de bout en bout.

Si vous démarrez un nouveau projet Next.js, adoptez-les dès le départ : vous gagnerez du temps sur les formulaires, les listes dynamiques et le data fetching côté client. Et si vous maintenez une app plus ancienne, migrez progressivement — le gain en lisibilité est immédiat.

Vous avez un projet web sur lequel vous aimeriez intégrer ces bonnes pratiques, ou un site à refondre avec la stack Next.js ? N'hésitez pas à me contacter pour en discuter.