Skip to content

Ders 03: Routing - Orta Seviye

  • Search params ile arama ve filtreleme nasıl yapılır?
  • Loader fonksiyonları ile veri yükleme
  • Loading ve error durumlarını handle etme
  • Programatik yönlendirme (redirects)
  • 404 sayfaları ve not found handling

Search params, URL’nin ? işaretinden sonraki kısmıdır. Örneğin: /arama?q=javascript&sort=date

import { createFileRoute } from '@tanstack/react-router'
import { zod } from 'zod'
// Search params şeması tanımla
const aramaSchema = z.object({
q: z.string().optional(), // Arama sorgusu
sort: z.enum(['date', 'pop']).optional(), // Sıralama
page: z.number().optional(), // Sayfa numarası
})
export const Route = createFileRoute('/arama')({
validateSearch: aramaSchema,
component: AramaPage,
})
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/arama')({
component: AramaPage,
validateSearch: (search) => {
// Basit validation (Zod kullanmadan)
return {
...search,
page: Number(search.page || 1),
}
},
})
function AramaPage() {
// Search params'i alma
const search = Route.useSearch()
const navigate = useNavigate()
return (
<div>
<h1>Arama</h1>
<p>Arama sorgusu: {search.q || 'Boş'}</p>
<p>Sayfa: {search.page}</p>
{/* Arama formu */}
<form
onSubmit={(e) => {
e.preventDefault()
const formData = new FormData(e.currentTarget)
navigate({
to: '/arama',
search: {
q: formData.get('q'),
page: 1,
},
})
}}
>
<input name="q" placeholder="Ara..." defaultValue={search.q} />
<button type="submit">Ara</button>
</form>
</div>
)
}
function AramaPage() {
const search = Route.useSearch()
const navigate = useNavigate()
const sirala = (yontem: 'date' | 'pop') => {
navigate({
to: '/arama',
search: (prev) => ({
...prev,
sort: yontem,
}),
})
}
return (
<div>
<button onClick={() => sirala('date')}>Tarihe Göre Sırala</button>
<button onClick={() => sirala('pop')}>Popülerliğe Göre Sırala</button>
</div>
)
}

Loader fonksiyonları, sayfa yüklenirken veri çekmemizi sağlar. Server-side ve client-side çalışır.

import { createFileRoute } from '@tanstack/react-router'
// Mock data
const URUNLER = [
{ id: 1, ad: 'Laptop', fiyat: 25000 },
{ id: 2, ad: 'Mouse', fiyat: 500 },
{ id: 3, ad: 'Klavye', fiyat: 1000 },
]
export const Route = createFileRoute('/urunler')({
loader: async () => {
// Veriyi getir (mock, gerçekte API çağrısı olurdu)
return { urunler: URUNLER }
},
component: UrunlerPage,
})
function UrunlerPage() {
// Loader'dan gelen veriye eriş
const { urunler } = Route.useLoaderData()
return (
<div>
<h1>Ürünler</h1>
<ul>
{urunler.map((urun) => (
<li key={urun.id}>
{urun.ad} - {urun.fiyat} TL
</li>
))}
</ul>
</div>
)
}
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/urunler/$kategori')({
loader: async ({ params }) => {
const { kategori } = params
// Mock API çağrısı
const urunler = await fetch(`/api/urunler?kategori=${kategori}`)
.then((res) => res.json())
return { kategori, urunler }
},
component: KategoriUrunlerPage,
})
function KategoriUrunlerPage() {
const { kategori, urunler } = Route.useLoaderData()
return (
<div>
<h1>{kategori} Ürünleri</h1>
<p>{urunler.length} ürün bulundu.</p>
</div>
)
}
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/blog')({
validateSearch: (search) => ({
q: search.q || '',
kategori: search.kategori || 'tum',
}),
loader: async ({ search }) => {
// Search params'e göre API çağrısı
const yazilar = await fetch(
`/api/blog?q=${search.q}&kategori=${search.kategori}`
).then((res) => res.json())
return { yazilar, arama: search.q }
},
component: BlogPage,
})
function BlogPage() {
const { yazilar, arama } = Route.useLoaderData()
return (
<div>
<h1>Blog</h1>
{aram && <p>Arama: "{arama}" için sonuçlar</p>}
<ul>
{yazilar.map((yazi) => (
<li key={yazi.id}>{yazi.baslik}</li>
))}
</ul>
</div>
)
}

