Expo Router สำหรับ React Native: คู่มือระบบ Navigation แบบ File-Based ฉบับสมบูรณ์

คู่มือฉบับสมบูรณ์สำหรับ Expo Router ใน React Native ครอบคลุมระบบ routing แบบ file-based, การตั้งค่า layout, tab navigation, dynamic routes, modal screens, typed routes, middleware และการป้องกันเส้นทาง พร้อมตัวอย่างโค้ดจริง

Expo Router file-based navigation system for React Native mobile applications

Expo Router เปลี่ยนแปลงวิธีจัดการระบบ navigation ในแอปพลิเคชัน React Native อย่างสิ้นเชิง ด้วยการนำแนวคิด file-based routing ที่คุ้นเคยจาก Next.js มาปรับใช้กับโลกของ mobile development ตั้งแต่ SDK Expo 55 และ Expo Router v6 เป็นต้นมา นักพัฒนาไม่จำเป็นต้องตั้งค่า navigator ด้วยตนเองอีกต่อไป เพียงสร้างไฟล์ในไดเรกทอรี app หน้าจอนั้นก็พร้อมใช้งานทันทีทั้งบน Android, iOS และเว็บ แนวทางนี้ลดความซับซ้อนของโปรเจกต์ข้ามแพลตฟอร์มลงอย่างมาก และเร่งกระบวนการพัฒนาให้รวดเร็วยิ่งขึ้น

เริ่มต้นอย่างรวดเร็ว

โปรเจกต์ Expo ใหม่ทุกโปรเจกต์มี Expo Router ติดตั้งมาให้พร้อมใช้งาน คำสั่ง npx create-expo-app@latest --template default@sdk-55 จะสร้างโปรเจกต์ที่ตั้งค่า file-based routing ไว้เรียบร้อยแล้ว สำหรับโปรเจกต์ที่มีอยู่เดิม เพียงติดตั้งแพ็กเกจ expo-router และอัปเดต entry point ของแอปพลิเคชัน

หลักการทำงานของ File-Based Routing ใน Expo Router

ไฟล์ทุกไฟล์ที่สร้างขึ้นภายในไดเรกทอรี app จะกลายเป็น route โดยอัตโนมัติ เส้นทางของไฟล์จะสอดคล้องกับเส้นทาง URL โดยตรง ทำให้ไม่จำเป็นต้องมีไฟล์ configuration แบบรวมศูนย์อีกต่อไป ตัวอย่างเช่น ไฟล์ app/settings.tsx จะสร้าง route /settings ในขณะที่ app/profile/edit.tsx จะตรงกับ /profile/edit

แนวทางนี้มีข้อได้เปรียบหลักสามประการเมื่อเทียบกับการตั้งค่า React Navigation แบบดั้งเดิม:

  • ไม่ต้องตั้งค่าใดๆ -- route ถูกสร้างขึ้นทันทีที่สร้างไฟล์
  • Deep linking อัตโนมัติ -- ทุกหน้าจอมี URL เป็นของตัวเอง ทำให้การแชร์ลิงก์และการทดสอบสะดวกยิ่งขึ้น
  • Navigation ที่มี type safety -- TypeScript รู้จัก route ทั้งหมดที่มีอยู่ตั้งแต่ขั้นตอน compilation
app/index.tsxtypescript
import { View, Text, StyleSheet } from 'react-native'
import { Link } from 'expo-router'

export default function HomeScreen() {
  return (
    <View style={styles.container}>
      <Text style={styles.title}>Welcome</Text>
      {/* Link maps directly to file path */}
      <Link href="/settings" style={styles.link}>
        Open Settings
      </Link>
      <Link href="/profile/edit" style={styles.link}>
        Edit Profile
      </Link>
    </View>
  )
}

const styles = StyleSheet.create({
  container: { flex: 1, justifyContent: 'center', padding: 24 },
  title: { fontSize: 28, fontWeight: 'bold', marginBottom: 16 },
  link: { fontSize: 16, color: '#61DAFB', marginTop: 12 },
})

