Bài 17.3: Form validation trong React-TypeScript

Bài 17.3: Form validation trong React-TypeScript
Chào mừng trở lại với series Lập trình Web Front-end! Hôm nay, chúng ta sẽ đi sâu vào một khía cạnh cực kỳ quan trọng của bất kỳ ứng dụng web nào: Form Validation (Kiểm tra hợp lệ dữ liệu biểu mẫu).
Form validation không chỉ giúp đảm bảo dữ liệu người dùng nhập vào là chính xác và đúng định dạng, mà còn là chìa khóa để nâng cao trải nghiệm người dùng (UX) và tăng cường bảo mật. Một form được validate tốt sẽ thông báo rõ ràng cho người dùng biết họ đã nhập sai ở đâu, giúp họ sửa lỗi dễ dàng hơn.
Khi làm việc với React, đặc biệt là kết hợp với sức mạnh của TypeScript, chúng ta có những công cụ rất mạnh mẽ để xây dựng logic validation sạch sẽ, dễ bảo trì và an toàn hơn. Hãy cùng nhau khám phá cách thực hiện điều đó!
Tại sao Form Validation Lại Quan Trọng?
Hãy nghĩ xem: Nếu người dùng cố gắng đăng ký tài khoản nhưng lại gõ sai định dạng email hoặc để trống các trường bắt buộc, điều gì sẽ xảy ra nếu không có validation?
- Dữ liệu rác: Cơ sở dữ liệu của bạn sẽ chứa dữ liệu không chính xác.
- Lỗi hệ thống: Logic backend có thể gặp lỗi khi xử lý dữ liệu không mong muốn.
- Trải nghiệm người dùng tồi tệ: Người dùng sẽ cảm thấy bực bội khi gửi form và không hiểu tại sao lại thất bại, hoặc phải chờ phản hồi từ server mới biết lỗi.
- Lỗ hổng bảo mật: Dữ liệu không được làm sạch hoặc kiểm tra có thể mở ra các tấn công như Injection.
Validation giúp ngăn chặn những vấn đề này ngay tại phía client, cung cấp phản hồi ngay lập tức cho người dùng.
Cơ Bản Về State và Form trong React
Trong React, các trường nhập liệu (<input>
, <textarea>
, <select>
) thường được quản lý bằng state của component. Chúng ta gọi đây là controlled components. State này sẽ giữ giá trị hiện tại của trường nhập liệu.
Hãy bắt đầu với một ví dụ đơn giản về một input được kiểm soát:
import React, { useState } from 'react';
function SimpleInput() {
const [inputValue, setInputValue] = useState<string>('');
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setInputValue(event.target.value);
console.log('Giá trị hiện tại:', event.target.value);
};
return (
<div>
<label htmlFor="myInput">Nhập gì đó:</label>
<input
id="myInput"
type="text"
value={inputValue}
onChange={handleChange}
/>
</div>
);
}
Giải thích code:
- Chúng ta sử dụng
useState<string>('')
để tạo một stateinputValue
kiểu string, giá trị khởi tạo là chuỗi rỗng. - Hàm
handleChange
được gọi mỗi khi giá trị input thay đổi. event: React.ChangeEvent<HTMLInputElement>
: Đây là lúc TypeScript phát huy tác dụng! Nó định kiểu chính xác cho đối tượng sự kiệnevent
, cho phép chúng ta truy cập các thuộc tính nhưevent.target
một cách an toàn và có gợi ý (autocomplete) từ IDE.event.target.value
lấy giá trị hiện tại của input.setInputValue(...)
cập nhật stateinputValue
.- Input được "kiểm soát" bằng cách gán
value={inputValue}
(giá trị hiển thị lấy từ state) vàonChange={handleChange}
(mỗi thay đổi cập nhật state).
Đây là nền tảng. Bây giờ, làm thế nào để thêm validation vào đây?
Thêm Logic Validation và Hiển Thị Lỗi
Validation bao gồm hai phần chính:
- Logic kiểm tra: Viết code để xác định xem giá trị nhập vào có hợp lệ hay không (ví dụ: không rỗng, là email hợp lệ, là số, v.v.).
- Hiển thị thông báo lỗi: Nếu không hợp lệ, hiển thị thông báo lỗi rõ ràng cho người dùng.
Chúng ta sẽ cần thêm một state nữa để lưu trữ thông báo lỗi.
import React, { useState } from 'react';
function ValidatedInput() {
const [value, setValue] = useState<string>('');
const [error, setError] = useState<string | null>(null); // State lưu lỗi, ban đầu là null
// Hàm validation đơn giản: kiểm tra không rỗng
const validate = (val: string): string | null => {
if (!val.trim()) { // trim() loại bỏ khoảng trắng ở đầu/cuối
return 'Trường này không được để trống.';
}
// Thêm các rule khác ở đây nếu cần
return null; // Không có lỗi
};
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setValue(event.target.value);
// Tùy chọn: Xóa lỗi ngay khi người dùng bắt đầu gõ lại
if (error) {
setError(null);
}
};
// Validate khi người dùng rời khỏi trường nhập (blur)
const handleBlur = () => {
const validationError = validate(value);
setError(validationError);
};
return (
<div>
<label htmlFor="validatedInput">Tên người dùng:</label>
<input
id="validatedInput"
type="text"
value={value}
onChange={handleChange}
onBlur={handleBlur} // Thêm sự kiện onBlur
className={error ? 'input-error' : ''} // Thêm class CSS để highlight input lỗi
/>
{error && <p style={{ color: 'red', fontSize: '0.9em' }}>{error}</p>} {/* Hiển thị lỗi nếu có */}
</div>
);
}
Giải thích code:
useState<string | null>(null)
: Stateerror
có thể là một chuỗi (thông báo lỗi) hoặcnull
(không có lỗi). TypeScript giúp ta định rõ điều này.- Hàm
validate(val: string): string | null
nhận giá trị input và trả về lỗi (chuỗi) hoặcnull
. Việc định kiểu trả vềstring | null
rất quan trọng. handleChange
: Cập nhật giá trị và tùy chọn xóa lỗi cũ ngay khi người dùng gõ để phản hồi nhanh hơn.handleBlur
: Sự kiện này xảy ra khi input mất focus. Đây là một thời điểm tốt để chạy validation và hiển thị lỗi.onBlur={handleBlur}
: Gắn hàm xử lý vào sự kiệnonBlur
.className={error ? 'input-error' : ''}
: Một cách phổ biến để thêm class CSS (ví dụ: border màu đỏ) vào input khi có lỗi.{error && <p>...</p>}
: Đây là cú pháp render có điều kiện của React. Đoạn code hiển thị<p>
chứa thông báo lỗi chỉ khierror
có giá trị (khácnull
).
Tuyệt vời! Chúng ta đã có thể validate một trường đơn giản. Nhưng một form thường có nhiều trường. Làm thế nào để quản lý state và lỗi cho nhiều input?
Quản Lý Form Với Nhiều Trường
Khi form của bạn có nhiều trường, việc sử dụng nhiều useState
riêng lẻ cho từng trường có thể trở nên cồng kềnh. Cách phổ biến và hiệu quả hơn là sử dụng một object trong state để quản lý toàn bộ dữ liệu form và một object khác để quản lý toàn bộ các lỗi.
Đây là lúc TypeScript thực sự tỏa sáng, giúp định nghĩa cấu trúc của dữ liệu form và object lỗi.
import React, { useState } from 'react';
// Định nghĩa kiểu dữ liệu cho form
interface FormData {
name: string;
email: string;
password: string;
}
// Định nghĩa kiểu dữ liệu cho lỗi (mỗi key tương ứng với 1 trường, giá trị là lỗi hoặc undefined)
interface FormErrors {
name?: string; // Sử dụng ? để chỉ ra rằng trường này có thể không có lỗi (undefined)
email?: string;
password?: string;
}
function MultiFieldForm() {
// State chứa dữ liệu form
const [formData, setFormData] = useState<FormData>({
name: '',
email: '',
password: '',
});
// State chứa lỗi cho từng trường
const [errors, setErrors] = useState<FormErrors>({});
// Hàm validation cho toàn bộ form data
const validateForm = (data: FormData): FormErrors => {
const newErrors: FormErrors = {}; // Khởi tạo object lỗi rỗng
// Rule validation cho name
if (!data.name.trim()) {
newErrors.name = 'Tên không được để trống.';
}
// Rule validation cho email
if (!data.email.trim()) {
newErrors.email = 'Email không được để trống.';
} else if (!/\S+@\S+\.\S+/.test(data.email)) { // Regex kiểm tra định dạng email cơ bản
newErrors.email = 'Email không hợp lệ.';
}
// Rule validation cho password
if (!data.password) {
newErrors.password = 'Mật khẩu không được để trống.';
} else if (data.password.length < 6) {
newErrors.password = 'Mật khẩu phải ít nhất 6 ký tự.';
}
return newErrors; // Trả về object chứa tất cả lỗi tìm được
};
// Xử lý thay đổi input chung cho tất cả các trường
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = event.target; // Lấy 'name' và 'value' của input
setFormData({
...formData, // Giữ lại các giá trị cũ của form
[name]: value, // Cập nhật giá trị mới cho trường có 'name' tương ứng
});
// Tùy chọn: Xóa lỗi của trường hiện tại ngay khi người dùng gõ lại
// if (errors[name as keyof FormErrors]) { // 'as keyof FormErrors' là một cách để TypeScript hiểu 'name' là một key hợp lệ
// setErrors({
// ...errors,
// [name as keyof FormErrors]: undefined, // Đặt lỗi của trường đó thành undefined
// });
// }
};
// Xử lý submit form
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault(); // Ngăn chặn hành vi submit mặc định của trình duyệt (load lại trang)
const validationErrors = validateForm(formData); // Chạy validation cho toàn bộ form
setErrors(validationErrors); // Cập nhật state lỗi
// Kiểm tra xem object lỗi có rỗng không (tức là không có lỗi nào)
if (Object.keys(validationErrors).length === 0) {
// Form hợp lệ, thực hiện hành động submit (ví dụ: gọi API)
alert('Form hợp lệ! Dữ liệu đã sẵn sàng gửi đi.');
console.log('Dữ liệu gửi đi:', formData);
// Tại đây bạn sẽ gọi API hoặc thực hiện các thao tác khác...
} else {
// Form không hợp lệ, các lỗi đã được hiển thị
console.log('Form có lỗi:', validationErrors);
}
};
return (
<form onSubmit={handleSubmit}>
<h2>Đăng ký tài khoản</h2>
<div>
<label htmlFor="name">Tên:</label>
<input
id="name"
type="text"
name="name" // Quan trọng: name phải khớp với key trong state formData
value={formData.name}
onChange={handleChange}
className={errors.name ? 'input-error' : ''}
/>
{errors.name && <p style={{ color: 'red', fontSize: '0.9em' }}>{errors.name}</p>}
</div>
<div>
<label htmlFor="email">Email:</label>
<input
id="email"
type="email"
name="email" // Quan trọng: name phải khớp với key trong state formData
value={formData.email}
onChange={handleChange}
className={errors.email ? 'input-error' : ''}
/>
{errors.email && <p style={{ color: 'red', fontSize: '0.9em' }}>{errors.email}</p>}
</div>
<div>
<label htmlFor="password">Mật khẩu:</label>
<input
id="password"
type="password"
name="password" // Quan trọng: name phải khớp với key trong state formData
value={formData.password}
onChange={handleChange}
className={errors.password ? 'input-error' : ''}
/>
{errors.password && <p style={{ color: 'red', fontSize: '0.9em' }}>{errors.password}</p>}
</div>
<button type="submit">Đăng ký</button>
</form>
);
}
Giải thích code:
- Interfaces
FormData
vàFormErrors
: Đây là nơi TypeScript thể hiện sức mạnh. Chúng ta định nghĩa rõ ràng cấu trúc dữ liệu của form và cấu trúc object chứa lỗi.?
trongFormErrors
chỉ ra rằng một trường có thể có lỗi (string
) hoặc không có lỗi (undefined
). Điều này giúp code an toàn và dễ đọc hơn rất nhiều. useState<FormData>(...)
vàuseState<FormErrors>(...)
: State giờ đây là object tuân thủ các interface đã định nghĩa.validateForm(data: FormData): FormErrors
: Hàm validation này nhận toàn bộ objectformData
và trả về objectFormErrors
chứa tất cả lỗi tìm thấy. Logic validation cho từng trường được viết tập trung tại đây.handleChange(event: React.ChangeEvent<HTMLInputElement>)
: Hàm này xử lý thay đổi cho tất cả các input.const { name, value } = event.target;
: Lấy thuộc tínhname
(tên của input) vàvalue
(giá trị hiện tại) từ sự kiện. Điều này yêu cầu các input của bạn phải có thuộc tínhname
khớp với các key trongformData
.setFormData({ ...formData, [name]: value });
: Sử dụng spread syntax (...formData
) để sao chép tất cả các giá trị hiện có trongformData
, sau đó dùng cú pháp computed property names[name]: value
để ghi đè (cập nhật) giá trị cho trường có tênname
tương ứng.- Phần tùy chọn xóa lỗi trong
handleChange
cũng sử dụngname
để xác định lỗi cần xóa.
handleSubmit(event: React.FormEvent<HTMLFormElement>)
:event.preventDefault();
: Quan trọng! Ngăn trình duyệt gửi form theo cách truyền thống (gây load lại trang).const validationErrors = validateForm(formData);
: Gọi hàm validation.setErrors(validationErrors);
: Cập nhật stateerrors
. Điều này sẽ khiến component render lại và hiển thị các thông báo lỗi mới.if (Object.keys(validationErrors).length === 0)
: Kiểm tra số lượng key trong objectvalidationErrors
. Nếu bằng 0, tức là không có lỗi nào, form hợp lệ và bạn có thể tiến hành gửi dữ liệu (ví dụ: gọi API sử dụngfetch
hoặcaxios
). Nếu có lỗi, logic tiếp theo sẽ bị bỏ qua và người dùng thấy các thông báo lỗi.
Khi Nào Nên Validate?
- On Change: Validate khi người dùng gõ ký tự. Phản hồi rất nhanh, nhưng có thể gây phiền phức nếu quy tắc phức tạp (ví dụ: mật khẩu yêu cầu ký tự đặc biệt, số, chữ hoa - hiển thị lỗi ngay khi gõ ký tự đầu tiên là chữ thường không có ý nghĩa).
- On Blur: Validate khi người dùng rời khỏi trường nhập. Cân bằng tốt giữa phản hồi nhanh và không quá gây phiền phức. Thường dùng cho các trường như email, username.
- On Submit: Luôn luôn cần validate khi người dùng nhấn nút submit. Đây là "lưới an toàn" cuối cùng trước khi xử lý dữ liệu. Logic này đảm bảo người dùng không thể gửi form với dữ liệu không hợp lệ, kể cả khi họ bỏ qua các trường mà không làm blur.
Trong ví dụ trên, chúng ta chủ yếu áp dụng validation on submit. Bạn có thể kết hợp thêm validation on blur cho từng trường để cải thiện UX.
Comments