Kullanıcıya yükleniyor durumunu ve hataları göstermek çok önemlidir.

import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/urunler')({
loader: async () => {
// Simüle edilmiş gecikme
await new Promise((resolve) => setTimeout(resolve, 2000))
const urunler = await fetch('/api/urunler')
.then((res) => res.json())
return { urunler }
},
component: UrunlerPage,
pendingComponent: UrunlerYukleniyor, // Loading component
errorComponent: UrunlerHata, // Error component
})
// Loading component
function UrunlerYukleniyor() {
return (
<div style={{ padding: '2rem', textAlign: 'center' }}>
<div style={{
border: '4px solid #f3f4f6',
borderTop: '4px solid #3b82f6',
borderRadius: '50%',
width: '50px',
height: '50px',
animation: 'spin 1s linear infinite',
margin: '0 auto 1rem',
}}></div>
<p>Ürünler yükleniyor...</p>
<style>{`
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
`}</style>
</div>
)
}
// Error component
function UrunlerHata({ error }: { error: unknown }) {
return (
<div style={{ padding: '2rem', backgroundColor: '#fee2e2', borderRadius: '8px' }}>
<h2 style={{ color: '#991b1b' }}>Hata!</h2>
<p>Ürünler yüklenirken bir sorun oluştu.</p>
<button
onClick={() => window.location.reload()}
style={{
padding: '0.5rem 1rem',
backgroundColor: '#ef4444',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
}}
>
Tekrar Dene
</button>
</div>
)
}

Tüm uygulamada kullanılmak üzere global error component:

src/router.tsx
import { createRouter, ErrorComponentProps } from '@tanstack/react-router'
import { routeTree } from './routeTree.gen'
export function getRouter() {
const router = createRouter({
routeTree,
defaultErrorComponent: ({ error }) => (
<div style={{
padding: '2rem',
backgroundColor: '#fee',
margin: '1rem',
borderRadius: '8px'
}}>
<h2>Bir şeyler ters gitti! 😢</h2>
<p>{error.message}</p>
</div>
),
})
return router
}
export const Route = createFileRoute('/urunler')({
loader: async () => {
throw new Error('API bağlantısı başarısız')
},
component: UrunlerPage,
errorComponent: UrunlerError,
})
function UrunlerError({ error }: ErrorComponentProps) {
return (
<div>
<h2>Ürünler yüklenemedi</h2>
<p>Hata: {error.message}</p>
<Link to="/">Ana sayfaya dön</Link>
</div>
)
}

Kullanıcıyı başka bir sayfaya yönlendirmek için redirect() kullanın.

import { createFileRoute, redirect } from '@tanstack/react-router'
export const Route = createFileRoute('/eski-sayfa')({
loader: async () => {
// Kullanıcıyı yeni sayfaya yönlendir
throw redirect({
to: '/yeni-sayfa',
// Veya harici URL
// href: 'https://ornek.com'
})
},
})
import { createFileRoute, redirect } from '@tanstack/react-router'
export const Route = createFileRoute('/admin')({
loader: async () => {
const kullanici = await getKullanici()
if (!kullanici?.isAdmin) {
throw redirect({
to: '/giris',
search: { redirect: '/admin' }, // Login sonrası buraya yönlendir
})
}
return { kullanici }
},
component: AdminPage,
})
function AdminPage() {
const { kullanici } = Route.useLoaderData()
return <h1>Hoş geldin {kullanici.ad}</h1>
}
export const Route = createFileRoute('/ozel-sayfa')({
beforeLoad: async () => {
const ozelMi = await kontrolEtOzelMi()
if (!ozelMi) {
throw redirect({ to: '/yetki-yok' })
}
},
component: OzelSayfa,
})

Kullanıcıya var olmayan bir sayfa için 404 gösterin.

