İçeriğe geç
Sedat Demir
Geri dön

useOptimistic Hook ile Anlık UI Güncellemeleri: React'te Kullanıcı Deneyimini Zirveye Taşıyın

useOptimistic Hook ile Anlık UI Güncellemeleri: React'te Kullanıcı Deneyimini Zirveye Taşıyın

Bir "Beğen" butonuna tıkladığınızda, beğeni sayısının sunucu yanıtını beklemeden anında artmasını beklemiyor musunuz? Ya da bir mesaj gönderdiğinizde, mesajın hemen sohbet ekranında görünmesini? İşte bu sezgisel deneyimin arkasındaki tekniğe Optimistic UI denir ve React 19, bu yaklaşımı birinci sınıf bir vatandaş haline getiren güçlü bir hook sunuyor: useOptimistic.

Bu yazıda, useOptimistic hook'unun ne olduğunu, neden ihtiyaç duyduğumuzu, nasıl çalıştığını ve gerçek dünya projelerinde nasıl kullanabileceğinizi kapsamlı bir şekilde ele alacağız.


Optimistic UI Nedir ve Neden Önemlidir?

Optimistic UI (İyimser Arayüz), kullanıcının gerçekleştirdiği bir eylemin sonucunun başarılı olacağını varsayarak arayüzü sunucu yanıtı gelmeden önce güncelleme stratejisidir.

Geleneksel Yaklaşımın Sorunu

Geleneksel bir akışta şu adımlar izlenir:

  1. Kullanıcı bir butona tıklar
  2. Sunucuya istek gönderilir
  3. Bir loading spinner gösterilir
  4. Sunucudan yanıt gelir
  5. Arayüz güncellenir

Bu akış, özellikle yavaş ağ bağlantılarında kullanıcıya yavaş ve tepkisiz bir deneyim sunar. Kullanıcı her eylemde birkaç saniye beklemek zorunda kalır.

Optimistic Yaklaşımın Avantajı

Optimistic UI ile akış şu şekilde değişir:

  1. Kullanıcı bir butona tıklar
  2. Arayüz anında güncellenir (iyimser güncelleme)
  3. Arka planda sunucuya istek gönderilir
  4. Sunucu başarılı yanıt verirse → her şey zaten yerinde
  5. Sunucu hata verirse → arayüz eski haline geri alınır (rollback)

Bu yaklaşım, Twitter'ın beğeni sistemi, Instagram'ın takip mekanizması, Slack'in mesaj gönderimi gibi modern uygulamalarda standart bir uygulama haline gelmiştir.


useOptimistic Hook'una Giriş

React 19 ile birlikte gelen useOptimistic hook'u, optimistic güncellemeleri deklaratif ve temiz bir şekilde yönetmenizi sağlar.

Temel Söz Dizimi

const [optimisticState, addOptimistic] = useOptimistic(state, updateFn);

Parametreleri inceleyelim:

Parametre Açıklama
state Mevcut gerçek state değeri
updateFn (currentState, optimisticValue) => newState şeklinde bir reducer fonksiyonu
optimisticState Sonuçta kullanılacak state (optimistic veya gerçek)
addOptimistic Optimistic güncellemeyi tetikleyen fonksiyon

Çalışma Mantığı

useOptimistic hook'u şu prensiple çalışır:


Pratik Örnek 1: Beğeni (Like) Butonu

En klasik optimistic UI senaryosuyla başlayalım — bir beğeni butonu:

'use client';

import { useOptimistic, useTransition } from 'react';
import { toggleLike } from '@/actions/likes';

function LikeButton({ postId, initialLikes, isLikedByUser }) {
  const [isPending, startTransition] = useTransition();

  const [optimisticLikes, setOptimisticLikes] = useOptimistic(
    { count: initialLikes, isLiked: isLikedByUser },
    (currentState, _optimisticValue) => ({
      count: currentState.isLiked
        ? currentState.count - 1
        : currentState.count + 1,
      isLiked: !currentState.isLiked,
    })
  );

  const handleLike = () => {
    startTransition(async () => {
      setOptimisticLikes(null); // optimistic güncellemeyi tetikle
      await toggleLike(postId); // sunucuya isteği gönder
    });
  };

  return (
    <button
      onClick={handleLike}
      disabled={isPending}
      className={`like-btn ${optimisticLikes.isLiked ? 'liked' : ''}`}
    >
      {optimisticLikes.isLiked ? '❤️' : '🤍'}
      <span>{optimisticLikes.count}</span>
    </button>
  );
}

export default LikeButton;

Bu örnekte kullanıcı butona tıkladığı anda kalp simgesi ve sayaç anında güncellenir. Sunucu yanıtı geldiğinde React, gerçek state ile optimistic state'i otomatik olarak senkronize eder.


