Native mobil uygulama yazmak isteyen NextJS geliştiricileri için velinimet olan Expo Router yapısı ile File-Based Routing işlemleri.

Kubilay
22 min readJul 19, 2023

--

Herkese merhaba uzun zaman sonra yeni bir içerik ile karşınızdayım son zamanlarda mobile biraz daha fazla ilgi duymaya başladım ve Native olarak uygulamalar geliştirmek için dökümanları incelerken fark ettiğim NextJS hayranı olan ve ReactRouting işlemlerini sevmeyen file-based routing işlemleri ile kod geliştirmeyi benim gibi seven kullanıcılar için veli nimet olduğunu düşündüğüm Expo Routing hakkında tutmuş olduğum notları sizinle paylaşmak istedim.

Öncelikle örnek bir proje oluşturalım bunun için hazır template yapısının ve konfigürasyonunun olduğu bir proje oluşturalım.

npx create-react-native-app -t with-router

Şimdi projemizi oluşturduktan sonra ilk olarak projemizin yapısını inceleyelim.

Projemizi oluşturduktan sonra dosya yapımıza dikkat ederseniz bir adet index.js dosyamız mevcut bu dosya içine baktığımız zaman tek satır bir kod göreceğiz basit bir anlatım yaparsak burada expo’nun router yapısını

import “expo-router/entry”;

Şimdi projemizi başlatalım ve nasıl bir cıktı aldığımıza göz atalım.

Gördüğünüz gibi Expo tarafından default olarak hazırlanan ekran bizim karşımıza çıktı şimdi ilk olarak uygulamanın alt kısmında bulunan
beyaz alana tıklayalım ve ne olacağını inceleyelim.

Görüldüğü gibi App adında bir klasör eklendi ve içinde index.js olarak bir dosya oluştu bu index.js bizim için uygulama açıldığı zaman kullanıcının göreceği ilk sayfa olacak. Şimdi yavaştan işlemlere başlayalım.

Statick Routing

Şimdi App dosyamızın içine bir adet profile isimli bir js dosyası oluşturalım ve index.js dosyamızı güncelleyelim. Kodlar uzun olmasın diye stillerimizi bilerek eklemedim kodlarımız uzamasın.

//Profile.js

import React from "react";
import { Link } from "expo-router";
import { Button } from "react-native";

export default function Profile() {
return (
<View style={styles.container}>
<View style={styles.main}>
<Text style={styles.title}>Profile Page</Text>
<Text style={styles.subtitle}>This is the first page of your app.</Text>
<Link style={styles.mybutton} href={"/"}>
<Text>Go Home</Text>
</Link>
</View>
</View>
);
}

//Index.js dosyamız.

import { Link } from "expo-router";
import { StyleSheet, Text, View } from "react-native";


export default function Page() {
return (
<View style={styles.container}>
<View style={styles.main}>
<Text style={styles.title}>Hello World</Text>
<Text style={styles.subtitle}>This is the first page of your app.</Text>
<Link style={styles.mybutton} href={"/profile"}>
<Text>Go Profile</Text>
</Link>
</View>
</View>
);
}

Şimdi sonuçları inceleyelim projemizi şimdi inceleyelim .

Gördüğünüz gibi iki adet sayfa oluşturduk ve routing işlemlerini Router Yapısında bulunan Link component’i sayesinde hallettik. NextJS’in yapısına ne kadar benzer ve güzel değil mi :)

Şimdi useRouter kullanarak geri gitme işlemimizi gerçekleştirelim.
Projemizde tekrar App dosyasının altında settings.js adında bir dosya oluşturalım ve useRouter kullanarak geri gitme işlemini yapalım.

//settings.js dosyamız. 

import { View, Text,StyleSheet } from "react-native";
import React from "react";
import { Link } from "expo-router";
import { useRouter } from "expo-router";
import { Button } from "react-native";

export default function Settings() {
let router=useRouter();
return (
<View style={styles.container}>
<View style={styles.main}>
<Text style={styles.title}>Settings Page</Text>
<Text style={styles.subtitle}>This is the first page of your app.</Text>
<Button style={styles.mybutton} onPress={()=>router.back()} title="Go Back"/></View>
</View>
);
}

NextJS içinde bulunan UseRouter hook’una ne kadar benzer olduğuna dikkat ettiniz mi :) Projemizi sonucuna tekrar bakalım …

SearchParams kullanarak Api’den veri çekme işlemleri.
Öncelikle şunu belirtmek isterim search parametrelerini bu örneğimizde normal şartlarda doğru bir şekilde kullanmayacağız .Konularda adım adım gitmek istediğim için dynamic olarak routing işlemlerinden henüz bahsetmediğimiz için burada bu konuyu göz ardı etmenizi rica ediyorum.
Şimdi projemizde iki adet yeni dosya oluşturalım.

//AddressBook.js dosyamız. 

import { Link, useRouter, useSearchParams } from "expo-router";
import React, { useEffect, useState } from "react";
import { Image } from "react-native";
import { SafeAreaView } from "react-native";
import { ScrollView } from "react-native";
import { View, Text, StyleSheet, Button } from "react-native";