import { createFileRoute, notFound, NotFoundComponentProps } from '@tanstack/react-router'
export const Route = createFileRoute('/blog/$slug')({
loader: async ({ params }) => {
const { slug } = params
const yazi = await fetchYazi(slug)
if (!yazi) {
// 404 fırlat
throw notFound()
}
return { yazi }
},
component: BlogDetay,
notFoundComponent: BlogYaziBulunamadi,
})
function BlogYaziBulunamadi() {
return (
<div style={{ padding: '2rem', textAlign: 'center' }}>
<h1 style={{ fontSize: '3rem', marginBottom: '1rem' }}>404</h1>
<p style={{ fontSize: '1.2rem', marginBottom: '2rem' }}>
Aradığınız yazı bulunamadı. 😔
</p>
<Link
to="/blog"
style={{
padding: '0.75rem 1.5rem',
backgroundColor: '#3b82f6',
color: 'white',
textDecoration: 'none',
borderRadius: '4px',
}}
>
Blog Listesine Dön
</Link>
</div>
)
}
src/routes/__root.tsx
export const Route = createRootRoute({
component: RootComponent,
notFoundComponent: NotFound,
})
function NotFound() {
return (
<div style={{ padding: '3rem', textAlign: 'center' }}>
<h1 style={{ fontSize: '4rem', marginBottom: '1rem' }}>404</h1>
<p style={{ fontSize: '1.2rem', marginBottom: '2rem', color: '#6b7280' }}>
Aradığınız sayfa bulunamadı. 😕
</p>
<Link
to="/"
style={{
padding: '0.75rem 1.5rem',
backgroundColor: '#3b82f6',
color: 'white',
textDecoration: 'none',
borderRadius: '4px',
}}
>
Ana Sayfaya Dön
</Link>
</div>
)
}

Şimdi öğrendiklerimizle tam özellikli bir ürün kataloğu yapalım!

src/routes/urunler.tsx
import { createFileRoute, Link, useNavigate } from '@tanstack/react-router'
import { zod } from 'zod'
// Mock data
const TUM_URUNLER = [
{ id: 1, ad: 'Laptop', kategori: 'elektronik', fiyat: 25000, stok: 5 },
{ id: 2, ad: 'Mouse', kategori: 'elektronik', fiyat: 500, stok: 15 },
{ id: 3, ad: 'Klavye', kategori: 'elektronik', fiyat: 1000, stok: 8 },
{ id: 4, ad: 'T-Shirt', kategori: 'giyim', fiyat: 200, stok: 50 },
{ id: 5, ad: 'Pantolon', kategori: 'giyim', fiyat: 400, stok: 20 },
]
const urunlerSchema = z.object({
kategori: z.enum(['tum', 'elektronik', 'giyim']).optional(),
minFiyat: z.string().optional(),
maxFiyat: z.string().optional(),
sirala: z.enum(['fiyat-asc', 'fiyat-desc', 'ad-asc']).optional(),
})
export const Route = createFileRoute('/urunler')({
validateSearch: urunlerSchema,
loader: async ({ search }) => {
// Simüle edilmiş API gecikmesi
await new Promise((resolve) => setTimeout(resolve, 500))
// Filtreleme
let filtrelenmis = [...TUM_URUNLER]
if (search.kategori && search.kategori !== 'tum') {
filtrelenmis = filtrelenmis.filter((u) => u.kategori === search.kategori)
}
if (search.minFiyat) {
filtrelenmis = filtrelenmis.filter((u) => u.fiyat >= Number(search.minFiyat))
}
if (search.maxFiyat) {
filtrelenmis = filtrelenmis.filter((u) => u.fiyat <= Number(search.maxFiyat))
}
// Sıralama
if (search.sirala === 'fiyat-asc') {
filtrelenmis.sort((a, b) => a.fiyat - b.fiyat)
} else if (search.sirala === 'fiyat-desc') {
filtrelenmis.sort((a, b) => b.fiyat - a.fiyat)
} else if (search.sirala === 'ad-asc') {
filtrelenmis.sort((a, b) => a.ad.localeCompare(b.ad))
}
return { urunler: filtrelenmis, search }
},
component: UrunlerPage,
pendingComponent: () => <div>Yükleniyor...</div>,
})
function UrunlerPage() {
const { urunler, search } = Route.useLoaderData()
const navigate = useNavigate()
const handleFiltre = (yeniArama) => {
navigate({
to: '/urunler',
search: (prev) => ({ ...prev, ...yeniArama }),
})
}
return (
<div style={{ padding: '2rem', maxWidth: '1200px', margin: '0 auto' }}>
<h1 style={{ fontSize: '2rem', marginBottom: '2rem' }}>Ürün Katalogu</h1>
{/* Filtreler */}
<div
style={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))',
gap: '1rem',
marginBottom: '2rem',
padding: '1.5rem',
backgroundColor: '#f9fafb',
borderRadius: '8px',
}}
>
<div>
<label style={{ display: 'block', marginBottom: '0.5rem' }}>
Kategori:
</label>
<select
value={search.kategori || 'tum'}
onChange={(e) => handleFiltre({ kategori: e.target.value })}
style={{ width: '100%', padding: '0.5rem' }}
>
<option value="tum">Tümü</option>
<option value="elektronik">Elektronik</option>
<option value="giyim">Giyim</option>
</select>
</div>
<div>
<label style={{ display: 'block', marginBottom: '0.5rem' }}>
Min Fiyat:
</label>
<input
type="number"
value={search.minFiyat || ''}
onChange={(e) => handleFiltre({ minFiyat: e.target.value || undefined })}
placeholder="Min"
style={{ width: '100%', padding: '0.5rem' }}
/>
</div>
<div>
<label style={{ display: 'block', marginBottom: '0.5rem' }}>
Max Fiyat:
</label>
<input
type="number"
value={search.maxFiyat || ''}
onChange={(e) => handleFiltre({ maxFiyat: e.target.value || undefined })}
placeholder="Max"
style={{ width: '100%', padding: '0.5rem' }}
/>
</div>
<div>
<label style={{ display: 'block', marginBottom: '0.5rem' }}>
Sıralama:
</label>
<select
value={search.sirala || ''}
onChange={(e) => handleFiltre({ sirala: e.target.value || undefined })}
style={{ width: '100%', padding: '0.5rem' }}
>
<option value="">Seçiniz</option>
<option value="fiyat-asc">Fiyat (Artan)</option>
<option value="fiyat-desc">Fiyat (Azalan)</option>
<option value="ad-asc">İsim (A-Z)</option>
</select>
</div>
</div>
{/* Sonuçlar */}
<div>
<p style={{ marginBottom: '1rem', color: '#6b7280' }}>
{urunler.length} ürün bulundu.
</p>
<div
style={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fill, minmax(250px, 1fr))',
gap: '1rem',
}}
>
{urunler.map((urun) => (
<div
key={urun.id}
style={{
border: '1px solid #e5e7eb',
borderRadius: '8px',
padding: '1rem',
}}
>
<Link
to={`/urunler/${urun.id}`}
style={{ textDecoration: 'none', color: 'inherit' }}
>
<h3 style={{ marginBottom: '0.5rem' }}>{urun.ad}</h3>
<p style={{ color: '#6b7280', fontSize: '0.9rem' }}>
Kategori: {urun.kategori}
</p>
<p style={{ fontSize: '1.2rem', fontWeight: 'bold', color: '#3b82f6' }}>
{urun.fiyat} TL
</p>
<p style={{ fontSize: '0.9rem', color: urun.stok < 5 ? '#ef4444' : '#10b981' }}>
Stok: {urun.stok} adet
</p>
</Link>
</div>
))}
</div>
</div>
</div>
)
}

