บทนำ: การนำทางในยุคใหม่ของ React Native
ระบบนำทาง (Navigation) ถือเป็นหนึ่งในองค์ประกอบสำคัญที่สุดของแอปมือถือทุกตัว จริงๆ แล้วถ้าถามว่าอะไรคือสิ่งแรกที่ผู้ใช้สัมผัสได้เมื่อเปิดแอป คำตอบก็คือระบบนำทางนี่แหละ ไม่ว่าจะเป็นแอปขนาดเล็กที่มีแค่ 2-3 หน้าจอ หรือแอปขนาดใหญ่ที่มีหลายสิบหน้าจอ การจัดการเส้นทางและการนำทางที่ดีจะส่งผลโดยตรงต่อประสบการณ์ของผู้ใช้งาน
ในปี 2025-2026 ระบบนำทางของ React Native ได้ก้าวเข้าสู่ยุคใหม่อย่างเต็มตัว ด้วยการมาถึงของ Expo Router v4/v5 ที่ทำงานร่วมกับ React Navigation v7 ซึ่งนำเสนอแนวคิดการจัดเส้นทางแบบ File-Based Routing ที่ได้แรงบันดาลใจจากเฟรมเวิร์กเว็บอย่าง Next.js มาปรับใช้กับแอปมือถือได้อย่างลงตัว
สำหรับนักพัฒนาที่เคยอ่านบทความก่อนหน้าเรื่อง New Architecture ของ React Native จะพบว่า Expo Router ถูกออกแบบมาให้ทำงานบน New Architecture ได้อย่างสมบูรณ์แบบ ทำให้ประสิทธิภาพของระบบนำทางดียิ่งขึ้นไปอีก
ในบทความนี้ เราจะพาคุณเรียนรู้ Expo Router และ React Navigation v7 อย่างละเอียดทุกแง่มุม ตั้งแต่พื้นฐานของ File-Based Routing, การใช้งาน Typed Routes, การตั้งค่า Deep Linking ไปจนถึงการสร้างระบบ Authentication พร้อมตัวอย่างโค้ดจริงที่ใช้งานได้ทันที
ทำไมต้อง Expo Router?
ก่อนที่จะลงลึก เรามาทำความเข้าใจกันก่อนดีกว่าว่าทำไม Expo Router ถึงกลายเป็นตัวเลือกหลักสำหรับระบบนำทางในปี 2025-2026
ข้อจำกัดของ React Navigation แบบดั้งเดิม
แม้ว่า React Navigation จะเป็นไลบรารีนำทางที่ได้รับความนิยมสูงสุดสำหรับ React Native มายาวนาน แต่การกำหนดค่าแบบ Dynamic API ก็มีปัญหาหลายอย่างที่นักพัฒนาหลายคน (รวมถึงผมเอง) รู้สึกปวดหัวอยู่ไม่น้อย:
- TypeScript ยุ่งยาก: ต้องกำหนด type definitions สำหรับ navigation parameters ด้วยตัวเองซึ่งเป็นเรื่องน่าเบื่อมากและเกิดข้อผิดพลาดได้ง่าย
- Deep Linking ซับซ้อน: ต้องกำหนดค่า linking configuration แยกต่างหากและต้องดูแลให้ตรงกับโครงสร้าง navigator เสมอ
- Boilerplate มาก: ต้องเขียนโค้ดซ้ำๆ มากมายในการตั้งค่า navigator, screen และ options
- ไม่รองรับ Web อย่างเต็มที่: การทำให้แอปทำงานบนเว็บต้องใช้ความพยายามเพิ่มเติมมาก
จุดเด่นของ Expo Router
Expo Router แก้ปัญหาเหล่านี้ทั้งหมดด้วยแนวคิดที่เรียบง่ายแต่ทรงพลัง:
- File-Based Routing: โครงสร้างไฟล์คือเส้นทาง — ไม่ต้องกำหนดค่าเส้นทางแยกต่างหาก
- Universal Deep Linking: ทุกหน้าจอมี URL โดยอัตโนมัติ ไม่ต้องตั้งค่า linking configuration เลย
- Typed Routes: TypeScript ทำงานอัตโนมัติจากโครงสร้างไฟล์ ไม่ต้องกำหนด type definitions เอง
- Cross-Platform: ทำงานได้ทั้ง Android, iOS และ Web ด้วยโค้ดชุดเดียว
- สร้างบน React Navigation: Expo Router เป็น wrapper ของ React Navigation v7 จึงใช้ API ของ React Navigation ได้ทั้งหมด
การเริ่มต้นใช้งาน Expo Router
การติดตั้ง
หากคุณสร้างโปรเจกต์ Expo ใหม่ด้วย create-expo-app ข่าวดีคือ Expo Router จะถูกติดตั้งมาให้อัตโนมัติแล้ว:
npx create-expo-app@latest my-app
cd my-app
npx expo start
แต่ถ้าคุณต้องการเพิ่ม Expo Router ในโปรเจกต์ที่มีอยู่แล้ว ให้ติดตั้ง dependencies ดังนี้:
npx expo install expo-router react-native-safe-area-context react-native-screens expo-linking expo-constants expo-status-bar
จากนั้นตั้งค่า entry point ในไฟล์ package.json:
{
"main": "expo-router/entry"
}
และเพิ่ม scheme ในไฟล์ app.json สำหรับ Deep Linking:
{
"expo": {
"scheme": "myapp",
"web": {
"bundler": "metro"
}
}
}
โครงสร้างไดเรกทอรี app
หัวใจของ Expo Router คือไดเรกทอรี app ที่อยู่ในโฟลเดอร์รากของโปรเจกต์ ทุกไฟล์ที่อยู่ภายในไดเรกทอรีนี้จะกลายเป็นเส้นทางโดยอัตโนมัติ:
app/
├── _layout.tsx ← Root Layout (Stack Navigator)
├── index.tsx ← หน้าแรก (/)
├── about.tsx ← หน้า About (/about)
├── (tabs)/
│ ├── _layout.tsx ← Tab Navigator
│ ├── index.tsx ← Tab แรก — หน้าหลัก
│ ├── search.tsx ← Tab ค้นหา
│ └── profile.tsx ← Tab โปรไฟล์
├── products/
│ ├── index.tsx ← รายการสินค้า (/products)
│ └── [id].tsx ← รายละเอียดสินค้า (/products/123)
└── settings/
├── _layout.tsx ← Stack ย่อยสำหรับ Settings
├── index.tsx ← หน้าตั้งค่าหลัก
└── notifications.tsx ← ตั้งค่าการแจ้งเตือน
เห็นไหมครับ? โครงสร้างไฟล์ตรงนี้มันบอกโครงสร้างการนำทางของแอปทั้งหมดเลย ไม่ต้องไปเขียน configuration อะไรเพิ่มเติม สำหรับคนที่เคยทำงานกับ Next.js มาก่อนจะรู้สึกคุ้นเคยมาก
แนวคิดหลักของ File-Based Routing
Layout Routes (_layout.tsx)
ไฟล์ _layout.tsx เป็นไฟล์พิเศษที่กำหนดว่าหน้าจอต่างๆ ภายในไดเรกทอรีนั้นจะถูกจัดเรียงอย่างไร — จะเป็น Stack, Tabs หรือ Drawer ก็ได้ มาดูตัวอย่าง Root Layout กัน:
// app/_layout.tsx
import { Stack } from 'expo-router';
import { StatusBar } from 'expo-status-bar';
export default function RootLayout() {
return (
<>
<StatusBar style="auto" />
<Stack>
<Stack.Screen
name="(tabs)"
options={{ headerShown: false }}
/>
<Stack.Screen
name="products/[id]"
options={{ title: 'รายละเอียดสินค้า' }}
/>
<Stack.Screen
name="settings"
options={{
headerShown: false,
presentation: 'modal'
}}
/>
</Stack>
</>
);
}
Root Layout จะถูก render ก่อนหน้าจออื่นๆ ทั้งหมด จึงเป็นที่ที่เหมาะสมสำหรับการโหลด fonts, จัดการ splash screen หรือเพิ่ม context providers ต่างๆ
Route Groups — การจัดกลุ่มเส้นทาง
ไดเรกทอรีที่ชื่อขึ้นต้นด้วยวงเล็บ เช่น (tabs) หรือ (auth) เรียกว่า Route Groups ซึ่งมีคุณสมบัติพิเศษคือ ไม่ส่งผลต่อ URL ตัวอย่างเช่น ไฟล์ app/(tabs)/profile.tsx จะมี URL เป็น /profile ไม่ใช่ /tabs/profile
ตรงนี้เป็นจุดที่หลายคนอาจจะสับสนตอนแรก แต่พอเข้าใจแล้วจะพบว่ามันมีประโยชน์มากในการจัดโครงสร้างโค้ดโดยไม่กระทบกับ URL scheme ของแอป ตัวอย่างที่พบบ่อย:
app/
├── (auth)/ ← กลุ่มหน้าจอสำหรับ Authentication
│ ├── _layout.tsx
│ ├── login.tsx ← URL: /login
│ └── register.tsx ← URL: /register
├── (app)/ ← กลุ่มหน้าจอหลักของแอป
│ ├── _layout.tsx
│ ├── (tabs)/
│ │ ├── _layout.tsx
│ │ ├── index.tsx ← URL: /
│ │ └── profile.tsx ← URL: /profile
│ └── settings.tsx ← URL: /settings
└── _layout.tsx ← Root Layout
Dynamic Routes — เส้นทางแบบ Dynamic
Expo Router รองรับ Dynamic Routes ด้วยการใช้ชื่อไฟล์ที่อยู่ในวงเล็บเหลี่ยม เช่น [id].tsx ซึ่ง parameter จะถูกส่งผ่าน URL มาดูตัวอย่างกัน:
// app/products/[id].tsx
import { useLocalSearchParams } from 'expo-router';
import { View, Text, StyleSheet } from 'react-native';
export default function ProductDetail() {
// ดึง parameter จาก URL
const { id } = useLocalSearchParams<{ id: string }>();
return (
<View style={styles.container}>
<Text style={styles.title}>สินค้าหมายเลข: {id}</Text>
{/* แสดงรายละเอียดสินค้า */}
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
padding: 20,
},
title: {
fontSize: 24,
fontWeight: 'bold',
},
});
นอกจากนี้ยังรองรับ Catch-All Routes ด้วยไฟล์ [...slug].tsx ที่จะจับ URL ทุกรูปแบบที่ไม่ตรงกับเส้นทางอื่นๆ ซึ่งเหมาะสำหรับทำหน้า 404:
// app/[...missing].tsx — หน้า 404
import { Link, Stack } from 'expo-router';
import { View, Text, StyleSheet } from 'react-native';
export default function NotFoundScreen() {
return (
<>
<Stack.Screen options={{ title: 'ไม่พบหน้าที่ต้องการ' }} />
<View style={styles.container}>
<Text style={styles.title}>ไม่พบหน้าที่คุณกำลังค้นหา</Text>
<Link href="/" style={styles.link}>
<Text>กลับหน้าหลัก</Text>
</Link>
</View>
</>
);
}
Tab Navigation อย่างละเอียด
Tab Navigation เป็นรูปแบบการนำทางที่พบได้บ่อยที่สุดในแอปมือถือ แทบทุกแอปที่เราใช้ในชีวิตประจำวันก็มี Tab Bar อยู่ด้านล่าง มาดูการสร้าง Tab Navigator แบบสมบูรณ์ด้วย Expo Router กัน:
// app/(tabs)/_layout.tsx
import { Tabs } from 'expo-router';
import { Ionicons } from '@expo/vector-icons';
import { useColorScheme } from 'react-native';
export default function TabLayout() {
const colorScheme = useColorScheme();
const isDark = colorScheme === 'dark';
return (
<Tabs
screenOptions={{
tabBarActiveTintColor: isDark ? '#60a5fa' : '#2563eb',
tabBarInactiveTintColor: isDark ? '#9ca3af' : '#6b7280',
tabBarStyle: {
backgroundColor: isDark ? '#1f2937' : '#ffffff',
borderTopColor: isDark ? '#374151' : '#e5e7eb',
},
headerStyle: {
backgroundColor: isDark ? '#1f2937' : '#ffffff',
},
headerTintColor: isDark ? '#f9fafb' : '#111827',
}}
>
<Tabs.Screen
name="index"
options={{
title: 'หน้าหลัก',
tabBarIcon: ({ color, size }) => (
<Ionicons name="home" color={color} size={size} />
),
}}
/>
<Tabs.Screen
name="search"
options={{
title: 'ค้นหา',
tabBarIcon: ({ color, size }) => (
<Ionicons name="search" color={color} size={size} />
),
}}
/>
<Tabs.Screen
name="notifications"
options={{
title: 'แจ้งเตือน',
tabBarIcon: ({ color, size }) => (
<Ionicons name="notifications" color={color} size={size} />
),
tabBarBadge: 3,
}}
/>
<Tabs.Screen
name="profile"
options={{
title: 'โปรไฟล์',
tabBarIcon: ({ color, size }) => (
<Ionicons name="person" color={color} size={size} />
),
}}
/>
</Tabs>
);
}
Stack ซ้อนภายใน Tab
ในกรณีที่ Tab หนึ่งๆ มีหลายหน้าจอ เช่น Tab หน้าหลักที่มีทั้งรายการสินค้าและรายละเอียดสินค้า เราสามารถซ้อน Stack Navigator ภายใน Tab ได้แบบนี้:
// โครงสร้างไฟล์
app/(tabs)/
├── _layout.tsx
├── search.tsx
├── notifications.tsx
├── profile.tsx
└── home/
├── _layout.tsx ← Stack ภายใน Tab หน้าหลัก
├── index.tsx ← รายการสินค้า
└── [productId].tsx ← รายละเอียดสินค้า
// app/(tabs)/home/_layout.tsx
import { Stack } from 'expo-router';
export const unstable_settings = {
initialRouteName: 'index',
};
export default function HomeLayout() {
return (
<Stack>
<Stack.Screen
name="index"
options={{ title: 'หน้าหลัก' }}
/>
<Stack.Screen
name="[productId]"
options={{ title: 'รายละเอียด' }}
/>
</Stack>
);
}
การตั้งค่า unstable_settings กำหนดว่าเมื่อ Deep Link เข้ามาที่หน้ารายละเอียดสินค้าโดยตรง ผู้ใช้จะสามารถกดปุ่มย้อนกลับไปหน้ารายการสินค้าได้ ถือว่าเป็น UX ที่ดีมากเลย
Typed Routes: ระบบ Type Safety อัตโนมัติ
พูดตรงๆ ว่า Typed Routes เป็นหนึ่งในฟีเจอร์ที่ผมชอบที่สุดของ Expo Router เลย มันสร้าง TypeScript types โดยอัตโนมัติจากโครงสร้างไฟล์ในไดเรกทอรี app ไม่ต้องมานั่งเขียน type definitions ยาวเหยียดเหมือนแต่ก่อน
การเปิดใช้งาน Typed Routes
เพิ่มการตั้งค่าในไฟล์ app.json แค่นี้:
{
"expo": {
"experiments": {
"typedRoutes": true
}
}
}
เมื่อเปิดใช้งานแล้ว Expo Router จะสร้างไฟล์ type definitions อัตโนมัติ ทำให้คุณได้ type safety เต็มรูปแบบ:
import { Link, router } from 'expo-router';
// TypeScript จะตรวจสอบ href ให้อัตโนมัติ
// ✅ ถูกต้อง — เส้นทางเหล่านี้มีอยู่ในไดเรกทอรี app
<Link href="/">หน้าหลัก</Link>
<Link href="/profile">โปรไฟล์</Link>
<Link href="/products/123">สินค้า</Link>
// ❌ Error — เส้นทางนี้ไม่มีอยู่จริง
<Link href="/nonexistent">ไม่มีหน้านี้</Link>
// การนำทางแบบ Programmatic ก็ได้ type safety เช่นกัน
router.push('/products/456');
router.replace('/login');
// Dynamic routes ก็รองรับ
router.push({
pathname: '/products/[id]',
params: { id: '789' }
});
ข้อดีของ Typed Routes คือหากคุณพิมพ์ชื่อเส้นทางผิด TypeScript จะแจ้งเตือนทันที ช่วยลดข้อผิดพลาดตั้งแต่ตอน develop เลย ไม่ต้องรอไปเจอ bug ตอน runtime
การนำทาง (Navigation) แบบต่างๆ
การใช้ Link Component
วิธีที่ง่ายที่สุดในการนำทางคือใช้ <Link> component:
import { Link } from 'expo-router';
import { View, Text, Pressable, StyleSheet } from 'react-native';
export default function HomeScreen() {
return (
<View style={styles.container}>
{/* Link แบบข้อความ */}
<Link href="/about">
<Text style={styles.link}>เกี่ยวกับเรา</Text>
</Link>
{/* Link แบบปุ่ม — ใช้ asChild */}
<Link href="/products" asChild>
<Pressable style={styles.button}>
<Text style={styles.buttonText}>ดูสินค้าทั้งหมด</Text>
</Pressable>
</Link>
{/* Link พร้อม Dynamic params */}
<Link
href={{
pathname: '/products/[id]',
params: { id: 'featured-001' }
}}
>
<Text style={styles.link}>สินค้าแนะนำ</Text>
</Link>
</View>
);
}
การใช้ router API
สำหรับการนำทางแบบ Programmatic ให้ใช้ router object ซึ่งมี method หลายตัวให้เลือกใช้ตามสถานการณ์:
import { router } from 'expo-router';
// push — เพิ่มหน้าจอใหม่ลงใน stack
router.push('/products/123');
// replace — แทนที่หน้าจอปัจจุบัน (ไม่สามารถกดย้อนกลับได้)
router.replace('/home');
// back — ย้อนกลับไปหน้าก่อนหน้า
router.back();
// navigate — นำทางไปหน้าที่ระบุ
// ใน React Navigation v7, navigate ทำงานเหมือน push
router.navigate('/settings');
// canGoBack — ตรวจสอบว่าย้อนกลับได้หรือไม่
if (router.canGoBack()) {
router.back();
}
// dismiss — ปิด modal ปัจจุบัน
router.dismiss();
// dismissAll — ปิด modal ทั้งหมด
router.dismissAll();
สิ่งสำคัญที่ต้องรู้: ใน React Navigation v7 (ที่ Expo Router v4+ ใช้) พฤติกรรมของ navigate เปลี่ยนไปจาก v6 โดยจะทำงานเหมือน push คือเพิ่มหน้าจอใหม่ทุกครั้ง แทนที่จะกลับไปหน้าจอเดิมที่มีอยู่ใน stack ตรงนี้ถ้าใครย้ายมาจาก v6 อาจจะต้องระวังหน่อย
Deep Linking: ทุกหน้าจอมี URL
หนึ่งในข้อดีที่สำคัญที่สุดของ Expo Router คือ Universal Deep Linking — ทุกหน้าจอในแอปจะมี URL โดยอัตโนมัติ ไม่ต้องตั้งค่าอะไรเพิ่มเติมเลย ซึ่งถ้าเทียบกับการ config deep linking แบบเดิมที่ต้องนั่งเขียน linking configuration ยาวเหยียด จะเห็นว่าสะดวกขึ้นมาก
Custom URL Scheme
การตั้งค่า Custom URL Scheme ทำได้ง่ายๆ ในไฟล์ app.json:
{
"expo": {
"scheme": "myapp"
}
}
หลังจากตั้งค่าแล้ว แอปของคุณจะตอบสนองต่อ URL เช่น myapp://products/123 โดยอัตโนมัติ โดยจะนำทางไปยังหน้าจอที่ตรงกับเส้นทางนั้น แค่นี้จริงๆ ไม่ต้องทำอะไรเพิ่มเลย
Universal Links และ App Links
สำหรับแอปที่จะเผยแพร่จริง แนะนำให้ใช้ Universal Links (iOS) และ App Links (Android) แทน Custom URL Scheme เพราะมีข้อดีหลายอย่าง:
- ใช้ URL แบบ
https://มาตรฐานที่ทำงานได้แม้ยังไม่ได้ติดตั้งแอป - หากยังไม่ได้ติดตั้งแอป จะนำทางไปยังเว็บไซต์หรือ App Store แทน
- ปลอดภัยกว่า Custom URL Scheme เพราะมีการตรวจสอบสิทธิ์โดเมน
การตั้งค่า Universal Links ด้วย Expo:
{
"expo": {
"scheme": "myapp",
"ios": {
"associatedDomains": [
"applinks:myapp.example.com"
]
},
"android": {
"intentFilters": [
{
"action": "VIEW",
"autoVerify": true,
"data": [
{
"scheme": "https",
"host": "myapp.example.com",
"pathPrefix": "/"
}
],
"category": ["BROWSABLE", "DEFAULT"]
}
]
}
}
}
เมื่อตั้งค่าแล้ว URL เช่น https://myapp.example.com/products/123 จะเปิดแอปและนำทางไปหน้ารายละเอียดสินค้าโดยตรงเลย
การจัดการ Deep Link ที่เข้ามา
Expo Router จัดการ Deep Link ให้อัตโนมัติอยู่แล้ว แต่หากต้องการจัดการเพิ่มเติม เช่น tracking หรือ analytics สามารถใช้ hook ได้แบบนี้:
// app/_layout.tsx
import { useURL } from 'expo-linking';
import { useEffect } from 'react';
import { Stack } from 'expo-router';
export default function RootLayout() {
const url = useURL();
useEffect(() => {
if (url) {
// บันทึก Deep Link สำหรับ analytics
console.log('Deep link received:', url);
// trackDeepLink(url);
}
}, [url]);
return <Stack />;
}
ระบบ Authentication กับ Expo Router
การจัดการ Authentication เป็นหนึ่งในส่วนที่สำคัญที่สุดของแอปมือถือ (และพูดตามตรง ก็เป็นส่วนที่ยุ่งยากที่สุดด้วย) Expo Router มีวิธีจัดการ Authentication อยู่ 2 แบบหลักๆ
วิธีที่ 1: Protected Routes (แนะนำ — SDK 53+)
ตั้งแต่ Expo SDK 53 ขึ้นไป มีฟีเจอร์ Stack.Protected ที่ทำให้การจัดการ Authentication ง่ายและชัดเจนมาก มาสร้าง Auth Context กันก่อน:
// contexts/AuthContext.tsx
import React, { createContext, useContext, useState, useEffect } from 'react';
import * as SecureStore from 'expo-secure-store';
interface AuthContextType {
session: string | null;
isLoading: boolean;
signIn: (token: string) => Promise<void>;
signOut: () => Promise<void>;
}
const AuthContext = createContext<AuthContextType>({
session: null,
isLoading: true,
signIn: async () => {},
signOut: async () => {},
});
export function useSession() {
return useContext(AuthContext);
}
export function AuthProvider({ children }: { children: React.ReactNode }) {
const [session, setSession] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
// ตรวจสอบ token ที่เก็บไว้
SecureStore.getItemAsync('authToken').then((token) => {
setSession(token);
setIsLoading(false);
});
}, []);
const signIn = async (token: string) => {
await SecureStore.setItemAsync('authToken', token);
setSession(token);
};
const signOut = async () => {
await SecureStore.deleteItemAsync('authToken');
setSession(null);
};
return (
<AuthContext.Provider value={{ session, isLoading, signIn, signOut }}>
{children}
</AuthContext.Provider>
);
}
จากนั้นใช้ Stack.Protected ใน Root Layout:
// app/_layout.tsx
import { Stack } from 'expo-router';
import { AuthProvider, useSession } from '../contexts/AuthContext';
import { ActivityIndicator, View } from 'react-native';
function RootNavigator() {
const { session, isLoading } = useSession();
if (isLoading) {
return (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<ActivityIndicator size="large" />
</View>
);
}
return (
<Stack screenOptions={{ headerShown: false }}>
{/* หน้าจอที่ต้องล็อกอิน */}
<Stack.Protected guard={!!session}>
<Stack.Screen name="(app)" />
</Stack.Protected>
{/* หน้าจอสำหรับผู้ที่ยังไม่ล็อกอิน */}
<Stack.Protected guard={!session}>
<Stack.Screen name="(auth)" />
</Stack.Protected>
</Stack>
);
}
export default function RootLayout() {
return (
<AuthProvider>
<RootNavigator />
</AuthProvider>
);
}
Stack.Protected จะทำงานดังนี้:
- เมื่อ
guardเป็นtrue— หน้าจอภายในจะสามารถเข้าถึงได้ - เมื่อ
guardเปลี่ยนจากtrueเป็นfalse— ผู้ใช้จะถูก redirect ออกอัตโนมัติ - รองรับ Deep Linking — หาก Deep Link เข้ามาตรงๆ ที่หน้าที่ต้องล็อกอิน ผู้ใช้จะถูกส่งไปหน้าล็อกอินก่อน
- ทำงานได้กับ Tabs และ Drawer เช่นกัน ไม่ใช่แค่ Stack
วิธีที่ 2: Redirect-Based Authentication (สำหรับ SDK รุ่นเก่า)
สำหรับโปรเจกต์ที่ยังใช้ SDK เวอร์ชันเก่ากว่า 53 สามารถใช้ <Redirect /> component ใน layout ได้:
// app/(app)/_layout.tsx
import { Redirect, Stack } from 'expo-router';
import { useSession } from '../../contexts/AuthContext';
import { ActivityIndicator, View } from 'react-native';
export default function AppLayout() {
const { session, isLoading } = useSession();
if (isLoading) {
return (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<ActivityIndicator size="large" />
</View>
);
}
// ถ้ายังไม่ล็อกอิน ให้ redirect ไปหน้า login
if (!session) {
return <Redirect href="/login" />;
}
return <Stack />;
}
วิธีนี้ใช้งานง่ายเหมือนกัน แต่ Stack.Protected จะจัดการ edge cases ต่างๆ ได้ดีกว่า เช่น การลบ navigation history ออกเมื่อล็อกเอาต์ ถ้าใช้ SDK 53+ ได้ก็แนะนำให้ใช้ Protected Routes เลยครับ
Modal Navigation
Modal เป็นรูปแบบการนำทางที่แสดงเนื้อหาซ้อนทับบนหน้าจอปัจจุบัน มักใช้สำหรับ form, dialog หรือเนื้อหาที่ต้องการให้ผู้ใช้ทำอะไรบางอย่างก่อนจะกลับไปหน้าเดิม:
// app/_layout.tsx
import { Stack } from 'expo-router';
export default function RootLayout() {
return (
<Stack>
<Stack.Screen
name="(tabs)"
options={{ headerShown: false }}
/>
{/* กำหนดให้แสดงเป็น Modal */}
<Stack.Screen
name="modal"
options={{
presentation: 'modal',
title: 'ตัวกรองสินค้า',
headerRight: () => (
<Pressable onPress={() => router.dismiss()}>
<Text>ปิด</Text>
</Pressable>
),
}}
/>
<Stack.Screen
name="create-post"
options={{
presentation: 'fullScreenModal',
title: 'สร้างโพสต์ใหม่',
headerShown: true,
}}
/>
</Stack>
);
}
ข้อควรรู้ใน React Navigation v7: หน้าจอที่ถูก push ทับบน modal จะแสดงเป็น modal โดยอัตโนมัติด้วย เพื่อป้องกัน animation ที่ไม่สมูท หากต้องการเปลี่ยนพฤติกรรมนี้ให้ตั้งค่า presentation: 'card'
React Navigation v7 Static API
แม้ว่า Expo Router จะเป็นทางเลือกหลักสำหรับโปรเจกต์ Expo แต่หากคุณใช้ React Native โดยไม่ใช้ Expo หรือต้องการควบคุมรายละเอียดมากกว่า React Navigation v7 Static API ก็เป็นตัวเลือกที่ดีเยี่ยม:
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { createStaticNavigation } from '@react-navigation/native';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
// สร้าง Tab Navigator
const HomeTabs = createBottomTabNavigator({
screens: {
Home: {
screen: HomeScreen,
options: {
title: 'หน้าหลัก',
tabBarIcon: ({ color }) => (
<Icon name="home" color={color} />
),
},
},
Profile: {
screen: ProfileScreen,
options: {
title: 'โปรไฟล์',
tabBarIcon: ({ color }) => (
<Icon name="person" color={color} />
),
},
},
},
});
// สร้าง Root Stack
const RootStack = createNativeStackNavigator({
screens: {
MainTabs: {
screen: HomeTabs,
options: { headerShown: false },
},
ProductDetail: {
screen: ProductDetailScreen,
options: { title: 'รายละเอียดสินค้า' },
linking: { path: 'products/:id' },
},
},
groups: {
Modal: {
screenOptions: { presentation: 'modal' },
screens: {
Settings: {
screen: SettingsScreen,
options: { title: 'ตั้งค่า' },
},
},
},
},
});
// สร้าง Navigation component
const Navigation = createStaticNavigation(RootStack);
// ใช้ใน App component
export default function App() {
return <Navigation />;
}
// TypeScript — สร้าง type อัตโนมัติ
type RootStackParamList = StaticParamList<typeof RootStack>;
declare global {
namespace ReactNavigation {
interface RootParamList extends RootStackParamList {}
}
}
ข้อดีของ Static API คือ TypeScript types ถูกสร้างอัตโนมัติจากการกำหนดค่า navigator ทำให้ useNavigation มี type safety เต็มรูปแบบโดยไม่ต้องกำหนด type definitions เอง คล้ายๆ กับ Typed Routes ของ Expo Router แต่ทำงานในระดับ React Navigation โดยตรง
เทคนิคขั้นสูง
การซ่อน Tab Bar ในบางหน้าจอ
ปัญหาที่พบบ่อยมากคือต้องการซ่อน Tab Bar เมื่ออยู่ในหน้าจอบางหน้า เช่น หน้ารายละเอียดสินค้า หลายคนพยายามตั้ง tabBarStyle: { display: 'none' } ซึ่งก็ใช้ได้แต่ไม่ใช่วิธีที่ดีที่สุด วิธีที่แนะนำคือ สลับลำดับชั้นของ Navigator:
// แนวทางที่แนะนำ:
// วาง Tab Navigator ภายใน Stack Navigator
// ไม่ใช่ Stack ภายใน Tab
app/
├── _layout.tsx ← Root Stack
├── (tabs)/
│ ├── _layout.tsx ← Tab Navigator
│ ├── index.tsx
│ └── profile.tsx
├── products/
│ └── [id].tsx ← หน้านี้จะไม่มี Tab Bar
└── checkout/
└── index.tsx ← หน้านี้ก็จะไม่มี Tab Bar
// app/_layout.tsx
import { Stack } from 'expo-router';
export default function RootLayout() {
return (
<Stack>
<Stack.Screen
name="(tabs)"
options={{ headerShown: false }}
/>
{/* หน้าจอเหล่านี้จะไม่มี Tab Bar */}
<Stack.Screen name="products/[id]" />
<Stack.Screen name="checkout" />
</Stack>
);
}
ด้วยวิธีนี้ หน้าจอที่อยู่นอก (tabs) จะไม่มี Tab Bar โดยธรรมชาติ ไม่ต้องเขียนโค้ดซ่อน/แสดงเพิ่มเติมเลย เรียบง่ายและสะอาดกว่ามาก
Error Boundaries
Expo Router รองรับ Error Boundaries ในระดับ route ทำให้จัดการข้อผิดพลาดได้อย่างละเอียด แต่ละเส้นทางสามารถมี Error Boundary ของตัวเอง:
// app/products/[id].tsx
import { ErrorBoundary } from 'expo-router';
import { View, Text, Pressable, StyleSheet } from 'react-native';
// Error Boundary สำหรับเส้นทางนี้โดยเฉพาะ
export function ErrorBoundary({ error, retry }: {
error: Error;
retry: () => void
}) {
return (
<View style={styles.errorContainer}>
<Text style={styles.errorTitle}>เกิดข้อผิดพลาด</Text>
<Text style={styles.errorMessage}>{error.message}</Text>
<Pressable style={styles.retryButton} onPress={retry}>
<Text style={styles.retryText}>ลองใหม่อีกครั้ง</Text>
</Pressable>
</View>
);
}
export default function ProductDetail() {
// component ปกติ
}
Async Routes (Lazy Loading)
สำหรับแอปขนาดใหญ่ Expo Router รองรับ Async Routes ที่ช่วยแบ่ง bundle ออกเป็นส่วนย่อยๆ ทำให้โหลดเฉพาะส่วนที่ต้องใช้จริง ไม่ต้องโหลดทั้งแอปตั้งแต่แรก:
// เปิดใช้งานใน app.json
{
"expo": {
"experiments": {
"typedRoutes": true
},
"web": {
"bundler": "metro",
"output": "single"
}
}
}
// metro.config.js — เปิดใช้ async routes ใน development
const { getDefaultConfig } = require('expo/metro-config');
const config = getDefaultConfig(__dirname);
config.transformer = {
...config.transformer,
asyncRequireModulePath: require.resolve(
'expo-router/async-require'
),
};
module.exports = config;
Async Routes ช่วยให้ระหว่างการพัฒนา หากเกิดข้อผิดพลาดในเส้นทางใดเส้นทางหนึ่ง จะไม่กระทบกับเส้นทางอื่นๆ ทำให้ debug ง่ายขึ้นและ upgrade dependencies ได้สะดวกขึ้นด้วย
แนวทางปฏิบัติที่ดีที่สุด (Best Practices)
1. จัดโครงสร้างโปรเจกต์ให้เหมาะสม
my-app/
├── app/ ← เฉพาะไฟล์เส้นทางเท่านั้น
│ ├── _layout.tsx
│ ├── index.tsx
│ └── (tabs)/
├── components/ ← UI components ที่ใช้ซ้ำ
│ ├── Button.tsx
│ └── Card.tsx
├── hooks/ ← Custom hooks
│ ├── useAuth.ts
│ └── useProducts.ts
├── contexts/ ← React contexts
│ └── AuthContext.tsx
├── services/ ← API calls
│ └── api.ts
├── constants/ ← ค่าคงที่
│ └── Colors.ts
└── utils/ ← Utility functions
└── helpers.ts
จุดสำคัญคือไดเรกทอรี app ควรมีเฉพาะไฟล์ที่เกี่ยวกับเส้นทางเท่านั้น อย่าเอา business logic หรือ components มาวางไว้ในนี้ แยก components, hooks, contexts และอื่นๆ ไว้ในไดเรกทอรีของตัวเอง
2. ใช้ Route Groups อย่างมีกลยุทธ์
Expo Router ประมวลผลโฟลเดอร์ตามลำดับตัวอักษร หากต้องการให้ tabs โหลดก่อน shared screens ให้ตั้งชื่อ shared group ให้เรียงอยู่หลัง tabs เช่นใช้ (zShared) เป็นเทคนิคเล็กๆ แต่ช่วยได้เยอะ
3. จัดการ Loading States อย่างถูกต้อง
// app/_layout.tsx
import { Stack, SplashScreen } from 'expo-router';
import { useFonts } from 'expo-font';
import { useEffect } from 'react';
// ป้องกันไม่ให้ splash screen หายก่อนที่จะโหลดเสร็จ
SplashScreen.preventAutoHideAsync();
export default function RootLayout() {
const [fontsLoaded] = useFonts({
'Sarabun-Regular': require('../assets/fonts/Sarabun-Regular.ttf'),
'Sarabun-Bold': require('../assets/fonts/Sarabun-Bold.ttf'),
});
useEffect(() => {
if (fontsLoaded) {
SplashScreen.hideAsync();
}
}, [fontsLoaded]);
if (!fontsLoaded) return null;
return <Stack />;
}
4. ใช้ Navigation Events สำหรับ Analytics
import { usePathname, useSegments } from 'expo-router';
import { useEffect } from 'react';
export function useAnalytics() {
const pathname = usePathname();
const segments = useSegments();
useEffect(() => {
// ส่งข้อมูลการเปลี่ยนหน้าจอไปยัง analytics
console.log('Screen view:', pathname);
// analytics.trackScreenView(pathname, segments);
}, [pathname, segments]);
}
Expo Router กับ React Navigation: เลือกใช้อะไรดี?
คำถามนี้เจอบ่อยมากเลย — ควรเลือกใช้ Expo Router หรือ React Navigation โดยตรงดี? คำตอบสั้นๆ คือขึ้นอยู่กับโปรเจกต์ของคุณ:
- ใช้ Expo Router เมื่อ: สร้างโปรเจกต์ใหม่ด้วย Expo, ต้องการ Deep Linking อัตโนมัติ, ต้องการ Typed Routes โดยไม่ต้อง config เอง, ต้องการให้แอปทำงานบน Web ด้วย หรือต้องการ SEO บน Web
- ใช้ React Navigation v7 โดยตรงเมื่อ: ใช้ React Native CLI โดยไม่ใช้ Expo, ต้องการควบคุมรายละเอียดของ navigation มากที่สุด, มีรูปแบบ navigation ที่ซับซ้อนเป็นพิเศษ หรือต้องการใช้ custom navigator
แต่ข้อสำคัญที่อยากให้จำไว้คือ Expo Router เป็น wrapper ของ React Navigation ดังนั้นคุณสามารถใช้ API ของ React Navigation ทั้งหมดภายใน Expo Router ได้เลย ไม่ได้เป็นการเลือกอย่างใดอย่างหนึ่งแบบขาวดำ
สรุป
ระบบนำทางของ React Native ในปี 2025-2026 ได้พัฒนาก้าวกระโดดไปอย่างมาก Expo Router v4/v5 ร่วมกับ React Navigation v7 นำเสนอประสบการณ์การพัฒนาที่ดีที่สุดเท่าที่เคยมีมาสำหรับ React Native:
- File-Based Routing ทำให้โครงสร้างไฟล์คือเส้นทางนำทาง ลดโค้ดที่ต้องเขียนและดูแลอย่างมาก
- Typed Routes ให้ type safety อัตโนมัติโดยไม่ต้องกำหนดค่าเอง
- Universal Deep Linking ทุกหน้าจอมี URL โดยอัตโนมัติ
- Protected Routes ทำให้ระบบ Authentication ง่ายและปลอดภัยขึ้น
- Cross-Platform ทำงานได้ทั้ง Android, iOS และ Web
สำหรับนักพัฒนาที่กำลังเริ่มโปรเจกต์ใหม่ Expo Router เป็นทางเลือกที่แนะนำอย่างยิ่ง สำหรับโปรเจกต์ที่มีอยู่แล้ว การอัปเกรดเป็น React Navigation v7 พร้อม Static API จะช่วยให้ประสบการณ์การพัฒนาดีขึ้นอย่างเห็นได้ชัด และเมื่อพร้อมก็สามารถย้ายมาใช้ Expo Router ได้ไม่ยาก เพราะท้ายที่สุดแล้ว Expo Router ก็สร้างอยู่บน React Navigation นั่นเอง