export default function AddressBook() {

let navigation = useRouter();

const [data, setData] = useState([]);

const getPosts = async () => {
console.log("work !! ");
try {
const response = await fetch(
"https://random-data-api.com/api/users/random_user?size=50"
);
const json = await response.json();
setData(json);
} catch (error) {
console.error(error);
}
};

useEffect(() => {
getPosts();
}, []);

//Yukarı kaydırınca yeniden api'den yeni verileri çekme işlemi.

const handleScroll = ({ nativeEvent }) => {
const { contentOffset, layoutMeasurement, contentSize } = nativeEvent;
const isScrolledToTop = contentOffset.y === 0;
const isNearBottom =
layoutMeasurement.height + contentOffset.y >= contentSize.height - 20;

if (isScrolledToTop) {
getPosts();
}
};

return (
<SafeAreaView className="flex flex-col justify-center items-center align-middle">
<View className="flex flex-row items-center justify-between w-full px-2">
<Text className="text-2xl text-red-400">Address Book</Text>
<Button onPress={() => navigation.back()} title="Go Back" />
</View>
<ScrollView
className="mt-4 p-2 w-full"
indicatorStyle="white"
contentInset={{ bottom: "#000000" }}
onScroll={handleScroll}
>
{data &&
data.map((post) => {
return (
<Link
key={post.id}
href={`SingleBook?UserID=${post.id}`}
className=" mb-3 p-3 w-full"
>
<View className="flex flex-row w-full gap-2">
<Image
className="rounded-full w-12 h-12 border-2"
source={{
uri: post.avatar,
}}
/>
<View className="w-fit">
<Text className="text-blue-400">{post.username}</Text>
<View className="d-flex flex-row flex-wrap w-fit">
<Text>{post.address.city}</Text>
<Text>{post.address.street_name}</Text>
<Text>{post.address.street_address}</Text>
<Text>{post.address.country}</Text>
</View>
</View>
</View>
</Link>
);
})}
</ScrollView>
</SafeAreaView>
);
}
//SingleBook.js dosyamız. 

import { Link, useRouter, useSearchParams } from "expo-router";
import React, { useEffect, useState } from "react";
import { Image } from "react-native";
import { SafeAreaView } from "react-native";
import { ScrollView } from "react-native";
import { View, Text, StyleSheet, Button } from "react-native";

export default function AddressBook() {
let navigation = useRouter();
let params = useSearchParams();

const [data, setData] = useState([]);

const getPosts = async () => {
try {
const response = await fetch(
`https://random-data-api.com/api/users/random_user?UserId=${params.UserID}`
);
const json = await response.json();
setData(json);
} catch (error) {
console.error(error);
}
};

useEffect(() => {
getPosts();
}, []);

let ContextStyle = "border-b-2 bg-red-400";
return (
<SafeAreaView className="flex flex-col justify-center items-center align-middle">
<View className="flex flex-row items-center justify-between w-full px-2">
<Text className="text-xl text-red-400">{data.username}'s details</Text>
<Button onPress={() => navigation.back()} title="Go Back" />
</View>
<ScrollView
className="mt-4 p-2 w-full"
indicatorStyle="white"
contentInset={{ bottom: "#000000" }}
>
<View className=" mb-3 p-3 w-full space-y-5">
<View className="flex flex-col w-full justify-center items-center gap-2">
<Image
className="rounded-full w-24 h-24 border-2"
source={{
uri: data.avatar,
}}
/>
<View className="w-full text-center justify-center items-center ">
<Text className="text-blue-400 text-base">{data.username}</Text>
<View className="flex flex-col mt-2 w-full text-center items-center justify-center">
<Text>{data.address && data.address.city}</Text>
<Text>{data.address && data.address.street_name}</Text>
<Text>{data.address && data.address.street_address}</Text>
<Text>{data.address && data.address.country}</Text>
</View>
</View>
</View>
</View>
</ScrollView>
</SafeAreaView>
);
}

Burada stillendirme olarak NativeWind kullanmayı tercih ettim kendisi NextJS kullanırken kelimenin tam anlamıyla aşık olduğum bir CSS framework diyebilirim. İncelemenizi tavsiye ederim.

Konuyu çok fazla uzatmadan devam edelim şimdi uygulamamızın genel dosyalarını incelemeden önce bir sonucumuza göz atalım isterseniz.

Şimdi eklemiş olduğumuz AddressBook isimli komponent bizim için bütün dataların listesini tutuyor. SingleBook ise sadece seçilen bir kullanıcının bilgilerini dönüyor.

const response = await fetch(
"https://random-data-api.com/api/users/random_user?size=50"
);


const response = await fetch(
`https://random-data-api.com/api/users/random_user?UserId=${params.UserID}`
);

Yukarıda görmüş olduğunuz ilk linkimizi biz random 50 adet data çekerken kullanıyoruz,
İkinci api’ ise bizim için UserId değerine göre o UserId değerine sahip kullanıcının bilgilerini tek bir obje olarak döndürüyor. (normal şartlarda random olarak döndürüyor yani seçilen kullanıcının bilgilerini vermiyor fakat biz öyle gibi düşünelim.)

NextJS olarak düşünelim.
localhots:3000/AddressBook =>50 adet kullanıcı bilgileri tutan listemiz.
localhots:3000/SingleBook?UserId=”1” => Tek bir kullanıcının bilgilerini görüyoruz.

Burada userID değerini nasıl elde ettiğimizi inceleyelim.

//Burada useSearchParams hook'unu kullanarak parametreleri elde edebiliyoruz.
let params = useSearchParams();

const response = await fetch(
`https://random-data-api.com/api/users/random_user?UserId=${params.UserID}`
);

Şimdi ise Dynamic Routing nasıl çalışır onu inceleyelim

NextJS’de Dynamic routing yapısında hatırlarsanız dosyamız [slug].js yada [id].js gibi isimler veriyorduk. Yada klasör adını [dynamic_isim] gibi ayarlıyorduk. Görsel olarak bakmak gerekir ise ,