Pratik Örnek 2: Mesaj Gönderme

Bir sohbet uygulamasında mesaj gönderme senaryosunu ele alalım:

'use client';

import { useOptimistic, useRef } from 'react';
import { sendMessage } from '@/actions/messages';

function ChatBox({ initialMessages }) {
  const formRef = useRef(null);

  const [optimisticMessages, addOptimisticMessage] = useOptimistic(
    initialMessages,
    (currentMessages, newMessage) => [
      ...currentMessages,
      {
        id: `temp-${Date.now()}`,
        text: newMessage,
        sender: 'me',
        status: 'sending', // gönderiliyor durumu
        createdAt: new Date().toISOString(),
      },
    ]
  );

  async function handleSubmit(formData) {
    const messageText = formData.get('message');
    if (!messageText.trim()) return;

    formRef.current?.reset();
    addOptimisticMessage(messageText);
    await sendMessage(messageText);
  }

  return (
    <div className="chat-container">
      <div className="messages">
        {optimisticMessages.map((msg) => (
          <div
            key={msg.id}
            className={`message ${msg.sender === 'me' ? 'sent' : 'received'}`}
          >
            <p>{msg.text}</p>
            {msg.status === 'sending' && (
              <span className="status-indicator">Gönderiliyor...</span>
            )}
          </div>
        ))}
      </div>

      <form ref={formRef} action={handleSubmit}>
        <input
          type="text"
          name="message"
          placeholder="Bir mesaj yazın..."
          autoComplete="off"
        />
        <button type="submit">Gönder</button>
      </form>
    </div>
  );
}

export default ChatBox;

Bu örnekte dikkat edilmesi gereken noktalar:


Pratik Örnek 3: Todo Listesi ile CRUD İşlemleri

Daha karmaşık bir senaryo olarak, ekleme ve silme işlemlerini birlikte yöneten bir todo uygulaması:

'use client';

import { useOptimistic } from 'react';
import { addTodo, deleteTodo } from '@/actions/todos';

function TodoList({ initialTodos }) {
  const [optimisticTodos, dispatchOptimistic] = useOptimistic(
    initialTodos,
    (currentTodos, action) => {
      switch (action.type) {
        case 'ADD':
          return [
            ...currentTodos,
            {
              id: `temp-${Date.now()}`,
              title: action.title,
              completed: false,
              isPending: true,
            },
          ];
        case 'DELETE':
          return currentTodos.filter((todo) => todo.id !== action.id);
        case 'TOGGLE':
          return currentTodos.map((todo) =>
            todo.id === action.id
              ? { ...todo, completed: !todo.completed, isPending: true }
              : todo
          );
        default:
          return currentTodos;
      }
    }
  );

  async function handleAdd(formData) {
    const title = formData.get('title');
    if (!title.trim()) return;

    dispatchOptimistic({ type: 'ADD', title });
    await addTodo(title);
  }

  async function handleDelete(id) {
    dispatchOptimistic({ type: 'DELETE', id });
    await deleteTodo(id);
  }

  return (
    <div className="todo-app">
      <form action={handleAdd}>
        <input name="title" placeholder="Yeni görev ekle..." required />
        <button type="submit">Ekle</button>
      </form>

      <ul className="todo-list">
        {optimisticTodos.map((todo) => (
          <li
            key={todo.id}
            className={`todo-item ${todo.isPending ? 'pending' : ''}`}
            style={{ opacity: todo.isPending ? 0.6 : 1 }}
          >
            <span className={todo.completed ? 'completed' : ''}>
              {todo.title}
            </span>
            <button onClick={() => handleDelete(todo.id)}>🗑️</button>
          </li>
        ))}
      </ul>
    </div>
  );
}

export default TodoList;

Burada updateFn'i bir reducer gibi kullanarak farklı eylem tiplerini tek bir fonksiyonda yönetiyoruz. Bu pattern, karmaşık state güncellemelerinde kodu daha organize tutar.


Hata Yönetimi Stratejileri

Optimistic UI'ın en kritik kısmı hata yönetimidir. Sunucu başarısız olursa ne olacak?

Otomatik Rollback

useOptimistic hook'u, async eylem tamamlandığında (başarılı veya başarısız) otomatik olarak gerçek state'e döner. Bu, doğal bir rollback mekanizması sağlar:

async function handleAction() {
  addOptimistic(newValue); // iyimser güncelleme

  try {
    await serverAction(); // sunucu eylemi
    // Başarılıysa: revalidation sonrası gerçek state güncellenir
  } catch (error) {
    // Hata durumunda: async eylem biter, optimistic state düşer
    // Gerçek state (eski değer) otomatik olarak geri gelir
    toast.error('İşlem başarısız oldu, lütfen tekrar deneyin.');
  }
}

