useOptimistic est un hook de React 19 qui permet à l'écran d'afficher le résultat d'une action avant qu'elle se termine. Vous définissez une valeur optimiste dans une action ; React l'affiche tant que l'action est en attente, puis revient à l'état réel. Si la requête a échoué, l'état réel n'a jamais changé, donc l'écran revient en arrière tout seul.
La fausse API de cet exemple remplace une vraie requête et échoue un appel sur trois, pour que vous puissiez voir les deux issues.
Cliquez trois fois sur le bouton, en attendant une seconde entre les clics. Les deux premiers basculent tout de suite et la ligne enregistrée suit une seconde plus tard. Le troisième bascule aussi tout de suite, puis revient en arrière quand la requête échoue. Remplacez calls % 3 par calls % 2 et un clic sur deux échoue.
La syntaxe
const [optimisticState, setOptimistic] = useOptimistic(state, updateFn?);
stateest la vraie valeur, généralement issue deuseState, des props ou deuseActionState. Quand aucune action n'est en attente,optimisticStateest exactement cette valeur.setOptimistic(value)définit la valeur optimiste pour toute la durée de l'action en cours.updateFnest facultatif :(currentState, optimisticValue) => nextState. Avec lui,setOptimisticreçoit un changement plutôt qu'une valeur entièrement nouvelle. Voir la forme reducer plus bas.
Le cycle de vie d'un clic est toujours le même :
- Dans une action, vous appelez
setOptimistic. L'écran se met à jour tout de suite. - L'action attend la requête.
- En cas de succès, vous mettez à jour l'état réel (dans
startTransitionquand cela vient après unawait). - L'action se termine.
optimisticStatesuit à nouveaustate: la valeur enregistrée en cas de succès, l'ancienne valeur en cas d'échec.
L'étape 4 explique pourquoi il n'y a aucun code de retour en arrière dans l'exemple. Le bloc catch ne fait que définir un message d'erreur.
Il doit s'exécuter dans une action
La valeur optimiste n'existe que pendant qu'une transition est en attente, donc setOptimistic doit être appelé dans une transition. Tous ces cas comptent :
- la fonction que vous passez à
startTransition(ou austartTransitiondeuseTransition), - une fonction passée à
<form action={...}>ou<button formAction={...}>, - l'action que vous donnez à useActionState.
Appelée depuis un simple onClick, la valeur optimiste n'a pas d'action dans laquelle vivre. React l'annule aussitôt, et en développement React affiche "An optimistic state update occurred outside a transition or action".
Les mises à jour de l'état réel qui ont lieu après un await doivent être enveloppées dans leur propre startTransition, comme startTransition(() => setLiked(next)) ci-dessus. Après un await, React ne peut pas savoir que vous êtes toujours dans la transition précédente.
Une liste de messages avec la forme reducer
Quand l'état optimiste est une liste, passez une fonction de mise à jour en second argument. Le setter reçoit alors le nouvel élément, et React l'ajoute à ce qu'est la vraie liste à ce moment-là.
Envoyez trois messages avec des textes différents, un par un. Chacun apparaît estompé avec "(sending...)" et devient net une fois livré ; le troisième disparaît du chat et apparaît comme "Not sent". Envoyez-les plutôt rapidement et les trois restent estompés jusqu'à la fin de la dernière requête, car React garde les valeurs optimistes jusqu'à ce que toutes les actions en attente soient terminées. L'action de formulaire est déjà une transition, donc addOptimistic n'a pas besoin ici de son propre startTransition.
Donnez à chaque élément optimiste une key qui n'entrera pas en conflit avec les vrais éléments. L'exemple utilise pending- suivi du texte, donc envoyer deux fois le même texte pendant que les deux sont en attente provoquerait un conflit ; une vraie application créerait un identifiant côté client.
Plusieurs mises à jour à la fois
Comme la fonction de mise à jour reçoit l'état actuel, les mises à jour optimistes s'empilent. Cliquez rapidement et chaque clic en attente s'applique par-dessus le précédent.
Cliquez quatre fois rapidement. Le panier affiche 4 tout de suite. Quand les requêtes se terminent, le compteur se stabilise à 3, car la troisième requête a échoué et la console le signale. React garde les valeurs optimistes jusqu'à ce que toutes les actions en attente soient terminées, puis affiche le vrai compteur.
Quand l'utiliser, et quand l'éviter
Les mises à jour optimistes conviennent aux actions qui réussissent presque toujours et sont faciles à annuler : likes, étoiles, bascules, renommage, ajout d'un message. Dans le cas courant, l'utilisateur ne voit aucun spinner.
Évitez-les là où un faux « c'est fait » induirait en erreur : paiements, suppression d'un compte, tout ce sur quoi l'utilisateur pourrait agir avant de connaître le résultat. Dans ces cas, affichez un état d'attente avec isPending issu de useActionState ou de useTransition et attendez la vraie réponse.
Prévenez toujours l'utilisateur quand un retour en arrière a lieu. Une valeur qui revient en arrière sans bruit ressemble à un bug. Les deux exemples ci-dessus gardent un message d'erreur dans un état ordinaire pour qu'il survive à l'action.
Avec useActionState
useOptimistic et useActionState s'accordent bien. L'action de useActionState est déjà une transition, vous pouvez donc définir la valeur optimiste au début de celle-ci, et l'état renvoyé est la vraie valeur sur laquelle la valeur optimiste se rabat.
const [state, formAction] = useActionState(async (previous, formData) => {
const title = formData.get('title');
setOptimisticTitle(title);
const saved = await saveTitle(title);
return { title: saved.title };
}, { title: 'Untitled' });
const [optimisticTitle, setOptimisticTitle] = useOptimistic(state.title);
Pendant l'enregistrement, la page affiche le nouveau titre. Quand il se résout, state.title contient la valeur enregistrée. Si saveTitle lève une erreur, l'erreur de l'action va à l'error boundary la plus proche ; pour afficher plutôt l'ancien titre avec un message, interceptez l'erreur dans l'action et renvoyez l'état précédent avec une erreur.
Erreurs courantes
Appeler le setter en dehors d'une action. La valeur optimiste ne reste pas à l'écran, et React avertit en développement. Enveloppez l'appel dans startTransition ou déplacez-le dans une action de formulaire.
Oublier de mettre à jour l'état réel. En cas de succès, l'action doit modifier l'état que useOptimistic reflète (setLiked, setMessages). Sinon, la valeur optimiste disparaît à la fin de l'action, et une requête réussie ressemble à un retour en arrière.
Mettre à jour l'état réel après un await sans transition. La mise à jour a bien lieu, mais React peut l'afficher à un autre moment que la fin de l'action. Enveloppez-la dans startTransition, comme le font les trois exemples.
Utiliser la valeur optimiste comme source de vérité. Envoyez les requêtes et calculez les totaux à partir de l'état réel. La valeur optimiste ne sert qu'à l'affichage et peut être jetée à tout moment.
Questions fréquentes
Que fait useOptimistic ?
Il vous donne une copie d'un élément d'état que vous pouvez modifier immédiatement pendant l'exécution d'une action async. Quand l'action se termine, la copie suit à nouveau l'état réel, qui contient alors soit le résultat enregistré, soit l'ancienne valeur.
Comment useOptimistic revient-il en arrière en cas d'erreur ?
Il n'a pas besoin d'un chemin d'erreur particulier. La valeur optimiste ne vit que pendant que l'action est en attente. Si la requête échoue et que vous ne mettez jamais à jour l'état réel, le hook affiche à nouveau l'état réel, c'est-à-dire la valeur d'avant le clic.
Pourquoi ai-je l'erreur "An optimistic state update occurred outside a transition or action" ?
Le setter a été appelé depuis un simple gestionnaire d'événement. Appelez-le dans une fonction passée à startTransition, dans un <form action>, ou dans une action de useActionState.
Quel est le second argument de useOptimistic ?
Une fonction de mise à jour facultative, (currentState, optimisticValue) => newState, comme un reducer. Avec elle, le setter reçoit seulement le changement (un nouveau message, +1) et React calcule l'état optimiste, même quand plusieurs mises à jour sont en attente en même temps.
useOptimistic est-il réservé aux formulaires ?
Non. Il fonctionne dans n'importe quelle transition, donc un bouton qui appelle startTransition(async () => { ... }) peut l'utiliser aussi bien qu'une action de formulaire.