Bài 15.3: Typing props trong React-TypeScript

Chào mừng trở lại với chuỗi bài viết về Lập trình Web Front-end! Sau khi đã làm quen với React và bắt đầu tích hợp sức mạnh của TypeScript, chúng ta đang dần xây dựng những ứng dụng mạnh mẽ và đáng tin cậy hơn. Trong bài học hôm nay, chúng ta sẽ đi sâu vào một khía cạnh cực kỳ quan trọng khi kết hợp React và TypeScript: định kiểu (typing) cho props.

Props (viết tắt của properties) là linh hồn của việc truyền dữ liệu giữa các component trong React. Chúng cho phép chúng ta tạo ra các component có thể tái sử dụng và cấu hình được. Tuy nhiên, trong JavaScript "thuần", không có cách nào đảm bảo rằng component nhận được đúng loại dữ liệu mà nó mong đợi. Đây chính là lúc TypeScript tỏa sáng! Bằng cách định kiểu rõ ràng cho props, chúng ta biến component từ một hộp đen tiềm ẩn lỗi thành một khối chức năng rõ ràng, an toàn và dễ hiểu.

Tại sao phải Typing Props?

Bạn có thể tự hỏi: "Tại sao phải thêm một bước nữa để định kiểu cho props khi component vẫn hoạt động mà không có nó?". Câu trả lời nằm ở chất lượng mã nguồnhiệu quả làm việc.

  1. Bắt lỗi sớm: TypeScript là một bộ kiểm tra kiểu tĩnh. Điều này có nghĩa là nó sẽ kiểm tra mã nguồn của bạn trước khi nó chạy trong trình duyệt. Nếu bạn truyền một prop sai kiểu (ví dụ: truyền số thay vì chuỗi), TypeScript sẽ báo lỗi ngay lập tức trong editor hoặc khi build, thay vì ứng dụng bị lỗi khi chạy. Đây là lợi ích lớn nhất, giúp tiết kiệm vô số thời gian debug!
  2. Tự động hoàn thành (Autocompletion) và gợi ý: Khi props được định kiểu, editor của bạn (như VS Code) sẽ biết chính xác các prop nào mà component chấp nhận và kiểu dữ liệu của chúng. Điều này mang lại trải nghiệm phát triển tuyệt vời với tính năng tự động hoàn thành và gợi ý mã, giúp bạn viết code nhanh hơn và chính xác hơn.
  3. Tài liệu sống (Living Documentation): Định nghĩa kiểu cho props hoạt động như một tài liệu rõ ràng về cách sử dụng component của bạn. Bất kỳ nhà phát triển nào nhìn vào định nghĩa kiểu đều có thể hiểu ngay component đó cần những gì.
  4. Refactoring an toàn: Khi bạn cần thay đổi cấu trúc dữ liệu của props, TypeScript sẽ giúp bạn xác định tất cả các nơi sử dụng component đó cần được cập nhật. Điều này giảm thiểu đáng kể rủi ro gây ra lỗi khi refactoring.

Với những lý do trên, việc typing props không còn là tùy chọn mà là một thực hành tốt nhất khi làm việc với React và TypeScript.

Cách Định Kiểu Props

Có nhiều cách để định kiểu props cho component functional trong React với TypeScript, phổ biến nhất là sử dụng interface hoặc type để mô tả cấu trúc của props, sau đó áp dụng định nghĩa này vào component.

Chúng ta sẽ tập trung vào cách sử dụng interface (hoặc type) và áp dụng nó trực tiếp vào chữ ký hàm của component, một phương pháp rõ ràng và được ưa chuộng hiện nay.

1. Định Kiểu Props Cơ Bản (string, number, boolean)

Hãy bắt đầu với ví dụ đơn giản nhất: một component hiển thị thông tin người dùng với tên và tuổi.

// src/components/UserProfile.tsx
import React from 'react';

// Định nghĩa interface cho props
interface UserProfileProps {
  name: string;
  age: number;
}

const UserProfile: React.FC<UserProfileProps> = ({ name, age }) => {
  return (
    <div>
      <h2>Chào, tôi  {name}!</h2>
      <p>Tôi năm nay {age} tuổi.</p>
    </div>
  );
};

export default UserProfile;