Burada şöyle bir senaryomuz var bizim 4adet klasörümüz mevcut .
[slug] , Products , User ve SingleProduct olmak üzere bu rout yapılarımız hakkında basit bir özet geçmek gerekirse,
localhost:3000/Product => Ürün listemizi verir.
localhost:3000/SingleProduct/[id] => Her bir ürünün detayını alır
Örnek olarak ;
localhost:3000/SingleProduct/1 => bir telefon olurken
localhost:3000/SingleProduct/12 = > bir atıştırma olabilir.
slug dosyası sayesinde ise ,
www.localhotst:3000/Çilek
www.localhotst:3000/Araba
www.localhotst:3000/Jeep213

gibi web sitlerini tek bir sayfada açabiliriz.

Yada User dosyası içinde bulunan index.js ve [id].js ile iç içe routing yapısı kurabiliriz.
www.localhotst:3000/User => Kullanıcının kendi sayfasını gösterirken
www.localhotst:3000/Araba => Başka bir kullanıcı hakkında bilgi veren bir sayfa olabilir.

Şimdi burada kodlar üzerinden inceleme yapalım

//Products dosyamızın altında bulunan index.js dosyamız.

import { View, Text, SafeAreaView, StyleSheet } from "react-native";
import React, { useEffect, useState } from "react";
import { Link, useRouter } from "expo-router";
import { SectionList } from "react-native";
import { ActivityIndicator } from "react-native";
import { ScrollView } from "react-native";
import { Image } from "react-native";
import { SafeAreaProvider } from "react-native-safe-area-context";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { FlatList } from "react-native";
import { TouchableOpacity } from "react-native";
export default function TwitterHomePage() {
let router = useRouter();
const insets = useSafeAreaInsets();
const [isLoading, setLoading] = useState(true);
const [data, setData] = useState([]);

const getMovies = async () => {
try {
const response = await fetch(
"https://dummyjson.com/products?limit=10&skip=0"
);
const json = await response.json();
setData(json);
} catch (error) {
console.error(error);
} finally {
setLoading(false);
}
};

useEffect(() => {
getMovies();
}, []);

if (isLoading) {
return <ActivityIndicator size="large" color="#fff" />;
} else {
return (
<SafeAreaView
style={{
paddingTop: insets.top + 12,
paddingLeft: insets.left + 24,
paddingRight: insets.right + 24,
paddingBottom: insets.bottom + 12,
}}
>
<View className="border-b mb-4">
<Text className="text-lg font-black">Product List</Text>
</View>
<FlatList
className="p-2"
scrollsToTop={true}
data={data.products}
renderItem={({ item }) => (
<TouchableOpacity
onPress={() => router.push(`/SingleProduct/${item.id}`)}
>
<View className="mt-3 flex-1 flex-row-reverse gap-w border border-Gray1 p-2 rounded flex justify-center items-center">
<Image
className="border border-blue-400"
source={{ uri: item.images[1] }}
style={{ width: 60, height: 60, borderRadius: 30 }}
/>
<View className="flex-1 flex-col">
<Text className="font-bold color-Primary">{item.title}</Text>
<Text
numberOfLines={2}
ellipsizeMode="tail"
className="font-normal"
>
{item.description}
</Text>
<View className="flex-1 mt-2 flex-row justify-between">
<Text className="text-Blue1">{item.price} TL</Text>
<Text>{item.price}TL</Text>
</View>
</View>
</View>
</TouchableOpacity>
)}
keyExtractor={(item) => item.id}
/>
</SafeAreaView>
);
}
}

const randomColor = () => {
const colors = [
"bg-red-200",
"bg-blue-200",
"bg-green-200",
"bg-yellow-200",
"bg-indigo-200",
"bg-purple-200",
"bg-pink-200",
"bg-teal-200",
"bg-orange-200",
];

const randomIndex = Math.floor(Math.random() * colors.length);
return colors[randomIndex];
};

const generateRandomColor = () => {
const randomBgColor = randomColor();
return randomBgColor;
};

const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#F7F7F7",
marginTop: 60,
},
listItem: {
margin: 10,
padding: 10,
backgroundColor: "#FFF",
width: "80%",
flex: 1,
alignSelf: "center",
flexDirection: "row",
borderRadius: 5,
},
});

function Item(item) {
console.log(item);
return <Text>{JSON.stringify(item)}</Text>;
}
// SingleProduct dosyamızın içinde bulunan [id].js dosyamız.

import {
View,
Text,
SafeAreaView,
StyleSheet,
TouchableHighlight,
} from "react-native";
import React, { useEffect, useState } from "react";
import { useLocalSearchParams, useRouter } from "expo-router";
import { ActivityIndicator } from "react-native";
import { ScrollView } from "react-native";
import { Image } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { Button } from "react-native";

