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

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 React và TypeScript đã 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:
- 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.
- 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.
- 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 formReact.KeyboardEvent
: Dành cho các sự kiện bàn phímReact.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ằngevent
sẽ có các thuộc tính của sự kiện chuột, và thuộc tínhtarget
(cũng nhưcurrentTarget
) sẽ có kiểu tương thích vớiHTMLButtonElement
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àoevent
kiểuReact.ChangeEvent<HTMLInputElement>
. Nhờ có<HTMLInputElement>
, TypeScript biết rằngevent.target
sẽ là mộtHTMLInputElement
, và do đó có thuộc tínhvalue
kiểustring
.
Tương tự, đối với <select>
và <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>
và<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àoevent
kiểuReact.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()
vàstopPropagation()
. Mặc dùevent.target
vàevent.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ớiemail
) 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àoevent
kiểuReact.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
handleFocus
vàhandleBlur
nhận vàoevent
kiểuReact.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ểustring
) vàevent
(kiểuReact.MouseEvent<HTMLButtonElement>
). Trong JSX, chúng ta không gán trực tiếphandleItemClick
vàoonClick
. 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àmhandleItemClick
vớiitem.id
(là dữ liệu tùy chỉnh) và đối tượng sự kiệne
. Bằng cách này, TypeScript biết chính xác kiểu của cảitemId
vàe
trong hàmhandleItemClick
.
Comments