Expo Router v4 ile Web ve Mobile İçin Tek Codebase Geliştirme Rehberi
Modern uygulama geliştirme dünyasında en büyük zorluklardan biri, aynı ürünü birden fazla platform için ayrı ayrı geliştirmektir. iOS, Android ve web için üç farklı codebase yönetmek; üç farklı takım, üç kat maliyet ve üç kat bakım yükü demektir. Expo Router v4 bu denklemi tamamen değiştiriyor: tek bir JavaScript/TypeScript codebase ile hem web hem de native mobil uygulamalar geliştirmenize olanak tanıyor.
Bu yazıda Expo Router v4'ün sunduğu yenilikleri, dosya tabanlı routing mekanizmasını, platform-spesifik stratejileri ve gerçek dünya senaryolarını detaylıca inceleyeceğiz.
Expo Router Nedir ve Neden v4?
Expo Router, Next.js'ten ilham alan dosya tabanlı bir routing sistemidir ve React Native uygulamalarına web'deki routing deneyimini getirmektedir. Expo SDK'nın bir parçası olarak sunulan bu araç, React Navigation üzerine inşa edilmiştir ancak çok daha sade ve deklaratif bir yapı sunar.
v4 ile Gelen Önemli Yenilikler
- Gelişmiş universal desteği: Web ve native arasında daha tutarlı davranış
- Typed routes: TypeScript ile tam tip güvenliği
- API Routes: Sunucu taraflı endpoint'ler oluşturma imkânı
- Gelişmiş head yönetimi: SEO için meta tag kontrolü
- Daha iyi static rendering: Web performansı için SSG desteği
- Async routes (lazy bundling): Route bazlı code splitting
Projeyi Sıfırdan Kurma
Expo Router v4 ile yeni bir universal proje oluşturmak son derece kolaydır:
npx create-expo-app@latest my-universal-app --template tabs
cd my-universal-appBu komut, dosya tabanlı routing yapısı hazır bir proje iskeleti oluşturur. Projeyi hem web hem de mobil platformlarda çalıştırmak için:
# iOS için
npx expo start --ios
# Android için
npx expo start --android
# Web için
npx expo start --webProje Yapısı
Expo Router v4'te app/ dizini routing'in kalbidir. Dosya sistemi doğrudan URL yapınızı belirler:
my-universal-app/
├── app/
│ ├── _layout.tsx # Root layout
│ ├── index.tsx # Ana sayfa (/)
│ ├── about.tsx # /about
│ ├── (tabs)/
│ │ ├── _layout.tsx # Tab layout
│ │ ├── home.tsx # /home tab'ı
│ │ └── profile.tsx # /profile tab'ı
│ ├── blog/
│ │ ├── index.tsx # /blog
│ │ └── [slug].tsx # /blog/:slug (dinamik route)
│ └── +not-found.tsx # 404 sayfası
├── components/
├── constants/
└── package.jsonDosya Tabanlı Routing Derinlemesine
Root Layout
Her Expo Router projesinde app/_layout.tsx dosyası, uygulamanın kök layout'unu tanımlar. Bu dosya hem web hem de native için ortak yapılandırmaları barındırır:
// app/_layout.tsx
import { Stack } from 'expo-router';
import { ThemeProvider } from '@react-navigation/native';
import { useFonts } from 'expo-font';
import * as SplashScreen from 'expo-splash-screen';
import { useEffect } from 'react';
import { useColorScheme } from 'react-native';
SplashScreen.preventAutoHideAsync();
export default function RootLayout() {
const colorScheme = useColorScheme();
const [fontsLoaded] = useFonts({
SpaceMono: require('../assets/fonts/SpaceMono-Regular.ttf'),
});
useEffect(() => {
if (fontsLoaded) {
SplashScreen.hideAsync();
}
}, [fontsLoaded]);
if (!fontsLoaded) return null;
return (
<ThemeProvider value={colorScheme === 'dark' ? DarkTheme : DefaultTheme}>
<Stack>
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
<Stack.Screen name="blog/[slug]" options={{ title: 'Blog Yazısı' }} />
<Stack.Screen name="+not-found" />
</Stack>
</ThemeProvider>
);
}Dinamik Route'lar
Dinamik URL parametreleri, köşeli parantez sözdizimi ile tanımlanır. Bu yapı hem web URL'leri hem de native deep link'ler için aynı şekilde çalışır:
// app/blog/[slug].tsx
import { useLocalSearchParams } from 'expo-router';
import { View, Text, StyleSheet, ScrollView } from 'react-native';
import { Stack } from 'expo-router';
export default function BlogPost() {
const { slug } = useLocalSearchParams<{ slug: string }>();
return (
<>
<Stack.Screen
options={{
title: slug?.replace(/-/g, ' ') || 'Blog Yazısı',
headerBackTitle: 'Geri',
}}
/>
<ScrollView style={styles.container}>
<Text style={styles.title}>
{slug?.replace(/-/g, ' ')}
</Text>
<Text style={styles.body}>
Bu blog yazısının içeriği burada gösterilecek.
Slug parametresi: {slug}
</Text>
</ScrollView>
</>
);
}
const styles = StyleSheet.create({
container: { flex: 1, padding: 16 },
title: { fontSize: 28, fontWeight: 'bold', marginBottom: 16 },
body: { fontSize: 16, lineHeight: 24, color: '#555' },
});Tab Navigasyonu
Grup dizinleri (parantez) ile tanımlanır ve URL yapısında görünmez. Tab navigasyonu için ideal bir yapıdır:
// app/(tabs)/_layout.tsx
import { Tabs } from 'expo-router';
import { Platform } from 'react-native';
import { Ionicons } from '@expo/vector-icons';
export default function TabLayout() {
return (
<Tabs
screenOptions={{
tabBarActiveTintColor: '#007AFF',
headerShown: Platform.OS === 'web' ? false : true,
tabBarStyle: Platform.select({
web: {
borderTopWidth: 1,
borderTopColor: '#e0e0e0',
height: 60,
},
default: {},
}),
}}
>
<Tabs.Screen
name="home"
options={{
title: 'Ana Sayfa',
tabBarIcon: ({ color, size }) => (
<Ionicons name="home" size={size} color={color} />
),
}}
/>
<Tabs.Screen
name="profile"
options={{
title: 'Profil',
tabBarIcon: ({ color, size }) => (
<Ionicons name="person" size={size} color={color} />
),
}}
/>
</Tabs>
);
}Platform-Spesifik Geliştirme Stratejileri
Tek codebase kullanmak, her platformda birebir aynı kodu çalıştırmak anlamına gelmez. Expo Router v4, platform bazında özelleştirme yapmanız için güçlü araçlar sunar.
Platform-Spesifik Dosyalar
Expo, dosya uzantılarına göre otomatik platform seçimi yapar:
components/
├── Header.tsx # Tüm platformlar (fallback)
├── Header.web.tsx # Sadece web
├── Header.ios.tsx # Sadece iOS
└── Header.android.tsx # Sadece Android// components/Header.web.tsx
import { View, Text, StyleSheet, Pressable } from 'react-native';
import { Link } from 'expo-router';
export default function Header() {
return (
<View style={styles.header}>
<Link href="/" asChild>
<Pressable>
<Text style={styles.logo}>MyApp</Text>
</Pressable>
</Link>
<View style={styles.nav}>
<Link href="/about" style={styles.navLink}>
<Text>Hakkımızda</Text>
</Link>
<Link href="/blog" style={styles.navLink}>
<Text>Blog</Text>
</Link>
</View>
</View>
);
}
const styles = StyleSheet.create({
header: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
padding: 16,
backgroundColor: '#fff',
borderBottomWidth: 1,
borderBottomColor: '#eee',
},
logo: { fontSize: 24, fontWeight: 'bold' },
nav: { flexDirection: 'row', gap: 24 },
navLink: { padding: 8 },
});// components/Header.tsx (native fallback)
import { View, Text, StyleSheet } from 'react-native';
export default function Header() {
return (
<View style={styles.header}>
<Text style={styles.title}>MyApp</Text>
</View>
);
}
const styles = StyleSheet.create({
header: { padding: 16, backgroundColor: '#007AFF' },
title: { fontSize: 20, fontWeight: 'bold', color: '#fff' },
});Platform Kontrolü ile Koşullu Render
// app/(tabs)/home.tsx
import { Platform, View, Text, StyleSheet, useWindowDimensions } from 'react-native';
import { Link } from 'expo-router';
export default function HomeScreen() {
const { width } = useWindowDimensions();
const isWideScreen = width > 768;
return (
<View style={[styles.container, isWideScreen && styles.wideContainer]}>
<Text style={styles.heading}>
Universal Uygulamaya Hoş Geldiniz
</Text>
<View style={[
styles.cardGrid,
isWideScreen && styles.cardGridWide
]}>
<Link href="/blog/react-native-rehberi" asChild>
<View style={styles.card}>
<Text style={styles.cardTitle}>React Native Rehberi</Text>
<Text style={styles.cardDesc}>
Başlangıçtan ileri seviyeye kapsamlı rehber
</Text>
</View>
</Link>
<Link href="/blog/expo-router-v4-yenilikleri" asChild>
<View style={styles.card}>
<Text style={styles.cardTitle}>Expo Router v4</Text>
<Text style={styles.cardDesc}>
Yeni özellikleri keşfedin
</Text>
</View>
</Link>
</View>
{Platform.OS === 'web' && (
<Text style={styles.webOnly}>
Bu içerik sadece web'de görüntülenir
</Text>
)}
</View>
);
}
const styles = StyleSheet.create({
container: { flex: 1, padding: 16 },
wideContainer: { maxWidth: 1200, alignSelf: 'center', width: '100%' },
heading: { fontSize: 32, fontWeight: 'bold', marginBottom: 24 },
cardGrid: { gap: 16 },
cardGridWide: { flexDirection: 'row', flexWrap: 'wrap' },
card: {
padding: 20,
backgroundColor: '#f8f9fa',
borderRadius: 12,
borderWidth: 1,
borderColor: '#e9ecef',
flex: 1,
minWidth: 280,
},
cardTitle: { fontSize: 18, fontWeight: '600', marginBottom: 8 },
cardDesc: { fontSize: 14, color: '#666' },
webOnly: { marginTop: 20, color: '#999', fontStyle: 'italic' },
});SEO ve Head Yönetimi (Web)
Web uygulamalarında SEO kritik öneme sahiptir. Expo Router v4, expo-head veya yerleşik Head bileşeni ile her sayfaya özel meta tag'ler eklemenize olanak tanır:
// app/blog/[slug].tsx
import Head from 'expo-router/head';
import { useLocalSearchParams } from 'expo-router';
export default function BlogPost() {
const { slug } = useLocalSearchParams<{ slug: string }>();
const title = slug?.replace(/-/g, ' ') || 'Blog';
return (
<>
<Head>
<title>{title} | MyApp Blog</title>
<meta name="description" content={`${title} hakkında detaylı yazı`} />
<meta property="og:title" content={title} />
<meta property="og:type" content="article" />
</Head>
{/* Sayfa içeriği */}
</>
);
}API Routes ile Sunucu Taraflı Mantık
Expo Router v4, Next.js benzeri API route'ları destekler. app/ dizininde +api.ts uzantılı dosyalar oluşturarak sunucu taraflı endpoint'ler tanımlayabilirsiniz:
// app/api/posts+api.ts
export async function GET(request: Request) {
const posts = [
{ id: 1, title: 'Expo Router v4 Rehberi', slug: 'expo-router-v4-rehberi' },
{ id: 2, title: 'React Native Performans', slug: 'react-native-performans' },
{ id: 3, title: 'Universal Uygulama Mimarisi', slug: 'universal-uygulama-mimarisi' },
];
return Response.json({ posts });
}
export async function POST(request: Request) {
const body = await request.json();
// Veritabanına kaydetme işlemi simülasyonu
console.log('Yeni post:', body);
return Response.json(
{ message: 'Post başarıyla oluşturuldu', data: body },
{ status: 201 }
);
}Bu endpoint'e hem web hem de native taraftan istek atabilirsiniz:
// Herhangi bir bileşende
import { useEffect, useState } from 'react';
function usePosts() {
const [posts, setPosts] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch('/api/posts')
.then(res => res.json())
.then(data => {
setPosts(data.posts);
setLoading(false);
});
}, []);
return { posts, loading };
}Navigasyon ve Deep Linking
Expo Router v4'te navigasyon, Link bileşeni ve router hook'u ile yapılır. En güzel yanı, aynı kodun hem web URL'leri hem de native deep link'ler için çalışmasıdır:
import { Link, router } from 'expo-router';
import { Pressable, Text, View } from 'react-native';
export default function Navigation() {
const handlePress = () => {
// Programatik navigasyon
router.push('/blog/expo-router-v4');
};
const handleReplace = () => {
// Geçmişi değiştirerek navigasyon
router.replace('/login');
};
return (
<View>
{/* Deklaratif navigasyon */}
<Link href="/about">
<Text>Hakkımızda</Text>
</Link>
{/* Typed route ile navigasyon */}
<Link
href={{
pathname: '/blog/[slug]',
params: { slug: 'expo-router-rehberi' },
}}
>
<Text>Blog Yazısı</Text>
</Link>
{/* Programatik navigasyon */}
<Pressable onPress={handlePress}>
<Text>Yazıya Git</Text>
</Pressable>
</View>
);
}Yapılandırma ve Dağıtım
app.json Yapılandırması
{
"expo": {
"name": "my-universal-app",
"slug": "my-universal-app",
"scheme": "myapp",
"platforms": ["ios", "android", "web"],
"web": {
"bundler": "metro",
"output": "static",
"favicon": "./assets/favicon.png"
},
"plugins": ["expo-router"],
"experiments": {
"typedRoutes": true
}
}
}Dağıtım Seçenekleri
Web için static export yaparak herhangi bir hosting servisine deploy edebilirsiniz:
# Static web build
npx expo export --platform web
# EAS Build ile native build
eas build --platform allEn İyi Pratikler ve İpuçları
Shared bileşenler oluşturun:
components/dizininde platform bağımsız bileşenler yazın, gerektiğinde.web.tsxve.native.tsxvaryantları ekleyin.Responsive tasarım kullanın:
useWindowDimensionshook'u ile ekran genişliğine göre layout değiştirin. Sabit breakpoint'ler belirleyin.Typed routes aktif edin:
experiments.typedRoutesayarını açarak derleme zamanında route hatalarını yakalayın.Lazy loading uygulayın: Büyük sayfaları async route'lar ile lazy load edin — özellikle web performansı için kritiktir.
Platform-spesifik stiller:
Platform.select()kullanarak her platformda doğal hissettiren stiller oluşturun.Test stratejisi: Universal bileşenlerinizi Jest ile test ederken
react-native-webresolver'ını yapılandırmayı unutmayın.
Sonuç
Expo Router v4, universal uygulama geliştirme paradigmasında önemli bir dönüm noktasını temsil etmektedir. Dosya tabanlı routing yapısı, geliştiricilerin Next.js'ten aşina olduğu sezgisel deneyimi React Native ekosistemine taşırken; platform-spesifik dosya uzantıları, API route'ları ve gelişmiş head yönetimi gibi özellikler, tek bir codebase ile profesyonel düzeyde hem web hem de mobil uygulamalar geliştirmeyi mümkün kılmaktadır.
Eğer halihazırda React veya React Native deneyiminiz varsa, Expo Router v4 öğrenme eğrinizi minimumda tutarken üretkenliğinizi maksimuma çıkaracaktır. Üç ayrı codebase yönetmek yerine, tüm enerjinizi ürün geliştirmeye odaklamak artık bir hayal değil — Expo Router v4 ile bu vizyonu bugün hayata geçirebilirsiniz.