
Optimistické mazání
Jak implementovat okamžitou reakci na uživatelské akce s optimistickým mazáním. UI vzor pro rychlejší aplikace.
Optimistické mazání je UX vzor, kde aplikace okamžitě reaguje na akci uživatele (například smazání položky) a teprve poté ji posílá na server. Pokud server vrátí chybu, změna se vrátí zpět. Výsledkem je rychlejší pocit aplikace bez čekání na odpověď ze serveru.
Problém tradičního přístupu
Při klasickém mazání musí uživatel čekat na odpověď serveru:
async function smazatPolozku(id) {
// Zobrazí loading spinner
setLoading(true);
try {
await fetch(`/api/polozky/${id}`, { method: 'DELETE' });
// Až teď zmizí z UI
odebratZUI(id);
} catch (error) {
zobrazChybu('Nepodařilo se smazat');
} finally {
setLoading(false);
}
}
Nevýhody:
- Uživatel čeká 200–500 ms (nebo déle při pomalém spojení)
- UI „zamrzne” s loading stavem
- Pocit pomalé aplikace, i když server reaguje rychle
Řešení: optimistický přístup
Optimistické mazání nejdřív aktualisuje UI a pak teprve volá server:
async function smazatPolozku(id) {
// Okamžitě zmizí z UI
const puvodni = odebratZUI(id);
try {
await fetch(`/api/polozky/${id}`, { method: 'DELETE' });
// Server potvrdil - vše OK
} catch (error) {
// Chyba - vrátíme zpět
vratitDoUI(puvodni);
zobrazChybu('Nepodařilo se smazat');
}
}
Uživatel vidí okamžitou reakci. Ve většině případů server smazání potvrdí a uživatel si ani nevšimne čekání. Pokud selže, položka se vrátí zpět.
Optimistické aktualisace s TanStack Query
Pro aplikace s hodně API voláním je nejlepší použít TanStack Query, která má vestavěnou podporu pro optimistické aktualisace:
import { useMutation, useQueryClient } from '@tanstack/react-query';
function TodoList() {
const queryClient = useQueryClient();
const deleteMutation = useMutation({
mutationFn: (id) => fetch(`/api/items/${id}`, { method: 'DELETE' }),
// Optimistická aktualisace
onMutate: async (id) => {
// Zrušit probíhající query
await queryClient.cancelQueries({ queryKey: ['todos'] });
// Uložit předchozí stav
const previousTodos = queryClient.getQueryData(['todos']);
// Optimisticky aktualisovat cache
queryClient.setQueryData(['todos'], (old) =>
old?.filter((todo) => todo.id !== id)
);
// Vrátit kontext pro rollback
return { previousTodos };
},
// Při chybě vrátit zpět
onError: (err, id, context) => {
if (context?.previousTodos) {
queryClient.setQueryData(['todos'], context.previousTodos);
}
},
// Vždy invalidovat query po dokončení
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['todos'] });
},
});
return (
<button onClick={() => deleteMutation.mutate(todoId)}>
Smazat
</button>
);
}
Výhody TanStack Query:
- Vestavěná podpora pro optimistické aktualisace
- Automatické zrušení probíhajících dotazů
- Rollback při chybě serveru
- Synchronisace cache napříč aplikací
- Podpora pro offline režim
Více informací v článku TanStack Query.
Jak řešit undo po smazání z UI
Položka okamžitě zmizí z UI (optimistický přístup), ale co s požadavkem na server? Máte tři hlavní možnosti:
1. Okamžité smazání + undo vytvoří znovu
Pošlete DELETE požadavek na server ihned, ale zobrazíte toast s tlačítkem „Vrátit zpět”. Pokud uživatel klikne na undo, vytvoříte položku znovu pomocí POST/PUT požadavku.
Výhody:
- Server okamžitě ví o smazání – konsistentní stav
- Jednoduchá implementace – žádné timeouty
- Funguje dobře i při zavření aplikace – smazání proběhlo
Nevýhody:
- Undo je složitější – musíte znovu vytvořit položku
- Potřebujete endpoint pro vytvoření (může mít nové ID)
- Dvě API volání při undo (DELETE + POST)
- Pokud se změní ID, musíte aktualisovat reference
2. Odložené smazání (čekání na timeout)
Zobrazíte toast „Smazáno”, ale DELETE požadavek pošlete až po 5 sekundách. Pokud uživatel klikne na undo, požadavek se nikdy nepošle.
Výhody:
- Jednoduchý undo – jen zrušíte timeout
- Jedno API volání (jen DELETE, žádný POST)
- Ušetříte síťový provoz pokud uživatel často používá undo
Nevýhody:
- Lže uživateli – říkáte „smazáno”, ale ještě není
- Server neví o smazání – nekonsistentní stav
- Problém při zavření aplikace – timeout se nevykoná
- Složitější správa timeoutů
- Pokud se seznam načte znovu ze serveru, položka se vrátí
3. Soft delete na backendu
Pošlete DELETE požadavek ihned, ale backend neodstraní položku, jen ji označí jako smazanou (např. deleted_at). Undo pak pošle požadavek, který soft delete zruší.
Výhody:
- Server okamžitě ví o smazání – konsistentní stav
- Undo je jednoduché – jen odvolání soft deletu
- Položka zachovává stejné ID
- Můžete implementovat „koš” na serveru
- Funguje i při zavření aplikace
- Audit trail – vidíte historii smazání
Nevýhody:
- Vyžaduje změnu na backendu (soft delete)
- Potřebujete endpoint pro restore
- Dvě API volání při undo (DELETE + POST restore)
- Složitější databázové query (musíte filtrovat
deleted_at IS NULL)
Doporučení
Volba přístupu záleží na konkrétní situaci a podmínkách vašeho projektu:
Soft delete na backendu (přístup 3) může být teoreticky nejčistší řešení s okamžitou konsistencí a jednoduchým undo, ale přináší komplexitu na backendu – složitější databázové dotazy, nutnost filtrovat smazané záznamy všude a řešit čištění starých dat.
Odložené smazání (přístup 2) je nejjednodušší na implementaci a šetří síťový provoz, ale lže uživateli o skutečném stavu a může způsobit problémy při zavření aplikace nebo obnovení dat ze serveru.
Okamžité smazání + undo vytvoří znovu (přístup 1) je pragmatický kompromis bez změn na backendu, ale undo je složitější a může změnit ID položky, což může způsobit problémy s referencemi.
Optimistické operace offline
V offline-first aplikacích můžete ukládat nepotvrzené operace do fronty:
class OfflineQueue {
constructor() {
this.queue = this.loadQueue();
}
async deleteItem(id) {
// Přidat do fronty
this.queue.push({
type: 'delete',
id,
timestamp: Date.now()
});
this.saveQueue();
// Zkusit odeslat
await this.processQueue();
}
async processQueue() {
if (!navigator.onLine) return;
while (this.queue.length > 0) {
const operation = this.queue[0];
try {
await fetch(`/api/items/${operation.id}`, {
method: 'DELETE'
});
// Úspěch - odebrat z fronty
this.queue.shift();
this.saveQueue();
} catch (error) {
// Chyba - zkusit později
break;
}
}
}
loadQueue() {
return JSON.parse(localStorage.getItem('queue') || '[]');
}
saveQueue() {
localStorage.setItem('queue', JSON.stringify(this.queue));
}
}
// Zpracovat frontu při obnovení spojení
window.addEventListener('online', () => {
queue.processQueue();
});
Kdy použít optimistické mazání
- Vysoká spolehlivost — server smazání téměř vždy potvrdí (99 %+)
- Rychlá odezva důležitá — seznamy úkolů, komentáře, příspěvky
- Vratné akce — mazání není destruktivní (lze obnovit ze zálohy)
- Nízké risiko — chyba nepoškodí data nebo nepřinese ztrátu
Kdy nepoužívat
- Finanční transakce — platby, převody (vyžadují potvrzení serveru)
- Kritická data — nenávratné smazání důležitých záznamů
- Složité validace — server může smazání zamítnout z mnoha důvodů
- Pomalé spojení — uživatel může odejít před odesláním požadavku
Tipy pro implementaci
- Preferujte soft delete — označte položku jako smazanou místo skutečného odstranění, je to jednodušší a spolehlivější
- Uložte původní stav — abyste mohli vrátit změny při chybě serveru
- Animujte změny — plynulý přechod je příjemnější než okamžité zmizení
- Dejte uživateli zpětnou vazbu — pokud selže, jasně to oznámte
- Nabídněte undo — jako Gmail snackbar s tlačítkem „Vrátit zpět” (5 sekund)
- Počítejte s offline režimem — ukládejte operace do fronty v localStorage
- Řešte souběžnost — co když uživatel klikne vícekrát rychle za sebou?
- Načasujte API volání — volejte server až po uplynutí undo časovače (5 s)
Srovnání přístupů
Interaktivní srovnání: Zkuste smazat položku v obou seznamech a porovnejte dobu odezvy.
⏱️ Klasické mazání
Čeká na server (simulováno 500ms)
- Úkol 1
- Úkol 2
- Úkol 3
⚡ Optimistické mazání
Okamžitá reakce
- Úkol 1
- Úkol 2
- Úkol 3
| Klasické mazání | Optimistické mazání | |
|---|---|---|
| Rychlost UI | Čeká na server (200–500 ms) | Okamžitá odezva |
| Konsistence | 100 % — zobrazí jen potvrzený stav | ~99 % — může se vrátit zpět |
| Složitost | Jednoduchá implementace | Vyžaduje rollback logiku |
| UX | Pomalejší pocit | Rychlejší pocit |
| Použití | Kritické operace | Běžné akce s nízkou chybovostí |
Související články
Jak vkládat 3D objekty na web pomocí Three.js
Které formáty použít, jak vytvářet modely pomocí AI a kdy raději použít obrázek nebo video.
Jak neotravovat uživatele validací formulářů
Kdy zobrazovat chyby, kdy ne, a jak pomoci uživatelům místo jejich trestání.
Automatické načítání firemních údajů z registru ARES
Návod na získání dat o firmě z IČO nebo DIČ pomocí veřejné API a jejich použití pro předvyplnění webových formulářů.