export default function TwitterHomePage() {
let { id } = useLocalSearchParams();
let router = useRouter();
const insets = useSafeAreaInsets();
const [isLoading, setLoading] = useState(true);
const [data, setData] = useState([]);

const getMovies = async () => {
try {
const response = await fetch(`https://dummyjson.com/products/${id}`);
const json = await response.json();
setData(json);
} catch (error) {
console.error(error);
} finally {
setLoading(false);
}
};

useEffect(() => {
getMovies();
}, []);
if (isLoading) {
return <ActivityIndicator size="large" color="#fff" />;
} else {
return (
<SafeAreaView
style={{
paddingTop: insets.top,
paddingLeft: insets.left + 24,
paddingRight: insets.right + 24,
paddingBottom: insets.bottom + 12,
}}
>
<View className="border-b mb-4">
<Text className="text-lg font-black">Single Product</Text>
</View>

<View
style={{
backgroundColor: "#ffb500",
position: "relative",
height: 100,
justifyContent: "flex-start",
paddingTop: 12,
gap: 12,
alignItems: "center",
}}
>
<Text className="text-white font-bold">{data.title}</Text>
<View className="bg-Primary p-4 -mb-[100px] rounded-3xl border-black border-2">
<Image
source={{ uri: data.images[0] }}
style={{
resizeMode: "contain",
height: 100,
width: 100,
padding: 12,
backgroundColor: "#ffb500",
border: 2,
}}
/>
</View>
</View>
<ScrollView className="mt-[100px]">
<Text
className="bg-Gray4 text-center text-white rounded uppercase p-0.5"
style={{ maxWidth: "40%" }}
>
{data.category}
</Text>
<Text className="font-bold text-base">{data.brand}</Text>
<Text className="font-medium text-base text-black/75">
{data.title}
</Text>
<View className=" w-fit justify-end items-end rounded-lg">
<Text className="font-bold text-lg text-white bg-Green2 w-24 text-center rounded-lg ">
{data.price} TL
</Text>
</View>
<View className="flex flex-1">
<Text>Total Star: {data.rating}</Text>
</View>
<TouchableHighlight
onPress={() => router.back()}
className="bg-Green2 mt-4 px-1 py-2"
>
<Text className="text-center text-xl text-white">Sepete Ekle</Text>
</TouchableHighlight>
</ScrollView>
</SafeAreaView>
);
}
}

Burada id değerine göre o ürünün detaylarına geçen bir E-Ticaret uygulaması yaptığımızı düşünelim.

let { id } = useLocalSearchParams();

Sayesinde O’an hangi sayfada olduğumuzu yani [id].js router dosyamızda id değerini karşılayan değeri öğrenmek için bu yöntemi kullanıyoruz. NextJS içinde bulunan 13. versiyon ile birlikte hayatımıza usePathName hook’u ile aynı görevi yapıyor .

Anlık Routumz : localhost:3000/SingleProduct/1
olsun burada api’a id değerini yollayarak istenen ürün için detay bilgilerini alıp ekrana bastığımızı unutmayalım. Kodumuzun çalışan halini inceleyelim.

Gördüğünüz gibi Go Product butonuna tıkladığımız zaman Products isimli Rout yolumuza gidiyoruz , burada ürünlerimizin listesi mevcut.
Daha sonra buradan , useRouter hook’unu kullanarak TouchableOpacity
‘nin içinde bulunan OnPress methodu ile nasıl bir sayfadan başka bir sayfaya geçiş yaparız onu inceleyelim.

 let router = useRouter();  //import etmemiz gereken hookumuz

<TouchableOpacity
onPress={() => router.push(`/SingleProduct/${item.id}`)}>
//......
</TouchableOpacity>

Gördüğünüz gibi burada onPresMethodu sayesinde aktif olan sayfamızı değiştirdik.

Bunun dışında başka bir yöntem olarak Link’de kullanabiliriz. NextJS’in Link yapısına ne kadar çok benzediğini burada görebilirsiniz.

import { Link, usePathname } from "expo-router";

<Link
className="bg-Primary w-full mt-5 text-white text-center p-4 text-lg"
href={"/"}>
<Text>Go Home Page</Text>
</Link>

Şimdi User Sayfasını incleyelim. Yani iç içe Routing yapısına bakalım

//User/index.js dosyamız

import { Link } from "expo-router";
import React from "react";
import { StyleSheet } from "react-native";
import { SafeAreaView, Text, Linking } from "react-native";

const generateRandomUsername = () => {
const usernames = ["Router1", "Router2", "Router3","Router4","Router5","Router6"];
const randomIndex = Math.floor(Math.random() * usernames.length);
return usernames[randomIndex];
};

const UserHomePage = () => {
const handleLinkClick = () => {
const randomUsername = generateRandomUsername();
const url = `/User/${randomUsername}`;
return url;
};

return (
<SafeAreaView>
<Text>UserHomePage</Text>
<Link style={styles.mybutton} href={handleLinkClick()}>
<Text>Go RandomUser</Text>
</Link>
</SafeAreaView>
);
};

const styles = StyleSheet.create({
mybutton: {
fontSize: 23,
padding: 3,
marginTop: 12,
backgroundColor: "#ffb500",
textAlign: "center",
borderRadius: 5,
},
});
export default UserHomePage;
// User altında bulunan [id].js dosyası
import { View, Text } from "react-native";
import React from "react";
import { Link, usePathname } from "expo-router";
import { StyleSheet } from "react-native";
import { SafeAreaView } from "react-native";

export default function UserPageWithSlug() {
let params = usePathname();
console.log(params);
return (
<SafeAreaView>
<Text>UserName:{JSON.stringify(params)} </Text>
<Link style={styles.mybutton} href={"/User"}>
<Text>Go User</Text>
</Link>
<Link style={styles.mybutton} href={"/"}>
<Text>Go HomePage</Text>
</Link>
</SafeAreaView>
);
}

const styles = StyleSheet.create({
mybutton: {
fontSize: 23,
padding: 3,
marginTop: 12,
backgroundColor: "#ffb500",
textAlign: "center",
borderRadius: 5,
},
});

Üst tarafta bulunan Username: kısmında aktif olan path adresimiz’i gösteriyoruz.

