Learning
Všechny články
Engineering7. září 2026 6 min

Optimistické UI zobrazuje tvrzení, ne potvrzený fakt

Po dobu akce ukazuje obrazovka, s čím server ještě nesouhlasil

Klient přestane zobrazovat stav a začne zobrazovat svůj odhad

Běžná webová appka má jednoduchý kontrakt. Obrazovka je projekce stavu, který server potvrdil. Uživatel něco udělá, klient počká, server odpoví a teprve pak se obrazovka pohne. Čekání je viditelné, ale to, co vidíš, je pravda.

Optimistické UI ten kontrakt láme záměrně. Po dobu běhu akce zobrazuje klient hodnotu, kterou sám tvrdí, že bude platit. Server o ní zatím neví. Ta hodnota není stav, je to predikce s vlastní dobou platnosti, a všechny důsledky téhle změny plynou právě z toho.


Dočasnost musí být strukturální, ne uklizená ručně

Naivní implementace drží predikci v obyčejném useState a po odpovědi ji přepíše. To znamená psát úklidový kód pro každou cestu: úspěch, chybu, timeout, souběžnou akci. Každá zapomenutá větev nechá na obrazovce viset lež.

React to řeší tím, že predikci upře vlastní životnost.

const [optimisticName, setOptimisticName] = useOptimistic(name);

useOptimistic vrací name vždycky, když neběží žádná akce. Predikce existuje jen uvnitř transition a v okamžiku, kdy transition skončí, hook prostě zase vrací původní hodnotu. Úklid nepíšeš, protože není co uklízet — dočasnost je vlastnost hooku, ne tvé disciplíny.

Sladění navíc proběhne v jednom commitu. Skutečná hodnota se zapíše a predikce zmizí ve stejném renderu, takže mezi nimi není snímek, kde by problikl starý stav.


Návrat není rollback, je to zastavení tvrzení

Tady se kontrakt obrací nejvíc. useOptimistic nemá žádný onError ani revert(). Když akce selže, obrazovka se vrátí prostě tím, že se rodičovská hodnota nezměnila.

Úspěch se tedy vyjadřuje pohybem rodičovského stavu a neúspěch jeho klidem. Klient nic nevrací zpět, jen přestane tvrdit. Praktický důsledek je tvrdý: rodičovský stav se smí aktualizovat výhradně z potvrzené odpovědi serveru. Když se aktualizuje i po chybě, predikce se tím povýší na fakt a lež na obrazovce zůstane natrvalo.

Druhá strana téhož je, že server musí umět dodat hodnotu, kterou klient hádal. U přepnutí lajku nebo přejmenování je to triviální. U operací, kde identitu nebo pořadí přiděluje server — nové ID, pozice ve frontě, serverový timestamp — klient hádat neumí a predikce se při potvrzení viditelně přeskládá.


Predikce nesmí být přišpendlená na starých datech

Poslední past je v tom, že mezi zahájením akce a jejím koncem se podkladová data mohou změnit. Přijde jiná odpověď, dorazí update z websocketu, doběhne souběžná akce.

Proto má useOptimistic druhý, volitelný parametr:

const [optimisticItems, addOptimistic] = useOptimistic(
  items,
  (current, newItem) => [...current, { ...newItem, pending: true }]
);

Reducer je čistá funkce a React ho přepočítá znovu proti nejnovějším items. Rozepsaná položka se tak vždycky skládá na aktuální data, ne na snímek pořízený v momentě kliknutí. S přímo předanou hodnotou tuhle vlastnost nemáš a při souběžných akcích se to projeví jako zmizelá nebo zdvojená položka.


Co si z toho odnést

Než někam nasadíš optimistický update, rozhodni, kdo je autorita nad výslednou hodnotou. Pokud ji dokáže spočítat klient, je optimistické UI skoro zadarmo. Pokud ji přiděluje server, kupuješ si rychlost za viditelný přeskok a je poctivější ukázat spinner.

Chybovou cestu navrhni jako absenci potvrzení, ne jako rollback. To znamená jediné pravidlo v kódu: rodičovský stav mění jen odpověď serveru. Dodrž ho a chybová větev nepotřebuje vlastní obsluhu, protože se vyřeší tím, že se nic nestane.

A predikci vždycky vyjádři jako transformaci aktuálních dat, ne jako hotovou hodnotu. Rozdíl je vidět až v souběhu, ale právě souběh je důvod, proč se optimistická UI v produkci rozpadají.

Zdroje

Poslech místo čtení — 1 zdroj k načtení.