React Router DOM — Định tuyến cho React App
Thông tin bài học
- Series: React với TypeScript · Bài số: 1
- Độ khó: Beginner Intermediate
- Thời gian: ~30 phút
- Stack: React 18, TypeScript, Zalo Mini App (ZMP)
1. Router là gì và tại sao cần?
React là thư viện xây dựng Single Page Application (SPA). Trong SPA, chỉ có một file HTML duy nhất được tải — mọi điều hướng xảy ra phía client, không reload trang.
Vấn đề: Làm sao React biết hiển thị component nào khi user truy cập /products hay /cart?
Giải pháp: react-router-dom — thư viện routing phổ biến nhất cho React, cho phép bạn:
- Ánh xạ URL Component
- Điều hướng mà không reload trang
- Quản lý lịch sử trình duyệt (back/forward)
- Truyền dữ liệu qua URL params và query string
graph LR
A[URL: /products/42] -->|Route matching| B(react-router-dom)
B --> C["<ProductDetail id='42' />"]
style A fill:#f9f,stroke:#333,stroke-width:2px
style C fill:#bbf,stroke:#333,stroke-width:2px
2. Các loại Router
react-router-dom cung cấp 3 loại router chính, mỗi loại phù hợp với môi trường khác nhau:
Sử dụng HTML5 History API (pushState). Phù hợp cho web app thông thường, có server hỗ trợ cấu hình fallback về index.html.
Sử dụng URL hash. Phù hợp cho static hosting (như GitHub Pages cũ) khi không cấu hình được server.
Lưu ý nền tảng: Zalo Mini App
Zalo WebView block HTML5 History API, do đó bắt buộc dùng MemoryRouter thay vì BrowserRouter.
3. Cài đặt
Sử dụng package manager yêu thích của bạn:
TypeScript Support
react-router-dom v6+ đã bao gồm sẵn TypeScript types — không cần cài @types/react-router-dom riêng.
4. Cấu hình cơ bản
Tách file constants cho route giúp ứng dụng có type-safety tốt hơn:
export const ROUTES = {
HOME: '/',
PRODUCTS: '/products',
PRODUCT_DETAIL: '/products/:id',
CART: '/cart',
PROFILE: '/profile',
} as const;
// Type-safe route paths
export type AppRoute = typeof ROUTES[keyof typeof ROUTES];
Thiết lập Routing Container:
import { Routes, Route, Navigate } from 'react-router-dom';
import { ROUTES } from '@/constants/routes';
import HomePage from '@/pages/home';
import ProductsPage from '@/pages/products';
export default function AppRouter() {
return (
<Routes>
<Route path={ROUTES.HOME} element={<HomePage />} />
<Route path={ROUTES.PRODUCTS} element={<ProductsPage />} />
{/* Fallback 404: redirect về màn hình chính */}
<Route path="*" element={<Navigate to={ROUTES.HOME} replace />} />
</Routes>
);
}
5. Các hook thiết yếu
useNavigate — Điều hướng programmatic
import { useNavigate } from 'react-router-dom';
function ProductCard({ id }: { id: number }) {
const navigate = useNavigate();
const handleClick = () => {
// Điều hướng đến trang chi tiết
navigate(`/products/${id}`);
// Truyền thêm state
// navigate('/checkout', { state: { productId: id } });
// Thay thế (replace: không lưu vào lịch sử)
// navigate('/home', { replace: true });
};
return <button onClick={handleClick}>Xem chi tiết</button>;
}
useParams & useSearchParams
Lấy giá trị từ dynamic segment (ví dụ :id).
Quản lý query string (?key=value).
import { useSearchParams } from 'react-router-dom';
function ProductList() {
const [searchParams, setSearchParams] = useSearchParams();
const page = searchParams.get('page') ?? '1';
const nextPage = () => setSearchParams({ page: String(Number(page) + 1) });
return <button onClick={nextPage}>Trang sau</button>;
}
6. Nested Routes & Layouts
Nested routes là pattern siêu mạnh mẽ: Component cha giữ layout cố định, Component con thay đổi dựa theo route.
import { Outlet } from 'react-router-dom';
function DashboardLayout() {
return (
<div className="layout-container">
<Sidebar />
<main className="content">
{/* Nơi render sub-route */}
<Outlet />
</main>
</div>
);
}
<Routes>
<Route path="/dashboard" element={<DashboardLayout />}>
<Route index element={<DashboardHome />} /> {/* /dashboard */}
<Route path="orders" element={<Orders />} /> {/* /dashboard/orders */}
<Route path="settings" element={<Settings />} /> {/* /dashboard/settings */}
</Route>
</Routes>
7. Protected Routes (Chặn điều hướng)
Sử dụng Higher-Order Component hoặc Wrapper Component để bảo vệ route:
Mã nguồn ProtectedRoute.tsx (Click để xem)
import { Navigate, Outlet } from 'react-router-dom';
import { useAuthStore } from '@/store/auth';
export default function ProtectedRoute({ redirectTo = '/login' }) {
const isAuthenticated = useAuthStore((s) => s.isAuthenticated);
if (!isAuthenticated) {
return <Navigate to={redirectTo} replace />;
}
return <Outlet />; // Cho phép đi tiếp
}
Cách dùng:
8. Tối ưu với Lazy Loading
Tải component theo nhu cầu giúp giảm dung lượng bundle ban đầu:
import { lazy, Suspense } from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
// Tải khi được gọi
const HomePage = lazy(() => import('./pages/HomePage'));
const ProductsPage = lazy(() => import('./pages/ProductsPage'));
const CartPage = lazy(() => import('./pages/CartPage'));
function App() {
return (
<BrowserRouter>
<Suspense fallback={<div className="spinner">Đang tải...</div>}>
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/products" element={<ProductsPage />} />
<Route path="/cart" element={<CartPage />} />
</Routes>
</Suspense>
</BrowserRouter>
);
}
9. Thực chiến: Zalo Mini App
Khác với Web thông thường, Zalo Mini App chạy trong WebView và chặn History API. Giải quyết bằng MemoryRouter.
Thứ tự Providers (Rất quan trọng)
Jotai Provider phải nằm NGOÀI Router
Nếu đặt <Provider> (của Jotai/Redux) bên trong <MemoryRouter>, mỗi lần điều hướng app có thể unmount/remount dẫn đến bị mất state toàn cục.
Cấu trúc chuẩn:
graph TD
A[Jotai Provider] --> B[ZMP App Theme]
B --> C[SnackbarProvider]
C --> D(MemoryRouter)
D --> E{Routes}
style A fill:#e1f5fe,stroke:#03a9f4,stroke-width:2px
style D fill:#f3e5f5,stroke:#9c27b0,stroke-width:2px
import { MemoryRouter, Routes, Route, Navigate } from 'react-router-dom';
import { App, SnackbarProvider } from 'zmp-ui';
import HomePage from '@/pages';
import { ROUTES } from '@/constants/routes';
export default function Layout() {
return (
<App theme="light">
<SnackbarProvider>
{/* Khởi tạo entry đầu tiên là HOME */}
<MemoryRouter initialEntries={[ROUTES.HOME]} initialIndex={0}>
<Routes>
<Route path={ROUTES.HOME} element={<HomePage />} />
<Route path="*" element={<Navigate to={ROUTES.HOME} replace />} />
</Routes>
</MemoryRouter>
</SnackbarProvider>
</App>
);
}
Tổng kết & Bài tập
Những gì bạn đã học
- 3 loại Router:
BrowserRouter,HashRouter,MemoryRouter - Hooks mạnh mẽ:
useNavigate,useParams,useLocation - Patterns quan trọng: Nested Routes, Protected Routes, Lazy Loading
- Đặc tả Platform: Cấu hình
MemoryRoutercho Zalo Mini App
Bài tập thực hành:
- [x] Định nghĩa một
ROUTESconstant file sử dụngas const. - [ ] Tạo thử một Nested Route (chia layout Dashboard / Sidebar).
- [ ] Chuyển file
layout.tsxcủa bạn từ zmp-ui router sangMemoryRouter.