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:
- Kullanıcı bir butona tıklar
- Sunucuya istek gönderilir
- Bir loading spinner gösterilir
- Sunucudan yanıt gelir
- 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:
- Kullanıcı bir butona tıklar
- Arayüz anında güncellenir (iyimser güncelleme)
- Arka planda sunucuya istek gönderilir
- Sunucu başarılı yanıt verirse → her şey zaten yerinde
- 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:
- Bir async eylem (genellikle Server Action veya form submission) devam ederken
addOptimisticile geçici bir state gösterir - Async eylem tamamlandığında, otomatik olarak gerçek state'e geri döner
- Hata durumunda da gerçek state korunduğu için doğal bir rollback mekanizması oluşur
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:
- Mesaj, form gönderildiği anda listede "Gönderiliyor..." etiketiyle görünür
- Form anında temizlenir, böylece kullanıcı yeni bir mesaj yazmaya başlayabilir
- Sunucu yanıt verdiğinde
initialMessagesgüncellenir ve geçici mesaj gerçek mesajla yer değiştirir status: 'sending'alanı sayesinde kullanıcıya görsel geri bildirim sağlanır
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:
- React 19 veya üzeri sürüm gereklidir
- Next.js 14+ ile Server Actions desteğiyle en iyi şekilde çalışır
- Client component'larda (
'use client'direktifi ile) kullanılır - Canary sürümlerde
experimental_useOptimisticolarak mevcuttu, artık stabil API'dır
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:
- Optimistic UI, sunucu yanıtını beklemeden arayüzü güncelleme stratejisidir
useOptimistic, bu stratejiyi React'te deklaratif ve güvenli bir şekilde uygulamanızı sağlar- Otomatik rollback mekanizması sayesinde hata durumlarını manuel yönetme zorunluluğu ortadan kalkar
- Server Actions ve
useTransitionile birlikte kullanıldığında en etkili sonuçları verir - Beğeni butonları, mesaj gönderme, todo listeleri gibi sık etkileşim gerektiren senaryolarda büyük fark yaratır
- Kritik ve geri dönüşü olmayan işlemlerde dikkatli olunmalıdır
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.