Bài 17.1: Typing event handlers trong React-TypeScript

Trong thế giới phát triển web hiện đại, việc kết hợp ReactTypeScript đã trở thành một tiêu chuẩn vàng. TypeScript mang lại sự an toàn kiểu mạnh mẽ, giúp chúng ta bắt lỗi ngay lúc phát triển (compile-time) thay vì khi ứng dụng đang chạy (runtime). Điều này không chỉ giúp giảm thiểu bug mà còn cải thiện đáng kể trải nghiệm của lập trình viên thông qua tính năng tự động hoàn thành (autocompletion) và gợi ý mã.

Đối với React, các hàm xử lý sự kiện (event handlers) là trái tim của mọi tương tác người dùng. Từ việc nhấp chuột, nhập liệu, đến gửi form, mọi thứ đều được xử lý thông qua các hàm này. Khi sử dụng TypeScript với React, việc định nghĩa kiểu chính xác cho các event handler này là vô cùng quan trọng. Nó đảm bảo rằng khi bạn truy cập vào các thuộc tính của đối tượng sự kiện (event object), bạn biết chắc chắn những thuộc tính nào tồn tại và kiểu dữ liệu của chúng là gì.

Vậy, làm thế nào để typing event handlers một cách đúng đắn trong React-TypeScript?

Tại sao cần Typing Event Handlers?

Nếu bạn không định nghĩa kiểu cho tham số sự kiện, TypeScript sẽ mặc định nó là any. Điều này làm mất đi toàn bộ lợi ích của TypeScript tại điểm đó.

// Không có kiểu
const handleClick = (event) => { // event có kiểu là 'any'
  console.log(event.target.value); // Có thể gây lỗi runtime nếu event.target không có value
};

Với kiểu chính xác, TypeScript sẽ giúp bạn:

  1. An toàn kiểu: Ngăn chặn truy cập vào các thuộc tính không tồn tại trên đối tượng sự kiện.
  2. Tự động hoàn thành: Trình soạn thảo mã (như VS Code) sẽ gợi ý các thuộc tính có sẵn của đối tượng sự kiện.
  3. Mã rõ ràng hơn: Kiểu giúp tài liệu hóa ý định của bạn.

Hệ thống Synthetic Events của React và Kiểu của TypeScript

React không sử dụng trực tiếp các đối tượng sự kiện gốc của trình duyệt. Thay vào đó, nó sử dụng một hệ thống Synthetic Events. Hệ thống này giúp chuẩn hóa các sự kiện trên các trình duyệt khác nhau, đảm bảo hành vi nhất quán. TypeScript cung cấp sẵn các định nghĩa kiểu cho các Synthetic Event này.

Hầu hết các kiểu sự kiện trong React đều nằm trong namespace React. Các kiểu phổ biến nhất thường có dạng React.TênSựKiện. Ví dụ:

  • React.MouseEvent: Dành cho các sự kiện liên quan đến chuột (click, hover, drag, etc.)
  • React.ChangeEvent: Dành cho các sự kiện thay đổi giá trị của phần tử form (input, select, textarea)
  • React.FormEvent: Dành cho sự kiện submit của form
  • React.KeyboardEvent: Dành cho các sự kiện bàn phím
  • React.FocusEvent: Dành cho các sự kiện focus/blur

Nhiều kiểu sự kiện này là các kiểu Generic, cho phép bạn chỉ định loại phần tử DOM mà sự kiện xảy ra. Ví dụ: React.MouseEvent<HTMLButtonElement> cho sự kiện click trên nút button, React.ChangeEvent<HTMLInputElement> cho sự kiện thay đổi trên ô input. Việc chỉ định loại phần tử cụ thể này là thực hành tốt (good practice) vì nó giúp TypeScript biết chính xác thuộc tính target thuộc loại nào.

Cách Tìm Kiểu Đúng

