Bài viết này được tài trợ bởi SurveyJS Có một mô hình tinh thần mà hầu hết các nhà phát triển React đều chia sẻ mà không bao giờ thảo luận thẳng thắn về nó. Các hình thức đó luôn được coi là thành phần. Điều này có nghĩa là một ngăn xếp như:

Biểu mẫu React Hook dành cho trạng thái cục bộ (kết xuất lại tối thiểu, đăng ký trường công thái học, tương tác bắt buộc). Zod để xác thực (độ chính xác đầu vào, xác thực ranh giới, phân tích cú pháp an toàn loại). Truy vấn phản ứng cho phần phụ trợ: gửi, thử lại, lưu vào bộ đệm, đồng bộ hóa máy chủ, v.v.

Và đối với phần lớn các biểu mẫu - màn hình đăng nhập, trang cài đặt, phương thức CRUD của bạn - điều này hoạt động thực sự tốt. Mỗi phần thực hiện công việc của nó, chúng được soạn thảo rõ ràng và bạn có thể chuyển sang các phần trong ứng dụng thực sự tạo nên sự khác biệt cho sản phẩm của bạn. Nhưng thỉnh thoảng, một biểu mẫu bắt đầu tích lũy những thứ như quy tắc hiển thị phụ thuộc vào các câu trả lời trước đó hoặc các giá trị dẫn xuất xếp tầng qua ba trường. Thậm chí có thể phải bỏ qua toàn bộ trang hoặc hiển thị dựa trên tổng số trang đang chạy. Bạn xử lý điều kiện đầu tiên bằng useWatch và một nhánh nội tuyến, điều này là ổn. Rồi cái khác. Sau đó, bạn đang sử dụng superRefine để mã hóa các quy tắc trường chéo mà lược đồ Zod của bạn không thể diễn đạt theo cách thông thường. Sau đó, điều hướng từng bước bắt đầu rò rỉ logic nghiệp vụ. Tại một thời điểm nào đó, bạn nhìn vào những gì mình đã xây dựng và nhận ra rằng biểu mẫu này không còn thực sự là giao diện người dùng nữa. Đó là một quá trình quyết định nhiều hơn và cây thành phần chính là nơi bạn tình cờ lưu trữ nó. Đây là lúc tôi nghĩ mô hình tinh thần về các biểu mẫu trong React bị hỏng và đó thực sự không phải lỗi của ai cả. Ngăn xếp RHF + Zod rất xuất sắc với những gì nó được thiết kế. Vấn đề là chúng ta có xu hướng tiếp tục sử dụng nó đến mức mà sự trừu tượng của nó phù hợp với vấn đề bởi vì giải pháp thay thế đòi hỏi một cách suy nghĩ hoàn toàn khác về các hình thức. Bài viết này là về sự thay thế đó. Để thể hiện điều này, chúng tôi sẽ xây dựng cùng một biểu mẫu gồm nhiều bước hai lần:

Với React Hook Form + Zod được kết nối với React Query để gửi, Với SurveyJS, xử lý biểu mẫu dưới dạng dữ liệu - một lược đồ JSON đơn giản - chứ không phải là cây thành phần.

Yêu cầu giống nhau, logic điều kiện giống nhau, lệnh gọi API giống nhau ở cuối. Sau đó, chúng tôi sẽ lập bản đồ chính xác những gì đã di chuyển và những gì ở lại, đồng thời đưa ra một cách thực tế để quyết định bạn nên sử dụng mô hình nào và khi nào. Biểu mẫu chúng tôi đang xây dựng:

Biểu mẫu này sẽ sử dụng quy trình 4 bước: Bước 1: Chi tiết

Họ tên (bắt buộc), Email (bắt buộc, định dạng hợp lệ).

Bước 2: Đặt hàng

Đơn giá, số lượng, Thuế suất, Bắt nguồn: Tổng phụ, thuế, Tổng cộng.

Bước 3: Tài khoản và phản hồi

Bạn có tài khoản không? (Có/Không) Nếu Có → tên người dùng + mật khẩu, cả hai đều được yêu cầu. Nếu Không → email đã được thu thập ở bước 1.

Đánh giá mức độ hài lòng (1–5) Nếu ≥ 4 → hỏi “Bạn thích món gì?” Nếu 2 → hỏi “Chúng ta có thể cải thiện điều gì?”

Bước 4: Xem xét

Chỉ xuất hiện nếu tổng >= 100 Đệ trình cuối cùng.

Điều này không phải là cực đoan. Nhưng nó đủ để bộc lộ sự khác biệt về kiến ​​trúc. Phần 1: Điều khiển theo thành phần (React Hook Form + Zod) Cài đặt npm cài đặt Reac-hook-form zod @hookform/resolvers @tanstack/Reac-query

