React’te “UI State” ile “Server State”i Ayırmak: Daha Basit Bileşenler, Daha Az Bug
React’te yerel UI state ile sunucudan gelen state’i ayırarak karmaşıklığı ve gereksiz render’ları azaltın.
React uygulamaları büyüdükçe çoğu sorun tek bir yerden çıkar: aynı state türlerini aynı sepete koymak. Modal açık mı? Seçili tab hangisi? API’den gelen kullanıcı listesi? Hepsi useState/global store’a yığıldığında bileşenler şişer, veri akışı bulanıklaşır.
Bu yazıda basit bir prensip anlatacağım: UI state (geçici, etkileşim odaklı) ile server state (sunucunun kaynağı olduğu, cache’lenebilir) ayrılmalı.
1) UI State vs Server State: Kısa Tanım
UI State (yerel):
- Modal/drawer açık mı?
- Formda şifre görünür mü?
- Seçili satır, pagination sayfası (URL’ye taşımıyorsanız)
- Hover, focus, optimistic “loading” bayrakları
Server State:
- Kullanıcı listesi, ürün detayı
- Sunucudan gelen filtrelenmiş sonuçlar
- Yetkiler/rol gibi backend kaynaklı gerçekler
Kural: Server state’i useState ile “kopyalama”. Sunucudan gelen veriyi bir kez state’e alıp sonra manuel güncellemek, stale data ve yarış koşullarını (race condition) davet eder.
2) Kötü Koku: Server Verisini Local State’e Kopyalamak
Aşağıdaki yaklaşım sık görülür:
const [users, setUsers] = useState<User[]>([]);
useEffect(() => {
fetch("/api/users").then(r => r.json()).then(setUsers);
}, []);
Sorunlar:
- Aynı veri başka yerde de çekilirse senkron kaybolur.
- “Invalidate/refresh” mantığı her yerde tekrar eder.
- Bir yerde
setUsersile güncellenir, başka yerde eski kalır.
3) Server State’i Cache’leyen Bir Katmana Verin (TanStack Query Örneği)
Server state için cache + refetch + retry + dedupe gibi ihtiyaçlar doğal. Bu yüzden TanStack Query gibi bir katman çok iş görür.
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
function useUsers() {
return useQuery({
queryKey: ["users"],
queryFn: async () => {
const res = await fetch("/api/users");
if (!res.ok) throw new Error("Fetch failed");
return res.json() as Promise<User[]>;
},
staleTime: 30_000,
});
}
Bileşen tarafı sadeleşir:
function UsersPage() {
const { data: users, isLoading, error } = useUsers();
const [selectedId, setSelectedId] = useState<string | null>(null); // UI state
if (isLoading) return <p>Yükleniyor…</p>;
if (error) return <p>Hata oluştu.</p>;
return (
<ul>
{users!.map(u => (
<li key={u.id}>
<button onClick={() => setSelectedId(u.id)}>
{u.name} {selectedId === u.id ? "(seçili)" : ""}
</button>
</li>
))}
</ul>
);
}
Burada kritik nokta: Seçili kullanıcı UI state, kullanıcı listesi server state.
4) Mutasyon Sonrası: “Elle Düzeltme” Yerine Invalidate
Kullanıcı ekleme örneği:
function useCreateUser() {
const qc = useQueryClient();
return useMutation({
mutationFn: async (payload: { name: string }) => {
const res = await fetch("/api/users", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
if (!res.ok) throw new Error("Create failed");
return res.json() as Promise<User>;
},
onSuccess: () => {
// Server state yeniden doğrulansın
qc.invalidateQueries({ queryKey: ["users"] });
},
});
}
Bu sayede setUsers([...users, newUser]) gibi “kopyayı güncelleme” dertleri azalır.
5) Pratik Bir Checklist
- Server’dan gelen veriyi mümkünse tek kaynak olarak tut: query cache.
- UI etkileşimlerini bileşene yakın tut:
useState,useReducer. - “Loading” bayraklarını ayır:
- Server:
isLoading/isFetching - UI:
isModalOpen,isSubmitting
- Server:
- “Derived state” üretmek için ekstra state açma; gerekirse
useMemokullan.
Sonuç
UI state ve server state’i bilinçli ayırmak; bileşenleri küçültür, veri tutarsızlıklarını azaltır ve debug süresini ciddi biçimde kısaltır. React’te mimariyi büyütmenin en temiz yollarından biri, state’i türüne göre doğru yere koymak.