Làm sao để biết chính xác kiểu nào cần dùng cho một sự kiện cụ thể trên một phần tử cụ thể?

Cách đơn giản nhất là hover chuột lên prop sự kiện trong JSX của bạn (nếu bạn đang sử dụng một trình soạn thảo hỗ trợ TypeScript như VS Code). TypeScript sẽ hiển thị gợi ý kiểu cho hàm xử lý sự kiện đó.

Ví dụ, hover lên onClick trên một <button> có thể hiển thị gợi ý kiểu là React.MouseEventHandler<HTMLButtonElement>. Đây là một kiểu tiện ích (utility type) đại diện cho một hàm nhận vào React.MouseEvent<HTMLButtonElement> và trả về void. Bạn có thể sử dụng kiểu này, hoặc đơn giản hơn là định nghĩa kiểu trực tiếp cho tham số của hàm: (event: React.MouseEvent<HTMLButtonElement>) => void. Cách thứ hai thường được ưa chuộng hơn, đặc biệt với arrow function.

Các Ví Dụ Phổ Biến

Hãy cùng xem qua một vài ví dụ về cách typing các event handler phổ biến.

1. Sự kiện Click (onClick)

Sự kiện click là một trong những sự kiện cơ bản nhất, thuộc nhóm Mouse Events.

// Component ví dụ
const MyButton: React.FC = () => {

  // Định nghĩa kiểu cho hàm xử lý sự kiện click
  // React.MouseEvent<HTMLButtonElement> cho biết đây là sự kiện chuột trên phần tử HTMLButtonElement
  const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
    console.log("Button đã được click!");
    // Truy cập các thuộc tính của sự kiện chuột
    console.log("Vị trí click:", event.clientX, event.clientY);
    // event.target: phần tử thực tế đã kích hoạt sự kiện (có thể là con của button)
    // event.currentTarget: phần tử mà event handler được gắn vào (chính là button)
    console.log("Target:", event.target);
    console.log("Current Target:", event.currentTarget);
  };

  return (
    <button onClick={handleClick}>
      Click Me
    </button>
  );
};
  • Giải thích: Chúng ta định nghĩa handleClick là một hàm nhận vào tham số event với kiểu là React.MouseEvent<HTMLButtonElement>. Điều này cho phép TypeScript biết rằng event sẽ có các thuộc tính của sự kiện chuột, và thuộc tính target (cũng như currentTarget) sẽ có kiểu tương thích với HTMLButtonElement hoặc các phần tử con của nó.
2. Sự kiện Thay Đổi Giá Trị (onChange)

Sự kiện này rất phổ biến với các phần tử form như <input>, <select>, <textarea>. Nó thuộc nhóm Change Events.

const MyInput: React.FC = () => {
  const [value, setValue] = React.useState('');

  // Định nghĩa kiểu cho hàm xử lý sự kiện thay đổi giá trị của input
  // React.ChangeEvent<HTMLInputElement> cho biết đây là sự kiện thay đổi trên phần tử HTMLInputElement
  const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    console.log("Giá trị input đã thay đổi:");
    console.log("event.target.value:", event.target.value); // Thuộc tính phổ biến nhất
    setValue(event.target.value);
  };

  return (
    <input
      type="text"
      value={value}
      onChange={handleInputChange} // Gắn handler vào prop onChange
      placeholder="Nhập gì đó..."
    />
  );
};
  • Giải thích: Hàm handleInputChange nhận vào event kiểu React.ChangeEvent<HTMLInputElement>. Nhờ có <HTMLInputElement>, TypeScript biết rằng event.target sẽ là một HTMLInputElement, và do đó có thuộc tính value kiểu string.

Tương tự, đối với <select><textarea>:

const MySelect: React.FC = () => {
  const [value, setValue] = React.useState('');

  const handleSelectChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
    console.log("Giá trị select đã thay đổi:", event.target.value);
    setValue(event.target.value);
  };

  return (
    <select value={value} onChange={handleSelectChange}>
      <option value="apple">Apple</option>
      <option value="banana">Banana</option>
    </select>
  );
};