Giải thích:

  • Chúng ta định nghĩa một interface tên là UserProfileProps. Interface này mô tả rằng component mong đợi hai prop: namestringagenumber.
  • Chúng ta sử dụng React.FC<UserProfileProps> để khai báo kiểu cho component functional. React.FC (Functional Component) là một kiểu generic được cung cấp bởi React, và chúng ta truyền UserProfileProps vào bên trong <...> để cho TypeScript biết kiểu của props.
  • Trong thân hàm component, chúng ta nhận props thông qua destructuring { name, age }. TypeScript đã biết kiểu của namestringagenumber, cho phép bạn sử dụng chúng một cách an toàn.

Khi sử dụng component này:

import UserProfile from './components/UserProfile';

function App() {
  return (
    <div>
      {/* Usage đúng */}
      <UserProfile name="Alice" age={30} />

      {/* Usage sai - TypeScript sẽ báo lỗi ở đây! */}
      {/* <UserProfile name={123} age="Bob" /> */}
    </div>
  );
}

TypeScript sẽ ngay lập tức báo lỗi nếu bạn cố gắng truyền prop sai kiểu, ví dụ như truyền số cho name hoặc chuỗi cho age.

2. Định Kiểu Props Tùy Chọn (Optional Props)

Đôi khi, một số props không bắt buộc phải có. TypeScript hỗ trợ điều này bằng cách sử dụng ký hiệu ? sau tên prop trong interface.

// src/components/Greeting.tsx
import React from 'react';

interface GreetingProps {
  name: string;
  message?: string; // prop 'message' là tùy chọn
}

const Greeting: React.FC<GreetingProps> = ({ name, message }) => {
  return (
    <div>
      <h1>Xin chào, {name}!</h1>
      {message && <p>{message}</p>} {/* Chỉ hiển thị message nếu  tồn tại */}
    </div>
  );
};

export default Greeting;

Giải thích:

  • Prop message được đánh dấu bằng ? sau tên (message?: string), cho biết nó có thể có hoặc không có.
  • Trong component, chúng ta kiểm tra sự tồn tại của message trước khi sử dụng nó (message && <p>{message}</p>).
  • Khi sử dụng component, bạn có thể bỏ qua prop message:
import Greeting from './components/Greeting';

function App() {
  return (
    <div>
      {/* Usage không có message */}
      <Greeting name="World" />

      {/* Usage có message */}
      <Greeting name="TypeScript" message="Học TypeScript thật thú vị!" />
    </div>
  );
}
3. Định Kiểu Props Là Đối Tượng (Object) hoặc Mảng (Array)

Props thường có thể là các cấu trúc dữ liệu phức tạp hơn như đối tượng hoặc mảng. Bạn có thể định nghĩa cấu trúc này trực tiếp trong interface props hoặc định nghĩa các interface riêng cho các kiểu phức tạp đó.

// src/components/ItemList.tsx
import React from 'react';

// Định nghĩa interface cho cấu trúc của mỗi item
interface Item {
  id: number;
  name: string;
}

// Định nghĩa interface cho props của component
interface ItemListProps {
  items: Item[]; // prop 'items' là một mảng các đối tượng kiểu Item
}

const ItemList: React.FC<ItemListProps> = ({ items }) => {
  return (
    <ul>
      {items.map(item => (
        <li key={item.id}>{item.name}</li>
      ))}
    </ul>
  );
};

export default ItemList;

Giải thích:

  • Chúng ta định nghĩa interface Item để mô tả cấu trúc của từng phần tử trong mảng.
  • Interface ItemListProps định nghĩa prop items là một mảng các đối tượng kiểu Item, được ký hiệu là Item[].
  • Component nhận mảng items và có thể truy cập an toàn vào các thuộc tính item.id (number) và item.name (string) bên trong vòng lặp map.

Cách sử dụng:

import ItemList from './components/ItemList';

function App() {
  const myItems = [
    { id: 1, name: 'Apple' },
    { id: 2, name: 'Banana' },
    { id: 3, name: 'Cherry' },
  ];

  // const wrongItems = [ { id: 'a', name: 'Date' } ]; // Lỗi TypeScript! id phải là number

  return (
    <div>
      <ItemList items={myItems} />
      {/* <ItemList items={wrongItems} /> */}
    </div>
  );
}
4. Định Kiểu Props Là Hàm (Function)

