Skip to content

Ders 06: State Management ve Data Fetching

  • 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, 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.

State TürüAçıklamaÖrnek
Client StateTarayıcıda tutulan verilerForm inputları, modal açık/kapalı
Server StateSunucudan gelen verilerKullanıcı bilgisi, veritabanı verisi
Shared StateHer ikisinde de senkronize olanSunucudan gelen verinin client’te de cache’i

TanStack Query, TanStack ekosisteminin bir parçasıdır ve client-side data fetching için mükemmel bir çözümdür.

Terminal window
# TanStack Query ve React Query entegrasyonu
pnpm add @tanstack/react-query
src/router.tsx
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
}
src/client.tsx
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>,
)

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>
)
}
import { queryOptions } from '@tanstack/react-query'
// Query options tanımla
const 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ım
export 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.

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>
)
}

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>
)
}

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>
)
}
DurumisLoadingisFetching
İlk yükleme✅ true❌ false
Arka plan yenileme❌ false✅ true
İkisi de❌ false❌ false
Yenileme var❌ false✅ true

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>
)
}

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>
)
}

Sonsuz kaydırma için infinite query kullanın:

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>
)
}

Şimdi öğrendiklerimizle tam özellikli bir Todo uygulaması yapalım!

src/routes/todos/index.tsx
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 functions
const 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 options
const todosOptions = () =>
queryOptions({
queryKey: ['todos'],
queryFn: todosGetir,
staleTime: 5 * 60 * 1000, // 5 dakika
})
// Route
export 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 component
function 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 component
function 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>
)
}

Bu derste öğrendiklerimiz:

KonuAçıklama
@tanstack/react-queryClient-side state management
useQueryVeri çekme hook’u
queryOptionsQuery tanımlama ve yeniden kullanma
useMutationVeri değiştirme işlemleri
invalidateQueriesQuery’leri yenileme
useInfiniteQueryInfinite scroll ve pagination
Optimistic updatesKullanıcıya hemen sonuç gösterme

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

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)

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ğunda
const 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

  1. Query ve loader arasındaki fark nedir?
  2. Optimistic update yapmazsak ne olur?
  3. Infinite query ne zaman kullanılmalı?

Bir sonraki derste görüşmek üzere! 👋