26.01.2026

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 useTransition kullanı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 disabled yapmak şart değil; amaç hızlı etkileşimi korumak. İsterseniz sadece isPending iken 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 toast tetikleyin.
  • Çoklu optimistic aksiyonlarda (ör. liste içinde birden fazla item) her item için ayrı AbortController tutun.
  • 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.