Ders 03: Routing - Orta Seviye
🎯 Bu Derste Neleri Öğreneceksiniz?
Section titled “🎯 Bu Derste Neleri Öğreneceksiniz?”- 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 (Arama Parametreleri)
Section titled “📚 Search Params (Arama Parametreleri)”Search params, URL’nin ? işaretinden sonraki kısmıdır. Örneğin: /arama?q=javascript&sort=date
Search Params Tanımlama
Section titled “Search Params Tanımlama”import { createFileRoute } from '@tanstack/react-router'import { zod } from 'zod'
// Search params şeması tanımlaconst 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,})Search Params Kullanma
Section titled “Search Params Kullanma”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> )}Search Params Güncelleme
Section titled “Search Params Güncelleme”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 ile Veri Yükleme
Section titled “📦 Loader ile Veri Yükleme”Loader fonksiyonları, sayfa yüklenirken veri çekmemizi sağlar. Server-side ve client-side çalışır.
Basit Loader Örneği
Section titled “Basit Loader Örneği”import { createFileRoute } from '@tanstack/react-router'
// Mock dataconst 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> )}Dinamik Loader (Parametere Göre)
Section titled “Dinamik Loader (Parametere Göre)”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> )}Search Params’a Göre Loader
Section titled “Search Params’a Göre Loader”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> )}⏳ Loading ve Error Handling
Section titled “⏳ Loading ve Error Handling”Kullanıcıya yükleniyor durumunu ve hataları göstermek çok önemlidir.
pendingComponent ile Loading State
Section titled “pendingComponent ile Loading State”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 componentfunction 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 componentfunction 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> )}Default Error Boundary
Section titled “Default Error Boundary”Tüm uygulamada kullanılmak üzere global error component:
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}Route-Level Error Component
Section titled “Route-Level Error Component”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> )}🔄 Redirects (Yönlendirme)
Section titled “🔄 Redirects (Yönlendirme)”Kullanıcıyı başka bir sayfaya yönlendirmek için redirect() kullanın.
Basit Redirect
Section titled “Basit Redirect”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' }) },})Koşullu Redirect (Authentication)
Section titled “Koşullu Redirect (Authentication)”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>}beforeLoad ile Redirect
Section titled “beforeLoad ile Redirect”export const Route = createFileRoute('/ozel-sayfa')({ beforeLoad: async () => { const ozelMi = await kontrolEtOzelMi()
if (!ozelMi) { throw redirect({ to: '/yetki-yok' }) } }, component: OzelSayfa,})🔍 404 ve Not Found Handling
Section titled “🔍 404 ve Not Found Handling”Kullanıcıya var olmayan bir sayfa için 404 gösterin.
notFoundComponent Kullanımı
Section titled “notFoundComponent Kullanımı”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> )}Global 404 Sayfası
Section titled “Global 404 Sayfası”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> )}🎨 Pratik Örnek: Ürün Katalogu
Section titled “🎨 Pratik Örnek: Ürün Katalogu”Şimdi öğrendiklerimizle tam özellikli bir ürün kataloğu yapalım!
import { createFileRoute, Link, useNavigate } from '@tanstack/react-router'import { zod } from 'zod'
// Mock dataconst 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> )}✅ Ders 3 Özeti
Section titled “✅ Ders 3 Özeti”Bu derste öğrendiklerimiz:
| Konu | Açıklama |
|---|---|
| validateSearch | Search params type-safe validation |
| useSearch | Search params’e erişim |
| loader | Sayfa yüklenirken veri çekme |
| useLoaderData | Loader’dan gelen veriye erişim |
| pendingComponent | Loading state’i göster |
| errorComponent | Error state’i göster |
| redirect() | Kullanıcıyı yönlendirme |
| notFound() | 404 hatası fırlatma |
📝 Alıştırmalar
Section titled “📝 Alıştırmalar”Alıştırma 1: Blog Arama Sayfası
Section titled “Alıştırma 1: Blog Arama Sayfası”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
Alıştırma 2: Todo Listesi
Section titled “Alıştırma 2: Todo Listesi”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
Alıştırma 3: Auth Protected Sayfa
Section titled “Alıştırma 3: Auth Protected Sayfa”Korumalı bir sayfa yapın:
- Loader’da kullanıcı kontrolü
- Giriş yapmamışsa
/loginsayfası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ü
💬 Sorularınız?
Section titled “💬 Sorularınız?”- Loader her zaman çalışır mı?
- Search params değişince loader tekrar çalışır mı?
- Redirect’ten sonra kod çalışmaya devam eder mi?
Bir sonraki derste görüşmek üzere! 👋