Expo Router trong React Native: Hướng dẫn đầy đủ về định tuyến dựa trên file

Expo Router mang hệ thống định tuyến dựa trên file đến React Native, lấy cảm hứng từ Next.js. Hướng dẫn toàn diện với cấu hình, tab navigation, dynamic routes, modal, middleware và bảo vệ route.

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

Expo Router đã thay đổi hoàn toàn cách quản lý điều hướng trong các ứng dụng React Native bằng việc áp dụng hệ thống định tuyến dựa trên file, lấy cảm hứng trực tiếp từ Next.js. Với SDK Expo 55 và Expo Router v6, việc cấu hình navigator thủ công đã trở thành quá khứ. Chỉ cần tạo một file trong thư mục app, màn hình tương ứng sẽ tự động có thể truy cập được. Android, iOS và web đều được hỗ trợ thống nhất mà không cần thêm bất kỳ cấu hình nào. Cách tiếp cận này giảm đáng kể độ phức tạp của các dự án đa nền tảng và tăng tốc quá trình phát triển ứng dụng di động.

Bắt đầu nhanh

Các dự án Expo mới đã tích hợp sẵn Expo Router. Lệnh npx create-expo-app@latest --template default@sdk-55 sẽ tạo một dự án được cấu hình sẵn với hệ thống định tuyến dựa trên file. Đối với các dự án hiện có, chỉ cần cài đặt package expo-router và cập nhật entry point của ứng dụng.

Cơ chế hoạt động của định tuyến dựa trên file trong Expo Router

Mỗi file được đặt trong thư mục app sẽ tự động trở thành một route. Đường dẫn của file tương ứng trực tiếp với đường dẫn URL, loại bỏ hoàn toàn nhu cầu sử dụng một file cấu hình tập trung. File app/settings.tsx tạo route /settings, trong khi app/profile/edit.tsx tương ứng với /profile/edit.

Quy ước này mang lại ba lợi thế quan trọng so với cách cấu hình truyền thống bằng React Navigation:

  • Không cần cấu hình: route tồn tại ngay khi file được tạo
  • Deep linking tự động: mỗi màn hình đều có URL riêng, thuận tiện cho việc chia sẻ và kiểm thử
  • Điều hướng có kiểu dữ liệu: TypeScript nhận biết các route hiện có ngay tại thời điểm biên dịch
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 },
})

Component Link xử lý điều hướng trên tất cả các nền tảng. Trên web, nó tạo ra thẻ anchor với thuộc tính href phù hợp cho SEO. Trên các nền tảng native, nó kích hoạt kiểu điều hướng stack.

Cấu trúc dự án và các file layout

Expo Router sử dụng các file _layout.tsx để định nghĩa các container điều hướng. Mỗi thư mục có thể chứa layout riêng của mình, tạo ra các cấu trúc điều hướng lồng nhau. Layout gốc bao bọc toàn bộ ứng dụng, trong khi các layout con kiểm soát từng phần cụ thể.

Dưới đây là cấu trúc dự án điển hình:

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

Các nhóm route -- thư mục được bao bọi dấu ngoặc đơn -- giúp tổ chức file mà không ảnh hưởng đến URL. Thư mục (tabs) trong ví dụ trên tạo một tab navigator, nhưng các URL vẫn giữ nguyên là /home, /search/profile thay vì /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 gốc cũng đóng vai trò là điểm khởi tạo cho việc tải font chữ, cấu hình các provider và các thiết lập toàn cục. Nó thay thế file App.tsx truyền thống với vai trò là entry point của ứng dụng.

Xây dựng hệ thống điều hướng bằng tab

Điều hướng bằng tab yêu cầu một file _layout.tsx nằm bên trong một nhóm route. Expo Router v6 giới thiệu NativeTabs để mang lại trải nghiệm native đặc thù cho từng nền tảng, tuy nhiên component Tabs tiêu chuẩn vẫn đáp ứng được phần lớn các trường hợp sử dụng.

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>
  )
}

Mỗi file màn hình tab export một React component tiêu chuẩn. Icon, nhãn và badge của thanh tab được cấu hình thông qua prop options trong layout.

Route động và tham số route

Các đoạn động sử dụng dấu ngoặc vuông trong tên file. File có tên [id].tsx khớp với một đoạn đơn lẻ, trong khi [...slug].tsx bắt tất cả các đoạn còn lại của đường dẫn.

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' },
})

Khi truy cập /product/42, màn hình này sẽ hiển thị với biến id được gán giá trị "42". Hook useLocalSearchParams cung cấp quyền truy cập có kiểu dữ liệu vào toàn bộ tham số route.

Đối với các route catch-all, [...slug].tsx bắt toàn bộ các đoạn đường dẫn:

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('/')} />
}

Sẵn sàng chinh phục phỏng vấn React Native?

Luyện tập với mô phỏng tương tác, flashcards và bài kiểm tra kỹ thuật.

