Chào mừng các bạn quay trở lại với chuỗi bài viết “React Roadmap“! Sau khi đã tìm hiểu về React là gì, sự khác biệt giữa Class và Functional Components, cách JSX kết hợp JavaScript và Markup, hay cách dữ liệu chảy trong component thông qua Props và State, cũng như cách quản lý Conditional Rendering, kết hợp Component, vòng đời Component, và làm việc với danh sách và Key, hôm nay chúng ta sẽ đi sâu vào một khía cạnh cực kỳ quan trọng để tạo ra các ứng dụng web tương tác: Xử Lý Sự Kiện (Event Handling).
Trong bất kỳ ứng dụng giao diện người dùng nào, việc phản hồi lại các hành động của người dùng (như click chuột, gõ phím, hover, submit form,…) là cốt lõi để tạo nên trải nghiệm tương tác. React, với triết lý “declarative” (khai báo), mang đến một cách tiếp cận xử lý sự kiện khác biệt và mạnh mẽ hơn so với cách làm truyền thống trong JavaScript. Chúng ta sẽ cùng khám phá cách React “làm sự kiện” như thế nào, những khác biệt chính, và làm thế nào để xử lý chúng một cách hiệu quả theo “React Way”.
Mục lục
Sự Khác Biệt Giữa Xử Lý Sự Kiện Truyền Thống và React
Nếu bạn đã quen với việc xử lý sự kiện trong JavaScript thuần (vanilla JS), bạn có thể sử dụng addEventListener
hoặc gán trực tiếp hàm xử lý vào thuộc tính on*
của phần tử HTML. React cũng làm điều tương tự nhưng với một số điểm khác biệt quan trọng:
- Đặt tên sự kiện (Event Naming): Trong HTML thuần, tên sự kiện thường là chữ thường (ví dụ:
onclick
,onchange
). Trong React, tên sự kiện được viết theo quy tắc camelCase (ví dụ:onClick
,onChange
). - Truyền hàm xử lý (Passing Functions): Thay vì truyền một chuỗi (string) chứa mã JavaScript như trong HTML thuần (
<button onclick="myFunction()">
), trong React, bạn truyền trực tiếp hàm xử lý sự kiện vào thuộc tính sự kiện. - Hệ thống sự kiện tổng hợp (Synthetic Event System): Đây là khác biệt lớn nhất. React không gắn trực tiếp trình lắng nghe sự kiện (event listener) vào các phần tử DOM riêng lẻ. Thay vào đó, nó sử dụng một hệ thống ủy quyền sự kiện (event delegation). React gắn một trình lắng nghe duy nhất ở cấp gốc của ứng dụng (thường là phần tử
<div id="root">
). Khi một sự kiện xảy ra trên bất kỳ phần tử con nào, sự kiện đó sẽ nổi bọt (bubble up) đến trình lắng nghe gốc. React sau đó tạo ra một đối tượng sự kiện tổng hợp (SyntheticEvent
) bọc lấy sự kiện gốc của trình duyệt và truyền nó tới hàm xử lý sự kiện của bạn. Hệ thống này giúp đảm bảo tính nhất quán của sự kiện trên các trình duyệt khác nhau và cải thiện hiệu suất nhờ tái sử dụng các đối tượng sự kiện.
Hãy xem một ví dụ đơn giản:
JavaScript Thuần:
<button onclick="alert('Xin chào!')">Nhấn tôi</button>
// Hoặc dùng addEventListener
const button = document.querySelector('button');
button.addEventListener('click', () => {
alert('Xin chào!');
});
React (trong JSX):
function MyButton() {
function handleClick() {
alert('Xin chào!');
}
return (
<button onClick={handleClick}>
Nhấn tôi
</button>
);
}
Như bạn thấy, trong React, chúng ta truyền trực tiếp hàm handleClick
vào thuộc tính onClick
, không có dấu ngoặc đơn ()
khi truyền hàm, vì chúng ta muốn truyền định nghĩa của hàm chứ không phải kết quả thực thi của nó ngay lập tức.
Xử Lý Sự Kiện trong Functional Components (Cách Hiện Đại)
Với sự phổ biến của Functional Components và Hooks, việc xử lý sự kiện trong React trở nên đơn giản và trực quan hơn nhiều. Đây là cách phổ biến nhất hiện nay.
Định nghĩa hàm xử lý sự kiện
Trong Functional Component, bạn chỉ cần định nghĩa một hàm JavaScript thông thường bên trong component đó. Thường thì người ta sử dụng cú pháp mũi tên (arrow function) và khai báo với const
để tránh các vấn đề về this
(mặc dù trong Functional Components thì this
không phải là vấn đề lớn như Class Components).
function MyComponent() {
// Định nghĩa hàm xử lý sự kiện
const handleButtonClick = () => {
console.log('Button đã được nhấn!');
};
const handleInputChange = (event) => {
console.log('Giá trị input thay đổi:', event.target.value);
};
return (
<div>
<button onClick={handleButtonClick}>Nhấn vào đây</button>
<input type="text" onChange={handleInputChange} placeholder="Gõ gì đó..." />
</div>
);
}
Truyền hàm xử lý vào phần tử
Sau khi định nghĩa hàm, bạn chỉ cần gán tên hàm đó vào thuộc tính sự kiện tương ứng trên phần tử JSX, sử dụng cặp ngoặc nhọn {}
.
<button onClick={handleButtonClick}>...</button>
<input onChange={handleInputChange} />
Lưu ý quan trọng: Không thêm dấu ngoặc đơn ()
sau tên hàm khi truyền vào thuộc tính sự kiện (onClick={handleClick}
thay vì onClick={handleClick()}
). Nếu thêm ()
, hàm sẽ được gọi ngay lập tức khi component được render, chứ không phải khi sự kiện xảy ra.
Đối tượng Sự kiện (Event Object)
Khi một sự kiện xảy ra, React sẽ tự động truyền một đối tượng sự kiện tổng hợp (SyntheticEvent
) làm đối số đầu tiên cho hàm xử lý của bạn. Đối tượng này tương thích với các sự kiện gốc của trình duyệt nhưng được chuẩn hóa để hoạt động nhất quán trên mọi trình duyệt.
Đối tượng SyntheticEvent
có các thuộc tính và phương thức giống như sự kiện gốc, ví dụ: event.target
, event.preventDefault()
, event.stopPropagation()
.
function HandleInput() {
const handleChange = (event) => {
// event.target là phần tử DOM gây ra sự kiện (input)
console.log('Input value:', event.target.value);
};
const handleClick = (event) => {
// event.preventDefault() ngăn chặn hành vi mặc định của trình duyệt
// Ví dụ: ngăn form submit reload trang
event.preventDefault();
console.log('Link clicked!');
};
return (
<div>
<input type="text" onChange={handleChange} />
<a href="#" onClick={handleClick}>Click me</a>
</div>
);
}
Ngăn chặn Hành vi Mặc định (Preventing Default)
Để ngăn chặn hành vi mặc định của trình duyệt (ví dụ: ngăn form submit, ngăn link điều hướng), bạn gọi phương thức preventDefault()
trên đối tượng sự kiện, tương tự như trong JavaScript thuần.
function MyForm() {
const handleSubmit = (event) => {
event.preventDefault(); // Ngăn chặn form submit và reload trang
console.log('Form submitted!');
// Xử lý logic submit form ở đây
};
return (
<form onSubmit={handleSubmit}>
<input type="text" />
<button type="submit">Submit</button>
</form>
);
}
Truyền Đối số cho Hàm Xử lý Sự kiện
Đôi khi bạn cần truyền thêm dữ liệu (đối số) cho hàm xử lý sự kiện của mình, ngoài đối tượng sự kiện mặc định. Có hai cách phổ biến để làm điều này:
- Sử dụng hàm mũi tên (arrow function) inline: Đây là cách ngắn gọn và dễ hiểu, đặc biệt khi bạn cần truy cập cả đối tượng sự kiện và đối số bổ sung.
- Sử dụng phương thức
bind
(ít dùng hơn trong Functional Components): Cách này phổ biến hơn trong Class Components để xử lý vấn đềthis
và truyền đối số, nhưng vẫn có thể dùng trong Functional Components nếu cần.
Ví dụ với Arrow Function Inline
function ItemList({ items }) {
const handleDeleteItem = (itemId, event) => {
console.log('Đang xóa item với ID:', itemId);
console.log('Sự kiện gốc:', event); // Bạn vẫn có thể truy cập event
};
return (
<ul>
{items.map(item => (
<li key={item.id}>
{item.name}
<button onClick={(event) => handleDeleteItem(item.id, event)}>
Xóa
</button>
</li>
))}
</ul>
);
}
Trong ví dụ này, chúng ta tạo một hàm mũi tên (event) => handleDeleteItem(item.id, event)
. Khi button được click, hàm mũi tên này sẽ được thực thi, và nó gọi hàm handleDeleteItem
với item.id
và đối tượng sự kiện event
làm đối số.
Lưu ý về hiệu suất khi dùng arrow function inline: Cách này tạo ra một hàm mới mỗi khi component render. Với các danh sách rất dài (hàng ngàn item), điều này có thể ảnh hưởng nhẹ đến hiệu suất do quá trình garbage collection. Tuy nhiên, với hầu hết các trường hợp thông thường, sự ảnh hưởng này là không đáng kể và lợi ích về tính dễ đọc, dễ viết thường lớn hơn.
Ví dụ với bind
// Dù ít dùng trong functional component nhưng vẫn có thể dùng
function LegacyExample({ userId }) {
const handleEditUser = (id, event) => {
console.log('Đang chỉnh sửa user ID:', id);
console.log('Sự kiện gốc:', event);
};
return (
<button onClick={handleEditUser.bind(null, userId)}>
Chỉnh sửa User
</button>
);
}
Phương thức bind()
tạo ra một hàm mới. Đối số đầu tiên của bind
là giá trị mà this
sẽ trỏ tới trong hàm mới (trong Functional Components, this
thường là undefined
hoặc null
, nên truyền null
ở đây là an toàn). Các đối số tiếp theo sẽ được truyền làm đối số ban đầu cho hàm gốc (handleEditUser
). Đối tượng sự kiện SyntheticEvent
sẽ được truyền làm đối số cuối cùng một cách tự động.
Xử Lý Sự Kiện trong Class Components (Cách Cũ hơn, cần chú ý ‘this’)
Mặc dù Functional Components là cách được khuyến khích hiện nay, bạn vẫn có thể gặp Class Components trong các dự án cũ hoặc một số trường hợp đặc biệt. Xử lý sự kiện trong Class Component yêu cầu bạn phải cẩn thận hơn với từ khóa this
.
Trong JavaScript, các phương thức trong class theo mặc định không tự động ràng buộc this
vào instance của class. Điều này có nghĩa là khi phương thức đó được gọi làm trình xử lý sự kiện (ví dụ: onClick={this.handleClick}
), this
bên trong phương thức handleClick
sẽ là undefined
, dẫn đến lỗi khi bạn cố gắng truy cập this.state
hoặc this.props
.
Để giải quyết vấn đề này, bạn cần đảm bảo this
được ràng buộc đúng trong phương thức xử lý sự kiện của class. Có một vài cách:
- Ràng buộc trong constructor: Đây là cách truyền thống.
- Sử dụng cú pháp thuộc tính class (arrow function): Cách này hiện đại hơn và thường được ưa chuộng trong Class Components vì nó tự động ràng buộc
this
. - Arrow function inline trong render: Giống như trong Functional Components, bạn có thể dùng arrow function ngay trong JSX, nhưng cũng cần lưu ý về hiệu suất.
Ví dụ Class Component với Ràng buộc trong Constructor
class MyClassButton extends React.Component {
constructor(props) {
super(props);
// Ràng buộc 'this' cho phương thức handleClick
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
console.log('Button đã được nhấn, this là:', this); // 'this' bây giờ trỏ đúng đến instance của component
// Có thể truy cập this.state hoặc this.props
}
render() {
return (
<button onClick={this.handleClick}>
Nhấn Class Button (Constructor Bind)
</button>
);
}
}
Ví dụ Class Component với Cú pháp Thuộc tính Class (Arrow Function)
class MyModernClassButton extends React.Component {
// Sử dụng cú pháp thuộc tính class với arrow function
// Arrow function tự động ràng buộc 'this'
handleClick = () => {
console.log('Button đã được nhấn, this là:', this); // 'this' tự động trỏ đúng
};
render() {
return (
<button onClick={this.handleClick}>
Nhấn Class Button (Arrow Function Property)
</button>
);
}
}
Cách sử dụng cú pháp thuộc tính class (với Babel) thường được ưa chuộng hơn trong Class Components vì nó gọn gàng hơn và tránh lặp lại việc ràng buộc trong constructor.
Synthetic Event System (Hệ Thống Sự Kiện Tổng Hợp)
Như đã đề cập, React không dùng sự kiện gốc của trình duyệt trực tiếp. Thay vào đó, nó triển khai một hệ thống sự kiện tổng hợp riêng. Khi một sự kiện được kích hoạt, React tạo ra một đối tượng SyntheticEvent
. Đối tượng này là một wrapper (lớp bọc) quanh sự kiện gốc của trình duyệt.
Tại sao React lại làm điều này?
- Tính Tương thích Cross-Browser: Sự kiện gốc của các trình duyệt khác nhau có thể có sự khác biệt nhỏ về hành vi hoặc thuộc tính.
SyntheticEvent
chuẩn hóa các sự kiện này, đảm bảo code của bạn hoạt động nhất quán trên mọi trình duyệt được React hỗ trợ. - Hiệu suất (Event Pooling): React tái sử dụng các đối tượng
SyntheticEvent
. Thay vì tạo một đối tượng mới cho mỗi sự kiện, React duy trì một pool các đối tượng và tái sử dụng chúng. Sau khi hàm xử lý sự kiện của bạn được gọi, các thuộc tính của đối tượngSyntheticEvent
sẽ bị xóa rỗng (nullified). Điều này có nghĩa là bạn không nên truy cập đối tượng sự kiện một cách bất đồng bộ (ví dụ: trong một hàmsetTimeout
) sau khi hàm xử lý sự kiện chính đã kết thúc, trừ khi bạn gọievent.persist()
. Tuy nhiên, với các phiên bản React hiện đại và Functional Components, sự kiện thường được xử lý đồng bộ nên vấn đề này ít gặp hơn. - Ủy quyền sự kiện (Event Delegation): Như đã giải thích, React lắng nghe sự kiện ở cấp gốc. Điều này giúp giảm số lượng trình lắng nghe sự kiện cần thiết và cải thiện hiệu suất, đặc biệt là với các ứng dụng lớn có nhiều phần tử tương tác.
Đối tượng SyntheticEvent
có cùng giao diện với sự kiện gốc, bao gồm stopPropagation()
và preventDefault()
. Để xem sự kiện gốc của trình duyệt, bạn có thể truy cập thuộc tính nativeEvent
: event.nativeEvent
.
Các Loại Sự Kiện Phổ Biến trong React
React hỗ trợ hầu hết các loại sự kiện DOM truyền thống, bao gồm:
- Mouse Events:
onClick
,onDoubleClick
,onMouseDown
,onMouseUp
,onMouseEnter
,onMouseLeave
,onMouseMove
,onMouseOut
,onMouseOver
,onContextMenu
- Keyboard Events:
onKeyDown
,onKeyPress
,onKeyUp
- Form Events:
onChange
,onInput
,onSubmit
,onFocus
,onBlur
,onSelect
- Touch Events:
onTouchCancel
,onTouchEnd
,onTouchMove
,onTouchStart
- UI Events:
onScroll
- … và nhiều loại khác
Bạn có thể tìm thấy danh sách đầy đủ và chi tiết về từng loại sự kiện trong tài liệu chính thức của React.
Best Practices (Các Thực Hành Tốt Nhất) khi Xử Lý Sự Kiện trong React
Để viết code xử lý sự kiện hiệu quả, dễ đọc và dễ bảo trì trong React, hãy lưu ý các điểm sau:
- Sử dụng Functional Components và Hooks: Đây là cách tiếp cận hiện đại và được khuyến khích. Quản lý state liên quan đến sự kiện thường được thực hiện với
useState
hook, và các tác vụ phụ trợ (side effects) như gửi request API sau sự kiện có thể dùnguseEffect
. - Giữ hàm xử lý sự kiện gần phần tử: Định nghĩa hàm xử lý ngay bên trong Functional Component hoặc là phương thức của Class Component. Điều này giúp code dễ theo dõi hơn.
- Đặt tên hàm rõ ràng: Sử dụng quy ước
handle[EventName][ElementName]
hoặcon[EventName]
, ví dụ:handleButtonClick
,handleChange
,handleSubmit
. - Tránh lạm dụng Arrow Function Inline trong JSX (đối với danh sách lớn): Như đã nói, tạo hàm mới trong mỗi lần render có thể ảnh hưởng hiệu suất nhẹ với danh sách rất dài. Trong các trường hợp này, bạn có thể định nghĩa hàm xử lý bên ngoài component (nếu không cần truy cập state/props) hoặc sử dụng
useCallback
hook để memoize hàm xử lý nếu cần truyền nó xuống component con và tránh re-render không cần thiết của component con (chủ đề này sẽ được đề cập sâu hơn khi nói về tối ưu hiệu suất và Hooks). Tuy nhiên, với các ứng dụng thông thường, arrow function inline là hoàn toàn chấp nhận được. - Hiểu về Event Propagation (Nổi bọt sự kiện): Sự kiện trong React cũng tuân theo cơ chế nổi bọt của DOM. Khi một sự kiện xảy ra trên một phần tử con, nó sẽ nổi bọt lên các phần tử cha. Bạn có thể ngăn chặn điều này bằng cách gọi
event.stopPropagation()
trên đối tượngSyntheticEvent
. Cẩn thận khi sử dụngstopPropagation
vì nó có thể làm cho code khó debug và hiểu hơn. - Quản lý State từ sự kiện: Rất thường xuyên, việc xử lý sự kiện sẽ dẫn đến việc cập nhật state của component. Hãy nhớ rằng việc cập nhật state trong React là bất đồng bộ. Nếu bạn cần sử dụng giá trị state ngay sau khi cập nhật, hãy dùng callback của hàm cập nhật state (ví dụ:
setState(updater, callback)
trong Class Components) hoặc hiểu rõ rằng giá trị state mới chỉ có hiệu lực trong lần render tiếp theo của Functional Component.
Dưới đây là bảng tóm tắt sự khác biệt chính giữa xử lý sự kiện trong JS thuần và React:
Đặc điểm | JavaScript Thuần (Vanilla JS) | React |
---|---|---|
Tên sự kiện | Chữ thường (onclick ) |
camelCase (onClick ) |
Cách gán trình xử lý | Gán hàm hoặc chuỗi code vào thuộc tính on* Dùng element.addEventListener() |
Gán trực tiếp hàm JavaScript vào thuộc tính JSX on* (<button onClick={myFunction}> ) |
Đối tượng sự kiện | Đối tượng sự kiện gốc của trình duyệt | Đối tượng SyntheticEvent (lớp bọc chuẩn hóa) |
Cơ chế lắng nghe | Gắn listener trực tiếp vào từng phần tử (hoặc dùng delegation thủ công) | Sử dụng ủy quyền sự kiện (Event Delegation) ở cấp gốc của ứng dụng |
Từ khóa this trong hàm xử lý (trong Class/Constructor) |
Thường trỏ đến phần tử DOM hoặc window (tùy cách gọi) |
Cần ràng buộc (bind) thủ công (trong Class Components) để trỏ đúng đến instance của component, trừ khi dùng arrow function property. Trong Functional Component thì không có this theo nghĩa này. |
Kết nối với các Chủ đề khác
Việc xử lý sự kiện không chỉ đơn thuần là phản ứng lại hành động người dùng, mà thường là điểm khởi đầu cho các thay đổi trong ứng dụng của bạn. Khi người dùng tương tác, bạn thường cần:
- Cập nhật State của component hoặc ứng dụng. Ví dụ: khi người dùng nhập liệu vào input (sự kiện
onChange
), bạn cập nhật state lưu trữ giá trị đó. Khi người dùng click nút “Like” (sự kiệnonClick
), bạn cập nhật state đếm số lượt like. - Thực hiện các tác vụ phụ trợ (Side Effects) dựa trên sự kiện, như gọi API, lưu dữ liệu vào localStorage. Điều này thường liên quan đến quản lý vòng đời hoặc sử dụng Vòng Đời Component (trong Class) hoặc
useEffect
hook (trong Functional Components). - Thay đổi giao diện dựa trên state đã cập nhật, liên quan đến Conditional Rendering hoặc cập nhật danh sách thông qua làm việc với Danh sách.
Hiểu rõ cách xử lý sự kiện là bước đệm vững chắc để bạn làm chủ luồng dữ liệu và tương tác trong ứng dụng React của mình.
Kết luận
Xử lý sự kiện là một kỹ năng thiết yếu trong phát triển giao diện người dùng, và React cung cấp một cách tiếp cận nhất quán, hiệu quả và “khai báo” để làm điều đó. Bằng cách sử dụng hệ thống SyntheticEvent
, đặt tên sự kiện theo camelCase và truyền trực tiếp các hàm xử lý, bạn có thể dễ dàng thêm tính tương tác vào các component của mình. Với sự chuyển dịch sang Functional Components và Hooks, việc quản lý state và side effects liên quan đến sự kiện cũng trở nên trực quan và dễ quản lý hơn.
Hãy thực hành viết các component đơn giản có khả năng phản hồi lại các hành động của người dùng. Đây là một trong những bước quan trọng để bạn làm chủ React và xây dựng các ứng dụng web hiện đại, năng động.
Trong bài viết tiếp theo của chuỗi “React Roadmap“, chúng ta sẽ đi sâu hơn vào việc xử lý form trong React – một chủ đề thường xuyên liên quan mật thiết đến xử lý sự kiện onChange
và onSubmit
. Đừng bỏ lỡ nhé!
Chúc bạn học tốt và hẹn gặp lại!