React’te İyimser Güncellemeler (Optimistic UI): useTransition + AbortController ile Akıcı ve Güvenli Etkileşim
Optimistic UI ile anlık geri bildirim ver, istek çakışmalarını iptal et, geri alma (rollback) mantığı kur.
Neden Optimistic UI?
Kullanıcı bir butona basınca “yükleniyor…” görmek yerine arayüzün hemen güncellenmesi (beğeni, takip, yapılacak iş ekleme vb.) deneyimi ciddi iyileştirir. Buna Optimistic UI denir: önce UI’ı güncelleriz, sonra sunucu sonucu gelince doğrularız; hata olursa geri alırız.
Bu yazıda iki problemi birlikte çözeceğiz:
- Anında UI güncelleme (optimistic update)
- Hızlı tıklamalarda istek çakışması ve “en son tıklama kazanmalı” kuralı (AbortController)
Örnek: “Like” butonu. Kullanıcı spam tıklasa bile UI akıcı kalacak, eski istekler iptal edilecek.
Basit bir istek katmanı: iptal edilebilir fetch
async function patchJSON<T>(url: string, body: unknown, signal?: AbortSignal): Promise<T> {
const res = await fetch(url, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
signal,
});
if (!res.ok) {
const text = await res.text().catch(() => "");
throw new Error(text || `HTTP ${res.status}`);
}
return res.json() as Promise<T>;
}
useOptimisticLike: Optimistic + iptal + rollback
Aşağıdaki hook:
- UI’ı hemen değiştirir
- Yeni tıklamada eski isteği abort eder
- Sunucu hata dönerse rollback yapar
- Ağ isteğini “düşük öncelikli” çalıştırmak için
useTransitionkullanır
import { useMemo, useRef, useState, useTransition } from "react";
type LikeResponse = { liked: boolean; likeCount: number };
export function useOptimisticLike(initial: LikeResponse, postId: string) {
const [state, setState] = useState<LikeResponse>(initial);
const [isPending, startTransition] = useTransition();
const abortRef = useRef<AbortController | null>(null);
const toggle = () => {
// 1) Optimistic update için önce snapshot al
const prev = state;
const next: LikeResponse = {
liked: !prev.liked,
likeCount: prev.likeCount + (prev.liked ? -1 : 1),
};
// 2) UI'ı hemen güncelle
setState(next);
// 3) Önceki isteği iptal et (en son tıklama kazanır)
abortRef.current?.abort();
const controller = new AbortController();
abortRef.current = controller;
// 4) Sunucuya yazmayı transition içinde yap
startTransition(async () => {
try {
const server = await patchJSON<LikeResponse>(
`/api/posts/${postId}/like`,
{ liked: next.liked },
controller.signal
);
// Sunucu nihai kaynaktır: state'i onunla hizala
setState(server);
} catch (err: any) {
// Abort normal bir akış: rollback yapma
if (err?.name === "AbortError") return;
// Hata: rollback
setState(prev);
}
});
};
return useMemo(
() => ({ ...state, toggle, isPending }),
[state.liked, state.likeCount, isPending]
);
}
Bileşende kullanım
function LikeButton({ initial, postId }: { initial: { liked: boolean; likeCount: number }; postId: string }) {
const { liked, likeCount, toggle, isPending } = useOptimisticLike(initial, postId);
return (
<button onClick={toggle} aria-pressed={liked} disabled={false}>
{liked ? "Liked" : "Like"} · {likeCount}
{isPending ? " (syncing…)" : null}
</button>
);
}
Notlar:
- Butonu tamamen
disabledyapmak şart değil; amaç hızlı etkileşimi korumak. İsterseniz sadeceisPendingiken görsel bir “syncing” etiketi gösterin. - Sunucu bazen sayaçları farklı döndürebilir (rate limit, kurallar). O yüzden son state sunucudan gelen olmalı.
Pratik ipuçları
- Hata mesajı göstermek istiyorsanız rollback sonrası bir
toasttetikleyin. - Çoklu optimistic aksiyonlarda (ör. liste içinde birden fazla item) her item için ayrı
AbortControllertutun. - Aynı endpoint’e arka arkaya istek atıyorsanız, abort yerine son isteğin sonucu geçerli kuralını da uygulayabilirsiniz; fakat abort genelde ağ yükünü azaltır.
Sonuç
Optimistic UI, doğru uygulandığında arayüzü “canlı” hissettirir. AbortController ile çakışan istekleri iptal ederek tutarlılığı korur, useTransition ile de kullanıcı etkileşimlerini akıcı tutarsınız.