Nếu bạn đã từng làm việc với các form trên website (ví dụ như: form đăng ký, đăng nhập, đặt hàng, cập nhật thông tin cá nhân,...), hẳn bạn hiểu việc validation là một phần quan trọng. Bạn muốn đảm bảo dữ liệu người dùng nhập vào phải đúng format, không được để trống các trường quan trọng, địa chỉ email phải hợp lệ, số điện thoại đủ độ dài, mật khẩu đủ độ phức tạp.
Có rất nhiều thư viện hỗ trợ cho việc validation form. Một trong số những lựa chọn phổ biến và linh hoạt chính là Yup. Yup là một thư viện giúp bạn định nghĩa schema để xác thực dữ liệu một cách đơn giản, dễ đọc, và mạnh mẽ. Nó tích hợp tốt với TypeScript, React Hook Form, Formik,... Với Yup, việc tách phần logic validation ra khỏi component trở nên dễ dàng, giúp code của bạn rõ ràng hơn.
Bài viết này mình sẽ đi qua từng phần từ khái niệm cơ bản về Yup, tại sao nên sử dụng Yup, cách áp dụng Yup để định nghĩa các schema validation. Mình cũng sẽ hướng dẫn bạn cách kết hợp Yup với React Hook Form - một combo mình thấy khá phổ biến hiện nay và được ưa chuộng rất nhiều.
1. Yup là gì?
Yup là một Javascript object schema validator cho phép bạn xây dựng schema validation. Schema ở đây có thể hiểu là một mô tả định dạng, kiểu dữ liệu, và quy tắc hợp lệ của dữ liệu.
Thay vì viết logic validation thủ công như "nếu email không match regex thì trả ra lỗi", "nếu password ngắn hơn 8 ký tự thì báo lỗi", sử dụng Yup để định nghĩa schema một cách khai báo (declarative). Sau đó Yup sẽ kiểm tra dữ liệu dựa trên schema này.
Lợi ích của Yup:
- Yup dùng một cú pháp dạng builder, bạn có thể liên tiếp gọi các hàm như: string().required(), email(), min(8),... để định nghĩa. Điều này giúp việc đọc code validation dễ hơn rất nhiều.
- Yup hỗ trợ các kiểu dữ liệu cơ bản như: string, number, boolean, date, array, object,.... Bạn có thể dễ dàng kết hợp chúng để tạo ra schema.
- Yup cung cấp type definition, bạn có thể dễ dàng kiểm soát kiểu dữ liệu đầu vào từ schema Yup.
- Yup rất dễ dàng để được tích hợp sử dụng với Formik, React Hook Form,...
- Bạn có thể dễ dàng viết custom các quy tắc validation, custom error message, hay kết hợp Yup với
i18n
để hiển thị thông báo bằng nhiều ngôn ngữ khác nhau.
2. Tại sao nên dùng Yup?
Khi mới bắt đầu làm form trong React, bạn có thể làm validation thủ công như sau: Trong hàm onSubmit, bạn lấy dữ liệu từ state (hoặc từ form), sau đó check từng trường, nếu không hợp lệ thì set error state, nếu ổn thì submit. Cách này có thể hoạt động với form nhỏ, nhưng nếu form phức tạp, bạn sẽ nhanh chóng thấy validation logic trở nên siêu rối. Bạn phải lặp lại check điều kiện, phải handle nhiều case, code validation đôi khi nằm rải rác khắp nơi.
Yup giúp bạn gom các logic validation vào chung một nơi duy nhất - schema. Bạn định nghĩa schema một lần, sau đó chỉ việc gọi schema.validate(values) để Yup trả về kết quả: hoặc là dữ liệu đã được kiểm duyệt (cleansed data). Điều này giúp code clean hơn, dễ bảo trì và mở rộng về sau.
3. Định nghĩa schema cơ bản với Yup
Mình sẽ bắt đầu với một ví dụ đơn giản, hay sử dụng nhất nha. Giả sử bạn có một form đăng ký tài khoản với 3 trường:
- email: đây là trường bắt buộc có giá trị và phải là email hợp lệ.
- password: cũng là trường bắt buộc và phải chứa ít nhất 8 ký tự.
- confirmPassword: là trường bắt buộc, phải trùng khớp với password.
Dựa trên 3 yêu cầu như trên, mình sẽ đi xây dựng schema như sau:
import * as yup from 'yup';
// -----------------------------------------
const signupSchema = yup.object({
email: yup.string().email('Email không hợp lệ').required('Vui lòng nhập email'),
password: yup.string().min(8, 'Mật khẩu phải có ít nhất 8 ký tự').required('Vui lòng nhập mật khẩu'),
confirmPassword: yup
.string()
.oneOf([yup.ref('password'), null], 'Mật khẩu xác nhận không khớp')
.required('Vui lòng xác nhận mật khẩu'),
});
Ở đây, mình dùng yup.object({...})
để định nghĩa schema cho object. Mỗi key trong object schema là schema con (string, number,…). Yup cung cấp nhiều method sẵn như: string(), number(), boolean(), array(), object(),.... Sau khi chọn kiểu dữ liệu bạn mong muốn.
Sau khi xây dựng một schema như trên, tiếp đến là sử dụng validate data theo schema, bạn có thể làm như bên dưới thế này:
async function validateFormData(formData: { email: string; password: string; confirmPassword: string }) {
try {
const validatedData = await signupSchema.validate(formData, { abortEarly: false });
console.log('Dữ liệu hợp lệ:', validatedData);
} catch (err) {
if (err instanceof yup.ValidationError) {
console.log('Có lỗi xảy ra:', err.errors);
}
}
}
Ở đây, bạn có thể thấy, mình có thêm một option {abortEarly: false}
option này để Yup trả về tất cả các error chứ không dừng lại ở lỗi đầu tiên. Điều này siêu hữu ích khi mà bạn muốn hiển thị đồng thời tất cả thông báo error cho người dùng.
Đôi khi dữ liệu form của bạn không chỉ là các trường đơn lẻ, mà có cấu trúc các object lồng nhau thì như thế nào. Ví dụ, bạn có một form yêu cầu người dùng nhập thông tin cá nhân, trong đó bao gồm địa chỉ có dạng object. Hoặc bạn có một form để nhập danh sách sản phẩm (array) kèm thông tin chi tiết.
Ví dụ, mình có một form để thu thập thông tin cơ bản với các yêu cầu về schema như sau:
- name: kiểu dữ liệu là string, bắt buộc.
- age: kiểu dữ liệu là number, từ 18 đến 60.
- address: là một object bao gồm street (bắt buộc) và city (bắt buộc).
- hobbies: là một array gồm các chuỗi, mỗi chuỗi ít nhất 3 ký tự, không được để trống.
const profileSchema = yup.object({
name: yup.string().required('Vui lòng nhập tên'),
age: yup.number().min(18, 'Tuổi phải >= 18').max(60, 'Tuổi phải <= 60').required('Vui lòng nhập tuổi'),
address: yup.object({
street: yup.string().required('Vui lòng nhập tên đường'),
city: yup.string().required('Vui lòng nhập thành phố'),
}),
hobbies: yup.array().of(
yup.string().min(3, 'Mỗi sở thích phải có ít nhất 3 ký tự').required('Vui lòng nhập sở thích')
).required('Danh sách sở thích không được bỏ trống'),
});
Ở schema này mình dùng yup.object({ ... })
để định nghĩa schema cho object con, và yup.array().of(...)
để định nghĩa schema cho array.
Đến lúc này, bạn sẽ thắc mắc rằng vậy nếu như tôi muốn check xem username có chứa ký tự đặc biệt hay không? Đối với các quy tắc validation đặc biệt này thì Yup không hỗ trợ, nhưng Yup cho phép bạn dùng test() để viết quy tắc tuỳ biến.
const usernameSchema = yup.string()
.required('Vui lòng nhập username')
.test('no-special-chars', 'Username không được chứa ký tự đặc biệt', (value) => {
if (!value) return false;
return /^[A-Za-z0-9]+$/.test(value);
});
Hay với ví dụ, bạn muốn xây dựng chức năng thay đổi password, có trường password và trường oldPassword, bạn muốn đảm bảo password mới không được trùng với oldPassword. Bạn có thể dùng test() kết hợp với this.options.context (khi bạn validate có truyền context vào schema).
const changePasswordSchema = yup.object({
oldPassword: yup.string().required('Vui lòng nhập mật khẩu cũ'),
password: yup.string()
.required('Vui lòng nhập mật khẩu mới')
.min(8, 'Mật khẩu mới phải ít nhất 8 ký tự')
.test('not-same-as-old', 'Mật khẩu mới không được trùng với mật khẩu cũ', function (value) {
const { oldPassword } = this.options.context as { oldPassword: string };
if (!value || !oldPassword) return true;
return value !== oldPassword;
}),
confirmPassword: yup.string()
.required('Vui lòng nhập lại mật khẩu mới')
.oneOf([yup.ref('password'), null], 'Mật khẩu xác nhận không khớp'),
});
4. Hướng dẫn kết hợp Yup với React Hook Form
React Hook Form (RHF) là một thư viện giúp việc quản lý form trong React trở nên nhẹ nhàng hơn. RHF không ràng buộc bạn phải dùng Yup, nhưng nó tích hợp sẵn với Yup thông qua @hookform/resolvers.
Khi dùng RHF và Yup cùng nhau, bạn có thể dễ dàng:
- Định nghĩa schema Yup cho form.
- Sử dụng Yup resolver để kết nối schema vào RHF.
- Mỗi khi submit form, dữ liệu được Yup kiểm tra. Nếu có lỗi, RHF sẽ hiểu và set error vào errors state, bạn chỉ việc hiển thị ra cho người dùng.
Giả sử bạn có một form đăng ký tài khoản với các trường sau:
- email: bắt buộc, email hợp lệ
- password: bắt buộc, ít nhất 8 ký tự, phải chứa ít nhất 1 ký tự đặc biệt
- confirmPassword: bắt buộc, trùng khớp với password
- name: bắt buộc, ít nhất 2 ký tự
- age: không bắt buộc, nếu nhập thì phải >=18
- gender: không bắt buộc, nếu nhập thì phải là male, female, hoặc other
- address.street, address.city: bắt buộc nếu address được cung cấp
- hobbies: array chuỗi không bắt buộc, nhưng nếu có thì mỗi phần tử >=3 ký tự
import React from 'react';
import { useForm } from 'react-hook-form';
import { yupResolver } from '@hookform/resolvers/yup';
import * as yup from 'yup';
// -----------------------------------------------------
const signUpSchema = yup.object({
email: yup.string().email('Email không hợp lệ').required('Email là bắt buộc'),
password: yup.string()
.min(8, 'Mật khẩu ít nhất 8 ký tự')
.matches(/[!@#$%^&*(),.?":{}|<>]/, 'Mật khẩu phải chứa ký tự đặc biệt')
.required('Mật khẩu là bắt buộc'),
confirmPassword: yup.string()
.oneOf([yup.ref('password'), null], 'Mật khẩu xác nhận không khớp')
.required('Vui lòng nhập lại mật khẩu'),
name: yup.string().min(2, 'Tên ít nhất 2 ký tự').required('Tên là bắt buộc'),
age: yup.number().nullable().notRequired()
.when('age', {
is: (val: number | null | undefined) => val != null,
then: (schema) => schema.min(18, 'Tuổi phải >= 18'),
}),
gender: yup.string().notRequired().oneOf(['male', 'female', 'other'], 'Giới tính không hợp lệ'),
address: yup.object({
street: yup.string().required('Vui lòng nhập tên đường'),
city: yup.string().required('Vui lòng nhập thành phố'),
}).notRequired(), // Nếu không cung cấp address thì bỏ qua
hobbies: yup.array().of(
yup.string().min(3, 'Sở thích phải ít nhất 3 ký tự')
).notRequired(),
});
type SignUpFormValues = yup.InferType<typeof signUpSchema>;
const SignUpForm: React.FC = () => {
const { register, handleSubmit, formState: { errors } } = useForm<SignUpFormValues>({
resolver: yupResolver(signUpSchema),
});
const onSubmit = (data: SignUpFormValues) => {
console.log('Dữ liệu hợp lệ:', data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<div>
<label>Email:</label>
<input {...register('email')} />
{errors.email && <p style={{color:'red'}}>{errors.email.message}</p>}
</div>
<div>
<label>Mật khẩu:</label>
<input type="password" {...register('password')} />
{errors.password && <p style={{color:'red'}}>{errors.password.message}</p>}
</div>
<div>
<label>Xác nhận mật khẩu:</label>
<input type="password" {...register('confirmPassword')} />
{errors.confirmPassword && <p style={{color:'red'}}>{errors.confirmPassword.message}</p>}
</div>
<div>
<label>Tên:</label>
<input {...register('name')} />
{errors.name && <p style={{color:'red'}}>{errors.name.message}</p>}
</div>
<div>
<label>Tuổi (tuỳ chọn):</label>
<input type="number" {...register('age')} />
{errors.age && <p style={{color:'red'}}>{errors.age.message}</p>}
</div>
<div>
<label>Giới tính (tuỳ chọn):</label>
<select {...register('gender')}>
<option value="">Chọn giới tính</option>
<option value="male">Nam</option>
<option value="female">Nữ</option>
<option value="other">Khác</option>
</select>
{errors.gender && <p style={{color:'red'}}>{errors.gender.message}</p>}
</div>
<div>
<label>Địa chỉ (tuỳ chọn):</label>
<input placeholder="Tên đường" {...register('address.street')} />
{errors.address?.street && <p style={{color:'red'}}>{errors.address.street.message}</p>}
<input placeholder="Thành phố" {...register('address.city')} />
{errors.address?.city && <p style={{color:'red'}}>{errors.address.city.message}</p>}
</div>
<div>
<label>Sở thích (tuỳ chọn - nhập nhiều, ngăn cách bằng dấu phẩy):</label>
{/* Giả sử có input text để nhập nhiều sở thích, sau đó split ra
Lưu ý: Cách này chỉ minh hoạ, tuỳ vào cách bạn handle nhiều giá trị nhé
*/}
<input {...register('hobbies')} placeholder="vd: reading, music, coding" />
{errors.hobbies && errors.hobbies.map((err, index) => (
<p key={index} style={{color:'red'}}>{err?.message}</p>
))}
</div>
<button type="submit">Đăng ký</button>
</form>
);
};
export default SignUpForm;
5. Một vài chức năng khác trong Yup
5.1 Chia nhỏ schema
Nếu form của bạn quá phức tạp, schema sẽ dài và khó quản lý. Hãy chia nhỏ schema thành nhiều module, hoặc định nghĩa schema cho từng phần, sau đó kết hợp lại. Yup cung cấp phương thức concat()
để bạn nối hai schema lại với nhau.
const baseUserSchema = yup.object({
email: yup.string().email().required(),
password: yup.string().min(8).required(),
});
const profileInfoSchema = yup.object({
name: yup.string().required(),
age: yup.number().min(18).max(60),
});
const userFullSchema = baseUserSchema.concat(profileInfoSchema);
Như vậy, userFullSchema
kết hợp cả hai schema. Bạn có thể tách schema ra nhiều file để dễ bảo trì.
5.2 Dùng transform() để xử lý dữ liệu trước validation
Nếu bạn muốn chuyển đổi dữ liệu trước khi validate (ví dụ trim khoảng trắng, hay chuyển chữ hoa thành chữ thường), Yup cung cấp hàm transform().)
const schemaWithTransform = yup.object({
email: yup.string()
.transform((value) => value.trim().toLowerCase())
.email()
.required(),
});
Mỗi khi validate, Yup sẽ áp dụng transform trước, sau đó mới kiểm tra các rule.
5.3 Custom error message
Bạn có thể gắn error lỗi ngay sau mỗi rule hoặc dùng setLocale()
để thiết lập error message mặc định cho từng loại rule. Điều này hữu ích khi bạn muốn format message riêng.
yup.setLocale({
string: {
email: 'Định dạng email không hợp lệ',
required: 'Trường này là bắt buộc',
},
});
Sau đó, khi bạn gọi yup.string().email().required(), message lỗi sẽ tự động dùng message bạn đã thiết lập.
5.4 Sử dụng when() để validation phụ thuộc lẫn nhau
Nếu bạn có logic validation phụ thuộc vào giá trị của trường khác, bạn có thể dùng when(). Ví dụ: Nếu field isEmployed
là true, thì employmentDetails
là bắt buộc, ngược lại thì không.
const employmentSchema = yup.object({
isEmployed: yup.boolean().required(),
employmentDetails: yup.string().when('isEmployed', {
is: true,
then: yup.string().required('Vui lòng điền thông tin công việc'),
otherwise: yup.string().notRequired(),
}),
});
6. Một vài lưu ý khi sử dụng Yup
- Đừng lạm dụng validation phía client: validation phía client giúp người dùng biết họ nhập sai gì ngay lập tức. Tuy nhiên, bạn vẫn cần validation phía server để đảm bảo an toàn dữ liệu, tránh trường hợp user bypass client-side checks.
- Sử dụng Yup schema ngay cả bên phía server: Yup cũng có thể dùng trên server. Khi bạn nhận request từ client, bạn có thể validate dữ liệu bằng cùng một schema Yup đã dùng trên client. Nó giúp bạn thống nhất về mặt logic và giảm bớt công sức, thời gian viết lại rule. Code có thể share qua một package nội bộ trong monorepo.
- Giữ schema rõ ràng, dễ đọc: khi schema quá phức tạp, hãy chia nhỏ, đặt tên biến schema mô tả, và comment logic phức tạp.
- Test schema validation: bạn có thể viết test unit cho schema validation. Kiểm tra các trường hợp boundary (vd: password ngắn 7 ký tự hay 8 ký tự?), kiểm tra case email hợp lệ và không hợp lệ. Testing schema giúp đảm bảo app chạy đúng.
7. Kết luận
Yup là một công cụ mạnh mẽ, dễ sử dụng, và rất phù hợp để quản lý validation schema trong các dự án React. Với Yup, bạn có thể định nghĩa các quy tắc validation ngắn gọn, rõ ràng và tách biệt khỏi logic UI.
Khi tích hợp với React Hook Form, Yup giúp form validation trở nên đơn giản hơn khá nhiều. Qua bài viết này, hy vọng mình đã mang đến cho bạn các kiến thức bổ ích về Yup. Nếu bạn thấy phù hợp với dự án của bạn, hãy mạnh dạn trải nghiệm nó vào dự án để hiểu rõ hơn.
Các bài viết liên quan:
Bài viết liên quan
Giới thiệu Ant Design: Hệ thống thiết kế UI dành cho Website
Dec 18, 2024 • 10 min read
Hướng dẫn tích hợp Sentry vào ứng dụng React
Dec 18, 2024 • 7 min read
MUI (Material UI): Công cụ rút ngắn thời gian xây dựng Giao diện
Dec 18, 2024 • 11 min read
Tìm hiểu Sentry: Công cụ Theo dõi Lỗi và Hiệu suất tự động
Dec 16, 2024 • 7 min read
Server-Side Rendering: Giải thích cơ chế hoạt động của SSR
Dec 11, 2024 • 15 min read
Client-Side Rendering: Giải thích cơ chế hoạt động của CSR
Dec 09, 2024 • 8 min read