Dosyamızın adınının [slug] olduğu duruma göz atalım.

// [slug]/index.js dosyamız.

import { View, Text } from "react-native";
import React from "react";
import { SafeAreaView } from "react-native";
import { Link, usePathname } from "expo-router";

export default function DynamicFolder() {
const generateRandomPage = () => {
const routes = ["Route1", "Route2", "Route3", "Route4", "Route5", "Route6"];
const randomIndex = Math.floor(Math.random() * routes.length);
return routes[randomIndex];
};
let router = usePathname();

return (
<SafeAreaView>
<View
style={{
height: "100%",
justifyContent: "center",
alignItems: "center",
}}
>
<Link
className="bg-Primary w-full text-white text-center p-4 text-lg"
href={generateRandomPage()}
>
<Text>Go Random Page</Text>
</Link>
<Text className="font-bold text-2xl mt-4">
Şuan bulunduğunuz Route :{" "}
</Text>
<View className="border-red-600 border w-full p-4 text-lg mt-4">
<Text className="text-center font-bold">
{JSON.stringify(router)}
</Text>
</View>
<Link
className="bg-Primary w-full mt-5 text-white text-center p-4 text-lg"
href={"/"}
>
<Text>Go Home Page</Text>
</Link>
</View>
</SafeAreaView>
);
}

Peki Screenler’in nasıl ayarlayacağız onun üzerine konuşalım daha önce react-navigation üzerinden nasıl Stack Navigation , Tabs Navigation ve Drawer navigation hakkında yazılar yazmıştım. Şimdi bunun expo için olan versiyonunu inceleyelim.

Stack Navigation

İki farklı kullanım mevcut . İki farklı yöntemi’de burada inceleyeceğiz.
İlk olarak navigasyon işlemlerini NextJS üzerinden düşünelim ,NextJS 13 ile birlikte layouth.js adında ek bir module ortaya çıktı bu modül uygulamamızda ortak bir layouth kullanmamıza olanak sağladı. Genelde bu dosya içinde Provider’lar ve her sayfada gözükecek olan componentlerin ekrana render edilmesi için kullanıyoruz, aslında benzer olarak düşünebiliriz benzer değil (bu konuda linç yemek istemiyorum :) ).

Öncelikle genel kurulumları yapalım.

İlk olarak projemizde bulunan bütün dosyaları silelim sadece index.js dosyamız kalsın ve index.js dosyamızı güncelleyelim.

import { Link } from "expo-router";
import { SafeAreaView } from "react-native";
import { StyleSheet, Text, View } from "react-native";

export default function Page() {
return (
<SafeAreaView>
<View
style={{
height: "100%",
justifyContent: "center",
alignItems: "center",
}}
>
<Text className="text-lg font-bold"> Stack Navigation </Text>
<View className="flex flex-col space-y-5 mt-4 w-11/12 justify-center items-center text-center">
<Link className="bg-Primary text-base text-center w-full text-white p-2" href={"/kubilay"}>Kubilay's Page</Link>
<Link className="bg-Primary text-base text-center w-full text-white p-2" href={"/Ali"}>Ali's Page</Link>
<Link className="bg-Primary text-base text-center w-full text-white p-2" href={"/Veli"}>Veli's Page</Link>
</View>
</View>
</SafeAreaView>
);
}

App dizinin hemen altına bir adet dosya ekleyelim. [username].js olarak isimlendirelim.

import { View, Text } from "react-native";
import React from "react";
import { Link, usePathname } from "expo-router";
import { StyleSheet } from "react-native";
import { SafeAreaView } from "react-native";

export default function UserPageWithSlug() {
let params = usePathname();
console.log(params);
return (
<SafeAreaView>
<View className=" h-full w-full justify-center align-middle items-center">
<Text className="text-lg font-bold border border-red-500 p-2">
<Text className="text-blue-500">UserName: </Text>
{JSON.stringify(params)}
</Text>
<Link
className="w-full bg-Primary text-white mt-4 p-3 text-center text-base border-1 border-black"
href={"/User"}
>
<Text>Go User</Text>
</Link>
<Link
className="w-full bg-Primary text-white mt-4 p-3 text-center text-base border-1 border-black"
href={"/"}
>
<Text>Go HomePage</Text>
</Link>
</View>
</SafeAreaView>
);
}

const styles = StyleSheet.create({
mybutton: {
fontSize: 23,
padding: 3,
marginTop: 12,
backgroundColor: "#ffb500",
textAlign: "center",
borderRadius: 5,
},
});

Şimdi burada son olarak yapmamız gereken tekrar bir adet dosya eklemek .

_layout.js isimi verdiğimiz bir dosya oluşturuyoruz.
Bu dosya sayesinde hangi Navigasyon türünü kullanacağımızı belirtiyoruz .

Daha önceki yazılarımda Stack Navigation kullanırken Stack.Screen kullanarak component ve title değerlerini giriyorduk .
Bu yöntemde, Expo sayesinde Stack.Screen kullanıp içeride title ve component bilgilerini vermemize gerek yok.

//_layout.js dosyamız

import { Stack } from "expo-router";

export default () => {
return <Stack></Stack>;
};

Projemizi çalıştırdığımız zaman karşımıza çıkan ekranı inceleyelim.

Gördüğünüz gibi hiçbir şekilde Stack.Screen kullanmadık ve Expo bizim için kendi router yapısını kullanarak Stack.Screenleri otomatik olarak tanımladı.

Şimdi Stack Navigasyon işlemimizi biraz özelleştirelim.

//_layout.js dosyamız

