Ders 06: State Management ve Data Fetching
🎯 Bu Derste Neleri Öğreneceksiniz?
Section titled “🎯 Bu Derste Neleri Öğreneceksiniz?”- TanStack Query ile client-side state management
- Server ve client state senkronizasyonu
- Loading, error ve success states yönetimi
- Optimistic updates
- Cache management ve revalidation
📚 State Management Nedir?
Section titled “📚 State Management Nedir?”State management, uygulamanızdaki değişen verileri yönetmektir. Kullanıcı girişi yapmış mı? Hangi todo’lar tamamlanmış? Form submission durumu nedir? Bunların hepsi state’tir.
Client vs Server State
Section titled “Client vs Server State”| State Türü | Açıklama | Örnek |
|---|---|---|
| Client State | Tarayıcıda tutulan veriler | Form inputları, modal açık/kapalı |
| Server State | Sunucudan gelen veriler | Kullanıcı bilgisi, veritabanı verisi |
| Shared State | Her ikisinde de senkronize olan | Sunucudan gelen verinin client’te de cache’i |
🚀 TanStack Query’e Giriş
Section titled “🚀 TanStack Query’e Giriş”TanStack Query, TanStack ekosisteminin bir parçasıdır ve client-side data fetching için mükemmel bir çözümdür.
Kurulum
Section titled “Kurulum”# TanStack Query ve React Query entegrasyonupnpm add @tanstack/react-queryRouter ile Query Entegrasyonu
Section titled “Router ile Query Entegrasyonu”import { createRouter } from '@tanstack/react-router'import { QueryClient } from '@tanstack/react-query'import { ReactQueryDevtools } from '@tanstack/react-query-devtools'import { routeTree } from './routeTree.gen'
export function getRouter() { const queryClient = new QueryClient()
const router = createRouter({ routeTree, queryClient, dehydrate: () => queryClient.dehydrate(), hydrate: () => queryClient.hydrate(), scrollRestoration: true, defaultPreload: 'intent', defaultPreloadStaleTime: 0, })
return router}Client Entry Point Güncelleme
Section titled “Client Entry Point Güncelleme”import { ReactQueryDevtools } from '@tanstack/react-query-devtools'import { StartClient } from '@tanstack/react-start/client'import { StrictMode } from 'react'import { hydrateRoot } from 'react-dom/client'
hydrateRoot( document, <StrictMode> <ReactQueryDevtools initialIsOpen={false} /> <StartClient /> </StrictMode>,)📊 Basit Query Kullanımı
Section titled “📊 Basit Query Kullanımı”useQuery ile Veri Çekme
Section titled “useQuery ile Veri Çekme”import { createFileRoute } from '@tanstack/react-router'import { useQuery, useQueryClient } from '@tanstack/react-query'
const fetchKullanicilar = async () => { const response = await fetch('/api/kullanicilar') if (!response.ok) throw new Error('API hatası') return response.json()}
export const Route = createFileRoute('/kullanicilar')({ component: KullanicilarPage,})
function KullanicilarPage() { const queryClient = useQueryClient()
// useQuery ile veri çekme const { data: kullanicilar, isLoading, isError, error, } = useQuery({ queryKey: ['kullanicilar'], queryFn: fetchKullanicilar, })
if (isLoading) { return <div>Yükleniyor...</div> }
if (isError) { return <div>Hata: {error.message}</div> }
return ( <div> <h1>Kullanıcılar</h1> <ul> {kullanicilar.map((k) => ( <li key={k.id}>{k.ad}</li> ))} </ul> </div> )}QueryOptions Kullanımı
Section titled “QueryOptions Kullanımı”import { queryOptions } from '@tanstack/react-query'
// Query options tanımlaconst kullanicilarOptions = (userId: string) => queryOptions({ queryKey: ['kullanicilar', userId], queryFn: () => fetchKullanicilar(userId), staleTime: 5 * 60 * 1000, // 5 dakika boyunca "fresh" gcTime: 10 * 60 * 1000, // 10 dakika bellekte tut })
// Route'ta kullanımexport const Route = createFileRoute('/kullanicilar/$userId')({ loader: async ({ params }) => { // Prefetch const queryClient = useQueryClient() await queryClient.prefetchQuery(kullanicilarOptions(params.userId))
// Server'dan da veri çek const serverData = await fetchKullanicilar(params.userId) return { kullanicilar: serverData } }, component: KullaniciDetayPage,})
function KullaniciDetayPage() { const { kullanicilar: serverData } = Route.useLoaderData() const { userId } = Route.useParams()
// useQuery ile client-side data fetching const { data: clientData } = useQuery(kullanicilarOptions(userId))
const data = clientData || serverData
return ( <div> <h1>Kullanıcı Detayı</h1> <p>{data?.ad}</p> </div> )}🔄 Server + Client State Senkronizasyonu
Section titled “🔄 Server + Client State Senkronizasyonu”TanStack Router ve Query’un mükemmel entegrasyonu ile server ve client state’i senkronize tutabilirsiniz.
Loader ve Query Kullanımı
Section titled “Loader ve Query Kullanımı”import { createFileRoute } from '@tanstack/react-router'import { useQuery, useQueryClient } from '@tanstack/react-query'
const fetchPost = async (postId: string) => { const response = await fetch(`/api/posts/${postId}`) if (!response.ok) throw new Error('Post bulunamadı') return response.json()}
const postOptions = (postId: string) => queryOptions({ queryKey: ['posts', postId], queryFn: () => fetchPost(postId), staleTime: 60 * 1000, // 1 dakika })
export const Route = createFileRoute('/posts/$postId')({ loader: async ({ params }) => { // Server'da veri çek const queryClient = useQueryClient() return queryClient.ensureQueryData(postOptions(params.postId)) }, component: PostDetayPage,})
function PostDetayPage() { // Query otomatik olarak loader'dan veriyi kullanır const { data: post } = useQuery(postOptions(Route.useParams().postId))
return ( <div> <h1>{post?.baslik}</h1> <p>{post?.icerik}</p> </div> )}invalidateQueries ile Yenileme
Section titled “invalidateQueries ile Yenileme”Veriyi güncellemek için invalidate kullanın:
import { createFileRoute } from '@tanstack/react-router'import { useQueryClient } from '@tanstack/react-query'
const createTodo = createServerFn({ method: 'POST' }) .inputValidator( z.object({ baslik: z.string(), aciklama: z.string(), }) ) .handler(async ({ data }) => { // Todo'yu oluştur return createTodoInDatabase(data) })
export const Route = createFileRoute('/todo/olustur')({ component: TodoOlusturPage,})
function TodoOlusturPage() { const queryClient = useQueryClient() const navigate = useNavigate()
const handleSubmit = async () => { // Todo oluştur await createTodo({ data: baslik })
// Query'yi yenile queryClient.invalidateQueries({ queryKey: ['todos'] })
// Todo listesine yönlendir navigate({ to: '/todos' }) }
return ( <div> <h1>Todo Oluştur</h1> <button onClick={handleSubmit}>Todo Ekle</button> </div> )}⚡ Loading States Yönetimi
Section titled “⚡ Loading States Yönetimi”isLoading ve isFetching
Section titled “isLoading ve isFetching”import { useQuery, useQueryClient } from '@tanstack/react-query'
function TodoList() { const queryClient = useQueryClient()
const { data: todos, isLoading, // İlk yükleme isFetching, // Arka planda yenileme isError, } = useQuery({ queryKey: ['todos'], queryFn: () => fetchTodos(), })
return ( <div> {/* İlk yükleme */} {isLoading && ( <div>Yükleniyor...</div> )}
{/* Hata durumu */} {isError && ( <div>Hata: {isError.message}</div> )}
{/* Veri yok ve yüklenme yok */} {!isLoading && !todos?.length && ( <div>Henüz todo yok</div> )}
{/* Liste */} {todos && ( <ul> {todos.map((todo) => ( <li key={todo.id}>{todo.baslik}</li> ))} </ul> )}
{/* Arka planda yenileme */} {isFetching && ( <div style={{ fontSize: '0.8rem', color: '#6b7280' }}> Yenileniyor... </div> )} </div> )}isLoading vs isFetching
Section titled “isLoading vs isFetching”| Durum | isLoading | isFetching |
|---|---|---|
| İlk yükleme | ✅ true | ❌ false |
| Arka plan yenileme | ❌ false | ✅ true |
| İkisi de | ❌ false | ❌ false |
| Yenileme var | ❌ false | ✅ true |
🎯 Mutations (Veri Değiştirme)
Section titled “🎯 Mutations (Veri Değiştirme)”useMutation Kullanımı
Section titled “useMutation Kullanımı”import { useMutation, useQueryClient } from '@tanstack/react-query'
const todoSil = createServerFn({ method: 'DELETE' }) .inputValidator((todoId: string) => todoId) .handler(async ({ data: todoId }) => { await deleteTodoFromDatabase(todoId) return { todoId } })
function TodoItem({ todo }: { todo: Todo }) { const queryClient = useQueryClient()
const mutation = useMutation({ mutationFn: todoSil, onSuccess: () => { // Query'yi yenile queryClient.invalidateQueries({ queryKey: ['todos'] }) }, })
const handleDelete = () => { mutation.mutate({ data: todo.id }) }
return ( <li> {todo.baslik} <button onClick={handleDelete} disabled={mutation.isPending}> {mutation.isPending ? 'Siliniyor...' : 'Sil'} </button> </li> )}Optimistic Updates
Section titled “Optimistic Updates”Kullanıcıya hemen sonuç göster, sonra arka planda güncelle:
const todoGuncelle = createServerFn({ method: 'PUT' }) .inputValidator( z.object({ id: z.number(), baslik: z.string(), tamamlandi: z.boolean(), }) ) .handler(async ({ data }) => { return guncelleTodo(data) })
function TodoItem({ todo }: { todo: Todo }) { const queryClient = useQueryClient()
const mutation = useMutation({ mutationFn: todoGuncelle, onMutate: async ({ data }) => { // Önceki state'i kaydet const oncekiTodos = queryClient.getQueryData(['todos'])!
// Optimistic update queryClient.setQueryData(['todos'], (eski) => eski.map((t: Todo) => t.id === data.id ? { ...t, ...data } : t ) )
// Önceki state'i geri yükleme için kaydet return { oncekiTodos } }, onSuccess: () => { // Başarılı, query'yi yenile queryClient.invalidateQueries({ queryType: 'active' }) }, onError: (error, variables, context) => { // Hata oldu, geri yükle queryClient.setQueryData(['todos'], context.oncekiTodos) }, })
return ( <li> <input defaultValue={todo.baslik} onBlur={(e) => mutation.mutate({ data: { id: todo.id, baslik: e.target.value })} /> <span style={{ marginLeft: '1rem' }}> {mutation.isPending ? 'Kaydediliyor...' : 'Kaydet'} </span> </li> )}📦 Infinite Query ve Pagination
Section titled “📦 Infinite Query ve Pagination”Sonsuz kaydırma için infinite query kullanın:
Infinite Scroll
Section titled “Infinite Scroll”import { useInfiniteQuery } from '@tanstack/react-query'
function TodoList() { const { data, fetchNextPage, hasNextPage, isFetchingNextPage, } = useInfiniteQuery({ queryKey: ['todos', 'infinite'], queryFn: async ({ pageParam = 0 }) => { const response = await fetch(`/api/todos?page=${pageParam}`) return response.json() }, getNextPageParam: (lastPage) => { if (lastPage.hasMore) return lastPage.page + 1 return undefined }, initialPageParam: 0, })
const todos = data?.pages.flat() || []
return ( <div> <ul> {todos.map((todo) => ( <li key={todo.id}>{todo.baslik}</li> ))} </ul>
{hasNextPage && ( <button onClick={() => fetchNextPage()} disabled={isFetchingNextPage} style={{ marginTop: '1rem' }} > {isFetchingNextPage ? 'Yükleniyor...' : 'Daha fazla'} </button> )} </div> )}🎨 Pratik Örnek: Todo Uygulaması
Section titled “🎨 Pratik Örnek: Todo Uygulaması”Şimdi öğrendiklerimizle tam özellikli bir Todo uygulaması yapalım!
import { createFileRoute } from '@tanstack/react-router'import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'import { createServerFn, useServerFn } from '@tanstack/react-start'import { zodValidator, z } from 'zod'import { createFileRoute, redirect, notFound } from '@tanstack/react-router'
// Mock veritabanılet TODOS = [ { id: 1, baslik: 'TanStack Start öğren', tamamlandi: false }, { id: 2, baslik: 'Pratik yap', tamamlandi: true }, { id: 3, baslik: 'Video izle', tamamlandi: false },]
// Server functionsconst todosGetir = createServerFn({ method: 'GET' }).handler(async () => { return TODOS})
const todoEkle = createServerFn({ method: 'POST' }) .inputValidator( z.object({ baslik: z.string().min(3, 'En az 3 karakter'), }) ) .handler(async ({ data }) => { const yeniTodo = { id: Date.now(), baslik: data.baslik, tamamlandi: false, } TODOS.push(yeniTodo) return yeniTodo })
const todoSil = createServerFn({ method: 'DELETE' }) .inputValidator((id: number) => id) .handler(async ({ data: id }) => { TODOS = TODOS.filter((t) => t.id !== id) return { id } })
const todoGuncelle = createServerFn({ method: 'PUT' }) .inputValidator( z.object({ id: z.number(), baslik: z.string().optional(), tamamlandi: z.boolean().optional(), }) ) .handler(async ({ data }) => { const todo = TODOS.find((t) => t.id === data.id) if (!todo) throw new Error('Todo bulunamadı') Object.assign(todo, data) return todo })
// Query optionsconst todosOptions = () => queryOptions({ queryKey: ['todos'], queryFn: todosGetir, staleTime: 5 * 60 * 1000, // 5 dakika })
// Routeexport const Route = createFileRoute('/todos')({ component: TodoListPage,})
function TodoListPage() { const queryClient = useQueryClient()
const { data: todos, isLoading, isError, } = useQuery(todosOptions())
const silmeMutation = useMutation({ mutationFn: todoSil, onSuccess: () => { queryClient.invalidateQueries({ queryType: 'active' }) }, })
const toggleMutation = useMutation({ mutationFn: todoGuncelle, onMutate: async ({ data }) => { const oncekiTodos = queryClient.getQueryData(['todos'])!
queryClient.setQueryData(['todos'], (eski) => eski.map((t) => t.id === data.id ? { ...t, ...data } : t ) )
return { oncekiTodos } }, onError: (error, variables, context) => { queryClient.setQueryData(['todos'], context.oncekiTodos) }, })
if (isLoading) return <div>Yükleniyor...</div> if (isError) return <div>Hata: {isError.message}</div>
return ( <div style={{ padding: '2rem', maxWidth: '800px', margin: '0 auto' }}> <h1 style={{ marginBottom: '2rem' }}>Todo Listesi</h1>
<TodoForm />
<ul style={{ listStyle: 'none', padding: 0 }}> {todos.map((todo) => ( <TodoItem key={todo.id} todo={todo} onToggle={toggleMutation.mutate} onSil={silmeMutation.mutate} /> ))} </ul> </div> )}
// TodoForm componentfunction TodoForm() { const queryClient = useQueryClient() const navigate = useNavigate()
const ekleMutation = useMutation({ mutationFn: todoEkle, onSuccess: () => { queryClient.invalidateQueries({ queryType: 'active' }) }, })
const handleSubmit = async (formData: FormData) => { const baslik = formData.get('baslik') as string await ekleMutation.mutate({ data: { baslik } }) }
return ( <form action={ekleMutation.mutate.url} onSubmit={handleSubmit} style={{ marginBottom: '2rem' }} > <input name="baslik" placeholder="Yeni todo..." required style={{ width: '100%', padding: '0.75rem', border: '1px solid #d1d5db', borderRadius: '4px', }} /> <button type="submit" disabled={ekleMutation.isPending} style={{ marginTop: '1rem', padding: '0.75rem 1.5rem', backgroundColor: '#3b82f6', color: 'white', border: 'none', borderRadius: '4px', cursor: ekleMutation.isPending ? 'not-allowed' : 'pointer', }} > {ekleMutation.isPending ? 'Ekleniyor...' : 'Ekle'} </button> </form )}
// TodoItem componentfunction TodoItem({ todo, onToggle, onSil,}: { todo: Todo onToggle: (data: { data: typeof Todo }) => void onSil: (data: { data: typeof Todo }) => void}) { const queryClient = useQueryClient()
return ( <li style={{ display: 'flex', alignItems: 'center', gap: '1rem', padding: '0.5rem', border: '1px solid #e5e7eb', borderRadius: '4px', marginBottom: '0.5rem', backgroundColor: todo.tamamlandi ? '#f0fdf4' : 'white', }} > <input type="checkbox" checked={todo.tamamlandi} onChange={() => onToggle({ data: { id: todo.id, tamamlandi: !todo.tamamlandi } })} style={{ cursor: 'pointer' }} /> <span style={{ textDecoration: todo.tamlandi ? 'line-through' : 'none', color: todo.tamlandi ? '#9ca3af' : 'inherit', flex: 1, }} > {todo.baslik} </span> <button onClick={() => onSil({ data: todo.id })} disabled={onSil.isPending} style={{ padding: '0.25rem 0.5rem', backgroundColor: '#ef4444', color: 'white', border: 'none', borderRadius: '4px', cursor: onSil.isPending ? 'not-allowed' : 'pointer', }} > {onSil.isPending ? 'Siliniyor...' : 'Sil'} </button> </li> )}✅ Ders 6 Özeti
Section titled “✅ Ders 6 Özeti”Bu derste öğrendiklerimiz:
| Konu | Açıklama |
|---|---|
| @tanstack/react-query | Client-side state management |
| useQuery | Veri çekme hook’u |
| queryOptions | Query tanımlama ve yeniden kullanma |
| useMutation | Veri değiştirme işlemleri |
| invalidateQueries | Query’leri yenileme |
| useInfiniteQuery | Infinite scroll ve pagination |
| Optimistic updates | Kullanıcıya hemen sonuç gösterme |
📝 Alıştırmalar
Section titled “📝 Alıştırmalar”Alıştırma 1: Kullanıcı Profili
Section titled “Alıştırma 1: Kullanıcı Profili”Kullanıcı detay sayfası yapın:
- Loader ile sunucudan temel bilgileri al
- Query ile ek bilgileri client’ten çek (kullanıcı ayarları vb.)
- Mutation ile profil güncelleme
Alıştırma 2: Real-time Sayaç
Section titled “Alıştırma 2: Real-time Sayaç”Her 5 saniyede yenilenen bir sayaç yapın:
const sayaclarOptions = queryOptions({ queryKey: ['sayaclar'], queryFn: fetchSayaclar, refetchInterval: 5000, // 5 saniyede bir})
const { data: sayaclar } = useQuery(sayaclarOptions)Alıştırma 3: Arama ve Filtreleme
Section titled “Alıştırma 3: Arama ve Filtreleme”Arama sonucunu otomatik olarak güncelleyin:
const aramaOptions = (query: string) => queryOptions({ queryKey: ['arama', query], queryFn: () => fetchArama(query), staleTime: 0, // Her zaman fresh data })
// Form submit olduğundaconst queryClient = useQueryClient()const navigate = useNavigate()
const handleAra = (query: string) => { navigate({ to: '/arama', search: { q: query } })}🚀 Sonraki Ders: SSR ve Rendering Modları
Section titled “🚀 Sonraki Ders: SSR ve Rendering Modları”Bir sonraki derste şunları öğreneceksiniz:
- 🌐 SSR nedir ve neden kullanmalıyız?
- 🎯 Selective SSR ile route bazlı kontrol
- 🔄 SPA mode tam olarak nedir?
- 🐳 Static prerendering nasıl yapılır?
- ⚡ Streaming SSR ile performans optimizasyonu
💬 Sorularınız?
Section titled “💬 Sorularınız?”- Query ve loader arasındaki fark nedir?
- Optimistic update yapmazsak ne olur?
- Infinite query ne zaman kullanılmalı?
Bir sonraki derste görüşmek üzere! 👋