Props rất thường được sử dụng để truyền các hàm xử lý sự kiện (event handlers) hoặc các callback function từ component cha xuống component con. TypeScript giúp chúng ta định nghĩa rõ ràng chữ ký của hàm này.

// src/components/MyButton.tsx
import React from 'react';

// Định nghĩa interface cho props
interface MyButtonProps {
  label: string;
  // Định nghĩa prop 'onClick' là một hàm không nhận tham số và không trả về giá trị
  onClick: () => void;
}

const MyButton: React.FC<MyButtonProps> = ({ label, onClick }) => {
  return (
    <button onClick={onClick}>
      {label}
    </button>
  );
};

export default MyButton;

Giải thích:

  • Interface MyButtonProps định nghĩa prop onClick. Kiểu () => void mô tả một hàm: () là phần tham số (không nhận tham số nào), và void là kiểu giá trị trả về (không trả về giá trị gì).
  • Component sử dụng prop onClick trực tiếp làm hàm xử lý sự kiện cho button.

Cách sử dụng:

import MyButton from './components/MyButton';

function App() {
  const handleButtonClick = () => {
    alert('Button clicked!');
  };

  return (
    <div>
      {/* Usage đúng */}
      <MyButton label="Click Me" onClick={handleButtonClick} />

      {/* Usage sai - TypeScript sẽ báo lỗi vì onClick cần là một hàm */}
      {/* <MyButton label="Click Me" onClick="not a function" /> */}
    </div>
  );
}

Nếu hàm xử lý sự kiện của bạn cần nhận tham số (ví dụ: sự kiện từ DOM, hoặc dữ liệu nào đó), bạn chỉ cần định nghĩa chữ ký hàm tương ứng trong interface. Ví dụ: onClick: (event: React.MouseEvent<HTMLButtonElement>) => void; hoặc onSave: (data: string) => void;.

5. Định Kiểu Props Là children

Trong React, children là một prop đặc biệt cho phép bạn truyền các phần tử con vào bên trong component. Ví dụ, bạn có thể truyền các đoạn text, các phần tử HTML, hoặc các component khác vào giữa thẻ mở và thẻ đóng của component cha.

Để định kiểu cho children khi sử dụng TypeScript, chúng ta sử dụng kiểu React.ReactNode. Kiểu này bao gồm mọi thứ mà React có thể render: các phần tử React (JSX), chuỗi, số, Portals, Fragments, hoặc thậm chí là null, undefined, và booleans.

// src/components/Card.tsx
import React, { ReactNode } from 'react';

// Định nghĩa interface cho props, bao gồm children
interface CardProps {
  title: string;
  children: ReactNode; // Định nghĩa prop 'children'
}

const Card: React.FC<CardProps> = ({ title, children }) => {
  return (
    <div style={{ border: '1px solid #ccc', padding: '15px', margin: '10px' }}>
      <h3>{title}</h3>
      <hr />
      {children} {/* Render nội dung children */}
    </div>
  );
};

export default Card;

Giải thích:

  • Chúng ta import ReactNode từ 'react'.
  • Interface CardProps định nghĩa prop children có kiểu là ReactNode.
  • Component chỉ đơn giản là render nội dung của children ở vị trí mong muốn.

Cách sử dụng:

import Card from './components/Card';

function App() {
  return (
    <div>
      {/* Usage với children là đoạn text và p tag */}
      <Card title="Thông tin cá nhân">
        <p>Tên: Alice</p>
        <p>Tuổi: 30</p>
      </Card>

      {/* Usage với children là component khác */}
      <Card title="Danh sách món hàng">
        {/* Giả sử ItemList là component đã định nghĩa ở trên */}
        <ItemList items={[{ id: 1, name: 'Laptop' }, { id: 2, name: 'Mouse' }]} />
      </Card>

       {/* Usage với children là text */}
      <Card title="Ghi chú">
        Đây  một ghi chú đơn giản.
      </Card>
    </div>
  );
}

Component Card giờ đây có thể chứa bất kỳ nội dung React nào được đặt giữa thẻ <Card></Card>, và TypeScript đảm bảo rằng prop children sẽ có kiểu phù hợp (ReactNode).

Comments

There are no comments at the moment.