คอมโพเนนต์ Link จัดการ navigation บนทุกแพลตฟอร์ม บนเว็บจะสร้าง anchor tag พร้อม attribute href ที่เหมาะสมสำหรับ SEO ส่วนบนแพลตฟอร์ม native จะทำงานเป็น stack navigation ตามปกติ

โครงสร้างโปรเจกต์และไฟล์ Layout

Expo Router ใช้ไฟล์ _layout.tsx สำหรับกำหนด navigation container แต่ละไดเรกทอรีสามารถมี layout เป็นของตัวเอง ทำให้เกิดโครงสร้าง navigation แบบซ้อนกันได้ Layout หลักที่อยู่ระดับ root จะครอบคลุมทั้งแอปพลิเคชัน ส่วน layout ที่ซ้อนอยู่ภายในจะควบคุมเฉพาะส่วนที่เกี่ยวข้อง

โครงสร้างโปรเจกต์ตัวอย่างมีลักษณะดังนี้:

text
app/
  _layout.tsx          # Root layout (Stack or custom)
  index.tsx            # Home screen (/)
  (tabs)/              # Tab group (parentheses = route group)
    _layout.tsx        # Tab navigator
    home.tsx           # /home tab
    search.tsx         # /search tab
    profile.tsx        # /profile tab
  settings/
    _layout.tsx        # Settings stack layout
    index.tsx          # /settings
    notifications.tsx  # /settings/notifications
    privacy.tsx        # /settings/privacy

Route groups คือไดเรกทอรีที่ล้อมรอบด้วยวงเล็บ ซึ่งทำหน้าที่จัดระเบียบไฟล์โดยไม่ส่งผลกระทบต่อ URL ไดเรกทอรี (tabs) ในตัวอย่างด้านบนสร้าง tab navigator ขึ้นมา แต่ URL ยังคงเป็น /home, /search และ /profile ไม่ใช่ /tabs/home

app/_layout.tsxtypescript
import { Stack } from 'expo-router'

export default function RootLayout() {
  return (
    <Stack
      screenOptions={{
        headerStyle: { backgroundColor: '#1a1a2e' },
        headerTintColor: '#ffffff',
        headerTitleStyle: { fontWeight: '600' },
      }}
    >
      <Stack.Screen name="index" options={{ title: 'Home' }} />
      <Stack.Screen name="(tabs)" options={{ headerShown: false }} />
      <Stack.Screen name="settings" options={{ title: 'Settings' }} />
    </Stack>
  )
}

Layout หลักยังทำหน้าที่เป็นจุดเริ่มต้นสำหรับการโหลดฟอนต์ การตั้งค่า provider ต่างๆ และการกำหนดค่าระดับ global อีกด้วย โดยมาแทนที่ไฟล์ App.tsx แบบดั้งเดิมในฐานะ entry point ของแอปพลิเคชัน

การสร้าง Tab Navigation

Tab navigation ต้องมีไฟล์ _layout.tsx อยู่ภายใน route group Expo Router v6 มีคอมโพเนนต์ NativeTabs สำหรับประสบการณ์ native เฉพาะแพลตฟอร์ม แต่คอมโพเนนต์ Tabs มาตรฐานก็ครอบคลุมกรณีการใช้งานส่วนใหญ่ได้เป็นอย่างดี

app/(tabs)/_layout.tsxtypescript
import { Tabs } from 'expo-router'
import { Ionicons } from '@expo/vector-icons'

export default function TabLayout() {
  return (
    <Tabs
      screenOptions={{
        tabBarActiveTintColor: '#61DAFB',
        tabBarInactiveTintColor: '#888',
        tabBarStyle: {
          backgroundColor: '#1a1a2e',
          borderTopColor: '#2d2d44',
        },
      }}
    >
      <Tabs.Screen
        name="home"
        options={{
          title: 'Home',
          tabBarIcon: ({ color, size }) => (
            <Ionicons name="home" size={size} color={color} />
          ),
        }}
      />
      <Tabs.Screen
        name="search"
        options={{
          title: 'Search',
          tabBarIcon: ({ color, size }) => (
            <Ionicons name="search" size={size} color={color} />
          ),
        }}
      />
      <Tabs.Screen
        name="profile"
        options={{
          title: 'Profile',
          tabBarIcon: ({ color, size }) => (
            <Ionicons name="person" size={size} color={color} />
          ),
        }}
      />
    </Tabs>
  )
}

