İçeriğe geç
Sedat Demir
Geri dön

Expo Router v4 ile Web ve Mobile İçin Tek Codebase Geliştirme Rehberi

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


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-app

Bu 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 --web

Proje 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.json

Dosya 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 all

En İyi Pratikler ve İpuçları

  1. Shared bileşenler oluşturun: components/ dizininde platform bağımsız bileşenler yazın, gerektiğinde .web.tsx ve .native.tsx varyantları ekleyin.

  2. Responsive tasarım kullanın: useWindowDimensions hook'u ile ekran genişliğine göre layout değiştirin. Sabit breakpoint'ler belirleyin.

  3. Typed routes aktif edin: experiments.typedRoutes ayarını açarak derleme zamanında route hatalarını yakalayın.

  4. Lazy loading uygulayın: Büyük sayfaları async route'lar ile lazy load edin — özellikle web performansı için kritiktir.

  5. Platform-spesifik stiller: Platform.select() kullanarak her platformda doğal hissettiren stiller oluşturun.

  6. Test stratejisi: Universal bileşenlerinizi Jest ile test ederken react-native-web resolver'ı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.


Share this post on:

Sonraki Yazı
tRPC v11: Schema ve Code Generation Olmadan Uçtan Uca Tip Güvenliği