Xử lý các sự kiện trong DOM và Window bằng React
- 13-10-2022
- Toanngo92
- 0 Comments
Trong phát triển web, các event (sự kiện) đại diện cho các hành động xảy ra trong trình duyệt web. Bằng cách phản hồi các sự kiện với trình xử lý sự kiện, bạn có thể tạo các ứng dụng JavaScript động phản hồi bất kỳ hành động nào của người dùng, bao gồm nhấp bằng chuột, cuộn dọc trang web, chạm vào màn hình cảm ứng, v.v.
Trong ứng dụng React, bạn có thể sử dụng trình xử lý sự kiện để cập nhật dữ liệu state, kích hoạt các thay đổi hỗ trợ hoặc ngăn các hành động mặc định của trình duyệt. Để làm điều này, React sử dụng một trình bao bọc SyntheticEvent thay vì interface event gốc. SyntheticEvent mô phỏng chặt chẽ sự kiện trình duyệt tiêu chuẩn, nhưng cung cấp hành vi nhất quán hơn cho các trình duyệt web khác nhau. React cũng cung cấp cho bạn các công cụ để thêm và xóa trình nghe sự kiện Window một cách an toàn khi một component gắn và ngắt kết nối khỏi Document Object Model (DOM), cho phép bạn kiểm soát các sự kiện của Window đồng thời ngăn chặn rò rỉ bộ nhớ từ các trình lắng nghe bị xóa không đúng cách.
Trong hướng dẫn này, bạn sẽ học cách xử lý các sự kiện trong React. Bạn sẽ xây dựng một số thành phần mẫu xử lý các sự kiện của người dùng, bao gồm thành phần đầu vào tự xác thực và chú giải công cụ cung cấp thông tin cho biểu mẫu đầu vào. Trong suốt hướng dẫn, chúng ta sẽ học cách thêm trình xử lý sự kiện vào các component, lấy thông tin từ SyntheticEvent và thêm và xóa trình xử lý sự kiện Window. Đến cuối hướng dẫn này, bạn sẽ có thể làm việc với nhiều trình xử lý sự kiện khác nhau và áp dụng danh mục các sự kiện được React hỗ trợ.
Mục lục
Trích xuất dữ liệu sự kiện với SyntheticEvent
Trong bước này, bạn sẽ tạo thành phần xác thực bằng cách sử dụng phần tử HTML và trình xử lý sự kiện onChange. Thành phần này sẽ chấp nhận đầu vào và xác thực nó hoặc đảm bảo rằng nội dung tuân theo một mẫu văn bản cụ thể. Bạn sẽ sử dụng trình bao bọc SyntheticEvent để chuyển dữ liệu sự kiện vào hàm callback và cập nhật component bằng cách sử dụng dữ liệu từ . Bạn cũng sẽ gọi các hàm từ SyntheticEvent, chẳng hạn như PreventDefault để ngăn các hành động chuẩn của trình duyệt.
Trong React, bạn không cần phải chọn các phần tử trước khi thêm trình xử lý sự kiện. Thay vào đó, bạn thêm trình xử lý sự kiện trực tiếp vào JSX của mình bằng cách sử dụng đạo cụ. Có một số lượng lớn các sự kiện được hỗ trợ trong React, bao gồm các sự kiện phổ biến như onClick hoặc onChange và các sự kiện ít phổ biến hơn như onWheel.
Không giống như các trình xử lý onevent DOM gốc, React chuyển một trình bao bọc đặc biệt có tên là SyntheticEvent tới trình xử lý sự kiện chứ không phải là Event của trình duyệt gốc. Tính trừu tượng giúp giảm sự không nhất quán giữa các trình duyệt và cung cấp cho các component của bạn một interface chuẩn để làm việc với các sự kiện. API cho SyntheticEvent tương tự như Event gốc, vì vậy hầu hết các tác vụ đều được thực hiện theo cùng một cách.
Để chứng minh điều này, bạn sẽ bắt đầu bằng cách thực hiện validate input trong form. Đầu tiên, chúng ta sẽ tạo một component có tên là FileNamer. Đây sẽ là một phần tử với input để mô phỏng việc đặt tên cho một tệp. Khi điền thông tin đầu vào, bạn sẽ thấy thông tin box preview được cập nhật phía trên component này. Component cũng sẽ bao gồm một nút save để validate, nếu thành công, thực sự form không gửi gì đi mà chỉ thông báo cho người dùng rằng đã validate thành công.
Tạo dự án mới:
npx create-react-app reactdomdemo
Tạo file components/FileNamer/FileNamer.js như sau:
import React, { useState } from 'react';
export default function FileNamer() {
const [name,setName] = useState('');
return (
<div className="wrapper">
<div className="preview">
<h2>Preview: {name}.js</h2>
</div>
<form>
<label>
<p>Name:</p>
<input name="name" />
</label>
<div>
<button>Save</button>
</div>
</form>
</div>
)
}
Sửa file App.js:
import logo from './logo.svg';
import './App.css';
import FileNamer from './component/FileNamer/FileNamer';
function App() {
return (
<FileNamer />
);
}
export default App;
Output:
Trong đoạn mã trên, chúng ta destructure useState thành một tên biến để giữ dữ liệu đầu vào và một hàm có tên setName để cập nhật dữ liệu. Sau đó, hiển thị tên trong box preview, theo sau là phần mở rộng .js, mô phỏng người dùng đang đặt tên tệp.
Giờ chúng ta thêm trình xử lý sự kiện cho input, tình huống này là onChange, sẽ kích hoạt mỗi khi component thay đổi. Tuy nhiên, bạn cũng có thể sử dụng các sự kiện bàn phím, chẳng hạn như onKeyDown, onKeyPress và onKeyUp. Sự khác biệt chủ yếu liên quan đến thời điểm sự kiện kích hoạt và thông tin được chuyển đến đối tượng SyntheticEvent.
Lựa chọn sự kiện của bạn cũng được xác định bởi loại dữ liệu bạn muốn chuyển đến SyntheticEvent. Ví dụ: sự kiện onKeyPress sẽ bao gồm Mã ký tự của phím mà người dùng đã nhấn, trong khi onChange sẽ không bao gồm mã ký tự cụ thể, nhưng sẽ bao gồm toàn bộ dữ liệu đầu vào. Điều này rất quan trọng nếu bạn muốn thực hiện các hành động khác nhau tùy thuộc vào phím mà người dùng đã nhấn.
Đối với hướng dẫn này, hãy sử dụng onChange để nắm bắt toàn bộ giá trị đầu vào chứ không chỉ khóa gần đây nhất. Điều này sẽ giúp bạn tiết kiệm công sức lưu trữ và nối giá trị trên mỗi thay đổi.
Sửa file components/FileNamer/FileNamer.js như sau:
import React, { useState } from 'react';
export default function FileNamer() {
const [name,setName] = useState('');
const changeInput = (event) => {
setName(event.target.value)
}
return (
<div className="wrapper">
<div className="preview">
<h2>Preview: {name}.js</h2>
</div>
<form>
<label>
<p>Name:</p>
<input name="name" onChange={(event) => changeInput(event)} />
</label>
<div>
<button>Save</button>
</div>
</form>
</div>
)
}
Output:
Tại thời điểm này, chúng ta có một trình xử lý sự kiện đang hoạt động. Chúng ta lấy thông tin người dùng nhập vào, lưu vào state và cập nhật thành phần khác với dữ liệu đó. Nhưng ngoài việc lấy thông tin từ một sự kiện, có những trường hợp bạn cần phải tạm dừng một sự kiện, chẳng hạn như nếu bạn muốn ngăn việc gửi form hoặc ngăn hành động nhấn phím chúng ta gọi hành động PreventDefault đối với sự kiện đó tương tự javascript tiêu chuẩn. Điều này sẽ ngăn trình duyệt thực hiện hành vi mặc định.
Trong trường hợp của thành phần FileNamer, có một số ký tự nhất định có thể phá vỡ quy trình chọn tệp mà ứng dụng của bạn nên cấm. Ví dụ: bạn sẽ không muốn người dùng thêm dấu * vào tên tệp vì nó xung đột với ký tự đại diện, ký tự này có thể được hiểu là để chỉ một nhóm tệp khác. Trước khi người dùng có thể gửi form, chúng ta sẽ cần kiểm tra để đảm bảo không có ký tự không hợp lệ. Nếu có một ký tự không hợp lệ, bạn sẽ ngăn trình duyệt gửi form và hiển thị thông báo cho người dùng.
Chúng ta tiến hành sửa file như sau:
import React, { useState } from 'react';
export default function FileNamer() {
const [name,setName] = useState('');
const [warning,setWarning] = useState(false);
const changeInput = (event) => {
setName(event.target.value)
}
const validateInput = (event) => {
if(/\*/.test(name)) {
event.preventDefault();
setWarning(true);
return;
}
setWarning(false);
}
return (
<div className="wrapper">
<div className="preview">
<h2>Preview: {name}.js</h2>
</div>
<form>
<label>
<p>Name:</p>
<input name="name" onChange={(event) => changeInput(event)} />
</label>
{warning && <label>Can not use character *</label>}
<div>
<button onClick={(event) => validateInput(event)}>Save</button>
</div>
</form>
</div>
)
}
Output:
Ở ví dụ phía trên, mình vừa mô phỏng một đoạn validate đơn giản, yêu cầầu người dùng tránh những ký tự *, nếu thỏa mãn yêu cầu thì để form submit như bình thường.
Thêm nhiều trình xử lý sự kiện vào cùng một phần tử
Có những trường hợp khi một thành phần duy nhất sẽ kích hoạt nhiều sự kiện và bạn sẽ cần có thể kết nối với các sự kiện khác nhau trên một thành phần. Ví dụ: trong bước này, bạn sẽ sử dụng trình xử lý sự kiện onFocus và onBlur để cung cấp cho người dùng thông tin kịp thời về thành phần. Đến cuối bước này, bạn sẽ biết thêm về các sự kiện được hỗ trợ khác nhau trong React và cách thêm chúng vào các component của bạn.
Hàm validate sử dụng để ngăn biểu mẫu của bạn gửi dữ liệu xấu, nhưng không hữu ích lắm cho trải nghiệm người dùng: Người dùng chỉ nhận được thông tin về các ký tự hợp lệ sau khi họ đã điền vào toàn bộ biểu mẫu. Nếu có nhiều trường, nó sẽ không cung cấp cho người dùng bất kỳ phản hồi nào cho đến bước cuối cùng. Để làm cho component này thân thiện hơn với người dùng, hãy hiển thị các ký tự được phép và không được phép khi người dùng vào trường bằng cách thêm trình xử lý sự kiện onFocus.
Chúng ta update file như sau:
import React, { useState } from 'react';
export default function FileNamer() {
const [name,setName] = useState('');
const [warning,setWarning] = useState(false);
const changeInput = (event) => {
setName(event.target.value)
}
const validateInput = (event) => {
if(/\*/.test(name)) {
event.preventDefault();
setWarning(true);
return;
}
setWarning(false);
}
return (
<div className="wrapper">
<div className="preview">
<h2>Preview: {name}.js</h2>
</div>
<form>
<label>
<p>Name:</p>
<input onKeyUp={(event) => validateInput(event)} name="name" onChange={(event) => changeInput(event)} />
</label>
{warning && <div>
<span role="img" aria-label="allowed">✅</span> Alphanumeric Characters
<br />
<span role="img" aria-label="not allowed">⛔️</span> *
</div>}
<div>
<button onClick={(event) => validateInput(event)}>Save</button>
</div>
</form>
</div>
)
}
Xét tình huống này, mình sử dụng onKeyUp để validate ngay tại thời điểm nhập liệu. validate sẽ thân thiện hơn với người dùng. Chúng ta áp dụng tương tự với các tình huống khác.
Thêm sự kiện Window
Trong bước này, chúng ta sẽ đưa thông tin người dùng vào một component và sẽ kích hoạt khi người dùng tập trung một đầu vào và sẽ đóng khi người dùng nhấp vào bất kỳ nơi nào khác trên trang. Để làm được hiệu ứng này, chúng ta sẽ thêm trình nghe sự kiện toàn cục vào Window
object bằng cách sử dụng useEffect
Hook . Chúng ta cũng cần xóa trình xử lý sự kiện khi thành phần ngắt kết nối để tránh rò rỉ bộ nhớ, khi ứng dụng của bạn chiếm nhiều bộ nhớ hơn mức cần thiết.
Khi kết thúc bước này, bạn sẽ có thể thêm và xóa trình nghe sự kiện một cách an toàn trên các component riêng lẻ. Chúng ta cũng sẽ nói về cách sử dụng useEffectHook để thực hiện các hành động khi một thành phần kích hoạt và ngắt kết nối.
Trong hầu hết các trường hợp, chúng ta sẽ thêm trình xử lý sự kiện trực tiếp vào các phần tử DOM trong JSX của mình. Điều này giữ cho mã tập trung và ngăn ngừa các tình huống khó hiểu trong đó một thành phần đang kiểm soát hành vi của component khác thông qua window object. Nhưng có những lúc bạn sẽ cần thêm trình nghe sự kiện toàn cục. Ví dụ: bạn có thể muốn một trình nghe cuộn tải nội dung mới hoặc bạn có thể muốn nắm bắt các sự kiện nhấp chuột bên ngoài một component.
Trong hướng dẫn này, chúng ta chỉ muốn hiển thị cho người dùng thông tin về đầu vào nếu họ yêu cầu cụ thể. Sau khi hiển thị thông tin, bạn sẽ muốn ẩn thông tin đó bất cứ khi nào người dùng nhấp vào khu vực bên ngoài component.
Chúng ta tiến hành sửa file components/FileNamer/FileNamer.js như sau:
import React, { useState } from 'react';
export default function FileNamer() {
const [name, setName] = useState('');
const [warning, setWarning] = useState(false);
const changeInput = (event) => {
setName(event.target.value)
}
const validateInput = (event) => {
if (/\*/.test(name)) {
event.preventDefault();
setWarning(true);
return;
}
setWarning(false);
}
return (
<div className="wrapper">
<div className="preview">
<h2>Preview: {name}.js</h2>
</div>
<form>
<label>
<p>Name:</p>
<input name="name" onChange={(event) => changeInput(event)} />
</label>
<div className="information-wrapper">
<button
className="information"
onClick={() => setWarning(true)}
type="button"
>
more information
</button>
{warning && <div className='warning-popup'>
<span role="img" aria-label="allowed">✅</span> Alphanumeric Characters
<br />
<span role="img" aria-label="not allowed">⛔️</span> *
</div>}
</div>
<div>
<button onClick={(event) => validateInput(event)}>Save</button>
</div>
</form>
</div>
)
}
Sửa luôn file App.css (tình huống này mình lười tạo file FileNamer.css nên sử dụng App.css để định kiểu luôn):
.information {
font-size: .75em;
color: blue;
cursor: pointer;
}
.wrapper button.information {
border: none;
}
.information-wrapper {
position: relative;
}
.warning-popup {
position: absolute;
background: white;
border: 1px darkgray solid;
padding: 10px;
top: -70px;
left: 0;
}
Output:
Ở đây, chúng ta thấy sau khi bấm vào more information, thông tin validate được hiện lên, mình muốn khi bấm ra bên ngoài khối html này, popup sẽ nên được tắt đi, với tình huống này mình sẽ sử dụng window object của javascript tiêu chuẩn để giải quyết, cấu trúc có thể như sau:
import React, { useState } from 'react';
export default function FileNamer() {
const [name, setName] = useState('');
const [warning, setWarning] = useState(false);
const changeInput = (event) => {
setName(event.target.value)
}
const validateInput = (event) => {
if (/\*/.test(name)) {
event.preventDefault();
setWarning(true);
return;
}
setWarning(false);
}
window.addEventListener('click', (event) => {
if(event.target != document.querySelector('.information')){
setWarning(false)
}
})
return (
<div className="wrapper">
<div className="preview">
<h2>Preview: {name}.js</h2>
</div>
<form>
<label>
<p>Name:</p>
<input name="name" onChange={(event) => changeInput(event)} />
</label>
<div className="information-wrapper">
<button
className="information"
onClick={() => setWarning(true)}
type="button"
>
more information
</button>
{warning && <div className='warning-popup'>
<span role="img" aria-label="allowed">✅</span> Alphanumeric Characters
<br />
<span role="img" aria-label="not allowed">⛔️</span> *
</div>}
</div>
<div>
<button onClick={(event) => validateInput(event)}>Save</button>
</div>
</form>
</div>
)
}
Output vẫn sẽ hiển thị đúng, tuy nhiên chúng ta cần lưu ý về thời điểm đặt trình xử lý sự kiện trong mã của mình. Ví dụ: không thể thêm trình xử lý sự kiện ở đầu mã component như đoạn mã trên, vì sau đó mỗi khi có gì đó thay đổi, thành phần sẽ re render và thêm trình xử lý sự kiện mới. Vì component của bạn có thể sẽ hiển thị lại nhiều lần, điều đó sẽ tạo ra rất nhiều trình nghe sự kiện không sử dụng, chiếm bộ nhớ, gây ra tình trạng memory leak.
Để giải quyết vấn đề này, React có một Hook đặc biệt được gọi là useEffect sẽ chỉ chạy khi các thuộc tính cụ thể thay đổi. Cấu trúc cơ bản là:
useEffect(() => {
// run code when anything in the array changes
}, [someProp, someOtherProp])
Cách hiểu là: React sẽ chạy mã trong hàm anonymous bất cứ khi nào someProp hoặc someOtherProp thay đổi. Các mục trong mảng được gọi là dependency . Hook này lắng nghe các thay đổi đối với các dependency và sau đó chạy các hàm sau khi thay đổi.
Giờ đây, bạn có các công cụ để thêm và xóa trình xử lý sự kiện toàn cục một cách an toàn bằng cách sử dụng useEffect để thêm trình xử lý sự kiện bất cứ khi nào warning true và xóa nó bất cứ khi warning nào false.
Còn một bước quan trọng là: Khi component ngắt kết nối, nó sẽ chạy bất kỳ hàm nào nào mà bạn trả về từ bên trong useEffect Hook của mình. Do đó, bạn cũng sẽ cần trả về một hàm loại bỏ trình xử lý sự kiện khi component ngắt kết nối.
Cấu trúc chính xác như sau:
useEffect(() => {
// run code when anything in the array changes
return () => {} // run code when the component unmounts
}, [someProp, someOtherProp])
Bây giờ, chúng ta sẽ tiến hành đưa useEffect vào component để xử lý popup trên:
import React, { useEffect, useState } from 'react';
export default function FileNamer() {
const [name, setName] = useState('');
const [warning, setWarning] = useState(false);
useEffect(() => {
const handleWindowClick = (event) => {
if(event.target !== document.querySelector('.information')){
setWarning(false)
}else{
setWarning(true)
}
};
if (warning) {
window.addEventListener('click', handleWindowClick);
}
else {
window.removeEventListener('click', handleWindowClick);
}
return () => window.removeEventListener('click', handleWindowClick);
}, [warning, setWarning])
const changeInput = (event) => {
setName(event.target.value)
}
const validateInput = (event) => {
if (/\*/.test(name)) {
event.preventDefault();
setWarning(true);
return;
}
setWarning(false);
}
return (
<div className="wrapper">
<div className="preview">
<h2>Preview: {name}.js</h2>
</div>
<form>
<label>
<p>Name:</p>
<input name="name" onChange={(event) => changeInput(event)} />
</label>
<div className="information-wrapper">
<button
className="information"
onClick={() => setWarning(true)}
type="button"
>
more information
</button>
{warning && <div className='warning-popup'>
<span role="img" aria-label="allowed">✅</span> Alphanumeric Characters
<br />
<span role="img" aria-label="not allowed">⛔️</span> *
</div>}
</div>
<div>
<button onClick={(event) => validateInput(event)}>Save</button>
</div>
</form>
</div>
)
}
useEffect Hook đã thêm và xóa thành công trình xử lý sự kiện toàn cục dựa trên tương tác của người dùng. Nó không bị ràng buộc với một phần tử DOM cụ thể, nhưng thay vào đó nó được kích hoạt bởi sự thay đổi trong state component.
Tổng kết
Trình xử lý sự kiện (event handling) cung cấp cho bạn cơ hội để điều chỉnh các component với các hành động của người dùng. Những điều này sẽ mang lại cho ứng dụng của bạn trải nghiệm phong phú và sẽ tăng khả năng tương tác của ứng dụng. Chúng cũng sẽ cung cấp cho bạn khả năng nắm bắt và phản hồi các hành động của người dùng.
Các trình xử lý sự kiện của React cho phép bạn tích hợp các lệnh gọi lại sự kiện của mình với HTML để bạn có thể chia sẻ chức năng và thiết kế trên một ứng dụng. Trong hầu hết các trường hợp, bạn nên tập trung vào việc thêm trình xử lý sự kiện trực tiếp vào các phần tử DOM, nhưng trong các tình huống cần nắm bắt các sự kiện bên ngoài component, bạn có thể thêm trình xử lý sự kiện và dọn dẹp chúng khi chúng không còn được sử dụng để tránh rò rỉ bộ nhớ và tạo các ứng dụng hiệu quả.