Kullanıcıya Geri Bildirim

Hata durumunda kullanıcıyı bilgilendirmek önemlidir:

import { useOptimistic, useTransition } from 'react';
import { toast } from 'sonner';

function OptimisticAction({ data }) {
  const [isPending, startTransition] = useTransition();
  const [optimisticData, setOptimisticData] = useOptimistic(data, updateFn);

  const handleClick = () => {
    startTransition(async () => {
      setOptimisticData(newValue);
      const result = await serverAction();

      if (result.error) {
        toast.error('Bir hata oluştu. Değişiklik geri alındı.');
        // state otomatik olarak gerçek değere dönecek
      }
    });
  };

  return (
    <button onClick={handleClick} disabled={isPending}>
      {isPending ? 'İşleniyor...' : 'Güncelle'}
    </button>
  );
}

useOptimistic vs Manuel State Yönetimi

useOptimistic olmadan aynı davranışı elde etmek oldukça zahmetlidir:

// ❌ useOptimistic OLMADAN (manuel yaklaşım)
function LikeButtonManual({ postId, initialLikes }) {
  const [likes, setLikes] = useState(initialLikes);
  const [isLiked, setIsLiked] = useState(false);
  const [previousState, setPreviousState] = useState(null);

  const handleLike = async () => {
    // Önceki state'i sakla (rollback için)
    setPreviousState({ likes, isLiked });

    // İyimser güncelleme
    setLikes((prev) => (isLiked ? prev - 1 : prev + 1));
    setIsLiked((prev) => !prev);

    try {
      await toggleLike(postId);
    } catch (error) {
      // Manuel rollback
      setLikes(previousState.likes);
      setIsLiked(previousState.isLiked);
    }
  };
  // ...
}
// ✅ useOptimistic İLE (temiz yaklaşım)
function LikeButtonOptimistic({ postId, initialLikes }) {
  const [optimistic, setOptimistic] = useOptimistic(
    { count: initialLikes, isLiked: false },
    (state) => ({
      count: state.isLiked ? state.count - 1 : state.count + 1,
      isLiked: !state.isLiked,
    })
  );
  // Rollback otomatik, önceki state'i saklamaya gerek yok
}

Fark açıkça görülüyor: useOptimistic ile kod daha kısa, daha okunabilir ve hata yapma ihtimali çok daha düşük.


En İyi Pratikler ve İpuçları

1. Geçici (Pending) Durumları Görsel Olarak Belirtin

{optimisticItems.map((item) => (
  <div
    key={item.id}
    style={{
      opacity: item.isPending ? 0.5 : 1,
      pointerEvents: item.isPending ? 'none' : 'auto',
    }}
  >
    {item.name}
    {item.isPending && <Spinner size="sm" />}
  </div>
))}

2. Server Actions ile Birlikte Kullanın

useOptimistic, React 19'un Server Actions özelliğiyle birlikte en iyi performansı gösterir. useTransition veya form action prop'u ile birlikte kullanarak async akışı doğru yönetin.

3. Karmaşık State'lerde Reducer Patternini Tercih Edin

Birden fazla eylem tipiniz varsa, update fonksiyonunuzu bir reducer gibi yapılandırın (Todo örneğinde gösterildiği gibi).

4. Race Condition'lara Dikkat Edin

Kullanıcı hızlı bir şekilde birden fazla işlem tetiklerse, sıralama sorunları oluşabilir. useTransition kullanmak bu tür sorunları minimize eder.

5. Kritik İşlemlerde Kullanmaktan Kaçının

Ödeme, hesap silme gibi geri dönüşü olmayan işlemlerde optimistic UI uygulamak risklidir. Bu tür işlemlerde geleneksel loading state yaklaşımını tercih edin.


Browser Uyumluluğu ve Gereksinimler

useOptimistic hook'unu kullanmak için:


Sonuç

useOptimistic hook'u, React 19'un en değerli yeniliklerinden biridir. Modern kullanıcıların beklediği anlık geri bildirim deneyimini minimal kod ile uygulamanıza kazandırır.

Özetle:

Artık kullanıcılarınızı spinner'larla bekletmek yerine, onlara anında yanıt veren, akıcı ve modern bir deneyim sunabilirsiniz. useOptimistic hook'unu bir sonraki projenizde mutlaka deneyin — kullanıcılarınız farkı hissedecektir.


Share this post on:

Sonraki Yazı
ElysiaJS ile Bun Üzerinde Ultra-Hızlı TypeScript API Geliştirme