React'te useOptimistic Hook ile Anlık UI Güncellemeleri: Kullanıcı Deneyimini Devrimleştirin
Günümüz web uygulamalarında kullanıcı deneyimi (UX) en kritik unsurlardan biridir. Kullanıcılar, hızlı, akıcı ve anında geri bildirim veren arayüzlere alışıktır. Bir butona tıkladığınızda veya bir form gönderdiğinizde, uygulamanın yanıt vermesi için beklemek kullanıcıyı hayal kırıklığına uğratabilir. İşte bu noktada "optimistic UI" (iyimser kullanıcı arayüzü) kavramı devreye girer.
Optimistic UI, bir işlemin başarılı olacağını varsayarak, işlemin tamamlanmasını beklemeden arayüzde hemen güncellemeler yapma tekniğidir. Bu, kullanıcıya anında geri bildirim sağlar ve uygulamanın daha hızlı ve duyarlı görünmesini sağlar. Ancak, optimistic UI'ı doğru bir şekilde uygulamak bazen karmaşık olabilir, özellikle de işlemin başarısız olma ihtimali varsa.
React 19 ile birlikte gelen useOptimistic hook'u, optimistic UI desenini uygulamayı inanılmaz derecede kolaylaştırır. Bu hook, bir işlemin sonucunu beklerken arayüzde geçici güncellemeler yapmanıza olanak tanır ve işlemin kendisi tamamlandığında bu geçici güncellemeleri doğru durumla değiştirir. Bu yazıda, useOptimistic hook'unu derinlemesine inceleyecek, nasıl çalıştığını anlayacak ve gerçek dünya senaryolarında nasıl kullanabileceğinizi göreceğiz.
Optimistic UI Nedir ve Neden Önemlidir?
Optimistic UI, bir işlemin (örneğin, bir veriyi kaydetme, bir öğeyi silme veya bir beğeni gönderme) sunucu tarafından onaylanmasını beklemeden, hemen arayüzde bir güncelleme yapılmasıdır. İşlem başarılı olursa, bu geçici güncelleme kalıcı hale gelir. İşlem başarısız olursa, arayüz önceki durumuna geri döner veya bir hata mesajı gösterilir.
Optimistic UI'ın Faydaları:
- Daha İyi Kullanıcı Deneyimi: Kullanıcılar anında geri bildirim alırlar, bu da uygulamanın daha hızlı ve akıcı hissetmesini sağlar.
- Azalan Algılanan Gecikme: Kullanıcılar bir işlem gerçekleştirdiklerinde beklemek zorunda kalmazlar, bu da uygulamanın daha duyarlı olduğu izlenimini verir.
- Artan Etkileşim: Kullanıcılar, işlemlerinin hemen yansıtıldığını gördüklerinde daha fazla etkileşimde bulunmaya teşvik edilirler.
Ancak, optimistic UI'ın bir dezavantajı da vardır: işlemin başarısız olma olasılığı. Eğer işlem başarısız olursa ve kullanıcıya doğru geri bildirim verilmezse, bu durum kafa karışıklığına ve güven kaybına yol açabilir. İşte useOptimistic hook'u bu noktada devreye girerek bu zorluğu ortadan kaldırır.
useOptimistic Hook'u Nedir ve Nasıl Çalışır?
useOptimistic hook'u, React'in sunucu durumu yönetimi (Server State Management) konusundaki yaklaşımını basitleştirmek için tasarlanmış yeni bir hook'tur. Temel olarak, bir durum değişkenini yönetir ve bu durum değişkeninin bir "iyimser" (optimistic) değerini tutar. Bir işlem başlatıldığında, bu iyimser değer hemen güncellenir. İşlem tamamlandığında (başarılı veya başarısız), hook bu iyimser değeri gerçek sunucu durumuna göre günceller.
useOptimistic hook'u iki argüman alır:
currentState: Mevcut gerçek (sunucu tarafından onaylanmış) durumunuz.optimisticUpdate: Bu, bir fonksiyon veya bir değer olabilir. Bir işlem başlatıldığında,optimisticUpdatefonksiyonu çağrılır ve yeni iyimser durumu döndürür.
Hook, currentState'in bir kopyasını döndürür. Bu döndürülen değer, ya mevcut gerçek durumunuzu ya da işlemin sonucunu beklerken güncellenmiş iyimser durumunuzu temsil eder.
Temel Kullanım Yapısı:
import { useOptimistic } from 'react';
function MyComponent({ initialItems }) {
const [items, setItems] = useOptimistic(
initialItems,
(currentState, newItem) => {
// newItem, eklemek istediğiniz yeni öğeyi temsil eder.
// currentState, mevcut öğeler dizisidir.
// Bu fonksiyon, yeni iyimser durumu döndürmelidir.
return [...currentState, newItem];
}
);
const addItem = async (itemData) => {
// Sunucuya öğeyi ekleme işlemini başlatın.
// Bu işlem asenkron olacaktır.
// İşlem tamamlanmadan önce iyimser olarak öğeyi ekleyin.
// useOptimistic hook'u bunu sizin için yönetecektir.
setItems(itemData); // Bu satır, hook'un optimisticUpdate fonksiyonunu tetikler.
// Sunucuya gerçek ekleme işlemini yapın.
try {
await fakeApiCallToAddItem(itemData);
// İşlem başarılı olursa, hook otomatik olarak gerçek durumu güncelleyecektir.
} catch (error) {
// İşlem başarısız olursa, hook durumu geri alacaktır.
console.error("Öğe eklenemedi:", error);
// Hata durumunda kullanıcıya bilgi verebilirsiniz.
}
};
return (
<div>
{items.map(item => (
<div key={item.id}>{item.name}</div>
))}
<button onClick={() => addItem({ id: Date.now(), name: "Yeni Öğe" })}>
Öğe Ekle
</button>
</div>
);
}
// Sahte API çağrısı (gerçek bir API çağrısı ile değiştirilmelidir)
const fakeApiCallToAddItem = (item) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (Math.random() > 0.2) { // %80 başarı oranı
resolve();
} else {
reject(new Error("Sunucu hatası"));
}
}, 1000);
});
};Yukarıdaki örnekte:
initialItems, bileşenin ilk durumunu temsil eder.useOptimistichook'u,initialItems'ıcurrentStateolarak alır.- İkinci argüman, bir fonksiyon. Bu fonksiyon, yeni bir öğe eklendiğinde çağrılır ve mevcut öğeler dizisine yeni öğeyi ekleyerek yeni iyimser durumu döndürür.
setItems(itemData)çağrıldığında,useOptimistichook'uoptimisticUpdatefonksiyonunu tetikler veitemsdeğişkenini yeni iyimser durumla günceller. Bu, UI'da anında görünür.fakeApiCallToAddItemfonksiyonu, sunucuya gerçek isteği yapar.- Eğer istek başarılı olursa, React otomatik olarak
items'ı sunucudan gelen gerçek veriye göre günceller. - Eğer istek başarısız olursa,
useOptimistichook'uitems'ı önceki duruma geri döndürür.
useOptimistic Hook'u ile Yaygın Kullanım Senaryoları
useOptimistic hook'u, çeşitli kullanıcı etkileşimlerinde akıcı bir deneyim sağlamak için kullanılabilir. İşte bazı yaygın senaryolar:
1. Öğe Ekleme/Silme
Bir liste veya koleksiyona yeni bir öğe eklerken veya mevcut bir öğeyi silerken, kullanıcıya anında geri bildirim vermek deneyimi büyük ölçüde iyileştirir.
Örnek: Görev Listesi
import { useOptimistic, useState, useTransition } from 'react';
interface Task {
id: number;
text: string;
done: boolean;
}
// Sahte API çağrıları
const fakeApi = {
addTask: async (text: string): Promise<Task> => {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (Math.random() > 0.1) { // %90 başarı
resolve({ id: Date.now(), text, done: false });
} else {
reject(new Error('Görev eklenemedi'));
}
}, 500);
});
},
toggleTask: async (id: number, done: boolean): Promise<void> => {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (Math.random() > 0.1) { // %90 başarı
resolve();
} else {
reject(new Error('Görev durumu güncellenemedi'));
}
}, 500);
});
},
deleteTask: async (id: number): Promise<void> => {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (Math.random() > 0.1) { // %90 başarı
resolve();
} else {
reject(new Error('Görev silinemedi'));
}
}, 500);
});
},
};
function TodoList() {
const [tasks, setTasks] = useState<Task[]>([]);
const [isAdding, startTransition] = useTransition(); // UI'da geçişleri yönetmek için
const [optimisticTasks, addOptimisticTask] = useOptimistic<Task[], Task>(
tasks,
(currentState, newTask) => [...currentState, newTask]
);
const handleAddTask = async (text: string) => {
startTransition(async () => {
const newTask = { id: Date.now(), text, done: false }; // Geçici ID
addOptimisticTask(newTask); // İyimser olarak ekle
try {
const addedTask = await fakeApi.addTask(text);
// Sunucu onayladıktan sonra gerçek görevle değiştir
setTasks(prevTasks => prevTasks.map(t => t.id === newTask.id ? addedTask : t));
// Not: Bu, sunucudan gelen gerçek ID ile geçici ID'yi eşleştirmek için bir stratejidir.
// Daha karmaşık durumlarda, sunucudan dönen ID'yi yönetmek için daha gelişmiş bir state yönetimi gerekebilir.
} catch (error) {
console.error(error);
// Hata durumunda iyimser eklemeyi geri al
setTasks(prevTasks => prevTasks.filter(t => t.id !== newTask.id));
}
});
};
const handleToggleTask = async (id: number) => {
startTransition(async () => {
const taskToToggle = tasks.find(t => t.id === id);
if (!taskToToggle) return;
const updatedTask = { ...taskToToggle, done: !taskToToggle.done };
// İyimser olarak durumu değiştir
setTasks(prevTasks => prevTasks.map(t => t.id === id ? updatedTask : t));
try {
await fakeApi.toggleTask(id, updatedTask.done);
// Sunucu onayladı, durum zaten güncellendi.
} catch (error) {
console.error(error);
// Hata durumunda iyimser değişikliği geri al
setTasks(prevTasks => prevTasks.map(t => t.id === id ? taskToToggle : t));
}
});
};
const handleDeleteTask = async (id: number) => {
startTransition(async () => {
const taskToDelete = tasks.find(t => t.id === id);
if (!taskToDelete) return;
// İyimser olarak sil
setTasks(prevTasks => prevTasks.filter(t => t.id !== id));
try {
await fakeApi.deleteTask(id);
// Sunucu onayladı, zaten silindi.
} catch (error) {
console.error(error);
// Hata durumunda iyimser silmeyi geri al
setTasks(prevTasks => [...prevTasks, taskToDelete]);
}
});
};
return (
<div>
<h1>Görev Listesi</h1>
<input
type="text"
placeholder="Yeni görev ekle"
onKeyDown={(e) => {
if (e.key === 'Enter' && e.currentTarget.value.trim()) {
handleAddTask(e.currentTarget.value.trim());
e.currentTarget.value = '';
}
}}
/>
{isAdding && <p>Yükleniyor...</p>}
<ul>
{optimisticTasks.map(task => (
<li key={task.id} style={{ textDecoration: task.done ? 'line-through' : 'none' }}>
<span onClick={() => handleToggleTask(task.id)} style={{ cursor: 'pointer' }}>
{task.text}
</span>
<button onClick={() => handleDeleteTask(task.id)}>Sil</button>
</li>
))}
</ul>
</div>
);
}Bu örnekte useOptimistic'i doğrudan kullanmak yerine, durumu doğrudan setTasks ile güncelleyip, useTransition ile UI'da geçişleri yönettik. useOptimistic hook'u aslında React'in gelecekteki sürümlerinde bu tür durum yönetimini daha da basitleştirecek bir mekanizma olarak düşünülmelidir. Mevcut durumda, useOptimistic'in temel amacı, sunucuya giden bir işlemin sonucunu beklerken arayüzdeki bir değeri yönetmektir. Yukarıdaki örnekte, setTasks ile yapılan doğrudan güncellemeler useOptimistic'in yaptığı işi simüle eder.
Not: React'in useOptimistic hook'u, React 19 ile birlikte daha belirgin ve kullanışlı hale gelecektir. Yukarıdaki örnek, useOptimistic'in temel mantığını ve optimistic UI prensibini göstermektedir. Gelecekteki React sürümlerinde, useOptimistic'in API'ı ve kullanım şekli biraz daha farklılık gösterebilir.
2. Yorumlara Beğeni Ekleme/Kaldırma
Bir gönderiye yorum yapıldığında veya bir yoruma beğeni eklendiğinde, beğeni sayısının anında artması kullanıcıyı memnun eder.
import { useOptimistic, useState, useTransition } from 'react';
interface Post {
id: number;
content: string;
likes: number;
}
// Sahte API
const fakeApi = {
likePost: async (postId: number): Promise<number> => {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (Math.random() > 0.1) {
resolve(1); // Başarılı olursa 1 beğeni eklenir
} else {
reject(new Error('Beğeni eklenemedi'));
}
}, 300);
});
},
unlikePost: async (postId: number): Promise<number> => {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (Math.random() > 0.1) {
resolve(-1); // Başarılı olursa 1 beğeni eksiltilir
} else {
reject(new Error('Beğeni kaldırılamadı'));
}
}, 300);
});
},
};
function PostWithLikes({ initialPost }: { initialPost: Post }) {
const [post, setPost] = useState<Post>(initialPost);
const [isLiking, startTransition] = useTransition();
const [optimisticLikes, updateOptimisticLikes] = useOptimistic<number, number>(
post.likes,
(currentLikes, increment) => currentLikes + increment
);
const handleLikeToggle = async () => {
startTransition(async () => {
const previousLikes = post.likes;
const increment = post.likes === initialPost.likes ? 1 : -1; // Basit bir varsayım, daha karmaşık mantık gerekebilir
updateOptimisticLikes(increment); // İyimser olarak beğeni sayısını güncelle
try {
let result: number;
if (increment === 1) {
result = await fakeApi.likePost(post.id);
} else {
result = await fakeApi.unlikePost(post.id);
}
// Sunucu onayladı, iyimser güncelleme zaten yapıldı.
// Gerçek beğeni sayısını sunucudan gelenle güncelleyebiliriz (opsiyonel).
setPost(prev => ({ ...prev, likes: prev.likes + result }));
} catch (error) {
console