title: "Les nouveaux hooks React 19 : use, useOptimistic et useActionState" description: "Guide pratique des quatre nouveaux hooks React 19 dans Next.js App Router : use(), useActionState, useOptimistic et useFormStatus pour des formulaires modernes." date: "2026-06-07" category: "Ingénierie" readingTime: "8 min" author: "Oliwer Fortin, Tech Lead chez Kreio"
React 19 officialise quatre hooks qui simplifient la gestion des données asynchrones et des formulaires dans les projets Next.js App Router : use(), useActionState, useOptimistic et useFormStatus. Voici comment les adopter concrètement en production, sans dépendance tierce supplémentaire.
Pourquoi React 19 change la gestion des formulaires
Avant React 19, un formulaire connecté à une API nécessitait au minimum trois états locaux distincts : loading, error et data. Chaque mutation était un assemblage de useState, useEffect et de logique de gestion d'erreur inlined dans le composant. Le code produit était verbeux et difficile à tester.
React 19 introduit le concept d'Actions. Une Action est une fonction asynchrone passée directement à l'attribut action d'un élément <form>, ou utilisée dans une transition useTransition. Elle peut s'exécuter côté client ou côté serveur (via les Server Actions de Next.js). Les nouveaux hooks ont été conçus pour fonctionner nativement avec ce modèle.
| Situation | Avant React 19 | Avec React 19 |
|---|---|---|
| État pending d'un formulaire | useState(false) pour isLoading | useFormStatus lit l'état automatiquement |
| Retour d'erreur d'une Action | try/catch + setState dans le composant | useActionState expose l'état retourné par l'Action |
| Feedback optimiste immédiat | Mise à jour manuelle avec rollback à gérer | useOptimistic gère l'état temporaire et le rollback |
| Lecture conditionnelle d'un contexte | Toujours au niveau racine du composant | use(Context) utilisable dans les branches conditionnelles |
Le hook use() : Promises et contextes conditionnels
use() est le seul hook de React 19 dont l'usage peut être conditionnel. Il peut être appelé à l'intérieur d'un bloc if, d'une boucle ou d'un callback, ce qui était interdit par les règles des hooks depuis React 16.8.
Son principal cas d'usage dans Next.js App Router : transmettre une Promise non résolue depuis un Server Component vers un Client Component.
// app/dashboard/page.tsx (Server Component)
import { Suspense } from 'react'
import UserList from './user-list'
export default function DashboardPage() {
const usersPromise = fetchUsers() // Pas d'await : la Promise est transmise telle quelle
return (
<Suspense fallback={<p>Chargement des utilisateurs...</p>}>
<UserList usersPromise={usersPromise} />
</Suspense>
)
}
// app/dashboard/user-list.tsx (Client Component)
'use client'
import { use } from 'react'
import type { User } from '@/types'
type Props = { usersPromise: Promise<User[]> }
export default function UserList({ usersPromise }: Props) {
const users = use(usersPromise) // React suspend jusqu'à résolution
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
)
}
Ce pattern déporte le déclenchement du fetch au niveau serveur tout en conservant l'interactivité côté client. La documentation officielle React sur use() détaille les règles de stabilité des Promises à respecter.
useActionState : gérer l'état retourné par une Server Action
useActionState remplace le pattern useState + handler manuel pour les formulaires connectés à des Server Actions. Il prend une Action et un état initial, et retourne l'état courant, une version wrappée de l'Action, et un booléen isPending.
// components/contact-form.tsx
'use client'
import { useActionState } from 'react'
import { sendContactForm } from '@/actions/contact'
type FormState = { success: boolean; error?: string }
const initialState: FormState = { success: false }
export default function ContactForm() {
const [state, action, isPending] = useActionState(sendContactForm, initialState)
return (
<form action={action}>
<input name="email" type="email" required />
<textarea name="message" required />
<button type="submit" disabled={isPending}>
{isPending ? 'Envoi en cours...' : 'Envoyer'}
</button>
{state.error && <p role="alert">{state.error}</p>}
{state.success && <p>Message envoyé avec succès.</p>}
</form>
)
}
La Server Action reçoit l'état précédent en premier argument, avant formData. Ce contrat est obligatoire pour que useActionState puisse chaîner les états :
// actions/contact.ts
'use server'
import type { FormState } from '@/types'
export async function sendContactForm(
prevState: FormState,
formData: FormData
): Promise<FormState> {
const email = formData.get('email') as string
const message = formData.get('message') as string
if (!email || !message) {
return { success: false, error: 'Tous les champs sont obligatoires.' }
}
await emailService.send({ to: email, body: message })
return { success: true }
}
Le gain est direct : le composant n'a plus besoin de gérer isLoading, ni d'intercepter les erreurs réseau. L'état d'erreur remonte du serveur vers le client via la valeur de retour de l'Action, sans serialisation manuelle.
useOptimistic : feedback instantané avant confirmation serveur
useOptimistic permet d'afficher immédiatement le résultat attendu d'une mutation. Si la requête échoue, React revient automatiquement à l'état précédent, sans code de rollback à écrire.
La logique repose sur une simple observation : si l'opération prend millisecondes côté réseau, l'utilisateur perçoit un délai de ms grâce à la mise à jour optimiste, contre ms sans ce pattern. Sur une latence mobile typique de 200 à 400 ms, l'impact sur la fluidité perçue est significatif.
'use client'
import { useOptimistic, useTransition } from 'react'
import { toggleFavorite } from '@/actions/favorites'
import type { Article } from '@/types'
export default function FavoriteButton({ article }: { article: Article }) {
const [optimisticArticle, setOptimistic] = useOptimistic(
article,
(current, isFavorite: boolean) => ({
...current,
isFavorite,
favoriteCount: current.favoriteCount + (isFavorite ? 1 : -1),
})
)
const [, startTransition] = useTransition()
function handleToggle() {
startTransition(async () => {
setOptimistic(!optimisticArticle.isFavorite)
await toggleFavorite(article.id)
})
}
return (
<button onClick={handleToggle} aria-pressed={optimisticArticle.isFavorite}>
{optimisticArticle.isFavorite ? 'Retirer des favoris' : 'Ajouter aux favoris'}
({optimisticArticle.favoriteCount})
</button>
)
}
La documentation React sur useOptimistic recommande de toujours envelopper l'appel dans startTransition pour éviter des états incohérents entre le rendu optimiste et la réponse serveur.
useFormStatus : lire l'état de soumission depuis un composant enfant
useFormStatus est importé depuis react-dom et non depuis react. Il lit l'état de soumission du formulaire parent le plus proche dans l'arbre de rendu. Son usage canonique est la création d'un bouton de soumission réutilisable et accessible :
// components/submit-button.tsx
'use client'
import { useFormStatus } from 'react-dom'
type Props = { label: string; pendingLabel?: string }
export function SubmitButton({ label, pendingLabel = 'En cours...' }: Props) {
const { pending } = useFormStatus()
return (
<button type="submit" disabled={pending} aria-busy={pending}>
{pending ? pendingLabel : label}
</button>
)
}
Le composant SubmitButton s'utilise ensuite dans n'importe quel formulaire sans passer isPending en prop :
<form action={action}>
<input name="name" type="text" />
<SubmitButton label="Sauvegarder" pendingLabel="Sauvegarde en cours..." />
</form>
Récapitulatif des quatre hooks
| Hook | Module | Cas d'usage principal |
|---|---|---|
use() | react | Lire une Promise ou un Context conditionnellement |
useActionState | react | Gérer l'état et les erreurs d'une Action de formulaire |
useOptimistic | react | Afficher un résultat immédiat avant confirmation serveur |
useFormStatus | react-dom | Lire le statut de soumission depuis un composant enfant |
En pratique
React 19 ne remplace pas React Query ou SWR pour les cas complexes : cache côté client persistant, requêtes paginées avec curseur, invalidation fine par clé, ou synchronisation en temps réel. En revanche, pour les formulaires connectés à des Server Actions dans Next.js App Router, useActionState et useFormStatus sont désormais le choix natif. Ils réduisent le nombre de lignes de code, améliorent l'accessibilité via les attributs aria-busy et aria-disabled, et s'intègrent sans friction au modèle serveur de Next.js.
Pour les mutations avec feedback immédiat, useOptimistic élimine le code de rollback manuel. Le hook use(), de son côté, simplifie les patterns de streaming entre Server Components et Client Components. Sur les pages à fort contenu dynamique, déporter le fetch côté serveur via une Promise non résolue réduit le Time to First Byte et améliore les Core Web Vitals.
La migration est progressive : adopter useFormStatus dans un formulaire existant ne nécessite pas de refactoriser l'ensemble de l'application.