Lược đồ Zod Hãy bắt đầu với lược đồ Zod, vì đó thường là nơi hình dạng của biểu mẫu được thiết lập. Đối với hai bước đầu tiên — chi tiết cá nhân và đầu vào đơn hàng — mọi thứ đều đơn giản: chuỗi bắt buộc, số có giá trị tối thiểu và enum. Phần thú vị bắt đầu khi bạn cố gắng diễn đạt các quy tắc có điều kiện.

nhập { z } từ "zod";

xuất const formSchema = z.object({ firstName: z.string().min(1, "Bắt buộc"), email: z.string().email("Email không hợp lệ"), price: z.number().min(0), số lượng: z.number().min(1), taxRate: z.number(), hasAccount: z.enum(["Yes", "No"]), tên người dùng: z.string().tùy chọn(), mật khẩu: z.string().tùy chọn(), sự hài lòng: z.number().min(1).max(5), dươngPhản hồi: z.string().tùy chọn(), cải thiệnPhản hồi: z.string().tùy chọn(),}).superRefine((data, ctx) => { if (data.hasAccount === "Yes") { if (!data.username) { ctx.addIssue({ code: "custom", path: ["username"], message: "Required" }); } if (!data.password || data.password.length < 6) { ctx.addIssue({ code: "custom", path: ["password"], message: "Tối thiểu 6 ký tự" } } }

if (data.satisfaction >= 4 && !data. PositiveFeedback) { ctx.addIssue({ code: "custom", path: [" PositiveFeedback"], message: "Hãy chia sẻ những gì bạn thích" }); }

if (data.satisfaction <= 2 && !data.improvementFeedback) { ctx.addIssue({ code: "custom", path:["improvementFeedback"], message: "Xin vui lòng cho chúng tôi biết những gì cần cải thiện" }); }});

loại xuất FormData = z.infer;

Lưu ý rằng tên người dùng và mật khẩu được nhập dưới dạng tùy chọn() mặc dù chúng được yêu cầu có điều kiện vì lược đồ cấp loại của Zod mô tả hình dạng của đối tượng chứ không phải các quy tắc chi phối thời điểm các trường quan trọng. Yêu cầu có điều kiện phải nằm bên trong superRefine, chạy sau khi hình dạng được xác thực và có quyền truy cập vào toàn bộ đối tượng. Sự tách biệt đó không phải là một thiếu sót; đó chính là mục đích mà công cụ này được thiết kế: superRefine là nơi xử lý logic đa trường khi nó không thể được biểu thị trong chính cấu trúc lược đồ. Điều đáng chú ý ở đây là lược đồ này không thể hiện điều gì. Nó không có khái niệm về trang, không có khái niệm về trường nào được hiển thị tại điểm nào và không có khái niệm về điều hướng. Tất cả những thứ đó sẽ sống ở một nơi khác. Thành phần biểu mẫu

nhập { useForm, useWatch } từ "react-hook-form";import { zodResolver } từ "@hookform/resolvers/zod";import { useMutation } từ "@tanstack/react-query";import { useState, useMemo } từ "react";import { formSchema, type FormData } from "./schema";

const STEPS = ["chi tiết", "đặt hàng", "tài khoản", "đánh giá"];

gõ OrderPayload = FormData & { tổng phụ: số; thuế: số; tổng cộng: số };

hàm xuất RHFMultiStepForm() { const [bước, setStep] = useState(0);

đột biến const = useMutation({ đột biếnFn: không đồng bộ (tải trọng: OrderPayload) => { const res = đang chờ tìm nạp("/api/orders", { phương thức: "BÀI", tiêu đề: { "Content-Type": "application/json" }, nội dung: JSON.stringify(tải trọng), }); if (!res.ok) ném Lỗi mới("Không thể gửi"); trả về res.json(); }, });

const { register, control, handSubmit, formState: {error }, } = useForm({ Resolver: zodResolver(formSchema), defaultValues: { price: 0, số lượng: 1, taxRate: 0.1, sự hài lòng: 3, hasAccount: "Không", }, }); const price = useWatch({ control, name: "price" }); const số lượng = useWatch({ control, name: "quantity" }); const taxRate = useWatch({ control, name: "taxRate" }); const hasAccount = useWatch({ control, name: "hasAccount" }); const sự hài lòng = useWatch({ control, name: "satisfaction" }); const tổng phụ = useMemo(() => (giá ?? 0) * (số lượng ?? 1), [giá, số lượng]); const tax = useMemo(() => tổng phụ * (taxRate ?? 0), [tổng phụ, taxRate]); const Total = useMemo(() => subtotal + tax, [subtotal, tax]); const onSubmit = (data: FormData) => Mutation.mutate({ ...data, subtotal, tax, Total }); const showSubmit = (bước === 2 && tổng < 100) || (bước === 3 && tổng >= 100)

return (

{step === 0 && ( <> )}

{step === 1 && ( <>

Tổng phụ: {subtotal
Thuế: {tax
Tổng cộng: {total
)}

{bước === 2 && ( <>

{hasAccount === "Có" && ( <> )}

{sự hài lòng >= 4 && (