ไฟล์หน้าจอของแต่ละ tab จะ export คอมโพเนนต์ React มาตรฐาน ไอคอน, ข้อความ และ badge ของ tab bar สามารถกำหนดค่าได้ผ่าน prop options ใน layout

Dynamic Routes และ Route Parameters

Segment แบบ dynamic ใช้วงเล็บเหลี่ยมในชื่อไฟล์ ไฟล์ที่ตั้งชื่อว่า [id].tsx จะจับคู่กับ segment เดียว ในขณะที่ [...slug].tsx จะจับคู่กับ segment ที่เหลือทั้งหมดในเส้นทาง

app/product/[id].tsxtypescript
import { View, Text, StyleSheet } from 'react-native'
import { useLocalSearchParams, Stack } from 'expo-router'

export default function ProductScreen() {
  // Extract the dynamic parameter from the URL
  const { id } = useLocalSearchParams<{ id: string }>()

  return (
    <View style={styles.container}>
      <Stack.Screen options={{ title: `Product ${id}` }} />
      <Text style={styles.heading}>Product Details</Text>
      <Text style={styles.id}>ID: {id}</Text>
    </View>
  )
}

const styles = StyleSheet.create({
  container: { flex: 1, padding: 24 },
  heading: { fontSize: 24, fontWeight: 'bold', marginBottom: 8 },
  id: { fontSize: 16, color: '#888' },
})

เมื่อเข้าถึง /product/42 หน้าจอนี้จะแสดงขึ้นพร้อมกับตัวแปร id ที่มีค่าเป็น "42" hook useLocalSearchParams ให้การเข้าถึง route parameters ทั้งหมดพร้อม type safety

สำหรับ catch-all routes นั้น [...slug].tsx จะจับคู่กับ path segments ทั้งหมด:

app/docs/[...slug].tsxtypescript
import { useLocalSearchParams } from 'expo-router'

export default function DocsScreen() {
  // /docs/getting-started/installation → slug = ['getting-started', 'installation']
  const { slug } = useLocalSearchParams<{ slug: string[] }>()

  return <DocViewer path={slug.join('/')} />
}

พร้อมที่จะพิชิตการสัมภาษณ์ React Native แล้วหรือยังครับ?

ฝึกฝนด้วยตัวจำลองแบบโต้ตอบ, flashcards และแบบทดสอบเทคนิคครับ

Typed Routes สำหรับความปลอดภัยในขั้นตอน Compilation

Expo Router สร้าง route types ขึ้นโดยอัตโนมัติเมื่อเปิดใช้ฟีเจอร์ typed routes การตรวจสอบประเภทนี้ช่วยตรวจจับลิงก์ที่เสียตั้งแต่ขั้นตอน compilation แทนที่จะปล่อยให้เกิดข้อผิดพลาดตอน runtime

เปิดใช้งานในไฟล์ app.json:

json
{
  "expo": {
    "experiments": {
      "typedRoutes": true
    }
  }
}

เมื่อเปิดใช้แล้ว prop href ของคอมโพเนนต์ Link และ argument ของ router.push() จะรับเฉพาะ string ที่ตรงกับ route ที่มีอยู่จริงเท่านั้น:

app/checkout.tsxtypescript
import { router } from 'expo-router'

function handleCheckout(cartId: string) {
  // TypeScript validates this route exists
  router.push(`/product/${cartId}`)

  // This would cause a compile error if /nonexistent doesn't exist
  // router.push('/nonexistent')
}

Typed routes ทำงานได้ดีเป็นพิเศษเมื่อใช้ร่วมกับ useLocalSearchParams เนื่องจาก types ที่สร้างขึ้นจะรับประกันว่าชื่อ parameter ในการกำหนด route ตรงกับชื่อที่ใช้ในคอมโพเนนต์ที่เรียกใช้ ช่วยป้องกัน bug ที่ยากต่อการตรวจจับซึ่งจะปรากฏก็ต่อเมื่อนำทางไปยังหน้าจอนั้นๆ เท่านั้น

