Bài 19.1: Cài đặt React Router với TypeScript

Bài 19.1: Cài đặt React Router với TypeScript
Chào mừng các bạn đã quay trở lại với chuỗi bài học Lập trình Web Front-end!
Trong thế giới hiện đại của phát triển web, các Ứng dụng Một Trang (Single Page Applications - SPAs) ngày càng phổ biến nhờ khả năng mang lại trải nghiệm người dùng mượt mà và nhanh chóng. Tuy nhiên, để một SPA có thể hoạt động như một trang web truyền thống với nhiều "trang" khác nhau (ví dụ: trang chủ, trang giới thiệu, trang sản phẩm), chúng ta cần một cơ chế quản lý các URL và hiển thị nội dung tương ứng. Đó chính là lúc Routing phát huy vai trò của mình.
Trong hệ sinh thái React, thư viện React Router là giải pháp de facto (mặc định được công nhận) cho việc quản lý định tuyến. Nó cung cấp một cách linh hoạt và hiệu quả để ánh xạ các URL tới các component React, giúp ứng dụng của bạn có thể chuyển đổi giữa các "trang" mà không cần tải lại toàn bộ trình duyệt.
Bài viết này sẽ tập trung vào việc hướng dẫn bạn cài đặt và thiết lập React Router trong một dự án React sử dụng TypeScript. Việc kết hợp TypeScript với React Router không chỉ giúp bạn viết code sạch sẽ, dễ bảo trì hơn mà còn giúp bắt được các lỗi liên quan đến định tuyến ngay trong quá trình phát triển.
Hãy cùng bắt tay vào việc!
1. Cài đặt React Router
Trước hết, chúng ta cần thêm thư viện React Router vào dự án của mình. Vì chúng ta đang làm việc với TypeScript, chúng ta cũng cần cài đặt các định nghĩa kiểu (type definitions) tương ứng.
Mở terminal trong thư mục gốc của dự án React của bạn và chạy lệnh sau:
npm install react-router-dom
npm install @types/react-router-dom --save-dev
Hoặc nếu bạn dùng Yarn:
yarn add react-router-dom
yarn add @types/react-router-dom --dev
react-router-dom
: Đây là gói chính chứa các component và hooks mà chúng ta sẽ sử dụng để định tuyến trong môi trường DOM (trình duyệt web).@types/react-router-dom
: Gói này chứa các tệp.d.ts
(declaration files) cung cấp thông tin kiểu cho TypeScript, giúp TypeScript hiểu được các component và hàm từreact-router-dom
và cung cấp tính năng tự động hoàn thành, kiểm tra kiểu trong trình soạn thảo code của bạn. Việc cài đặt gói@types
là bắt buộc khi làm việc với TypeScript và các thư viện JavaScript thông thường.
Sau khi cài đặt xong, chúng ta đã sẵn sàng để tích hợp React Router vào ứng dụng.
2. Thiết lập cơ bản: Bọc ứng dụng với Router
Để React Router có thể hoạt động, toàn bộ ứng dụng (hoặc phần ứng dụng cần định tuyến) cần được đặt bên trong một Router. React Router cung cấp hai loại Router chính cho môi trường web:
BrowserRouter
: Sử dụng API History của trình duyệt (ví dụ:pushState
,replaceState
) để giữ cho UI đồng bộ với URL. Đây là loại Router phổ biến nhất và được khuyến khích dùng trong hầu hết các trường hợp vì nó tạo ra các URL "đẹp" và dễ đọc (ví dụ:/about
,/dashboard
). Tuy nhiên, nó yêu cầu server của bạn phải cấu hình để trả về cùng một tệp HTML (index.html
) cho tất cả các đường dẫn, để khi người dùng truy cập trực tiếp vào một URL sâu (như/about
) thì trình duyệt vẫn tải ứng dụng lên và React Router sẽ xử lý phần định tuyến.HashRouter
: Sử dụng phần hash của URL (ví dụ:/#/about
,/#/dashboard
) để giữ cho UI đồng bộ. Ưu điểm của nó là không yêu cầu cấu hình đặc biệt từ phía server vì mọi yêu cầu đều hướng về/
. Nhược điểm là URL trông không được thân thiện và có thể ảnh hưởng đến SEO.
Trong phần lớn các dự án SPA hiện đại, BrowserRouter
là lựa chọn ưu tiên. Chúng ta sẽ sử dụng nó làm ví dụ.
Thông thường, bạn sẽ đặt BrowserRouter
ở cấp độ cao nhất của cây component, thường là trong tệp index.tsx
nơi ứng dụng React của bạn được render.
Mở tệp src/index.tsx
(hoặc tương đương) và chỉnh sửa như sau:
import React from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom'; // Import BrowserRouter
import App from './App';
import './index.css'; // Giả sử bạn có tệp CSS
const container = document.getElementById('root');
// Kiểm tra xem container có tồn tại không (thường là div có id="root" trong public/index.html)
if (container) {
const root = ReactDOM.createRoot(container);
root.render(
<React.StrictMode>
{/* Bọc component App bên trong BrowserRouter */}
<BrowserRouter>
<App />
</BrowserRouter>
</React.StrictMode>
);
} else {
console.error('Root container missing in index.html');
}
- Giải thích:
- Chúng ta import
BrowserRouter
từreact-router-dom
. - Component gốc của ứng dụng (
<App />
) được bọc bên trong<BrowserRouter>
. Điều này giúp tất cả các component con bên trongApp
có thể sử dụng các tính năng định tuyến của React Router.
- Chúng ta import
3. Định nghĩa các Tuyến đường (Routes)
Sau khi ứng dụng đã được bọc trong Router, chúng ta cần định nghĩa các tuyến đường (routes). React Router v6 trở lên sử dụng các component <Routes>
và <Route>
để làm điều này.
<Routes>
: Component này là một container chứa tất cả các<Route>
. Nó sẽ duyệt qua các<Route>
con và render component của tuyến đường đầu tiên có đường dẫn (path
) khớp với URL hiện tại.<Route>
: Component này định nghĩa một tuyến đường cụ thể. Nó có hai prop chính:path
: Đường dẫn URL mà tuyến đường này sẽ khớp (ví dụ:/
,/about
,/users/:id
).element
: Component React sẽ được render khi đường dẫn khớp.
Bạn thường định nghĩa các tuyến đường trong component gốc của ứng dụng, ví dụ như App.tsx
.
Giả sử bạn có các component đơn giản cho các trang Home và About:
// src/components/Home.tsx
import React from 'react';
const Home: React.FC = () => {
return (
<div>
<h2>Trang Chủ</h2>
<p>Chào mừng bạn đến với trang web của chúng tôi!</p>
</div>
);
};
export default Home;
// src/components/About.tsx
import React from 'react';
const About: React.FC = () => {
return (
<div>
<h2>Giới thiệu</h2>
<p>Đây là trang giới thiệu về chúng tôi.</p>
</div>
);
};
export default About;
Bây giờ, hãy cập nhật src/App.tsx
để định nghĩa các tuyến đường:
import React from 'react';
import { Routes, Route, Link } from 'react-router-dom'; // Import Routes, Route, Link
import Home from './components/Home';
import About from './components/About';
// Import các component trang khác nếu có
const App: React.FC = () => {
return (
<div>
{/* Thanh điều hướng */}
<nav>
<ul>
<li>
<Link to="/">Trang Chủ</Link> {/* Link tới đường dẫn gốc */}
</li>
<li>
<Link to="/about">Giới thiệu</Link> {/* Link tới đường dẫn /about */}
</li>
{/* Thêm các Link khác nếu cần */}
</ul>
</nav>
<hr /> {/* Đường kẻ phân cách */}
{/* Container cho các tuyến đường */}
<Routes>
{/* Định nghĩa tuyến đường cho trang chủ */}
<Route path="/" element={<Home />} />
{/* Định nghĩa tuyến đường cho trang giới thiệu */}
<Route path="/about" element={<About />} />
{/* Bạn có thể thêm tuyến đường cho trang 404 (Page Not Found) */}
{/* <Route path="*" element={<NotFound />} /> */}
</Routes>
</div>
);
};
export default App;
- Giải thích:
- Chúng ta import
Routes
vàRoute
từreact-router-dom
. - Bên trong component
App
, chúng ta sử dụng<Routes>
để bao bọc các định nghĩa<Route>
. - Mỗi
<Route>
component:- Prop
path
xác định đường dẫn URL tương ứng. - Prop
element
nhận một JSX element (thường là component của trang) sẽ được hiển thị khi đường dẫnpath
khớp.
- Prop
- Ở đây,
<Route path="/" element={<Home />} />
nghĩa là khi URL là gốc (/
), componentHome
sẽ được render. - Tương tự,
<Route path="/about" element={<About />} />
nghĩa là khi URL là/about
, componentAbout
sẽ được render. - Chúng ta cũng thêm một thanh điều hướng đơn giản sử dụng component
<Link>
(sẽ nói rõ hơn ở phần sau) để người dùng có thể dễ dàng chuyển đổi giữa các trang.
- Chúng ta import
Bây giờ, khi bạn chạy ứng dụng, bạn sẽ thấy nội dung của Home
hoặc About
component hiển thị tùy thuộc vào URL trên thanh địa chỉ của trình duyệt (ví dụ: localhost:3000/
hoặc localhost:3000/about
).
4. Điều hướng giữa các Trang (Navigation)
Có hai cách chính để điều hướng trong ứng dụng React Router:
a) Sử dụng Link
(Điều hướng khai báo)
<Link>
component là cách phổ biến nhất để tạo các liên kết điều hướng trong React Router. Nó tương tự như thẻ <a>
thông thường, nhưng thay vì gây ra việc tải lại toàn bộ trang, nó chỉ thay đổi URL và cho phép React Router xử lý việc cập nhật UI. Điều này duy trì trải nghiệm SPA mượt mà.
Chúng ta đã sử dụng Link
trong ví dụ App.tsx
ở trên:
import { Link } from 'react-router-dom';
// ... trong component
<nav>
<ul>
<li>
<Link to="/">Trang Chủ</Link> {/* 'to' là đường dẫn đích */}
</li>
<li>
<Link to="/about">Giới thiệu</Link>
</li>
</ul>
</nav>
- Giải thích:
Link
được import từreact-router-dom
.- Prop
to
chỉ định đường dẫn màLink
sẽ điều hướng đến khi được click. Đường dẫn này sẽ được React Router sử dụng để khớp với các<Route>
đã định nghĩa.
Lưu ý quan trọng: Luôn sử dụng <Link>
cho các liên kết nội bộ trong ứng dụng SPA của bạn. Chỉ sử dụng thẻ <a>
HTML thông thường khi bạn muốn liên kết đến một trang bên ngoài ứng dụng của mình hoặc khi bạn thực sự muốn tải lại trang.
b) Sử dụng useNavigate
(Điều hướng theo chương trình)
Đôi khi, bạn cần điều hướng người dùng dựa trên một sự kiện nào đó không phải là việc họ nhấp vào một liên kết tĩnh – ví dụ: sau khi gửi biểu mẫu thành công, sau khi đăng nhập, hoặc khi một điều kiện nào đó được thỏa mãn. Trong những trường hợp này, bạn cần điều hướng theo chương trình (programmatic navigation).
React Router cung cấp hook useNavigate
để làm điều này. Hook này trả về một hàm cho phép bạn điều hướng.
Ví dụ, giả sử bạn có một component form và muốn chuyển hướng người dùng đến trang chủ sau khi submit thành công:
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom'; // Import useNavigate
const MyForm: React.FC = () => {
const [formData, setFormData] = useState('');
const navigate = useNavigate(); // Sử dụng hook useNavigate
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
// Xử lý dữ liệu form...
console.log('Form submitted:', formData);
// Sau khi xử lý xong, điều hướng đến trang chủ
navigate('/');
// Bạn cũng có thể truyền state
// navigate('/success', { state: { message: 'Submission successful!' } });
};
return (
<form onSubmit={handleSubmit}>
<input
type="text"
value={formData}
onChange={(e) => setFormData(e.target.value)}
/>
<button type="submit">Submit</button>
</form>
);
};
export default MyForm;
- Giải thích:
- Hook
useNavigate
được import và gọi trong component hàm. navigate
là hàm được trả về từ hook.- Khi gọi
navigate('/')
, React Router sẽ chuyển URL sang/
và hiển thị component tương ứng với đường dẫn đó (trong ví dụ trước làHome
). - Hàm
navigate
cũng có thể nhận tham số thứ hai là một object{ state: ... }
để truyền dữ liệu state qua lại giữa các route, điều này rất hữu ích!
- Hook
5. Tuyến đường động và useParams
Một tính năng mạnh mẽ của React Router là khả năng xử lý các tuyến đường động, nơi một phần của URL đại diện cho một ID hoặc một giá trị biến đổi. Ví dụ: /users/123
hoặc /products/abc-xyz
.
Để định nghĩa một tuyến đường động, bạn sử dụng ký hiệu hai chấm (:
) trước tên tham số trong prop path
của <Route>
.
Ví dụ, để có một trang chi tiết người dùng dựa trên ID:
// src/App.tsx (thêm tuyến đường động)
// ... import cũ
import UserProfile from './components/UserProfile'; // Component mới
const App: React.FC = () => {
return (
<div>
<nav>
<ul>
{/* ... các Link cũ */}
<li>
{/* Ví dụ Link tới user ID 1 */}
<Link to="/users/1">User 1</Link>
</li>
<li>
{/* Ví dụ Link tới user ID 5 */}
<Link to="/users/5">User 5</Link>
</li>
</ul>
</nav>
<hr />
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
{/* Định nghĩa tuyến đường động cho user profile */}
<Route path="/users/:userId" element={<UserProfile />} />
</Routes>
</div>
);
};
export default App;
Trong component UserProfile
(hoặc bất kỳ component nào được render bởi tuyến đường động), bạn có thể truy cập giá trị của tham số URL (userId
trong trường hợp này) bằng cách sử dụng hook useParams
.
Và đây là lúc TypeScript tỏa sáng! Khi sử dụng useParams
với TypeScript, bạn nên định nghĩa một interface để mô tả cấu trúc của các tham số dự kiến. Điều này giúp đảm bảo bạn luôn truy cập các tham số với tên và kiểu dữ liệu chính xác, tránh lỗi runtime.
// src/components/UserProfile.tsx
import React from 'react';
import { useParams } from 'react-router-dom'; // Import useParams
// Định nghĩa interface cho các tham số URL
interface UserParams {
userId?: string; // userId là tên của tham số trong path="/users/:userId"
}
const UserProfile: React.FC = () => {
// Sử dụng useParams với interface đã định nghĩa
const { userId } = useParams<UserParams>();
// Ở đây, bạn có thể sử dụng userId để tải dữ liệu người dùng từ API, vv.
// TypeScript biết rằng userId là một string (hoặc undefined nếu route không khớp hoàn toàn)
return (
<div>
<h2>Chi tiết Người dùng</h2>
{userId ? (
<p>Đang hiển thị thông tin cho người dùng có ID: **{userId}**</p>
) : (
<p>Không tìm thấy ID người dùng.</p>
)}
{/* Hiển thị thông tin chi tiết về người dùng */}
</div>
);
};
export default UserProfile;
- Giải thích:
- Chúng ta import
useParams
từreact-router-dom
. - Chúng ta định nghĩa interface
UserParams
mô tả các tham số URL mà chúng ta mong đợi (ở đây chỉ cóuserId
). Sử dụng?
để chỉ ra rằng nó có thểundefined
(mặc dù trong route/users/:userId
thìuserId
sẽ luôn tồn tại khi route khớp, nhưng việc định nghĩa kiểu có thể làstring | undefined
là an toàn trong một số trường hợp phức tạp hơn hoặc khi bạn gọiuseParams
ở nơi không chắc chắn về route). Tuy nhiên, cách phổ biến hơn khi route có tham số là định nghĩa nó làstring
. - Chúng ta gọi
useParams<UserParams>()
. Bằng cách truyền interfaceUserParams
như một đối số kiểu chouseParams
, TypeScript biết rằng object trả về sẽ có thuộc tínhuserId
với kiểustring | undefined
. Điều này ngăn chặn các lỗi như gõ sai tên tham số (ví dụ:user_id
thay vìuserId
). - Chúng ta truy cập
userId
từ object trả về và sử dụng nó để hiển thị hoặc tìm nạp dữ liệu.
- Chúng ta import
Việc sử dụng TypeScript với useParams
và các hook khác của React Router (như useLocation
, useNavigate
) giúp tăng cường đáng kể tính an toàn kiểu và khả năng đọc hiểu code của bạn.
6. Các Hook Khác Hữu Ích
Ngoài useParams
và useNavigate
, React Router còn cung cấp các hook khác mà bạn có thể thấy hữu ích, đặc biệt khi kết hợp với TypeScript:
useLocation
: Trả về một objectlocation
đại diện cho URL hiện tại. Object này chứa các thông tin nhưpathname
(đường dẫn),search
(query string),hash
, vàstate
(dữ liệu được truyền khi điều hướng bằngnavigate
). TypeScript giúp bạn định nghĩa kiểu cho thuộc tínhstate
nếu bạn sử dụng nó.import { useLocation } from 'react-router-dom'; interface MyLocationState { message?: string; } const CurrentLocation: React.FC = () => { // Sử dụng useLocation với kiểu state tùy chỉnh const location = useLocation(); const state = location.state as MyLocationState | null; // Ép kiểu để truy cập state return ( <div> <p>Current Pathname: **{location.pathname}**</p> <p>Current Search: **{location.search}**</p> {state && state.message && ( <p>Message from state: **{state.message}**</p> )} </div> ); };
Lưu ý: Thuộc tính
state
trênlocation
có kiểu làunknown
, nên bạn cần ép kiểu (type assertion) nó sang kiểu mong muốn của mình khi sử dụng với TypeScript để truy cập các thuộc tính an toàn.useMatch
: Trả về một objectmatch
nếu đường dẫn hiện tại khớp với một đường dẫn được cung cấp, hoặcnull
nếu không khớp. Nó rất hữu ích khi bạn muốn kiểm tra xem một đường dẫn cụ thể có đang hoạt động hay không, ví dụ để làm nổi bật menu item đang active.import { useMatch, useResolvedPath } from 'react-router-dom'; interface NavLinkProps { to: string; children: React.ReactNode; } const CustomNavLink: React.FC<NavLinkProps> = ({ to, children }) => { let resolved = useResolvedPath(to); // Giải quyết đường dẫn tương đối let match = useMatch({ path: resolved.pathname, end: true }); // Kiểm tra khớp chính xác return ( <li className={match ? 'active-link' : ''}> <Link to={to}>{children}</Link> </li> ); }; // Sử dụng trong App.tsx {/* ... trong nav ul */} <CustomNavLink to="/">Trang Chủ</CustomNavLink> <CustomNavLink to="/about">Giới thiệu</CustomNavLink> {/* ... */}
- Giải thích:
useMatch
kết hợp vớiuseResolvedPath
(để xử lý đường dẫn tương đối) và tùy chọnend: true
(đảm bảo khớp chính xác đến cuối đường dẫn) giúp xác định xemLink
có đang trỏ đến route hiện tại hay không.
- Giải thích:
7. Tóm lược Cấu trúc
Sau khi cài đặt và thiết lập các phần cơ bản, cấu trúc ứng dụng React của bạn sẽ trông đại khái như thế này:
my-react-app/
├── public/
│ └── index.html <-- Chứa div#root, được server cấu hình trả về cho mọi đường dẫn
├── src/
│ ├── index.tsx <-- Nơi bạn render App và bọc nó trong BrowserRouter
│ ├── App.tsx <-- Nơi bạn định nghĩa các Routes và có thể là thanh điều hướng
│ ├── components/
│ │ ├── Home.tsx <-- Component cho trang chủ
│ │ ├── About.tsx <-- Component cho trang giới thiệu
│ │ ├── UserProfile.tsx <-- Component sử dụng useParams
│ │ └── ... các component trang khác
│ └── ... các tệp khác (css, assets, vv.)
├── package.json <-- Chứa dependency react-router-dom và @types/react-router-dom
└── tsconfig.json <-- Cấu hình TypeScript
Đây là cấu trúc cơ bản và có thể mở rộng ra tùy thuộc vào độ phức tạp của ứng dụng.
Comments