import { Stack } from "expo-router";

export default () => {
return (
<Stack
screenOptions={{
headerStyle: {
backgroundColor: "blue",
},
headerTintColor: "white",
headerTitleStyle: {
fontWeight: "bold",
},
}}
></Stack>
);
};

Peki biz bu Stack ekranlarında sadece, header kısımlarını sayfalara özel olarak değiştirmek istersek ne yapabiliriz.

1-)Hedef dosyaya gidip dosya içinde Stack.Screen tanımlamak.
2-)_layout.js içinde Stack.Screen tanımlamak.

Bu iki yöntemin avantaj ve dezavantajları mevcut.

1-)Hedef dosyaya gidip dosya içinde Stack.Screen tanımlamak.

Mesela şöyle bir yöntem izleyelim , AnaSayfa’da yani uygulama ilk açıldığı zaman Header’ın içinde index yazısını Home Page olarak değiştirmek isteyelim.

<Stack.Screen options={{ title: "HomePage" }} />

Kodumuzda herhangi bir yere yerleştirelim.

//index.js dosyam
import { Link, Stack } from "expo-router";
import { SafeAreaView } from "react-native";
import { StyleSheet, Text, View } from "react-native";

export default function Page() {
return (
<SafeAreaView>
<Stack.Screen options={{ title: "HomePage" }} />
<View
style={{
height: "100%",
justifyContent: "center",
alignItems: "center",
}}
>
<Text className="text-lg font-bold"> Stack Navigation </Text>
<View className="flex flex-col space-y-5 mt-4 w-11/12 justify-center items-center text-center">
<Link
className="bg-Primary text-base text-center w-full text-white p-2"
href={"/kubilay"}
>
Kubilay's Page
</Link>
<Link
className="bg-Primary text-base text-center w-full text-white p-2"
href={"/Ali"}
>
Ali's Page
</Link>
<Link
className="bg-Primary text-base text-center w-full text-white p-2"
href={"/Veli"}
>
Veli's Page
</Link>
</View>
</View>
</SafeAreaView>
);
}

Sonuca bakarsak AnaSayfamızda bulunan index kısmının Homepage olarak değiştiğini görebilirsiniz.

Şimdi aynı değişimi [username].js için yapalım ama bu sefer üst tarafta kullanıcı adının olmasını isteyelim.

//[username].js dosyamız 
import { View, Text } from "react-native";
import React from "react";
import { Link, Stack, usePathname } from "expo-router";
import { StyleSheet } from "react-native";
import { SafeAreaView } from "react-native";

export default function UserPageWithSlug() {
let params = usePathname();
console.log(params);
return (
<SafeAreaView>
<Stack.Screen options={{ title: params?params.normalize():'UserPage' }} />
<View className=" h-full w-full justify-center align-middle items-center">
<Text className="text-lg font-bold border border-red-500 p-2">
<Text className="text-blue-500">UserName: </Text>
{JSON.stringify(params)}
</Text>
<Link
className="w-full bg-Primary text-white mt-4 p-3 text-center text-base border-1 border-black"
href={"/User"}
>
<Text>Go User</Text>
</Link>
<Link
className="w-full bg-Primary text-white mt-4 p-3 text-center text-base border-1 border-black"
href={"/"}
>
<Text>Go HomePage</Text>
</Link>
</View>
</SafeAreaView>
);
}

const styles = StyleSheet.create({
mybutton: {
fontSize: 23,
padding: 3,
marginTop: 12,
backgroundColor: "#ffb500",
textAlign: "center",
borderRadius: 5,
},
});

Gördüğünüz için hedef sayanının içinde <Stack.Screen …/> oluşturduğumuz zaman header içinde sayfaya özel olan dataları ekrana basabiliyoruz.

2-)_layout.js içinde Stack.Screen tanımlamak.

Bu yöntemde ise index.js ve [username].js içinde bulunan dosyaları eski haline alalım ve _layout.js dosyamıza bu eklediğimiz <Stack.Screen … / >
konfigürasyonlarını ekleyelim.

import { Stack } from "expo-router";

export default () => {
return (
<Stack
screenOptions={{
headerStyle: {
backgroundColor: "blue",
},
headerTintColor: "white",
headerTitleStyle: {
fontWeight: "bold",
},
}}
>
<Stack.Screen options={{ title: "HomePage" }} />
<Stack.Screen
options={{ title: params ? params.normalize() : "UserPage" }}
/>
</Stack>
);
};

Şimdi burada bir hata ile karşılaşacağız , burada params değerinin ne olduğunu bilmiyor Expo bundan dolayı hata alıyoruz.
Bu ikinci yöntemin dezavantajı bu bu yöntem ile Header içine yada Stack’de herhangi bir alana o sayfaya özel değeleri malesef giremiyoruz.
(Bunun basit yada uygun bir yöntemi var mı açıkcası bilmiyorum şimdiye kadar Expo ile geliştirdiğim projelerde bu konfigürasyonu pek tercih etmedim ben .)

      <Stack.Screen name="index" options={{ title: "HomePage" }} />
<Stack.Screen name="[username]" options={{ title: "UserPage" }} />

Hatayı düzeltmen için üst alanda bulunan Stack.Screen’leri bu değeler ile değiştirmemiz ve name değerini vermemiz gerekmekte,
Name değeri hedef dosyanın adı ile aynı olmalı

Modal