const MyTextArea: React.FC = () => {
  const [value, setValue] = React.useState('');

  const handleTextAreaChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
    console.log("Giá trị textarea đã thay đổi:", event.target.value);
    setValue(event.target.value);
  };

  return (
    <textarea value={value} onChange={handleTextAreaChange} placeholder="Nhập đoạn văn..."></textarea>
  );
};
  • Giải thích: Kiểu Generic của React.ChangeEvent thay đổi tùy theo phần tử: <HTMLSelectElement> cho <select><HTMLTextAreaElement> cho <textarea>.
3. Sự kiện Gửi Form (onSubmit)

Khi người dùng nhấn nút submit trong form, sự kiện onSubmit được kích hoạt. Sự kiện này thuộc nhóm Form Events.

const MyForm: React.FC = () => {
  const [email, setEmail] = React.useState('');

  const handleEmailChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    setEmail(event.target.value);
  };

  // Định nghĩa kiểu cho hàm xử lý sự kiện submit của form
  // React.FormEvent<HTMLFormElement> cho biết đây là sự kiện form trên phần tử HTMLFormElement
  const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
    // Rất quan trọng trong React SPA để ngăn chặn hành vi mặc định của trình duyệt (load lại trang)
    event.preventDefault();
    console.log("Form đã được gửi!");
    console.log("Email:", email);
    // Thường xử lý logic gửi dữ liệu tại đây
  };

  return (
    <form onSubmit={handleSubmit}> {/* Gắn handler vào prop onSubmit */}
      <input
        type="email"
        value={email}
        onChange={handleEmailChange}
        placeholder="Nhập email của bạn"
      />
      <button type="submit">Gửi</button>
    </form>
  );
};
  • Giải thích: handleSubmit nhận vào event kiểu React.FormEvent<HTMLFormElement>. Thuộc tính quan trọng nhất của kiểu này là các phương thức như preventDefault()stopPropagation(). Mặc dù event.targetevent.currentTarget vẫn tồn tại, bạn thường lấy giá trị từ các input thông qua state của component (như ví dụ trên với email) thay vì truy cập trực tiếp vào form elements qua sự kiện submit.
4. Sự kiện Bàn Phím (onKeyDown, onKeyUp, onKeyPress)

Các sự kiện này rất hữu ích để phản ứng với thao tác gõ phím của người dùng, thuộc nhóm Keyboard Events.

const MyTextAreaWithKeyHandler: React.FC = () => {

  // Định nghĩa kiểu cho hàm xử lý sự kiện bàn phím
  // React.KeyboardEvent<HTMLTextAreaElement> cho biết đây là sự kiện bàn phím trên phần tử HTMLTextAreaElement
  const handleKeyDown = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
    console.log("Phím đã nhấn (xuống):", event.key); // Tên phím (ví dụ: 'Enter', 'A', 'Shift')
    console.log("Mã phím:", event.keyCode); // Mã phím (đã cũ nhưng vẫn dùng được)
    console.log("Ctrl pressed:", event.ctrlKey); // Kiểm tra phím Ctrl

    if (event.key === 'Enter' && event.ctrlKey) {
      console.log("Bạn vừa nhấn Ctrl + Enter!");
      // Xử lý gửi tin nhắn trong chat app chẳng hạn
    }
  };

  return (
    <textarea
      onKeyDown={handleKeyDown} // Gắn handler vào prop onKeyDown
      placeholder="Nhấn phím gì đó..."
    ></textarea>
  );
};
  • Giải thích: handleKeyDown nhận vào event kiểu React.KeyboardEvent<HTMLTextAreaElement>. Kiểu này cung cấp các thuộc tính như key, keyCode, altKey, ctrlKey, shiftKey, metaKey để kiểm tra phím nào đã được nhấn và các phím bổ trợ.
