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 @typesbắ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:

  1. 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.
  2. 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 trong App có thể sử dụng các tính năng định tuyến của React Router.

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><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  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 RoutesRoute 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ẫn path khớp.
    • Ở đây, <Route path="/" element={<Home />} /> nghĩa là khi URL là gốc (/), component Home sẽ được render.
    • Tương tự, <Route path="/about" element={<About />} /> nghĩa là khi URL là /about, component About 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.

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'  đườ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!

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  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ể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ọi useParams ở 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 interface UserParams như một đối số kiểu cho useParams, TypeScript biết rằng object trả về sẽ có thuộc tính userId với kiểu string | 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.

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ểukhả năng đọc hiểu code của bạn.

6. Các Hook Khác Hữu Ích

Ngoài useParamsuseNavigate, 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 object location đạ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ằng navigate). TypeScript giúp bạn định nghĩa kiểu cho thuộc tính state 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ên location 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 object match nếu đường dẫn hiện tại khớp với một đường dẫn được cung cấp, hoặc null 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ới useResolvedPath (để xử lý đường dẫn tương đối) và tùy chọn end: true (đảm bảo khớp chính xác đến cuối đường dẫn) giúp xác định xem Link có đang trỏ đến route hiện tại hay không.

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

There are no comments at the moment.