Les mises à jour optimistes dans React peuvent donner l'impression d'immédiateté. Apprenez des méthodes sûres pour concilier la vérité serveur, gérer les échecs et prévenir la dérive des données.

L'UI optimiste dans React consiste à mettre à jour l'affichage comme si une modification avait déjà réussi, avant que le serveur ne la confirme. Quelqu'un clique sur « J'aime », le compteur augmente immédiatement, et la requête s'exécute en arrière-plan.
Ce retour instantané rend l'application réactive. Sur un réseau lent, c'est souvent la différence entre une sensation « vive » et « est-ce que ça a marché ? »
Le compromis, c'est la dérive des données : ce que l'utilisateur voit peut petit à petit diverger de la vérité sur le serveur. La dérive apparaît généralement sous forme d'incohérences mineures et frustrantes, dépendantes du timing et difficiles à reproduire.
Les utilisateurs remarquent la dérive quand l'interface « change d'avis » plus tard : un compteur saute puis revient en arrière, un élément apparaît puis disparaît après un rafraîchissement, une modification semble prise en compte jusqu'à ce que vous reveniez sur la page, ou deux onglets affichent des valeurs différentes.
Cela se produit parce que l'UI fait une supposition, et que le serveur peut renvoyer une vérité différente. Des règles de validation, du dédoublonnage, des vérifications de permissions, des limites de taux ou un autre appareil modifiant le même enregistrement peuvent tous changer le résultat final. Une cause fréquente est aussi les requêtes qui se chevauchent : une réponse plus ancienne arrive en dernier et écrase l'action plus récente de l'utilisateur.
Exemple : vous renommez un projet en « Q1 Plan » et l'affichez tout de suite dans l'en-tête. Le serveur peut tronquer les espaces, rejeter certains caractères ou générer un slug. Si vous ne remplacez jamais la valeur optimiste par la valeur finale du serveur, l'UI semblera correcte jusqu'au prochain rafraîchissement, quand elle « changera mystérieusement ».
L'UI optimiste n'est pas toujours le bon choix. Soyez prudent (ou évitez-la) pour l'argent et la facturation, les actions irréversibles, les changements de rôle et de permission, les workflows avec règles serveur complexes, ou tout ce qui a des effets secondaires nécessitant une confirmation explicite.
Bien utilisée, l'optimistic UI rend une application instantanée — à condition de prévoir la réconciliation, l'ordre d'application et la gestion des échecs.
L'UI optimiste fonctionne mieux quand vous séparez deux types d'état :
La plupart des dérives commencent quand une supposition locale est traitée comme une vérité confirmée.
Règle simple : si une valeur a une portée métier hors de l'écran courant, le serveur est la source de vérité. Si elle n'affecte que le comportement de l'écran (ouvert/fermé, input focus, texte de brouillon), gardez-la locale.
En pratique, conservez la vérité serveur pour des éléments comme les permissions, les prix, les soldes, l'inventaire, les champs calculés ou validés, et tout ce qui peut changer ailleurs (un autre onglet, un autre utilisateur). Gardez l'état UI local pour les brouillons, les flags « en train d'éditer », les filtres temporaires, les lignes développées et les bascules d'animation.
Certaines actions sont « sûres à deviner » parce que le serveur les accepte presque toujours et qu'elles sont faciles à inverser, comme marquer un élément comme favori ou basculer une préférence simple.
Quand un champ n'est pas sûr à deviner, vous pouvez quand même donner l'impression de rapidité sans prétendre que le changement est définitif. Conservez la dernière valeur confirmée et ajoutez un signal clair indiquant qu'une validation est en cours.
Par exemple, sur un écran CRM où vous cliquez sur « Mark as paid », le serveur peut le rejeter (permissions, validation, remboursement déjà effectué). Au lieu de réécrire instantanément tous les montants dérivés, mettez à jour le statut avec un discret « Saving… », laissez les totaux inchangés et ne mettez à jour les totaux qu'après confirmation.
Les bons patterns sont simples et cohérents : un petit badge « Saving… » à côté de l'élément modifié, désactiver temporairement l'action (ou la transformer en Annuler) jusqu'à ce que la requête soit résolue, ou marquer visuellement la valeur optimiste comme temporaire (texte plus pâle ou petit spinner).
Si la réponse serveur peut affecter beaucoup d'endroits (totaux, tri, champs calculés, permissions), le rafraîchissement est généralement plus sûr que d'essayer de patcher tout manuellement. Si c'est un petit changement isolé (renommer une note, basculer un flag), patcher localement suffit souvent.
Une règle utile : patcher uniquement la chose modifiée par l'utilisateur, puis refetcher les données dérivées, agrégées ou partagées entre écrans.
L'UI optimiste marche quand votre modèle de données distingue clairement ce qui est confirmé et ce qui reste une supposition. Si vous modélisez explicitement cet écart, les moments « pourquoi ça est revenu en arrière ? » deviennent rares.
Pour les éléments nouvellement créés, attribuez un ID client temporaire (comme temp_12345 ou un UUID), puis remplacez-le par le vrai ID serveur quand la réponse arrive. Cela permet aux listes, à la sélection et à l'état d'édition de se réconcilier proprement.
Exemple : un utilisateur ajoute une tâche. Vous la render immédiatement avec id: "temp_a1". Quand le serveur répond avec id: 981, vous remplacez l'ID en un seul endroit, et tout ce qui est keyé par ID continue de fonctionner.
Un simple flag de chargement au niveau de l'écran est trop grossier. Suivez le statut sur l'élément (ou même sur le champ) qui change. Ainsi vous pouvez afficher une UI en attente discrète, relancer uniquement ce qui a échoué et éviter de bloquer des actions non liées.
Une forme d'objet pratique :
id : réel ou temporairestatus : pending | confirmed | failedoptimisticPatch : ce que vous avez changé localement (petit et spécifique)serverValue : dernière donnée confirmée (ou un confirmedAt)rollbackSnapshot : la valeur confirmée précédente que vous pouvez restaurerLes mises à jour optimistes sont plus sûres quand vous touchez uniquement ce que l'utilisateur a réellement changé (par exemple basculer completed) au lieu de remplacer l'objet entier par une supposée « nouvelle version ». Le remplacement global peut écraser des éditions plus récentes, des champs ajoutés par le serveur ou des changements concurrents.
Une bonne mise à jour optimiste donne une sensation d'instantanéité, tout en finissant par correspondre à la vérité serveur. Traitez le changement optimiste comme temporaire et conservez assez d'informations pour le confirmer ou l'annuler en toute sécurité.
Exemple : un utilisateur modifie le titre d'une tâche dans une liste. Vous voulez que le titre change tout de suite, mais vous devez aussi gérer les erreurs de validation et le formatage côté serveur.
Appliquez le changement optimiste immédiatement dans l'état local. Stockez un petit patch (ou snapshot) pour pouvoir revenir en arrière.
Envoyez la requête avec un ID de requête (un nombre incrémental ou un ID aléatoire). C'est ainsi que vous associez les réponses à l'action qui les a déclenchées.
Marquez l'élément comme pending. Le pending n'a pas à bloquer l'UI : cela peut être un petit spinner, du texte estompé ou « Saving… ». L'essentiel est que l'utilisateur comprenne que ce n'est pas encore confirmé.
En cas de succès, remplacez les données client temporaires par la version serveur. Si le serveur a ajusté quelque chose (tronqué des espaces, changé la casse, mis à jour des timestamps), mettez à jour l'état local pour correspondre.
En cas d'échec, restaurez seulement ce que cette requête a changé et affichez une erreur locale claire. Évitez de remettre en arrière des parties non liées de l'écran.
Voici une petite forme à suivre (agnostique vis-à-vis des librairies) :
const requestId = crypto.randomUUID();
applyOptimistic({ id, title: nextTitle, pending: requestId });
try {
const serverItem = await api.updateTask({ id, title: nextTitle, requestId });
confirmSuccess({ id, requestId, serverItem });
} catch (err) {
rollback({ id, requestId });
showError("Could not save. Your change was undone.");
}
Deux détails qui évitent beaucoup de bugs : stockez l'ID de requête sur l'élément tant qu'il est pending, et ne confirmez ou n'annulez que si les IDs correspondent. Cela empêche des réponses plus anciennes d'écraser des éditions plus récentes.
L'UI optimiste se casse quand le réseau répond dans le désordre. Un échec classique : l'utilisateur édite un titre, l'édite encore immédiatement, et la première requête finit en dernier. Si vous appliquez cette réponse tardive, l'UI revient à une valeur plus ancienne.
La solution consiste à traiter chaque réponse comme « peut-être pertinente » et à ne l'appliquer que si elle correspond à l'intention utilisateur la plus récente.
Un pattern pratique est d'attacher un ID de requête côté client (un compteur) à chaque changement optimiste. Conservez le dernier ID par enregistrement. Quand une réponse arrive, comparez les IDs. Si la réponse est plus ancienne que la dernière, ignorez-la.
Des vérifications de version aident aussi. Si votre serveur renvoie updatedAt, version ou un etag, n'acceptez que les réponses plus récentes que ce que l'UI affiche déjà.
D'autres options à combiner :
Exemple (garde d'ID de requête) :
let nextId = 1;
const latestByItem = new Map();
async function saveTitle(itemId, title) {
const requestId = nextId++;
latestByItem.set(itemId, requestId);
// optimistic update
setItems(prev => prev.map(i => i.id === itemId ? { ...i, title } : i));
const res = await api.updateItem(itemId, { title, requestId });
// ignore stale response
if (latestByItem.get(itemId) !== requestId) return;
// reconcile with server truth
setItems(prev => prev.map(i => i.id === itemId ? { ...i, ...res.item } : i));
}
Si les utilisateurs tapent vite (notes, titres, recherche), envisagez d'annuler ou de retarder les sauvegardes tant qu'ils n'ont pas fait de pause. Cela réduit la charge serveur et diminue le risque que des réponses tardives provoquent des retours visibles.
Les échecs sont là où l'UI optimiste peut perdre la confiance des utilisateurs. La pire expérience est un rollback soudain sans explication. Une bonne approche par défaut pour les éditions : gardez la valeur de l'utilisateur à l'écran, marquez-la comme non sauvegardée et affichez une erreur en ligne à l'endroit exact où il a modifié.
Si quelqu'un renomme un projet de « Alpha » à « Q1 Launch », ne le remettez pas automatiquement à « Alpha » à moins que ce soit nécessaire. Gardez « Q1 Launch », ajoutez « Non sauvegardé. Nom déjà pris » et laissez-le corriger.
Le feedback en ligne reste attaché au champ ou à la ligne qui a échoué. Il évite le moment « que s'est-il passé ? » où un toast apparaît mais l'UI change silencieusement en arrière-plan.
Des indications fiables : « Saving… » pendant la requête, « Not saved » en cas d'échec, un léger surlignage de la ligne affectée et un message court expliquant la marche à suivre.
Retry est presque toujours utile. Undo convient aux actions rapides que l'on peut regretter (archiver), mais il peut être déroutant pour des éditions où l'utilisateur veut clairement la nouvelle valeur.
Quand une mutation échoue :
Si vous devez rollback (par ex. permissions modifiées et plus d'accès), expliquez et restaurez la vérité serveur : « Impossible d'enregistrer. Vous n'avez plus les droits d'édition. »
Traitez la réponse serveur comme un reçu, pas seulement un flag de succès. Après la fin de la requête, réconciliez : conservez ce que l'utilisateur voulait et acceptez ce que le serveur sait mieux.
Un refetch complet est le plus sûr quand le serveur a pu changer plus de choses que votre supposition locale. C'est aussi plus facile à raisonner.
Refetch est souvent préférable quand la mutation affecte beaucoup d'enregistrements (déplacer des items entre listes), quand les permissions ou règles de workflow peuvent changer le résultat, quand le serveur renvoie des données partielles, ou quand d'autres clients mettent souvent à jour la même vue.
Si le serveur renvoie l'entité mise à jour (ou suffisamment de champs), merger peut offrir une meilleure expérience : l'UI reste stable tout en acceptant la vérité serveur.
La dérive vient souvent du fait d'écraser des champs détenus par le serveur avec un objet optimiste. Pensez aux compteurs, champs calculés, timestamps et formats normalisés.
Exemple : vous définissez optimistement likedByMe=true et incrémentez likeCount. Le serveur peut dédupliquer les likes et renvoyer un likeCount différent, plus un updatedAt rafraîchi.
Une approche de merge simple :
Quand il y a conflit, décidez à l'avance. « Dernière écriture gagne » suffit pour des toggles. La fusion au niveau des champs est meilleure pour des formulaires.
Suivre un flag « dirty since request » par champ (ou un numéro de version local) permet d'ignorer les valeurs serveur pour les champs que l'utilisateur a modifiés après le début de la mutation, tout en acceptant la vérité serveur pour le reste.
Si le serveur rejette la mutation, préférez des erreurs spécifiques et légères plutôt qu'un rollback surprise. Gardez la saisie de l'utilisateur, mettez en évidence le champ et affichez le message. Les rollbacks de sauvegarde conviennent si l'action ne peut vraiment pas être acceptée (par exemple suppression optimiste refusée par le serveur).
Les listes sont là où l'UI optimiste est très agréable mais peut facilement casser. Un élément qui change peut affecter l'ordre, les totaux, les filtres et plusieurs pages.
Pour les créations, affichez le nouvel élément immédiatement mais marquez-le comme pending, avec un ID temporaire. Gardez sa position stable pour qu'il ne saute pas.
Pour les suppressions, un pattern sûr est de masquer l'élément tout de suite mais de conserver un enregistrement « fantôme » en mémoire pendant un court instant jusqu'à confirmation serveur. Cela permet l'undo et facilite la gestion des échecs.
Le réordonnancement est délicat car il touche beaucoup d'éléments. Si vous réordonnez de manière optimiste, stockez l'ordre précédent pour pouvoir le restaurer si nécessaire.
Avec la pagination ou l'infinite scroll, décidez où placer les insertions optimistes. Dans un fil, les nouveaux éléments vont généralement en haut. Dans des catalogues triés par le serveur, l'insertion locale peut induire en erreur car le serveur peut placer l'élément ailleurs. Compromis pratique : insérer dans la liste visible avec un badge pending, puis être prêt à déplacer l'élément après la réponse serveur si la clé de tri finale diffère.
Quand un ID temporaire devient un ID réel, dédupliquez par une clé stable. Si vous ne matchez que par ID, vous pouvez montrer deux fois le même élément (temp et confirmé). Gardez une map tempId→realId et remplacez en place pour que la position de scroll et la sélection ne se réinitialisent pas.
Les compteurs et les filtres sont aussi de l'état de liste. Mettez à jour les compteurs de façon optimiste seulement quand vous êtes sûr que le serveur sera d'accord. Sinon, marquez-les comme en rafraîchissement et réconciliez après la réponse.
La plupart des bugs d'optimistic update ne viennent pas vraiment de React. Ils viennent du fait de traiter un changement optimiste comme « la nouvelle vérité » au lieu d'une supposition temporaire.
Mettre à jour de façon optimiste un objet entier ou un écran entier alors qu'un seul champ a changé élargit le rayon d'impact. Les corrections serveur ultérieures peuvent écraser des éditions non liées.
Exemple : un formulaire de profil remplace tout l'objet user quand vous basculez un réglage. Pendant que la requête est en vol, l'utilisateur modifie son nom. À l'arrivée de la réponse, votre remplacement peut remettre l'ancien nom.
Gardez les patches optimistes petits et ciblés.
Une autre source de dérive est d'oublier de supprimer les flags pending après succès ou erreur. L'UI reste à moitié en chargement, et une logique ultérieure peut la considérer encore comme optimiste.
Si vous suivez l'état pending par élément, effacez-le avec la même clé que celle que vous avez utilisée pour le définir. Les IDs temporaires provoquent souvent des éléments « pending fantômes » quand le vrai ID n'est pas mappé partout.
Les bugs de rollback surviennent quand le snapshot est stocké trop tard ou trop globalement.
Si un utilisateur fait deux éditions rapides, vous pouvez finir par rollback l'édition #2 en utilisant le snapshot d'avant l'édition #1. L'UI saute vers un état que l'utilisateur n'a jamais vu.
Solution : snapshottez la portion exacte que vous restaurerez, et scopez-la à une tentative de mutation spécifique (souvent via l'ID de requête).
Les sauvegardes réelles sont souvent multi-étapes. Si l'étape 2 échoue (par ex. upload d'image), ne désactivez pas silencieusement l'étape 1. Montrez ce qui a été sauvegardé, ce qui ne l'a pas été, et ce que l'utilisateur peut faire ensuite.
De plus, n'assumez pas que le serveur renverra exactement ce que vous avez envoyé. Les serveurs normalisent le texte, appliquent des permissions, définissent des timestamps, attribuent des IDs et suppriment des champs. Réconciliez toujours à partir de la réponse (ou refetch) au lieu de faire confiance au patch optimiste pour toujours.
L'UI optimiste fonctionne quand elle est prévisible. Traitez chaque changement optimiste comme une mini-transaction : il a un ID, un état visible en attente, un swap clair en cas de succès et un chemin d'échec qui ne surprend pas.
Checklist à revoir avant mise en production :
Si vous prototypez rapidement, limitez la première version : un écran, une mutation, une mise à jour de liste. Des outils comme Koder.ai (koder.ai) peuvent vous aider à esquisser l'UI et l'API plus vite, mais la même règle s'applique : modélisez pending vs confirmed pour que le client ne perde jamais la trace de ce que le serveur a réellement accepté.
L'UI optimiste met à jour l'écran immédiatement, avant que le serveur ne confirme le changement. Cela rend l'application plus réactive, mais il faut ensuite concilier avec la réponse serveur pour éviter que l'UI ne dérive de l'état réellement enregistré.
La dérive des données survient lorsque l'UI garde une supposition optimiste comme si elle était confirmée, alors que le serveur a finalement sauvegardé quelque chose de différent ou a rejeté la modification. Elle apparaît souvent après un rafraîchissement, dans un autre onglet, ou lorsque des réseaux lents font arriver les réponses dans le désordre.
Évitez ou soyez très prudent avec les updates optimistes pour l'argent, la facturation, les actions irréversibles, les changements de permissions et les workflows avec des règles serveur strictes. Dans ces cas, il est plus sûr d'afficher un état en attente clair et d'attendre la confirmation avant de modifier des totaux ou des accès.
Considérez le backend comme la source de vérité pour tout ce qui a une signification métier hors de l'écran courant : prix, permissions, champs calculés, compteurs partagés. Gardez l'état local pour les brouillons, le focus, le flag « en train d'éditer », les filtres temporaires et l'état purement présentatif.
Affichez un signal petit et cohérent à l'endroit du changement, comme « Saving… », un texte estompé ou un petit spinner. Le but est de montrer que la valeur est temporaire sans bloquer toute la page.
Utilisez un ID client temporaire (comme un UUID ou temp_...) lors de la création d'un élément, puis remplacez-le par l'ID réel du serveur en cas de succès. Cela garde stables les clés de liste, la sélection et l'état d'édition pour éviter clignotements ou duplications.
Ne vous fiez pas à un flag global de chargement ; suivez l'état en attente par élément (ou par champ) afin que seule la chose modifiée apparaisse comme en attente. Stockez un petit patch optimiste et un snapshot de rollback pour confirmer ou annuler juste ce changement sans affecter l'UI non liée.
Attachez un ID de requête à chaque mutation et conservez le dernier ID par élément. À la réception d'une réponse, appliquez-la uniquement si son ID correspond au dernier ID enregistré ; sinon ignorez-la pour empêcher des réponses tardives de ramener l'UI sur une valeur plus ancienne.
Pour la plupart des éditions, gardez la valeur de l'utilisateur visible, marquez-la comme non sauvegardée, et affichez une erreur en ligne à l'endroit modifié avec une option de Retry claire. Ne faites un rollback dur que si le changement est réellement impossible (par exemple perte de permission), et expliquez pourquoi.
Refetchez lorsque le changement peut affecter beaucoup d'endroits (totaux, tri, permissions, champs dérivés), car patcher tout correctement est facile à rater. Fusionnez localement quand c'est une mise à jour isolée et que le serveur renvoie l'entité mise à jour, puis acceptez les champs serveur comme les timestamps et les valeurs calculées.
Patchs petits et ciblés, IDs temporaires, badges en attente, retries et réconciliation claire : ce sont les ingrédients d'une bonne approche pour les listes. Gardez une map tempId→realId pour remplacer en place et éviter les duplications qui forcent le scroll ou la sélection à se réinitialiser.