Modal yapısını kullanmak için projemizin kök dizinine bir adek modal.js adında bir dosya ekleyelim Daha sonra Stack Navigation içinde yeni bir Stack.Screen oluşturup name değerini modal yapalım. Burada önemli olan kodu option konfigürasyonları yaparken presentation:‘modal’ olarak ayarlamak olacaktır Modal olarak açılmasını sağlıyoruz bu sayede yeni bir sayfa olarak değil.

import { Stack } from "expo-router";

export default () => {
return (
<Stack
screenOptions={{
headerStyle: {
backgroundColor: "blue",
},
headerTintColor: "white",
headerTitleStyle: {
fontWeight: "bold",
},
}}
>
<Stack.Screen name="index" options={{ title: "HomePage" }} />
<Stack.Screen name="[username]" options={{ title: "UserPage" }} />
<Stack.Screen
name="modal"
options={{
// Set the presentation mode to modal for our modal route.
presentation: "modal",
}}
/>
</Stack>
);
};

Modal dosyamızın içine aşağıda eklediğim kodları ekleyelim, burada expo’nun ufak bir trick’inden bahsedeyim.

Gördüğünüz gibi href={“../”} olarak ayarlanmış bu bir önceki sayfaya geç demek.

index.js dosyamızın içine yeni bir alan ekleyelim.

          <Link className="bg-blue-400 p-4 text-white rounded-xl"  href={"/modal"}>
<Text>Open Modal</Text>
</Link>

Modal dosyamıza ekte bulunan kodları yapıştıralım.

//Modal dosyamız
import { View } from "react-native";
import { Link, useRouter } from "expo-router";
import { StatusBar } from "expo-status-bar";
import { Pressable } from "react-native";
import { Text } from "react-native";
export default function Modal() {
// If the page was reloaded or navigated to directly, then the modal should be presented as
// a full screen page. You may need to change the UI to account for this.
let router = useRouter();
return (
<View style={{ flex: 1, alignItems: "center", justifyContent: "center" }}>
{/* Use `../` as a simple way to navigate to the root. This is not analogous to "goBack". */}
<Link className="bg-blue-400 p-4 text-white rounded-xl" href={"../"}>
<Text>Close</Text>
</Link>
{/* Native modals have dark backgrounds on iOS, set the status bar to light content. */}
<StatusBar style="light" />
</View>
);
}

Tabs Navigation

Bir çok uygulamada hatta bir çok demek doğru olmaz en popüler navigastion tipidir. Ekranın en alt kısımda genelde icon ile beraber olan ve genelde 3 yada 4 elemandan oluşan bir menü tipidir. Twitter bu konu için en çok konuyu anlamamıza yardımcı olacak örnek olabilir.

Yukarıda görmüş olduğunuz görsel üzerinden anlatmak en anlaşılır ve en kolay yöntem olacak diye düşünüyorum.Gördüğünüz gibi AnaSayfa , Arama , Bildirim ve Mesaj olarak 4 adet Tab bulunuyor. Her tab ayrı bir sayfayı temsil ediyor ve bu tablere her tıklandığında mevcut sayfa artık o tabin temsil ettiği sayfaya routing edilmiş oluyor.

//_layout.js dosyamızı güncelleyelim.

import { Stack, Tabs } from "expo-router";

export default () => {
return (
<Tabs>
<Tabs.Screen name="index" options={{ title: "Home Page" }} />
<Tabs.Screen name="Search" options={{ title: "Search" }} />
</Tabs>

);
};

Bu değişiklikten sonra hemen [username].js isimli dosyamızın içine hemen <Tabs.Scree… /> satırını ekleyelim.

 import { View, Text } from "react-native";
import React from "react";
import { Link, Tabs, usePathname } from "expo-router";
import { StyleSheet } from "react-native";
import { SafeAreaView } from "react-native";

export default function UserPageWithSlug() {
let params = usePathname();
return (
<SafeAreaView>
<Tabs.Screen options={{ title: params }} />
<View className=" h-full w-full justify-center align-middle items-center">
<Text className="text-lg font-bold border border-red-500 p-2">
<Text className="text-blue-500">UserName: </Text>
{JSON.stringify(params)}
</Text>
<Link
className="w-full bg-Primary text-white mt-4 p-3 text-center text-base border-1 border-black"
href={"/User"}
>
<Text>Go User</Text>
</Link>
<Link
className="w-full bg-Primary text-white mt-4 p-3 text-center text-base border-1 border-black"
href={"/"}
>
<Text>Go HomePage</Text>
</Link>
</View>
</SafeAreaView>
);
}

const styles = StyleSheet.create({
mybutton: {
fontSize: 23,
padding: 3,
marginTop: 12,
backgroundColor: "#ffb500",
textAlign: "center",
borderRadius: 5,
},
});

Bu sayede Tabs ekranınıda bu kadar basit bir şekilde çözmüş olduk.

Peki iç içe routing yapısını nasıl oluştururuz ?

Şimdi projemiz içinde olan bütün dosyaları silelim .
Kök dizinimizde index.js ve Register.js olmak üzere 2 adet dosya oluşturalım

//index.js dosyamız.
import { Link, useRouter } from "expo-router";
import { Pressable, SafeAreaView } from "react-native";
import { StyleSheet, Text, View } from "react-native";

export default function Page() {
let router = useRouter();

const handlePress = () => {
router.replace("Home");
};

return (
<SafeAreaView>
<View
style={{
height: "100%",
justifyContent: "center",
alignItems: "center",
gap: 12,
}}
>
<Pressable onPress={handlePress}>
<Text>Login</Text>
</Pressable>
<Link href={"/Register"} asChild>
<Pressable>
<Text>Create Account</Text>
</Pressable>
</Link>
</View>
</SafeAreaView>
);
}
//register.js dosyamız.
import { View, Text } from 'react-native'
import React from 'react'