Bu derste öğrendiklerimiz:

KonuAçıklama
validateSearchSearch params type-safe validation
useSearchSearch params’e erişim
loaderSayfa yüklenirken veri çekme
useLoaderDataLoader’dan gelen veriye erişim
pendingComponentLoading state’i göster
errorComponentError state’i göster
redirect()Kullanıcıyı yönlendirme
notFound()404 hatası fırlatma

Blog için arama sayfası yapın:

  • Search param: q (arama sorgusu)
  • Search param: yil (filtreleme)
  • Loader’da API’ye istek atın
  • pending ve error component ekleyin

Todo uygulaması için:

  • /todos - Tüm todo’lar
  • /todos?durum=tamamlanan - Filtreli liste
  • Loader ile veri çekme
  • Arama formu ile search params güncelleme

Korumalı bir sayfa yapın:

  • Loader’da kullanıcı kontrolü
  • Giriş yapmamışsa /login sayfasına redirect
  • Hata durumunda error component göster

🚀 Sonraki Ders: Server Functions - Giriş

Section titled “🚀 Sonraki Ders: Server Functions - Giriş”

Bir sonraki derste şunları öğreneceksiniz:

  • 🖥️ Server functions nedir ve neden kullanılır?
  • 🔒 Server-side only code nasıl yazılır?
  • 📡 Client’ten server fonksiyon çağırma
  • ✅ Input validation ile güvenli API’lar
  • 🍪 Middleware ile auth kontrolü

  1. Loader her zaman çalışır mı?
  2. Search params değişince loader tekrar çalışır mı?
  3. Redirect’ten sonra kod çalışmaya devam eder mi?

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