مقدمه
ناوبری یکی از اون چیزهاییه که شاید ساده به نظر برسه، ولی وقتی واقعاً میخواید یک اپلیکیشن موبایل جدی بسازید، میفهمید چقدر میتونه پیچیده بشه. کاربرها انتظار دارن بین صفحات مختلف راحت جابجا بشن، به صفحه قبلی برگردن و تجربهای روان داشته باشن. در دنیای React Native، ابزارهای مختلفی برای مدیریت ناوبری وجود داره، ولی Expo Router با رویکرد مسیریابی مبتنی بر فایل (File-Based Routing) بازی رو عوض کرده.
Expo Router یک کتابخانه مسیریابی متنباز برای اپلیکیشنهای یونیورسال React Nativeه که تیم Expo توسعهش داده. این کتابخانه از ساختار فایلی پروژه شما برای تعریف مسیرهای ناوبری استفاده میکنه — دقیقاً مشابه کاری که Next.js و Nuxt.js در دنیای وب انجام میدن. با انتشار نسخه ۵ در Expo SDK 53، این ابزار واقعاً به بلوغ رسیده و حالا قابلیتهایی مثل React Server Components، مسیرهای محافظتشده (Protected Routes)، مسیرهای API و توابع سروری رو پشتیبانی میکنه.
خب، بیاید با هم تمام جنبههای Expo Router رو از مفاهیم پایه تا الگوهای پیشرفته بررسی کنیم. فرقی نمیکنه تازهکار باشید یا توسعهدهنده باتجربه — این مقاله قراره کمکتون کنه ناوبری اپلیکیشنهای React Native خودتون رو به بهترین شکل پیادهسازی کنید.
چرا Expo Router؟ مزایا نسبت به React Navigation سنتی
قبل از اینکه بریم سراغ جزئیات فنی، بذارید اول ببینیم چرا اصلاً باید Expo Router رو به تنظیم دستی React Navigation ترجیح بدیم:
- ساختار پروژه سازمانیافته: هر فایل در پوشه
app/یک مسیر ناوبریه. دیگه نیازی نیست دستی ناویگیتورها رو تعریف کنید و مسیرها رو پیکربندی کنید. - دیپلینکینگ خودکار: هر صفحهای که میسازید، خودش یک URL دریافت میکنه. هم توی وب کار میکنه، هم به عنوان دیپلینک در موبایل.
- چندسکویی واقعی: همون کد ناوبری روی اندروید، iOS و وب کار میکنه — بدون تنظیمات جداگانه.
- مسیرهای تایپشده (Typed Routes): پشتیبانی کامل از TypeScript با تولید خودکار تایپها. خطاهای ناوبری رو موقع کامپایل گیر میندازید، نه توی پروداکشن!
- تقسیم بسته خودکار (Bundle Splitting): با مسیرهای ناهمزمان (Async Routes) صفحات رو lazy load کنید و حجم بسته اولیه رو کم کنید.
- سازگاری کامل با React Navigation: از اونجایی که Expo Router رو پایه React Navigation ساختن، تمام مستندات و قابلیتهای React Navigation هنوز قابل استفادهن.
شروع کار: راهاندازی پروژه با Expo Router
خبر خوب اینه که اگه پروژه جدیدی با Expo بسازید، Expo Router از قبل تنظیم شده. بیاید یه پروژه جدید بسازیم:
npx create-expo-app@latest my-app
cd my-app
npx expo start
ساختار پروژهای که ایجاد میشه اینجوریه:
my-app/
├── app/
│ ├── _layout.tsx # لایهبندی اصلی
│ ├── index.tsx # صفحه اصلی (/)
│ ├── +not-found.tsx # صفحه ۴۰۴
│ └── (tabs)/
│ ├── _layout.tsx # لایهبندی تبها
│ ├── index.tsx # تب اول
│ └── explore.tsx # تب دوم
├── components/
├── constants/
├── assets/
└── package.json
یه نکته مهم: پوشه app/ منحصراً برای فایلهای مسیریابی استفاده میشه. کامپوننتها، توابع کمکی و هوکها باید توی پوشههای جداگانه مثل components/ یا utils/ باشن تا Expo Router اشتباهی اونها رو صفحه حساب نکنه.
مفاهیم پایه مسیریابی مبتنی بر فایل
قوانین نامگذاری فایلها
توی Expo Router، ساختار فایلی پروژه مستقیماً نقشه مسیرهای ناوبری رو تعیین میکنه. هر فایل در پوشه app/ یک مسیر ناوبری ایجاد میکنه:
index.tsx— نقطه ورود هر دایرکتوری. به مسیر/اون دایرکتوری نگاشت میشه._layout.tsx— فایل لایهبندی که قبل از بقیه مسیرها رندر میشه و ساختار ناویگیتور رو مشخص میکنه.[param].tsx— مسیر پویا (Dynamic Route) که یه پارامتر URL دریافت میکنه.[...slug].tsx— مسیر Catch-All که تمام مسیرهای تودرتو رو هندل میکنه.+not-found.tsx— صفحه ۴۰۴ سفارشی برای مسیرهای ناموجود.+api.ts— مسیر API سمت سرور.
نمونه ساختار فایلی و مسیرهای تولیدشده
app/
├── index.tsx → /
├── about.tsx → /about
├── settings/
│ ├── index.tsx → /settings
│ └── profile.tsx → /settings/profile
├── user/
│ └── [id].tsx → /user/:id
└── blog/
└── [...slug].tsx → /blog/*
هر مسیری که ایجاد میشه، خودکار یک URL معادل دریافت میکنه. توی وب، این URL در نوار آدرس مرورگر نشون داده میشه. توی اپهای موبایل هم همین URL به عنوان دیپلینک کار میکنه — مثلاً myapp://user/42 کاربر رو مستقیم به پروفایل کاربر ۴۲ میبره.
لایهبندی (Layouts): ستون فقرات ناوبری اپلیکیشن
فایلهای _layout.tsx واقعاً قلب Expo Router هستن. این فایلها ساختار ناوبری رو تعریف میکنن و مشخص میکنن صفحات چجوری نمایش داده بشن. سه نوع ناویگیتور اصلی داریم:
Stack Navigator (ناویگیتور پشتهای)
رایجترین نوع ناوبری. صفحات رو مثل یه دسته کارت روی هم میچینه. وقتی به صفحه جدید میرید، صفحه قبلی زیرش میمونه و دکمه بازگشت خودکار ظاهر میشه:
// app/_layout.tsx
import { Stack } from 'expo-router';
export default function RootLayout() {
return (
<Stack>
<Stack.Screen
name="index"
options={{ title: 'خانه' }}
/>
<Stack.Screen
name="profile"
options={{
title: 'پروفایل',
headerStyle: { backgroundColor: '#2196F3' },
headerTintColor: '#fff',
}}
/>
<Stack.Screen
name="settings"
options={{
title: 'تنظیمات',
presentation: 'modal', // نمایش به صورت مودال
}}
/>
</Stack>
);
}
Tab Navigator (ناویگیتور تبی)
برای ناوبری بین بخشهای اصلی اپ از طریق تبهای پایین صفحه. احتمالاً بیشتر اپهایی که روزانه استفاده میکنید (اینستاگرام، تلگرام و...) از همین الگو استفاده میکنن:
// app/(tabs)/_layout.tsx
import { Tabs } from 'expo-router';
import { Ionicons } from '@expo/vector-icons';
export default function TabsLayout() {
return (
<Tabs
screenOptions={{
tabBarActiveTintColor: '#2196F3',
tabBarInactiveTintColor: '#888',
}}
>
<Tabs.Screen
name="index"
options={{
title: 'خانه',
tabBarIcon: ({ color, size }) => (
<Ionicons name="home" size={size} color={color} />
),
}}
/>
<Tabs.Screen
name="search"
options={{
title: 'جستجو',
tabBarIcon: ({ color, size }) => (
<Ionicons name="search" size={size} color={color} />
),
}}
/>
<Tabs.Screen
name="profile"
options={{
title: 'پروفایل',
tabBarIcon: ({ color, size }) => (
<Ionicons name="person" size={size} color={color} />
),
}}
/>
</Tabs>
);
}
Drawer Navigator (ناویگیتور کشویی)
منوی کشویی از کنار صفحه. با کشیدن انگشت یا زدن دکمه منو باز میشه:
// app/_layout.tsx
import { Drawer } from 'expo-router/drawer';
export default function DrawerLayout() {
return (
<Drawer>
<Drawer.Screen
name="index"
options={{ drawerLabel: 'خانه', title: 'خانه' }}
/>
<Drawer.Screen
name="settings"
options={{ drawerLabel: 'تنظیمات', title: 'تنظیمات' }}
/>
</Drawer>
);
}
لایهبندی تودرتو (Nested Layouts)
قدرت واقعی Expo Router وقتی مشخص میشه که ناویگیتورها رو ترکیب کنید. میتونید لایهبندیها رو توی هم بذارید — مثلاً یه Stack به عنوان لایهبندی اصلی و تبها به عنوان یکی از صفحاتش:
app/
├── _layout.tsx # Stack (اصلی)
├── (tabs)/
│ ├── _layout.tsx # Tabs (تودرتو)
│ ├── index.tsx # تب خانه
│ └── profile.tsx # تب پروفایل
├── settings.tsx # صفحه تنظیمات (در Stack اصلی)
└── modal.tsx # صفحه مودال
روشهای ناوبری
Expo Router دو روش اصلی برای ناوبری بین صفحات داره: روش اعلانی (Declarative) با کامپوننت Link و روش دستوری (Imperative) با هوک useRouter. بسته به موقعیت، هرکدوم کاربرد خودشون رو دارن.
کامپوننت Link (روش اعلانی)
مشابه تگ <a> در وب. سادهترین و خواناترین روش ناوبریه:
import { Link } from 'expo-router';
import { View, Text, Pressable } from 'react-native';
export default function HomePage() {
return (
<View>
{/* لینک ساده */}
<Link href="/about">درباره ما</Link>
{/* لینک با پارامتر پویا */}
<Link href="/user/42">مشاهده کاربر</Link>
{/* لینک با آبجکت مسیر */}
<Link
href={{
pathname: '/user/[id]',
params: { id: '42' }
}}
>
مشاهده کاربر
</Link>
{/* لینک با کامپوننت سفارشی */}
<Link href="/settings" asChild>
<Pressable style={{ padding: 16, backgroundColor: '#2196F3' }}>
<Text style={{ color: '#fff' }}>تنظیمات</Text>
</Pressable>
</Link>
{/* لینک با پیشبارگذاری */}
<Link href="/heavy-page" prefetch>
صفحه سنگین (پیشبارگذاری شده)
</Link>
</View>
);
}
هوک useRouter (روش دستوری)
وقتی میخواید بعد از یه اتفاق خاص (مثل ارسال فرم یا لاگین موفق) کاربر رو به صفحه دیگهای ببرید، از هوک useRouter استفاده میکنید:
import { useRouter } from 'expo-router';
import { View, Button, Alert } from 'react-native';
export default function LoginPage() {
const router = useRouter();
const handleLogin = async () => {
try {
await authService.login(email, password);
// router.navigate: اگر صفحه در پشته باشد، به آن برمیگردد
// وگرنه صفحه جدید push میشود
router.navigate('/dashboard');
// router.push: همیشه صفحه جدید push میکند
// router.push('/dashboard');
// router.replace: صفحه فعلی را جایگزین میکند
// (کاربر نمیتواند با دکمه بازگشت به صفحه لاگین برگردد)
// router.replace('/dashboard');
} catch (error) {
Alert.alert('خطا', 'ورود ناموفق بود');
}
};
return (
<View>
<Button title="ورود" onPress={handleLogin} />
<Button title="بازگشت" onPress={() => router.back()} />
</View>
);
}
تفاوت navigate، push و replace
صادقانه بگم، درک تفاوت این سه متد خیلی مهمه و خیلیها اولش باهاشون قاطی میکنن:
router.navigate('/path')— هوشمندترین گزینه. اگه صفحه مقصد از قبل توی پشته ناوبری باشه، پشته رو باز میکنه (unwind) و برمیگرده به اون صفحه. اگه نباشه، صفحه جدید push میکنه. این متد جلوی تکرار صفحات توی پشته رو میگیره.router.push('/path')— همیشه یه صفحه جدید اضافه میکنه، حتی اگه همون صفحه از قبل توی پشته باشه. مناسب وقتی که میخواید کاربر بتونه چند نمونه از یه صفحه رو باز کنه.router.replace('/path')— صفحه فعلی رو با صفحه جدید عوض میکنه. کاربر نمیتونه با دکمه بازگشت به صفحه قبلی برگرده. عالیه برای صفحه لاگین یا فرآیندهای چندمرحلهای.
مسیرهای پویا (Dynamic Routes)
یکی از قابلیتهای کلیدی Expo Router، مسیرهای پویاست. کافیه اسم فایل رو توی براکت بذارید تا یه پارامتر پویا ایجاد بشه. سادهست، مگه نه؟
// app/product/[id].tsx
import { useLocalSearchParams } from 'expo-router';
import { View, Text, Image, ScrollView } from 'react-native';
import { useEffect, useState } from 'react';
export default function ProductDetail() {
const { id } = useLocalSearchParams<{ id: string }>();
const [product, setProduct] = useState(null);
useEffect(() => {
fetchProduct(id).then(setProduct);
}, [id]);
if (!product) {
return <Text>در حال بارگذاری...</Text>;
}
return (
<ScrollView>
<Image source={{ uri: product.image }} style={{ height: 300 }} />
<Text style={{ fontSize: 24 }}>{product.name}</Text>
<Text style={{ fontSize: 18, color: '#2196F3' }}>
{product.price} تومان
</Text>
<Text>{product.description}</Text>
</ScrollView>
);
}
// ناوبری به این صفحه:
// <Link href="/product/42">مشاهده محصول</Link>
// router.push('/product/42');
// router.push({ pathname: '/product/[id]', params: { id: '42' } });
مسیرهای Catch-All
برای مسیرهایی که ممکنه چندین بخش تودرتو داشته باشن (مثلاً URL بلاگ با سال/ماه/عنوان)، الگوی [...slug] به کارتون میاد:
// app/blog/[...slug].tsx
import { useLocalSearchParams } from 'expo-router';
export default function BlogPost() {
// برای مسیر /blog/2026/01/my-post
// slug = ['2026', '01', 'my-post']
const { slug } = useLocalSearchParams<{ slug: string[] }>();
return (
<View>
<Text>سال: {slug[0]}</Text>
<Text>ماه: {slug[1]}</Text>
<Text>عنوان: {slug[2]}</Text>
</View>
);
}
گروههای مسیر (Route Groups)
گروههای مسیر یکی از اون قابلیتهای هوشمندانهایه که وقتی باهاش آشنا بشید، عاشقش میشید. با قرار دادن اسم پوشه توی پرانتز، مسیرها رو سازماندهی میکنید بدون اینکه روی URL تأثیری بذاره:
app/
├── _layout.tsx
├── (auth)/ # گروه مسیرهای احراز هویت
│ ├── _layout.tsx
│ ├── login.tsx → /login (نه /auth/login)
│ ├── register.tsx → /register
│ └── forgot-password.tsx → /forgot-password
├── (main)/ # گروه مسیرهای اصلی
│ ├── _layout.tsx
│ ├── (tabs)/ # تبهای داخل گروه اصلی
│ │ ├── _layout.tsx
│ │ ├── index.tsx → /
│ │ ├── search.tsx → /search
│ │ └── profile.tsx → /profile
│ └── settings.tsx → /settings
└── +not-found.tsx
این ساختار بهتون اجازه میده مسیرهای احراز هویت و مسیرهای اصلی اپ رو با لایهبندیهای کاملاً متفاوت مدیریت کنید. مثلاً مسیرهای (auth) میتونن بدون هدر و تببار باشن، ولی مسیرهای (main) ناوبری کامل داشته باشن.
مسیرهای محافظتشده (Protected Routes)
از Expo SDK 53 به بعد، یه قابلیت فوقالعاده به اسم Stack.Protected اضافه شده که کنترل دسترسی اعلانی (Declarative Access Control) رو ممکن میکنه. به نظر من این یکی از بهترین اضافهشدنهای اخیر Expo Routerه:
// app/_layout.tsx
import { Stack } from 'expo-router';
import { useAuth } from '../hooks/useAuth';
export default function RootLayout() {
const { isLoggedIn, isAdmin } = useAuth();
return (
<Stack>
{/* صفحات عمومی - همیشه قابل دسترسی */}
<Stack.Screen name="index" options={{ title: 'خانه' }} />
<Stack.Screen name="about" options={{ title: 'درباره ما' }} />
{/* صفحات احراز هویت - فقط برای کاربران لاگیننشده */}
<Stack.Protected guard={!isLoggedIn}>
<Stack.Screen name="login" options={{ title: 'ورود' }} />
<Stack.Screen name="register" options={{ title: 'ثبتنام' }} />
</Stack.Protected>
{/* صفحات کاربر - فقط برای کاربران لاگینشده */}
<Stack.Protected guard={isLoggedIn}>
<Stack.Screen name="dashboard" options={{ title: 'داشبورد' }} />
<Stack.Screen name="profile" options={{ title: 'پروفایل' }} />
{/* صفحات مدیر - نیاز به لاگین و دسترسی مدیر */}
<Stack.Protected guard={isAdmin}>
<Stack.Screen name="admin" options={{ title: 'پنل مدیریت' }} />
<Stack.Screen name="users" options={{ title: 'مدیریت کاربران' }} />
</Stack.Protected>
</Stack.Protected>
</Stack>
);
}
نکات مهم درباره مسیرهای محافظتشده
- فقط سمت کلاینت:
Stack.Protectedفقط جلوی ناوبری سمت کلاینت رو میگیره. این جایگزین احراز هویت سمت سرور نیست — حتماً API هاتون رو هم محافظت کنید. - ریدایرکت خودکار: اگه کاربر سعی کنه به مسیر محافظتشده بره، خودکار به مسیر لنگر (Anchor Route) یعنی صفحه index ریدایرکت میشه.
- حذف تاریخچه: وقتی شرط guard از
trueبهfalseتغییر کنه، تمام ورودیهای تاریخچه مربوط به اون مسیرها پاک میشن. - تب و Drawer هم پشتیبانی میشن: مسیرهای محافظتشده با
Tabs.ProtectedوDrawer.Protectedهم کار میکنن.
الگوی احراز هویت با Context
بهترین روش برای مدیریت وضعیت احراز هویت، استفاده از React Contextه. بذارید یه پیادهسازی کامل ببینیم:
// contexts/AuthContext.tsx
import { createContext, useContext, useState, useEffect } from 'react';
import * as SecureStore from 'expo-secure-store';
interface AuthContextType {
isLoggedIn: boolean;
user: User | null;
login: (email: string, password: string) => Promise<void>;
logout: () => Promise<void>;
}
const AuthContext = createContext<AuthContextType>(null!);
export function AuthProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
// بازیابی توکن ذخیرهشده در هنگام راهاندازی
const restoreToken = async () => {
try {
const token = await SecureStore.getItemAsync('userToken');
if (token) {
const userData = await api.getUser(token);
setUser(userData);
}
} catch (e) {
console.error('خطا در بازیابی توکن:', e);
} finally {
setIsLoading(false);
}
};
restoreToken();
}, []);
const login = async (email: string, password: string) => {
const { token, user } = await api.login(email, password);
await SecureStore.setItemAsync('userToken', token);
setUser(user);
};
const logout = async () => {
await SecureStore.deleteItemAsync('userToken');
setUser(null);
};
return (
<AuthContext.Provider
value={{ isLoggedIn: !!user, user, login, logout }}
>
{!isLoading && children}
</AuthContext.Provider>
);
}
export const useAuth = () => useContext(AuthContext);
// app/_layout.tsx - استفاده از AuthProvider
import { AuthProvider } from '../contexts/AuthContext';
export default function RootLayout() {
return (
<AuthProvider>
<Stack>{/* ... */}</Stack>
</AuthProvider>
);
}
مسیرهای تایپشده (Typed Routes)
اگه با TypeScript کار میکنید (که اگه نمیکنید، واقعاً پیشنهاد میکنم شروع کنید!)، مسیرهای تایپشده یکی از بهترین قابلیتهای Expo Routerه. خطاهای ناوبری رو موقع کامپایل شناسایی میکنه و تکمیل خودکار هم بهتون میده.
فعالسازی مسیرهای تایپشده
// app.json
{
"expo": {
"experiments": {
"typedRoutes": true
}
}
}
بعد از فعالسازی و اجرای npx expo start، Expo CLI فایل expo-env.d.ts رو خودکار تولید میکنه که شامل تعریف تایپ تمام مسیرهاست:
// اینها معتبر هستند و TypeScript آنها را تأیید میکند ✅
<Link href="/about" />
<Link href="/user/42" />
<Link href={{ pathname: '/user/[id]', params: { id: '42' } }} />
router.push('/settings');
// اینها خطای TypeScript تولید میکنند ❌
<Link href="/nonexistent" /> // مسیر وجود ندارد
<Link href="/user/[id]" /> // باید از params استفاده شود
router.push('/unknown-route'); // مسیر ناشناخته
دیپلینکینگ و Universal Links
یکی از بزرگترین مزایای Expo Router اینه که دیپلینکینگ خودکاره. هر مسیری بسازید، از طریق URL قابل دسترسیه. اینکه بدون هیچ تنظیم اضافهای این قابلیت رو دارید، واقعاً ارزشمنده.
دیپلینکینگ با URI Scheme
سادهترین روش دیپلینکینگ، استفاده از URI Scheme سفارشیه:
// app.json
{
"expo": {
"scheme": "myapp"
}
}
// اکنون این لینکها کار میکنند:
// myapp:// → صفحه اصلی
// myapp://profile → صفحه پروفایل
// myapp://product/42 → صفحه محصول ۴۲
// myapp://blog/2026/01/post → پست وبلاگ
Universal Links (iOS) و App Links (اندروید)
برای دیپلینکینگ حرفهای با URLهای HTTPS واقعی، باید Universal Links و App Links رو تنظیم کنید:
// app.json
{
"expo": {
"ios": {
"associatedDomains": ["applinks:example.com"]
},
"android": {
"intentFilters": [
{
"action": "VIEW",
"autoVerify": true,
"data": [
{
"scheme": "https",
"host": "example.com",
"pathPrefix": "/"
}
],
"category": ["BROWSABLE", "DEFAULT"]
}
]
}
}
}
با این تنظیمات، لینکهایی مثل https://example.com/product/42 مستقیماً اپ رو باز میکنن (البته اگه نصب باشه) یا کاربر رو به وبسایت هدایت میکنن.
مسیرهای API و توابع سروری
Expo Router فقط ناوبری سمت کلاینت نیست. از مسیرهای API سمت سرور هم پشتیبانی میکنه! کافیه فایلهایی با پسوند +api.ts بسازید تا اندپوینتهای API داشته باشید:
// app/api/products+api.ts
export async function GET(request: Request) {
const products = await db.query('SELECT * FROM products');
return Response.json(products);
}
export async function POST(request: Request) {
const body = await request.json();
const { name, price, description } = body;
const product = await db.query(
'INSERT INTO products (name, price, description) VALUES (?, ?, ?)',
[name, price, description]
);
return Response.json(product, { status: 201 });
}
// app/api/products/[id]+api.ts
export async function GET(request: Request, { id }: { id: string }) {
const product = await db.query(
'SELECT * FROM products WHERE id = ?',
[id]
);
if (!product) {
return new Response('محصول یافت نشد', { status: 404 });
}
return Response.json(product);
}
export async function DELETE(request: Request, { id }: { id: string }) {
await db.query('DELETE FROM products WHERE id = ?', [id]);
return new Response(null, { status: 204 });
}
این مسیرهای API توی محیط سرور اجرا میشن و از متدهای HTTP استاندارد (GET، POST، PUT، PATCH، DELETE) پشتیبانی میکنن. برای محیط تولید، باید سرور رو روی یه میزبان مستقر کنید و origin رو توی تنظیمات Expo Router پیکربندی کنید.
React Server Components در Expo Router v5
خب، بیاید درباره یکی از هیجانانگیزترین قابلیتهای Expo Router v5 صحبت کنیم. برای اولین بار، React Server Components و Server Actions توی اپهای نیتیو قابل استفادهن! این یعنی میتونید بخشی از کامپوننتها رو توی سرور رندر کنید و فقط نتیجه نهایی رو به کلاینت بفرستید:
// app/products.tsx - Server Component (پیشفرض)
// این کامپوننت در سرور اجرا میشود
import { View, Text, FlatList } from 'react-native';
export default async function Products() {
// واکشی مستقیم داده در سرور - بدون نیاز به useEffect
const products = await fetch('https://api.example.com/products')
.then(res => res.json());
return (
<View>
<Text style={{ fontSize: 24 }}>محصولات</Text>
{products.map(product => (
<ProductCard key={product.id} product={product} />
))}
</View>
);
}
// components/AddToCart.tsx - Client Component
'use client';
import { useState } from 'react';
import { Button, Text, View } from 'react-native';
export default function AddToCart({ productId }: { productId: string }) {
const [count, setCount] = useState(0);
return (
<View>
<Text>تعداد: {count}</Text>
<Button title="افزودن به سبد" onPress={() => setCount(c => c + 1)} />
</View>
);
}
با Server Components، حجم بسته جاوااسکریپت سمت کلاینت کمتر میشه، واکشی داده سریعتر انجام میشه و مهمتر از همه، کلیدهای API و اطلاعات حساس توی سرور میمونن و به کلاینت ارسال نمیشن.
الگوهای رایج ناوبری
مودالها (Modals)
نمایش صفحات به صورت مودال با تنظیم presentation: 'modal' خیلی سادهست:
// app/_layout.tsx
import { Stack } from 'expo-router';
export default function Layout() {
return (
<Stack>
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
<Stack.Screen
name="modal"
options={{
presentation: 'modal',
title: 'مودال',
}}
/>
</Stack>
);
}
// app/modal.tsx
import { useRouter } from 'expo-router';
import { View, Text, Button } from 'react-native';
export default function Modal() {
const router = useRouter();
return (
<View style={{ flex: 1, justifyContent: 'center', padding: 20 }}>
<Text style={{ fontSize: 20, textAlign: 'center' }}>
محتوای مودال
</Text>
<Button title="بستن" onPress={() => router.back()} />
</View>
);
}
صفحه ۴۰۴ سفارشی
هیچکس دوست نداره صفحه ۴۰۴ ببینه، ولی حداقل میتونیم تجربهشو بهتر کنیم:
// app/+not-found.tsx
import { Link, Stack } from 'expo-router';
import { View, Text, StyleSheet } from 'react-native';
export default function NotFound() {
return (
<>
<Stack.Screen options={{ title: 'صفحه یافت نشد!' }} />
<View style={styles.container}>
<Text style={styles.emoji}>🔍</Text>
<Text style={styles.title}>صفحه مورد نظر یافت نشد</Text>
<Text style={styles.subtitle}>
متأسفانه صفحهای که به دنبال آن هستید وجود ندارد.
</Text>
<Link href="/" style={styles.link}>
بازگشت به صفحه اصلی
</Link>
</View>
</>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
padding: 20,
},
emoji: { fontSize: 64 },
title: { fontSize: 24, fontWeight: 'bold', marginTop: 16 },
subtitle: { fontSize: 16, color: '#666', marginTop: 8, textAlign: 'center' },
link: { fontSize: 16, color: '#2196F3', marginTop: 24 },
});
ریدایرکت بر اساس شرایط
یه الگوی خیلی رایج: ریدایرکت کاربر بر اساس وضعیتش (لاگین کرده؟ آنبوردینگ رو تموم کرده؟):
// app/index.tsx
import { Redirect } from 'expo-router';
import { useAuth } from '../hooks/useAuth';
export default function Index() {
const { isLoggedIn, hasCompletedOnboarding } = useAuth();
if (!hasCompletedOnboarding) {
return <Redirect href="/onboarding" />;
}
if (!isLoggedIn) {
return <Redirect href="/login" />;
}
return <Redirect href="/dashboard" />;
}
بهترین شیوهها و نکات عملی
بعد از کار کردن با Expo Router، یه سری نکته عملی هست که واقعاً تفاوت ایجاد میکنن:
۱. بارگذاری فونت و اسپلش اسکرین در لایهبندی اصلی
// app/_layout.tsx
import { Stack } from 'expo-router';
import { useFonts } from 'expo-font';
import * as SplashScreen from 'expo-splash-screen';
import { useEffect } from 'react';
SplashScreen.preventAutoHideAsync();
export default function RootLayout() {
const [fontsLoaded] = useFonts({
'Vazirmatn-Regular': require('../assets/fonts/Vazirmatn-Regular.ttf'),
'Vazirmatn-Bold': require('../assets/fonts/Vazirmatn-Bold.ttf'),
});
useEffect(() => {
if (fontsLoaded) {
SplashScreen.hideAsync();
}
}, [fontsLoaded]);
if (!fontsLoaded) {
return null;
}
return (
<Stack screenOptions={{ headerTitleStyle: { fontFamily: 'Vazirmatn-Bold' } }}>
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
</Stack>
);
}
۲. سازماندهی فایلها
فقط فایلهای مسیریابی رو توی پوشه app/ بذارید. بقیه کدها رو توی پوشههای مخصوص خودشون سازماندهی کنید:
my-app/
├── app/ # فقط مسیرها
├── components/ # کامپوننتهای قابل استفاده مجدد
├── hooks/ # هوکهای سفارشی
├── contexts/ # React Context ها
├── services/ # سرویسهای API
├── utils/ # توابع کمکی
├── constants/ # ثابتها و تنظیمات
├── assets/ # تصاویر، فونتها و ...
└── types/ # انواع دادهای TypeScript
۳. مدیریت خطاها در ناوبری
همیشه یه ErrorBoundary داشته باشید. هیچچیز بدتر از کرش کردن اپ نیست:
// app/_layout.tsx
import { ErrorBoundary } from 'expo-router';
export { ErrorBoundary };
// یا ErrorBoundary سفارشی:
export function ErrorBoundary({ error, retry }: {
error: Error;
retry: () => void;
}) {
return (
<View style={{ flex: 1, justifyContent: 'center', padding: 20 }}>
<Text style={{ fontSize: 18, color: 'red' }}>خطایی رخ داد!</Text>
<Text style={{ marginTop: 8 }}>{error.message}</Text>
<Button title="تلاش مجدد" onPress={retry} />
</View>
);
}
۴. استفاده از initialRouteName برای دیپلینک
وقتی کاربر از دیپلینک وارد صفحه داخلی میشه، مطمئن بشید با دکمه بازگشت بتونه به صفحه اصلی برگرده (نه اینکه اپ بسته بشه!):
// app/(tabs)/_layout.tsx
export const unstable_settings = {
initialRouteName: 'index',
};
export default function TabsLayout() {
return (
<Tabs>
<Tabs.Screen name="index" />
<Tabs.Screen name="profile" />
</Tabs>
);
}
۵. پیشبارگذاری مسیرها
برای تجربه کاربری بهتر، مسیرهایی که احتمالاً کاربر بهشون میره رو از قبل بارگذاری کنید:
import { Link } from 'expo-router';
// پیشبارگذاری خودکار
<Link href="/heavy-page" prefetch>
صفحه سنگین
</Link>
// یا پیشبارگذاری دستی با useRouter
import { useRouter } from 'expo-router';
const router = useRouter();
router.prefetch('/settings');
مهاجرت از React Navigation به Expo Router
اگه پروژهای دارید که از React Navigation استفاده میکنه، خبر خوب اینه که مهاجرت به Expo Router نسبتاً سادهست. چون Expo Router خودش بر پایه React Navigation ساخته شده، بیشتر مفاهیم مشترکن.
مراحل مهاجرت
- ایجاد پوشه
app/: یه پوشهappدر ریشه پروژه بسازید. - تبدیل ناویگیتورها به فایلهای لایهبندی: هر ناویگیتور (Stack، Tab، Drawer) رو به یه فایل
_layout.tsxدر دایرکتوری مناسب تبدیل کنید. - تبدیل صفحات به فایلهای مسیر: هر صفحه رو به یه فایل جداگانه توی پوشه
app/منتقل کنید. - جایگزینی
navigation.navigate: از هوکuseRouterبه جایuseNavigationاستفاده کنید. - جایگزینی پارامترها:
useLocalSearchParamsرو جایگزینroute.paramsکنید.
// قبل - React Navigation
function App() {
return (
<NavigationContainer>
<Stack.Navigator>
<Stack.Screen name="Home" component={HomeScreen} />
<Stack.Screen name="Profile" component={ProfileScreen} />
</Stack.Navigator>
</NavigationContainer>
);
}
// بعد - Expo Router
// app/_layout.tsx
import { Stack } from 'expo-router';
export default function Layout() {
return (
<Stack>
<Stack.Screen name="index" options={{ title: 'خانه' }} />
<Stack.Screen name="profile" options={{ title: 'پروفایل' }} />
</Stack>
);
}
// قبل - React Navigation ناوبری
navigation.navigate('Profile', { userId: 42 });
const { userId } = route.params;
// بعد - Expo Router ناوبری
router.push('/profile/42');
const { id } = useLocalSearchParams();
جمعبندی
Expo Router با رویکرد مسیریابی مبتنی بر فایل، واقعاً کار ناوبری در React Native رو متحول کرده. نسخه ۵ با قابلیتهایی مثل مسیرهای محافظتشده، React Server Components و مسیرهای API، این ابزار رو به یه راهحل فولاستک واقعی تبدیل کرده.
بذارید مهمترین نکات رو خلاصه کنم:
- از ساختار فایلی استاندارد پیروی کنید و فقط فایلهای مسیریابی رو توی پوشه
app/بذارید. - گروههای مسیر رو برای سازماندهی بدون تأثیر بر URL به کار ببرید.
- حتماً مسیرهای تایپشده رو فعال کنید — جلوی کلی باگ رو میگیره.
Stack.Protectedعالیه برای کنترل دسترسی سمت کلاینت، ولی احراز هویت سمت سرور رو هم فراموش نکنید.- از پیشبارگذاری مسیرها برای تجربه کاربری روانتر استفاده کنید.
- برای API سمت سرور، مسیرهای
+api.tsرو امتحان کنید.
با یادگیری Expo Router، میتونید اپهای چندسکویی حرفهای بسازید که ناوبری روان، دیپلینکینگ کامل و تجربه کاربری عالی ارائه بدن. به نظر من، اگه الان دارید یه پروژه React Native جدید شروع میکنید، Expo Router بهترین انتخابه.