const Register = () => {
return (
<View>
<Text>Register</Text>
</View>
)
}

export default Register

Daha sonra bir klasör oluşturalım (tabs) adını verelim.

Daha sonra tabs klasörümüzün içine Home.js ve Profile.js isimli iki dosya oluşturalım.

//Home.js
import { View, Text } from "react-native";
import React from "react";
import { SafeAreaView } from "react-native";

const home = () => {
return (
<SafeAreaView>
<View
style={{
height: "100%",
justifyContent: "center",
alignItems: "center",
gap: 12,
}}
>
<Text className="">HomePage</Text>
</View>
</SafeAreaView>
);
};

export default home;
//Profile.js
import { View, Text, Pressable } from "react-native";
import React, { useEffect } from "react";
import { SafeAreaView } from "react-native";
import { Link, useRouter } from "expo-router";

const Profile = () => {
let router = useRouter();

return (
<SafeAreaView>
<View
style={{
height: "100%",
justifyContent: "center",
alignItems: "center",
gap: 12,
}}
>
<Text>Profile</Text>
<Pressable onPress={() => router.push("/")}>
<Text>LogOut</Text>
</Pressable>
</View>
</SafeAreaView>
);
};

export default Profile;

Ve bir adet _layouth.js dosyası oluşturalım.

import { Stack } from "expo-router";

export default () => {
return (
<Stack>
<Stack.Screen name="index" options={{ headerShown: true }} />
<Stack.Screen name="(tabs)" options={{ headerShown: true }} />
</Stack>
);
};

Şimdi Login kısmına tıklayınca /HomePage URL adresine yönlendiriyoruz.

Gördüğünüz gibi burada Login butonuna tıklıyoruz ve (tabs) altında bulunan Home sayfasına yönlendiriliyoruz.
Bu (tabs) klasörünün altında bulunan bütün sayfaların Tabs olarak ayarlanmasını isteyelim yapmamız gereken neydi _layout.js dosyası oluşturmak hemen oluşturalım.

// (tabs)/_layouth.js dosyamız
import { Tabs } from "expo-router";
import { AntDesign } from "@expo/vector-icons";
import { Feather } from "@expo/vector-icons";
export default () => {
return (
<Tabs>
<Tabs.Screen
name="Home"
options={{
tabBarLabel: "HomePage",
headerTitle: "Home Page",
tabBarIcon: ({ color, size }) => (
<AntDesign name="home" size={size} color={color} />
),
}}
/>
<Tabs.Screen
name="List"
options={{
tabBarLabel: "List",
headerTitle: "List",
tabBarIcon: ({ color, size }) => (
<Feather name="list" size={size} color={color} />
),
headerShown:false
}}
/>
<Tabs.Screen
name="Profile"
options={{
tabBarLabel: "Profile",
headerTitle: "Profile",
tabBarIcon: ({ color, size }) => (
<AntDesign name="home" size={size} color={color} />
),
}}
/>
</Tabs>
);
};

Burada bir sorun var gördüğünüz gibi hem Stack hem Tabs’in Header alanı oluşmuş bunu engellemek için ilk olarak.

Kök klasörümüzün altında bulunan _layout.js dosyamızı düzenlememiz gerekiyor.

import { Stack } from "expo-router";

export default () => {
return (
<Stack>
<Stack.Screen name="index" options={{ headerShown: false }} />
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
</Stack>
);
};

Gördüğünüz gibi Stack’in Header’ını gizledik.
Şimdi yeni bir klasör ekleyelim (tabs)’in altına
ismine List diyelim. İçine iki adet dosya oluşturalım.

[id].js ve index.js olarak isimlendirelim bu dosyaları.

//İd.js dosyamız
import { View, Text } from "react-native";
import React from "react";
import { SafeAreaView } from "react-native";
import {useRouter, useSearchParams } from "expo-router";

const SingleItem = () => {
let params = useSearchParams();
return (
<>
<SafeAreaView>
<View
style={{
height: "100%",
justifyContent: "center",
alignItems: "center",
gap: 12,
}}
>
<Text>SingleItem</Text>
<Text>{JSON.stringify(params.id)}</Text>
</View>
</SafeAreaView>
</>
);
};

export default SingleItem;
//index.js dosyamız
import { View, Text } from "react-native";
import React from "react";
import { SafeAreaView } from "react-native";
import { Link } from "expo-router";

const ItemList = () => {
return (
<SafeAreaView>
<View
style={{
height: "100%",
justifyContent: "center",
alignItems: "center",
gap: 12,
}}
>
<Link href="/List/1">News One</Link>
<Link href="/List/2">News Two</Link>
<Link href="/List/3">News Three</Link>
</View>
</SafeAreaView>
);
};

export default ItemList;

Burada gördüğünüz gibi List ve List/id olarak iki adet Tab oluştu bu bir problem bunu nasıl engelleriz.
List klasörümüzün altında hemen bir _layout.js dosyası oluşturalım.

import { Stack } from "expo-router";

export default () => {
return (
<Stack>
<Stack.Screen name="index" options={{ headerTitle: "ItemList" ,headerBackTitleVisible:false}} />
</Stack>
);
};

Gördüğünüz gibi Tab alanlarımız istediğimiz gibi şekillendi. Projemizin son hali ;

Elimden geldiği kadarıyla konuyu anlatmak istedim. Hatam kusurum yazım yanlışım olduysa affola.

--

--