Bài 19.2: Typing route parameters

Trong thế giới phát triển web hiện đại, việc xây dựng các trang web động là vô cùng phổ biến. Thay vì tạo ra hàng nghìn trang tĩnh, chúng ta sử dụng route động (dynamic routes) để hiển thị các nội dung khác nhau dựa trên một phần của URL. Ví dụ, /products/123, /users/john-doe, hay /blog/typescript-guide.

Phần 123, john-doe, hay typescript-guide chính là tham số route (route parameters). Chúng mang thông tin cần thiết để component của bạn biết phải hiển thị dữ liệu gì.

Tuyệt vời, nhưng làm thế nào để chúng ta chắc chắn rằng khi lấy các tham số này ra từ URL, chúng ta biết chắc kiểu dữ liệu của nó là gì? Liệu productId luôn là một chuỗi (string) có thể chuyển đổi thành số, hay nó có thể là undefined? Việc lấy nhầm tên tham số, hoặc truy cập một tham số mà ta nghĩ là có nhưng thực tế lại không tồn tại trên route hiện tại, có thể dẫn đến lỗi runtime không ngờ tới, khiến ứng dụng của bạn gặp sự cố ngay trước mắt người dùng.

Đây chính là lúc TypeScript toả sáng! Bằng cách định kiểu (typing) cho các tham số route, chúng ta có thể thêm một lớp bảo vệ mạnh mẽ cho ứng dụng của mình.

Tại sao cần Typing Route Parameters?

Khi sử dụng các thư viện routing như React Router trong ứng dụng React SPA (Single Page Application) hoặc hệ thống routing của Next.js (cả Pages Router và App Router), các tham số route thường được cung cấp cho component của bạn dưới dạng một đối tượng. Mặc định, nếu không có TypeScript, đối tượng này có kiểu là any hoặc một kiểu chung chung như {[key: string]: string | undefined}.

Điều này có nghĩa là:

  1. Không có kiểm tra lỗi tĩnh: TypeScript sẽ không báo lỗi nếu bạn cố gắng truy cập một tham số không tồn tại (ví dụ: params.usesrId thay vì params.userId). Lỗi này chỉ xuất hiện khi chạy code.
  2. Không có tự động hoàn thành (Autocompletion): IDE của bạn không biết các tên tham số nào có sẵn trong đối tượng params, làm giảm hiệu suất code.
  3. Không rõ ràng: Đồng nghiệp (hoặc chính bạn sau này) sẽ không biết ngay lập tức component này mong đợi những tham số route nào chỉ bằng cách nhìn vào định nghĩa props hoặc cách sử dụng hook.

Bằng cách typing route parameters, chúng ta sẽ:

  • Bắt lỗi sớm: TypeScript sẽ cảnh báo ngay tại thời điểm viết code (hoặc build) nếu bạn truy cập sai tên tham số hoặc sử dụng tham số sai kiểu (ví dụ: cố gắng dùng một chuỗi làm số mà chưa parse).
  • Tăng năng suất: Nhận được gợi ý tự động hoàn thành chính xác từ IDE về các tham số route.
  • Nâng cao tính dễ đọc và bảo trì: Code trở nên minh bạch hơn về dữ liệu đầu vào của component.

Hãy cùng xem cách thực hiện điều này với một vài ví dụ cụ thể.

Typing với React Router

Trong React Router v6 trở lên, chúng ta thường sử dụng hook useParams để lấy các tham số route. Hook này có hỗ trợ generic type để bạn có thể truyền vào kiểu của đối tượng params mong đợi.

Bước 1: Định nghĩa kiểu cho tham số

Sử dụng interface hoặc type để mô tả cấu trúc của đối tượng tham số route. Các giá trị tham số từ URL luôn là chuỗi (string).

// src/types/routeParams.ts (hoặc bất kỳ file nào bạn muốn)

export interface ProductDetailParams {
  productId: string; // Tên tham số phải khớp với tên trong định nghĩa route, ví dụ: /products/:productId
}

export interface UserPostParams {
  userId: string; // Tên tham số khớp với :userId
  postId: string; // Tên tham số khớp với :postId
}

export interface OptionalCategoryParams {
  categoryId?: string; // Dấu '?' biểu thị tham số này là tùy chọn (có thể là string hoặc undefined)
}

Bước 2: Sử dụng kiểu với useParams

Import interface đã định nghĩa và truyền nó vào hook useParams bằng cú pháp generic <T>.

import { useParams } from 'react-router-dom';
import { ProductDetailParams, UserPostParams, OptionalCategoryParams } from './types/routeParams'; // Đường dẫn tới file types của bạn

// Ví dụ 1: Route /products/:productId
function ProductDetailPage() {
  // Truyền kiểu ProductDetailParams vào useParams
  const params = useParams<ProductDetailParams>();
  const { productId } = params; // Hoặc destruct ngay

  // ✨ Bây giờ, TypeScript biết chắc 'productId' là một 'string'.
  // Nếu bạn gõ 'params.producsId', TS sẽ báo lỗi ngay!

  console.log("Đang xem sản phẩm với ID:", productId);
  // Note: productId là string. Nếu cần số, bạn phải parse: const idAsNumber = parseInt(productId, 10);

  return (
    <div>
      <h1>Chi tiết sản phẩm: {productId}</h1>
      {/* Logic hiển thị chi tiết sản phẩm */}
    </div>
  );
}