Modal ใน Expo Router คือหน้าจอปกติที่กำหนดค่า presentation: 'modal' ไว้ใน layout แนวทางนี้ยังคงยึดตามหลักการของ file-based routing กล่าวคือ modal ก็เป็นเพียง route หนึ่งเช่นเดียวกับ route อื่นๆ

app/_layout.tsxtypescript
import { Stack } from 'expo-router'

export default function RootLayout() {
  return (
    <Stack>
      <Stack.Screen name="index" />
      <Stack.Screen name="(tabs)" options={{ headerShown: false }} />
      {/* Modal screen slides up from the bottom */}
      <Stack.Screen
        name="create-post"
        options={{
          presentation: 'modal',
          headerTitle: 'New Post',
        }}
      />
    </Stack>
  )
}
app/create-post.tsxtypescript
import { View, TextInput, Button, StyleSheet } from 'react-native'
import { router } from 'expo-router'
import { useState } from 'react'

export default function CreatePostModal() {
  const [title, setTitle] = useState('')

  const handleSubmit = () => {
    // Submit logic here
    router.back() // Dismiss the modal
  }

  return (
    <View style={styles.container}>
      <TextInput
        style={styles.input}
        placeholder="Post title"
        value={title}
        onChangeText={setTitle}
      />
      <Button title="Publish" onPress={handleSubmit} />
    </View>
  )
}

const styles = StyleSheet.create({
  container: { flex: 1, padding: 24 },
  input: {
    borderWidth: 1,
    borderColor: '#333',
    borderRadius: 8,
    padding: 12,
    fontSize: 16,
    marginBottom: 16,
  },
})

เมื่อเข้าถึง /create-post หน้าจอจะแสดงในรูปแบบ modal เลื่อนขึ้นจากด้านล่าง การเรียก router.back() จะปิด modal และนำผู้ใช้กลับไปยังหน้าจอก่อนหน้าใน navigation stack

การนำทางแบบ Programmatic และ Router API

นอกเหนือจากคอมโพเนนต์ Link แล้ว Expo Router ยังมี imperative API ผ่านออบเจ็กต์ router ซึ่งช่วยจัดการ navigation ที่ถูกเรียกจาก business logic แทนการกดปุ่มของผู้ใช้โดยตรง

typescript
import { router } from 'expo-router'

// Push a new screen onto the stack
router.push('/profile/settings')

// Replace the current screen (no back button)
router.replace('/login')

// Go back to the previous screen
router.back()

// Navigate with parameters
router.push({
  pathname: '/product/[id]',
  params: { id: '42', source: 'recommendations' },
})

// Check if going back is possible
import { useRouter } from 'expo-router'

function BackButton() {
  const router = useRouter()
  return router.canGoBack() ? (
    <Button title="Back" onPress={() => router.back()} />
  ) : null
}

ความแตกต่างระหว่าง router.push กับ router.replace มีความสำคัญอย่างยิ่งในกระบวนการ authentication หลังจากเข้าสู่ระบบสำเร็จ การใช้ router.replace('/dashboard') จะป้องกันไม่ให้ผู้ใช้กลับไปยังหน้าจอเข้าสู่ระบบได้ด้วยปุ่มย้อนกลับ

การนำทางและ Redirect

router.replace() จะแทนที่รายการปัจจุบันใน navigation history สำหรับ authentication guard ที่ต้อง redirect ผู้ใช้ที่ยังไม่ได้เข้าสู่ระบบ ควรใช้ <Redirect href="/login" /> ในส่วน render ของคอมโพเนนต์แทน เนื่องจากจะทำงานในขั้นตอน render และรองรับ server-side rendering บนเว็บได้อย่างถูกต้อง

Middleware และการป้องกันเส้นทาง

Expo Router v6 เปิดตัว server middleware สำหรับ logic ที่ต้องทำงานก่อนจะถึงคอมโพเนนต์ของ route ไฟล์ +middleware.ts จะดักจับ request ก่อนที่จะถึง route component ทำให้สามารถตรวจสอบ authentication, redirect และจัดการ HTTP headers ได้