Kiểu dữ liệu cho route đảm bảo an toàn tại thời điểm biên dịch

Expo Router tự động sinh các kiểu dữ liệu cho route khi tính năng typed routes được kích hoạt. Việc kiểm tra này phát hiện các liên kết bị hỏng ngay tại thời điểm biên dịch thay vì khi chạy ứng dụng.

Kích hoạt trong file app.json:

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

Sau khi kích hoạt, prop href của component Link và tham số của router.push() chỉ chấp nhận các chuỗi tương ứng với route hợp lệ:

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')
}

Tính năng typed routes phát huy hiệu quả đặc biệt khi kết hợp với useLocalSearchParams. Các kiểu được sinh tự động đảm bảo sự khớp giữa tên tham số trong định nghĩa route và các tham số được sử dụng bởi component, ngăn chặn những lỗi tiềm ẩn chỉ xuất hiện khi điều hướng đến một màn hình cụ thể.

Màn hình modal và tùy chọn hiển thị

Các modal trong Expo Router là những màn hình thông thường được cấu hình với presentation: 'modal' trong layout. Cách tiếp cận này tuân thủ quy ước của hệ thống định tuyến dựa trên file: một modal đơn giản chỉ là một route như bất kỳ route nào khác.

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,
  },
})

Khi truy cập /create-post, màn hình modal sẽ được hiển thị. Gọi router.back() sẽ đóng modal và đưa người dùng quay lại màn hình trước đó trong ngăn xếp điều hướng.

Điều hướng lập trình và API của router

Ngoài component Link, Expo Router cung cấp một API mệnh lệnh thông qua đối tượng router. API này cho phép quản lý điều hướng được kích hoạt bởi logic nghiệp vụ thay vì bởi tương tác trực tiếp của người dùng.

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
}

Sự khác biệt giữa router.pushrouter.replace rất quan trọng trong các luồng xác thực. Sau khi đăng nhập thành công, router.replace('/dashboard') ngăn không cho người dùng quay lại màn hình đăng nhập bằng nút quay lại.

Điều hướng và chuyển hướng

router.replace() thay thế mục hiện tại trong lịch sử điều hướng. Đối với các route guard chuyển hướng người dùng chưa đăng nhập, nên sử dụng <Redirect href="/login" /> trong phần render của component. Phương thức này được kích hoạt trong giai đoạn render và hoạt động chính xác với server-side rendering trên web.

Middleware và bảo vệ route

Expo Router v6 giới thiệu middleware phía server cho logic được áp dụng ở cấp độ route. File +middleware.ts chặn các request trước khi chúng đến component của route, cho phép thực hiện kiểm tra xác thực, chuyển hướng và thao tác với 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
}

Trên các nền tảng native, middleware phía server không được thực thi. Việc bảo vệ route khi đó dựa vào các guard phía client. Mẫu thiết kế phổ biến nhất là bao bọc các layout được bảo vệ bằng một lớp kiểm tra xác thực:

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 />
}
Đặc thù theo nền tảng

Middleware phía server chỉ hoạt động trên web với server-side rendering. Đối với các ứng dụng native, việc triển khai các guard phía client trong các component layout là bắt buộc. Kết hợp cả hai phương pháp đảm bảo việc bảo vệ nhất quán trên toàn bộ các nền tảng.

Kết luận

Expo Router v6 mang đến cho hệ sinh thái React Native một cách tiếp cận điều hướng phù hợp với các quy ước đã được thiết lập bởi các framework web hiện đại. Những điểm chính cần ghi nhớ:

  • Expo Router v6 thay thế việc cấu hình điều hướng thủ công bằng các quy ước dựa trên file, trong đó mỗi file trong thư mục app tự động trở thành một route
  • Các layout được định nghĩa qua file _layout.tsx tạo ra các cấu trúc điều hướng -- stack, tab và drawer -- mà không cần file cấu hình tập trung
  • Route động với [param].tsx và route catch-all với [...slug].tsx xử lý điều hướng có tham số với hỗ trợ TypeScript đầy đủ
  • Tính năng typed routes phát hiện các liên kết điều hướng bị hỏng tại thời điểm biên dịch khi được kích hoạt trong app.json
  • Màn hình modal, điều hướng lập trình qua router.push/replace/back và middleware phía server hoàn thiện bộ công cụ định tuyến
  • Các nhóm route bằng thư mục trong dấu ngoặc đơn giúp tổ chức mã nguồn mà không ảnh hưởng đến URL, duy trì cấu trúc thư mục rõ ràng khi ứng dụng mở rộng
  • Guard phía client trong layout xử lý xác thực trên native, trong khi middleware phía server bao quát web -- kết hợp cả hai đảm bảo bao phủ đa nền tảng

Bắt đầu luyện tập!

Kiểm tra kiến thức với mô phỏng phỏng vấn và bài kiểm tra kỹ thuật.

Thẻ

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

Chia sẻ

Bài viết liên quan