// Ví dụ 2: Route /users/:userId/posts/:postId
function UserPostDetailPage() {
  const { userId, postId } = useParams<UserPostParams>();

  // ✨ userId và postId đều được typed là 'string'.
  console.log(`Đang xem bài viết ${postId} của người dùng ${userId}`);

  return (
    <div>
      <h2>Bài viết {postId}</h2>
      <p>của người dùng {userId}</p>
      {/* Logic hiển thị bài viết */}
    </div>
  );
}

// Ví dụ 3: Route /products/:categoryId? (tham số tùy chọn)
function ProductListPage() {
  const { categoryId } = useParams<OptionalCategoryParams>();

  // ✨ categoryId được typed là 'string | undefined'.
  // TS sẽ nhắc bạn kiểm tra sự tồn tại trước khi sử dụng.

  console.log("categoryId:", categoryId);

  return (
    <div>
      <h1>{categoryId ? `Sản phẩm trong danh mục ${categoryId}` : 'Tất cả sản phẩm'}</h1>
      {/* Logic hiển thị danh sách sản phẩm */}
    </div>
  );
}

Giải thích:

  • Chúng ta tạo các interface (ProductDetailParams, UserPostParams, etc.) để định nghĩa rõ ràng những tham số nào mong đợi có trên URL và kiểu dữ liệu của chúng (luôn là string cho các tham số route thu được từ URL).
  • Dấu ? trong categoryId?: string; chỉ ra rằng tham số categoryId là tùy chọn, tức là nó có thể có giá trị là string hoặc undefined nếu không có trong URL.
  • Khi gọi useParams<MyParamsInterface>(), chúng ta báo cho TypeScript biết rằng đối tượng được trả về từ hook này sẽ tuân theo cấu trúc của MyParamsInterface.
  • Nhờ đó, khi bạn truy cập params.productId (hoặc productId sau khi destructuring), TypeScript biết chính xác kiểu của nó và có thể thực hiện kiểm tra lỗi tĩnh, cung cấp gợi ý tự động hoàn thành.

Typing với Next.js App Router

Trong Next.js App Router (từ phiên bản 13 trở lên), các tham số route từ cấu trúc thư mục sẽ được truyền xuống page hoặc layout component thông qua prop params. Chúng ta cũng có thể định nghĩa kiểu cho prop này.

Bước 1: Định nghĩa kiểu cho tham số và props

Tương tự như React Router, định nghĩa kiểu cho các tham số. Sau đó, định nghĩa kiểu cho toàn bộ object params và cho props của component.

// Ví dụ: Trong app/products/[productId]/page.tsx

// Định nghĩa kiểu cho các tham số route cụ thể cho page này
interface ProductPageParams {
  productId: string; // Tên key phải khớp với tên thư mục động [productId]
}

// Định nghĩa kiểu cho props mà page component nhận được
interface ProductPageProps {
  params: ProductPageParams;
  // Nếu bạn cũng cần query params, thêm dòng này:
  // searchParams: { [key: string]: string | string[] | undefined };
}

// Bước 2: Sử dụng kiểu trong định nghĩa component

// Áp dụng kiểu cho props của page component
export default function ProductPage({ params }: ProductPageProps) {
  const { productId } = params; // Destructure params đã được typed

  // ✨ productId đã được typed là 'string'.

  console.log("Đang xem sản phẩm với ID:", productId);

  return (
    <div>
      <h1>Chi tiết sản phẩm Next.js: {productId}</h1>
      {/* Logic hiển thị chi tiết sản phẩm */}
    </div>
  );
}

Ví dụ với nhiều tham số:

Nếu bạn có cấu trúc app/users/[userId]/posts/[postId]/page.tsx, các tham số sẽ là userIdpostId.

// Ví dụ: Trong app/users/[userId]/posts/[postId]/page.tsx

interface PostPageParams {
  userId: string; // Tên key khớp với [userId]
  postId: string; // Tên key khớp với [postId]
}

interface PostPageProps {
  params: PostPageParams;
}

export default function PostPage({ params }: PostPageProps) {
  const { userId, postId } = params; // params đã được typed

  // ✨ userId và postId đều được typed là 'string'.

  console.log(`Đang xem bài viết ${postId} của người dùng ${userId} (Next.js)`);

  return (
    <div>
      <h2>Bài viết {postId}</h2>
      <p>của người dùng {userId}</p>
      {/* Logic hiển thị bài viết */}
    </div>
  );
}

Giải thích:

  • Trong Next.js App Router, các tham số route được tự động thu thập từ tên thư mục động (ví dụ [productId]) và nhóm lại thành một đối tượng params được truyền làm prop cho component page hoặc layout.
  • Chúng ta tạo interface (ví dụ ProductPageParams, PostPageParams) để mô tả cấu trúc mong đợi của đối tượng params. Các tên key trong interface phải chính xác với tên thư mục động (không có dấu ngoặc vuông []).
  • Tiếp theo, chúng ta tạo interface cho toàn bộ props mà component nhận được (ví dụ ProductPageProps), bao gồm cả params và gán kiểu đã định nghĩa ở trên cho nó.
  • Cuối cùng, áp dụng kiểu props này vào định nghĩa function component.
  • Kết quả tương tự như React Router: TypeScript biết chính xác các tham số có sẵn và kiểu của chúng, mang lại lợi ích về an toàn kiểu và tự động hoàn thành.

(Lưu ý: Next.js App Router xử lý các route "catch-all" như [[...slug]] thành mảng (slug: string[] | undefined), bạn cũng cần định nghĩa kiểu tương ứng cho trường hợp này.)

Comments

There are no comments at the moment.