app/+middleware.tstypescript
import { type MiddlewareRequest } from 'expo-router/server'

export function middleware(request: MiddlewareRequest) {
  const { pathname } = request.nextUrl

  // Protect dashboard routes
  const protectedPaths = ['/dashboard', '/settings', '/profile']
  const isProtected = protectedPaths.some(p => pathname.startsWith(p))

  if (isProtected) {
    const token = request.cookies.get('session')
    if (!token) {
      return Response.redirect(new URL('/login', request.url))
    }
  }

  return undefined // Continue to route
}

บนแพลตฟอร์ม native นั้น server middleware จะไม่ทำงาน การป้องกัน route จึงต้องอาศัย guard ฝั่ง client แทน รูปแบบที่นิยมมากที่สุดคือการครอบ layout ที่ต้องการป้องกันด้วยการตรวจสอบ authentication:

app/(authenticated)/_layout.tsxtypescript
import { Redirect, Stack } from 'expo-router'
import { useAuth } from '@/hooks/useAuth'

export default function AuthenticatedLayout() {
  const { isLoggedIn, isLoading } = useAuth()

  if (isLoading) return null
  if (!isLoggedIn) return <Redirect href="/login" />

  return <Stack />
}
ความแตกต่างระหว่างแพลตฟอร์ม

Server middleware ทำงานเฉพาะบนเว็บที่มีการใช้ server-side rendering เท่านั้น สำหรับแอปพลิเคชัน native จำเป็นต้องสร้าง guard ฝั่ง client ไว้ใน layout component การใช้ทั้งสองแนวทางร่วมกันจะรับประกันการป้องกันที่ครอบคลุมบนทุกแพลตฟอร์ม

สรุป

Expo Router v6 นำแนวทาง navigation ที่สอดคล้องกับมาตรฐานของเว็บเฟรมเวิร์กสมัยใหม่มาสู่ระบบนิเวศของ React Native สิ่งสำคัญที่ควรจดจำมีดังนี้:

  • Expo Router v6 แทนที่การตั้งค่า navigation ด้วยตนเองด้วยหลักการ file-based convention โดยทุกไฟล์ในไดเรกทอรี app จะกลายเป็น route โดยอัตโนมัติ
  • Layout ที่กำหนดผ่านไฟล์ _layout.tsx สร้างโครงสร้าง navigation แบบลำดับชั้น ไม่ว่าจะเป็น stack, tab หรือ drawer โดยไม่ต้องมีไฟล์ configuration แบบรวมศูนย์
  • Dynamic routes ด้วย [param].tsx และ catch-all routes ด้วย [...slug].tsx จัดการ navigation ที่มี parameter พร้อม TypeScript support อย่างเต็มรูปแบบ
  • Typed routes ตรวจจับลิงก์ navigation ที่เสียตั้งแต่ขั้นตอน compilation เมื่อเปิดใช้ฟีเจอร์นี้ใน app.json
  • Modal screens, programmatic navigation ผ่าน router.push/replace/back และ server middleware เป็นเครื่องมือครบชุดสำหรับระบบ routing
  • Route groups ที่ใช้ไดเรกทอรีล้อมด้วยวงเล็บช่วยจัดระเบียบโค้ดโดยไม่กระทบต่อ URL ทำให้โครงสร้างโปรเจกต์ยังคงอ่านง่ายแม้แอปพลิเคชันจะเติบโตขึ้น
  • Guard ฝั่ง client ใน layout จัดการ authentication บน native ในขณะที่ server middleware ครอบคลุมฝั่งเว็บ การใช้ทั้งสองแนวทางร่วมกันให้การป้องกันที่ครบถ้วนบนทุกแพลตฟอร์ม

เริ่มฝึกซ้อมเลย!

ทดสอบความรู้ของคุณด้วยตัวจำลองสัมภาษณ์และแบบทดสอบเทคนิคครับ

แท็ก

#react-native
#expo
#expo-router
#navigation
#mobile-development
#tutorial

แชร์

บทความที่เกี่ยวข้อง