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

คู่มือฉบับสมบูรณ์สำหรับ Expo Router ใน React Native ครอบคลุมระบบ routing แบบ file-based, layout, dynamic routes, typed navigation, และรูปแบบขั้นสูงอย่าง modal และ tab

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

Expo Router นำระบบ routing แบบ file-based มาสู่ React Native แทนที่การตั้งค่า navigation แบบ manual ด้วยแนวทางที่อิงตามข้อตกลง (convention-driven) ซึ่งได้รับแรงบันดาลใจจาก Next.js ตั้งแต่ Expo SDK 57 และ Expo Router การสร้าง navigation ข้ามแพลตฟอร์มสำหรับ Android, iOS และเว็บเพียงแค่สร้างไฟล์ในไดเรกทอรีที่ถูกต้อง

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

โปรเจกต์ Expo ใหม่ทุกโปรเจกต์มี Expo Router ติดตั้งมาให้พร้อมใช้งาน คำสั่ง npx create-expo-app@latest --template default จะสร้างโปรเจกต์ที่ตั้งค่า 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 อีกด้วย

การสร้าง Tab Navigation ด้วย Expo Router

Tab navigation ต้องมีไฟล์ _layout.tsx อยู่ภายใน route group Expo Router มีคอมโพเนนต์ NativeTabs สำหรับประสบการณ์ native เฉพาะแพลตฟอร์ม และคอมโพเนนต์ Tabs มาตรฐานสำหรับกรณีการใช้งานส่วนใหญ่ ตั้งแต่ SDK 56 Native Tabs รองรับ property disabled เพื่อป้องกันการเลือก tab ในขณะที่ยังคงแสดงผลอยู่

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 ของ React Native หลังจากเข้าสู่ระบบสำเร็จ การใช้ router.replace('/dashboard') จะป้องกันไม่ให้ผู้ใช้กลับไปยังหน้าจอเข้าสู่ระบบได้ด้วยปุ่มย้อนกลับ

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

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

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

Expo Router มี 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 การใช้ทั้งสองแนวทางร่วมกันจะรับประกันการป้องกันที่ครอบคลุมบนทุกแพลตฟอร์ม

การ Migrate SDK 56: Fork React Navigation

ตั้งแต่ Expo SDK 56 Expo Router ไม่ได้พึ่งพา package @react-navigation/* อีกต่อไป หากโปรเจกต์มีการ import จาก React Navigation โดยตรง ต้องอัปเดต import เหล่านั้น:

Package เดิมตำแหน่งใหม่
@react-navigation/nativeexpo-router/react-navigation
@react-navigation/stackexpo-router/js-stack
@react-navigation/bottom-tabsexpo-router/js-tabs

รัน codemod เพื่ออัปเดต import โดยอัตโนมัติ:

bash
npx expo-codemod sdk-56-expo-router-react-navigation-replace src

Runtime API ยังคงเหมือนเดิม เปลี่ยนเพียง module specifier เท่านั้น Library จาก third-party ที่ยังคง import จาก @react-navigation/core จะทำงานได้โดยอัตโนมัติเนื่องจาก Expo CLI จะ rewrite import เหล่านั้นระหว่างการ bundling

แหล่งข้อมูลอ้างอิง

สิ่งที่ผู้สัมภาษณ์คาดหวังเกี่ยวกับ Expo Router

  • Expo Router map แต่ละไฟล์ในไดเรกทอรี app ไปยัง route โดยอัตโนมัติ ขจัดการตั้งค่า navigation แบบ manual
  • Layout ที่กำหนดผ่านไฟล์ _layout.tsx สร้างโครงสร้าง navigation สำหรับ stack, tab และ drawer
  • 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 ครอบคลุมฝั่งเว็บ
  • ตั้งแต่ SDK 56 import จาก @react-navigation/* ต้องอัปเดตเป็น expo-router/* ที่เทียบเท่า

สำหรับเนื้อหาเพิ่มเติมเกี่ยวกับสถาปัตยกรรม React Native และการเตรียมตัวสัมภาษณ์ ดูที่ module native modules

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

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

ชาเลนจ์ประจำวัน

คุณหาบั๊กใน React Native เจอไหม

โค้ดจริงหนึ่งชิ้น บั๊กที่ซ่อนอยู่หนึ่งจุด วันละหนึ่งครั้ง ลองได้โดยไม่ต้องมีบัญชี

Anthony Fillion-Maillet

เขียนโดย

Anthony Fillion-Maillet

ผู้ก่อตั้ง SharpSkill

เป็นนักพัฒนาฟูลสแตกมากว่า 10 ปี ดูแล SharpSkill และรับผิดชอบทุกสิ่งที่เผยแพร่ที่นี่

อัปเดตเมื่อ 6 ตุลาคม 2569

แท็ก

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

แชร์

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