
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 soumissionmethod: 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 :
optimisticLikesse met à jour immédiatement (+1)- La requête part vers le serveur
- Si succès :
likesse met à jour avec la vraie valeur - Si erreur :
optimisticLikesrevient 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 :
- React appelle ton action avec FormData
- Le formulaire reste interactif (pas de rechargement de page)
- 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 champs | formData.get() dans les actions |
useState pour le loading | isPending des hooks |
useState pour les erreurs | Retourner l'état d'erreur de l'action |
useEffect pour les soumissions | Attribut action du formulaire |
| Mises à jour optimistes manuelles | useOptimistic |
| Context pour le statut du formulaire | useFormStatus |
Le pattern : laisse React gérer le cycle de vie, toi tu gères la logique.
La cheat sheet
| Hook/API | Ce qu'il fait | Utilise quand |
|---|---|---|
useActionState | Wrappe une action async, track state + pending | Soumissions de formulaire, toute action async |
useFormStatus | Accède au statut du formulaire parent | Composants de formulaire imbriqués (boutons, inputs) |
useOptimistic | Mises à jour UI instantanées qui reviennent en arrière si erreur | Likes, 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 native | Toute 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 :
- Hooks quotidiens → useState, useEffect, useContext
- Hooks sous-utilisés → useRef, useMemo, useCallback
- Débloqueurs de patterns → useReducer, useLayoutEffect, useImperativeHandle
- Hooks oubliés → useId, useDebugValue, useSyncExternalStore, useDeferredValue, useTransition
- 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.