Khai phá xác thực mạnh mẽ, tăng cường trong các biểu mẫu đa giai đoạn của React. Tìm hiểu cách tận dụng hook useFormState để có trải nghiệm người dùng liền mạch, tích hợp máy chủ.
Công cụ xác thực React useFormState: Đi sâu vào xác thực biểu mẫu đa giai đoạn
Trong thế giới phát triển web hiện đại, việc tạo ra trải nghiệm người dùng trực quan và mạnh mẽ là tối quan trọng. Điều này càng trở nên quan trọng hơn trong các biểu mẫu, cổng tương tác chính của người dùng. Mặc dù các biểu mẫu liên hệ đơn giản rất dễ thực hiện, nhưng độ phức tạp tăng vọt với các biểu mẫu đa giai đoạn—hãy nghĩ đến các trình hướng dẫn đăng ký người dùng, quy trình thanh toán thương mại điện tử hoặc các bảng cấu hình chi tiết. Các quy trình nhiều bước này đặt ra những thách thức đáng kể trong việc quản lý trạng thái, xác thực và duy trì luồng người dùng liền mạch. Trong lịch sử, các nhà phát triển đã phải vật lộn với trạng thái client-side phức tạp, các nhà cung cấp ngữ cảnh (context providers) và các thư viện bên thứ ba để kiểm soát sự phức tạp này.
Hãy đến với hook `useFormState` của React. Được giới thiệu như một phần của quá trình phát triển của React hướng tới các thành phần tích hợp máy chủ, hook mạnh mẽ này cung cấp một giải pháp hợp lý, thanh lịch để quản lý trạng thái biểu mẫu và xác thực, đặc biệt trong bối cảnh các biểu mẫu đa giai đoạn. Bằng cách tích hợp trực tiếp với Server Actions, `useFormState` tạo ra một công cụ xác thực mạnh mẽ giúp đơn giản hóa mã, nâng cao hiệu suất và thúc đẩy nâng cao dần (progressive enhancement). Bài viết này cung cấp hướng dẫn toàn diện cho các nhà phát triển trên toàn thế giới về cách kiến trúc một công cụ xác thực đa giai đoạn tinh vi bằng cách sử dụng `useFormState`, biến một nhiệm vụ phức tạp thành một quy trình có thể quản lý và mở rộng.
Thách thức bền bỉ của biểu mẫu đa giai đoạn
Trước khi đi sâu vào giải pháp, điều quan trọng là phải hiểu những khó khăn chung mà các nhà phát triển phải đối mặt với các biểu mẫu đa giai đoạn. Những thách thức này không hề nhỏ và có thể ảnh hưởng đến mọi thứ, từ thời gian phát triển đến trải nghiệm người dùng cuối.
- Phức tạp trong quản lý trạng thái: Làm thế nào để duy trì dữ liệu khi người dùng điều hướng giữa các bước? Trạng thái nên nằm trong một thành phần cha, ngữ cảnh toàn cục hay bộ nhớ cục bộ (local storage)? Mỗi cách tiếp cận đều có những đánh đổi riêng, thường dẫn đến việc truyền prop (prop-drilling) hoặc logic đồng bộ hóa trạng thái phức tạp.
- Phân mảnh logic xác thực: Việc xác thực nên diễn ra ở đâu? Xác thực tất cả mọi thứ ở cuối sẽ mang lại trải nghiệm người dùng kém. Xác thực ở mỗi bước tốt hơn, nhưng điều này thường đòi hỏi phải viết logic xác thực bị phân mảnh, cả ở phía client (để phản hồi tức thì) và trên máy chủ (để bảo mật và toàn vẹn dữ liệu).
- Trở ngại về trải nghiệm người dùng: Người dùng mong đợi có thể di chuyển qua lại giữa các bước mà không làm mất dữ liệu của họ. Họ cũng mong đợi các thông báo lỗi rõ ràng, theo ngữ cảnh và phản hồi tức thì. Việc triển khai trải nghiệm linh hoạt này có thể liên quan đến lượng lớn mã mẫu (boilerplate code).
- Đồng bộ hóa trạng thái máy chủ-client: Nguồn sự thật cuối cùng thường là máy chủ. Việc giữ trạng thái client-side đồng bộ hoàn hảo với các quy tắc xác thực và logic nghiệp vụ phía máy chủ là một cuộc chiến không ngừng, thường dẫn đến mã trùng lặp và các mâu thuẫn tiềm ẩn.
Những thách thức này nêu bật nhu cầu về một cách tiếp cận tích hợp, gắn kết hơn—một cách tiếp cận thu hẹp khoảng cách giữa client và máy chủ. Đây chính là nơi `useFormState` tỏa sáng.
Giới thiệu `useFormState`: Một cách tiếp cận hiện đại để xử lý biểu mẫu
Hook `useFormState` được thiết kế để quản lý trạng thái biểu mẫu cập nhật dựa trên kết quả của một hành động biểu mẫu. Đây là nền tảng trong tầm nhìn của React về các ứng dụng được nâng cao dần (progressively enhanced applications) hoạt động liền mạch dù có hoặc không có JavaScript được bật trên client.
`useFormState` là gì?
Về cốt lõi, `useFormState` là một React Hook nhận hai đối số: một hàm server action và một trạng thái ban đầu (initial state). Nó trả về một mảng chứa hai giá trị: trạng thái hiện tại của biểu mẫu và một hàm action mới để truyền vào phần tử `
);
}
Bước 1: Thu thập và xác thực thông tin cá nhân
Trong bước này, chúng ta chỉ muốn xác thực các trường `name` và `email`. Chúng ta sẽ sử dụng một trường ẩn `_step` để cho server action của chúng ta biết logic xác thực nào cần chạy.
// Step1.jsx component
{state.errors.name} {state.errors.email}
export function Step1({ state }) {
return (
Bước 1: Thông tin cá nhân
{state.errors?.name &&
{state.errors?.email &&
);
}
Bây giờ, hãy cập nhật server action của chúng ta để xử lý xác thực cho Bước 1.
// actions.js (đã cập nhật)
// ... (imports và định nghĩa schema)
export async function onbordingAction(prevState, formData) {
// ... (lấy dữ liệu biểu mẫu)
const step = Number(formData.get('_step'));
if (step === 1) {
const validatedFields = schema.pick({ name: true, email: true }).safeParse({ name, email });
if (!validatedFields.success) {
return {
...currentState,
step: 1,
errors: validatedFields.error.flatten().fieldErrors,
};
}
// Thành công, chuyển sang bước tiếp theo
return {
...currentState,
step: 2,
errors: {},
};
}
// ... (logic cho các bước khác)
}
Khi người dùng nhấp vào "Tiếp theo", biểu mẫu sẽ được gửi. Server action kiểm tra xem đó có phải là Bước 1 không, chỉ xác thực các trường `name` và `email` bằng phương thức `pick` của Zod, và trả về một trạng thái mới. Nếu xác thực thất bại, nó sẽ trả về các lỗi và giữ nguyên ở Bước 1. Nếu thành công, nó sẽ xóa các lỗi và cập nhật `step` thành 2, khiến thành phần `OnboardingForm` chính của chúng ta render thành phần `Step2`.
Bước 2: Xác thực tăng cường cho chi tiết công ty
Vẻ đẹp của cách tiếp cận này là trạng thái từ Bước 1 tự động được chuyển tiếp. Chúng ta chỉ cần render nó trong các trường ẩn để nó được bao gồm trong lần gửi biểu mẫu tiếp theo.
// Step2.jsx component
{state.errors.companyName} {state.errors.role}
export function Step2({ state }) {
return (
Bước 2: Chi tiết công ty
{/* Giữ lại dữ liệu từ bước trước đó */}
{state.errors?.companyName &&
{state.errors?.role &&
);
}
Và chúng ta cập nhật server action để xử lý Bước 2.
// actions.js (đã cập nhật)
// ...
if (step === 2) {
const validatedFields = schema.pick({ companyName: true, role: true }).safeParse({ companyName, role });
if (!validatedFields.success) {
return {
...currentState,
step: 2,
errors: validatedFields.error.flatten().fieldErrors,
};
}
// Thành công, chuyển sang xem xét cuối cùng
return {
...currentState,
step: 3,
errors: {},
};
}
// ...
Logic giống hệt như Bước 1, nhưng nó nhắm mục tiêu vào các trường cho Bước 2. Hook `useFormState` quản lý quá trình chuyển đổi một cách liền mạch, bảo toàn tất cả dữ liệu và cung cấp luồng xác thực sạch sẽ, tăng cường.
Bước 3: Xem xét và gửi cuối cùng
Ở bước cuối cùng, chúng ta hiển thị tất cả dữ liệu đã thu thập để người dùng xem xét. Việc gửi cuối cùng sẽ kích hoạt xác thực toàn diện tất cả các trường trước khi chúng ta cam kết dữ liệu vào cơ sở dữ liệu.
// Step3.jsx component
{state.message} {state.message}
export function Step3({ state }) {
return (
Bước 3: Xác nhận chi tiết
{state.message && state.message.startsWith('Success') &&
{state.message && state.message.startsWith('Error') &&
);
}
Logic server action cuối cùng thực hiện xác thực đầy đủ và logic nghiệp vụ cuối cùng.
// actions.js (phiên bản cuối cùng)
// ...
if (step === 3) {
// Xác thực cuối cùng, đầy đủ
const validatedFields = schema.safeParse({ name, email, companyName, role });
if (!validatedFields.success) {
// Lẽ ra không nên xảy ra nếu xác thực từng bước là đúng, nhưng là một biện pháp bảo vệ tốt
return {
...currentState,
step: 1, // Gửi người dùng trở lại bước đầu tiên với lỗi
errors: validatedFields.error.flatten().fieldErrors,
message: 'Lỗi: Dữ liệu không hợp lệ được tìm thấy. Vui lòng xem lại.'
};
}
try {
// console.log('Đang gửi đến cơ sở dữ liệu:', validatedFields.data);
// await saveToDatabase(validatedFields.data);
return { message: 'Thành công! Quy trình giới thiệu của bạn đã hoàn tất.', step: 4 }; // Một bước thành công cuối cùng
} catch (dbError) {
return { ...currentState, step: 3, message: 'Lỗi: Không thể lưu dữ liệu.' };
}
}
// ...
Với điều này, chúng ta có một biểu mẫu đa giai đoạn hoàn chỉnh, mạnh mẽ, với xác thực tăng cường, do máy chủ ủy quyền, tất cả được điều phối gọn gàng bởi hook `useFormState`.
Các chiến lược nâng cao cho trải nghiệm người dùng đẳng cấp thế giới
Xây dựng một biểu mẫu chức năng là một chuyện; làm cho nó trở nên thú vị khi sử dụng là một chuyện khác. Dưới đây là một số kỹ thuật nâng cao để nâng tầm các biểu mẫu đa giai đoạn của bạn.
Quản lý điều hướng: Di chuyển qua lại
Logic hiện tại của chúng ta chỉ di chuyển về phía trước. Để cho phép người dùng quay lại, chúng ta không thể sử dụng một nút `type="submit"` đơn giản. Thay vào đó, chúng ta sẽ quản lý bước trong trạng thái của thành phần phía client và chỉ sử dụng hành động biểu mẫu để tiến lên. Tuy nhiên, một cách tiếp cận đơn giản hơn tuân theo mô hình tập trung vào máy chủ là có một nút "Quay lại" cũng gửi biểu mẫu nhưng với một ý định khác.
// Trong một thành phần bước...
// Trong server action...
const intent = formData.get('intent');
if (intent === 'back') {
return { ...currentState, step: step - 1, errors: {} };
}
Cung cấp phản hồi tức thì với `useFormStatus`
Hook `useFormStatus` cung cấp trạng thái đang chờ xử lý của một lần gửi biểu mẫu trong cùng một `
// SubmitButton.jsx
'use client';
import { useFormStatus } from 'react-dom';
export function SubmitButton({ text }) {
const { pending } = useFormStatus();
return (
{pending ? 'Đang gửi...' : text}
);
}
Bạn có thể sử dụng `
Cấu trúc Server Action của bạn để có khả năng mở rộng
Khi biểu mẫu của bạn phát triển, chuỗi `if/else if` trong server action có thể trở nên khó quản lý. Một câu lệnh `switch` hoặc một mô hình mô-đun hơn được khuyến nghị để tổ chức tốt hơn.
// actions.js với câu lệnh switch
switch (step) {
case 1:
// Xử lý xác thực Bước 1
break;
case 2:
// Xử lý xác thực Bước 2
break;
// ... v.v.
}
Khả năng tiếp cận (a11y) là không thể thương lượng
Đối với một khán giả toàn cầu, khả năng tiếp cận là điều bắt buộc. Đảm bảo các biểu mẫu của bạn có thể tiếp cận được bằng cách:
- Sử dụng `aria-invalid="true"` trên các trường nhập liệu có lỗi.
- Kết nối các thông báo lỗi với các trường nhập liệu bằng cách sử dụng `aria-describedby`.
- Quản lý tiêu điểm (focus) một cách thích hợp sau khi gửi, đặc biệt khi xuất hiện lỗi.
- Đảm bảo tất cả các điều khiển biểu mẫu có thể điều hướng bằng bàn phím.
Quan điểm toàn cầu: Quốc tế hóa và `useFormState`
Một trong những lợi thế đáng kể của xác thực điều khiển bằng máy chủ là dễ dàng quốc tế hóa (i18n). Các thông báo xác thực không còn cần phải được mã hóa cứng trên client. Server action có thể phát hiện ngôn ngữ ưa thích của người dùng (từ các tiêu đề như `Accept-Language`, tham số URL hoặc cài đặt hồ sơ người dùng) và trả về lỗi bằng ngôn ngữ mẹ đẻ của họ.
Ví dụ, sử dụng thư viện như `i18next` trên máy chủ:
// Server action với i18n
import { i18n } from 'your-i18n-config';
// ...
const t = await i18n.getFixedT(userLocale); // ví dụ: 'es' cho tiếng Tây Ban Nha
const schema = z.object({
email: z.string().email(t('errors.invalid_email')),
});
Cách tiếp cận này đảm bảo rằng người dùng trên toàn thế giới nhận được phản hồi rõ ràng, dễ hiểu, cải thiện đáng kể tính toàn diện và khả năng sử dụng của ứng dụng của bạn.
`useFormState` so với các thư viện Client-Side: Một cái nhìn so sánh
Mô hình này so sánh như thế nào với các thư viện đã có như Formik hoặc React Hook Form? Không phải cái nào tốt hơn, mà là cái nào phù hợp với công việc.
- Các thư viện Client-Side (Formik, React Hook Form): Chúng rất xuất sắc cho các biểu mẫu phức tạp, có tính tương tác cao, nơi phản hồi tức thì phía client là ưu tiên hàng đầu. Chúng cung cấp các bộ công cụ toàn diện để quản lý trạng thái biểu mẫu, xác thực và gửi hoàn toàn trong trình duyệt. Thách thức chính của chúng có thể là sự trùng lặp của logic xác thực giữa client và máy chủ.
- `useFormState` với Server Actions: Cách tiếp cận này xuất sắc ở những nơi máy chủ là nguồn sự thật cuối cùng. Nó đơn giản hóa kiến trúc tổng thể bằng cách tập trung hóa logic, đảm bảo tính toàn vẹn của dữ liệu và hoạt động liền mạch với nâng cao dần (progressive enhancement). Sự đánh đổi là một chuyến đi khứ hồi mạng để xác thực, mặc dù với cơ sở hạ tầng hiện đại, điều này thường không đáng kể.
Đối với các biểu mẫu đa giai đoạn liên quan đến logic nghiệp vụ đáng kể hoặc dữ liệu phải được xác thực với cơ sở dữ liệu (ví dụ: kiểm tra xem tên người dùng đã được sử dụng chưa), mô hình `useFormState` cung cấp một kiến trúc trực tiếp hơn và ít lỗi hơn.
Kết luận: Tương lai của biểu mẫu trong React
Hook `useFormState` không chỉ là một API mới; nó đại diện cho một sự thay đổi triết lý trong cách chúng ta xây dựng biểu mẫu trong React. Bằng cách áp dụng mô hình tập trung vào máy chủ, chúng ta có thể tạo ra các biểu mẫu đa giai đoạn mạnh mẽ hơn, an toàn hơn, dễ tiếp cận hơn và dễ bảo trì hơn. Mô hình này loại bỏ toàn bộ các loại lỗi liên quan đến đồng bộ hóa trạng thái và cung cấp một cấu trúc rõ ràng, có khả năng mở rộng để xử lý các luồng người dùng phức tạp.
Bằng cách xây dựng một công cụ xác thực với `useFormState`, bạn không chỉ quản lý trạng thái; bạn đang kiến trúc một quy trình thu thập dữ liệu kiên cường, thân thiện với người dùng dựa trên các nguyên tắc phát triển web hiện đại. Đối với các nhà phát triển xây dựng ứng dụng cho một đối tượng đa dạng, toàn cầu, hook mạnh mẽ này cung cấp nền tảng để tạo ra trải nghiệm người dùng thực sự đẳng cấp thế giới.