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

Hướng dẫn toàn diện về Expo Router cho React Native bao gồm routing dựa trên file, layout, route động, navigation có kiểu dữ liệu, và các mẫu nâng cao như modal và tab.

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

Expo Router mang hệ thống routing dựa trên file đến React Native, thay thế việc cấu hình navigation thủ công bằng cách tiếp cận dựa trên quy ước, lấy cảm hứng từ Next.js. Từ Expo SDK 57 và Expo Router, việc xây dựng navigation đa nền tảng cho Android, iOS và web chỉ đơn giản là tạo các file trong đúng thư mục.

Bắt đầu nhanh

Các dự án Expo mới được tích hợp sẵn Expo Router. Lệnh npx create-expo-app@latest --template default sẽ tạo một dự án được cấu hình sẵn với hệ thống routing 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 routing 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.

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

  • 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ử
  • Navigation 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ý navigation 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 navigation 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 navigation. 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 navigation 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, tức thư mục được bao bọc 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 và /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.

Xây dựng Tab Navigation với Expo Router

Tab navigation yêu cầu một file _layout.tsx nằm bên trong một nhóm route. Expo Router cung cấp NativeTabs để mang lại trải nghiệm tab đặc thù cho từng nền tảng và component Tabs tiêu chuẩn cho phần lớn các trường hợp sử dụng. Từ SDK 56, Native Tabs hỗ trợ thuộc tính disabled để ngăn việc chọn tab trong khi vẫn hiển thị nó.

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.

Typed Routes đả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 routing 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 navigation.

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ý navigation đượ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.push và router.replace rất quan trọng trong các luồng xác thực của React Native. 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.

Navigation và chuyển hướng

router.replace() thay thế mục hiện tại trong lịch sử navigation. Đố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 cung cấp 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.

Chuyển đổi SDK 56: Fork React Navigation

Từ Expo SDK 56, Expo Router không còn phụ thuộc vào các package @react-navigation/*. Nếu dự án import trực tiếp từ React Navigation, cần cập nhật các import đó:

Package gốcVị trí mới
@react-navigation/nativeexpo-router/react-navigation
@react-navigation/stackexpo-router/js-stack
@react-navigation/bottom-tabsexpo-router/js-tabs

Chạy codemod để cập nhật import tự động:

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

API runtime vẫn giữ nguyên. Chỉ có module specifier thay đổi. Các thư viện bên thứ ba vẫn import từ @react-navigation/core sẽ hoạt động tự động vì Expo CLI viết lại các import đó trong quá trình bundling.

Nguồn tham khảo

Những điều nhà tuyển dụng mong đợi về Expo Router

  • Expo Router ánh xạ mỗi file trong thư mục app thành một route tự động, loại bỏ việc cấu hình navigation thủ công
  • Các layout được định nghĩa qua file _layout.tsx tạo ra các cấu trúc navigation cho stack, tab và drawer
  • Route động với [param].tsx và route catch-all với [...slug].tsx xử lý navigation có tham số với hỗ trợ TypeScript đầy đủ
  • Typed routes phát hiện các liên kết navigation 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, navigation lập trình qua router.push/replace/back và middleware phía server hoàn thiện bộ công cụ routing
  • 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
  • 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
  • Từ SDK 56, các import từ @react-navigation/* phải được cập nhật sang các tương đương expo-router/*

Để tìm hiểu thêm về kiến trúc React Native và chuẩn bị phỏng vấn, xem module native modules.

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ử thách hôm nay

Bạn có tìm ra lỗi trong React Native không?

Một đoạn mã thật, một lỗi ẩn, mỗi ngày một lượt. Không cần tài khoản để thử.

Anthony Fillion-Maillet

Viết bởi

Anthony Fillion-Maillet

Người sáng lập SharpSkill

Lập trình viên fullstack hơn 10 năm. Anh điều hành SharpSkill và chịu trách nhiệm về mọi nội dung đăng tại đây.

Cập nhật ngày 6 tháng 10, 2026

Thẻ

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

Chia sẻ

Bài viết liên quan