5. Sự kiện Focus (onFocus, onBlur)

Các sự kiện này xảy ra khi một phần tử nhận (focus) hoặc mất (blur) sự chú ý từ người dùng hoặc qua lập trình, thuộc nhóm Focus Events.

const MyFocusInput: React.FC = () => {

  // Định nghĩa kiểu cho hàm xử lý sự kiện focus
  // React.FocusEvent<HTMLInputElement> cho biết đây là sự kiện focus trên phần tử HTMLInputElement
  const handleFocus = (event: React.FocusEvent<HTMLInputElement>) => {
    console.log("Input đã nhận focus");
    // event.target, event.currentTarget vẫn có sẵn
  };

  // Định nghĩa kiểu cho hàm xử lý sự kiện blur
  const handleBlur = (event: React.FocusEvent<HTMLInputElement>) => {
    console.log("Input đã mất focus");
    // event.relatedTarget: phần tử vừa nhận focus sau khi phần tử hiện tại mất focus (có thể null)
    console.log("Related Target:", event.relatedTarget);
  };

  return (
    <input
      type="text"
      onFocus={handleFocus} // Gắn handler vào prop onFocus
      onBlur={handleBlur}   // Gắn handler vào prop onBlur
      placeholder="Focus vào tôi..."
    />
  );
};
  • Giải thích: Các hàm handleFocushandleBlur nhận vào event kiểu React.FocusEvent<HTMLInputElement>. Kiểu này cho phép bạn phản ứng với trạng thái focus của phần tử, hữu ích cho việc hiển thị/ẩn các phần tử giao diện hoặc thực hiện validate khi người dùng rời khỏi ô nhập.

Xử lý Event Handler với Dữ liệu Tùy chỉnh

Đôi khi, bạn cần truyền thêm dữ liệu vào event handler cùng với đối tượng sự kiện. Một mẫu phổ biến là sử dụng một arrow function làm lớp bọc (wrapper) trong JSX.

interface Item {
  id: string;
  name: string;
}

const items: Item[] = [
  { id: '1', name: 'Sản phẩm A' },
  { id: '2', name: 'Sản phẩm B' },
];

const ItemList: React.FC = () => {

  // Hàm xử lý sự kiện thực tế, nhận thêm item ID và đối tượng sự kiện
  // Chúng ta định nghĩa rõ ràng các tham số và kiểu của chúng
  const handleItemClick = (itemId: string, event: React.MouseEvent<HTMLButtonElement>) => {
    console.log(`Đã click vào sản phẩm có ID: ${itemId}`);
    console.log("event.currentTarget:", event.currentTarget); // Vẫn có thể truy cập sự kiện nếu cần
    // Thực hiện logic xử lý click cho item cụ thể
  };

  return (
    <div>
      {items.map(item => (
        // Sử dụng arrow function làm wrapper để truyền thêm item.id
        // Arrow function này nhận event từ React và gọi handleItemClick với item.id và event
        <button key={item.id} onClick={(e) => handleItemClick(item.id, e)}>
          {item.name}
        </button>
      ))}
    </div>
  );
};
  • Giải thích: Ở đây, hàm handleItemClick được định nghĩa để nhận hai tham số: itemId (kiểu string) và event (kiểu React.MouseEvent<HTMLButtonElement>). Trong JSX, chúng ta không gán trực tiếp handleItemClick vào onClick. Thay vào đó, chúng ta dùng một arrow function (e) => .... Arrow function này chính là handler được React gọi. Nó nhận đối tượng sự kiện từ React (đặt tên là e), sau đó gọi hàm handleItemClick với item.id (là dữ liệu tùy chỉnh) và đối tượng sự kiện e. Bằng cách này, TypeScript biết chính xác kiểu của cả itemIde trong hàm handleItemClick.

Comments

There are no comments at the moment.