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 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.
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
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:
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/privacyCá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.
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ó.
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.
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:
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:
{
"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ệ:
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.
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>
)
}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.
Navigation 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ý 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.
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.
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.
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:
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 />
}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ốc | Vị trí mới |
|---|---|
@react-navigation/native | expo-router/react-navigation |
@react-navigation/stack | expo-router/js-stack |
@react-navigation/bottom-tabs | expo-router/js-tabs |
Chạy codemod để cập nhật import tự động:
npx expo-codemod sdk-56-expo-router-react-navigation-replace srcAPI 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
- Tài liệu Expo Router - hướng dẫn chính thức và tham chiếu API
- Changelog Expo SDK 57 - ghi chú phát hành bản ổn định hiện tại
- Blog Post Expo Router v56 - giải thích về việc fork React Navigation
- Hướng dẫn chuyển đổi SDK 55 sang 56 - các bước chuyển đổi import
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
appthà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.tsxtạo ra các cấu trúc navigation cho stack, tab và drawer - Route động với
[param].tsxvà route catch-all với[...slug].tsxxử 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/backvà 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 đươngexpo-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.
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ử.

Viết bởi
Anthony Fillion-MailletNgườ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ẻ
Chia sẻ
Bài viết liên quan

Hướng Dẫn Phát Triển Ứng Dụng React Native 2026: Xây Dựng Ứng Dụng Production và Câu Hỏi Phỏng Vấn
Hướng dẫn toàn diện về phát triển ứng dụng React Native 2026 bao gồm New Architecture, Expo SDK 56, Hermes V1, và các câu hỏi phỏng vấn dành cho developer React Native.

Phát triển ứng dụng React Native 2026: Hướng dẫn đầy đủ và câu hỏi phỏng vấn
Hướng dẫn toàn diện về phát triển ứng dụng React Native năm 2026. Tìm hiểu về New Architecture, JSI, Fabric, TurboModules và các câu hỏi phỏng vấn thường gặp.

Hermes V1 trong React Native 0.84: Hiệu năng, Bytecode Precompiled và Câu hỏi Phỏng vấn
Hướng dẫn chuyên sâu về engine JavaScript Hermes V1 trong React Native 0.84, bao gồm biên dịch bytecode, garbage collector Hades, chiến lược tối ưu bộ nhớ và câu hỏi phỏng vấn kỹ thuật.