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
return (
);}Xem Pen SurveyJS-03-RHF [phân nhánh] bởi Sixextinction. Có khá nhiều điều đang xảy ra ở đây và bạn nên chậm lại để xem mọi thứ kết thúc ở đâu.
Các giá trị dẫn xuất — tổng phụ, thuế, tổng — được tính toán trong thành phần thông qua useWatch và useMemo vì chúng phụ thuộc vào giá trị trường trực tiếp và không có vị trí tự nhiên nào khác cho chúng. Các quy tắc hiển thị cho tên người dùng, mật khẩu, Phản hồi tích cực và Phản hồi cải tiến tồn tại trong JSX dưới dạng các điều kiện nội tuyến. Logic bỏ qua bước — trang đánh giá chỉ xuất hiện khi tổng số >= 100 — được nhúng vào biến showSubmit và điều kiện kết xuất ở bước 3. Bản thân điều hướng chỉ là một bộ đếm useState mà chúng tôi đang tăng dần theo cách thủ công. React Query xử lý các lần thử lại, lưu vào bộ đệm và vô hiệu hóa. Biểu mẫu chỉ gọi Mutation.mutate với dữ liệu đã được xác thực.
Không có điều nào trong số này là sai cả. Đây vẫn là React thành ngữ và thành phần này hoạt động khá hiệu quả nhờ cách RHF tách biệt các kết xuất lại. Nhưng nếu bạn giao cái này cho một người chưa viết nó và yêu cầu họ giải thích trang đánh giá xuất hiện trong những điều kiện nào, họ sẽ phải theo dõi qua showSubmit, điều kiện hiển thị bước 3 và logic nút điều hướng — ba vị trí riêng biệt — để xây dựng lại một quy tắc có thể được nêu trong một dòng. Đúng là biểu mẫu này hoạt động nhưng hành vi này không thực sự có thể kiểm tra được dưới dạng hệ thống. Nó phải được thực hiện về mặt tinh thần. Quan trọng hơn, việc thay đổi nó đòi hỏi sự tham gia của kỹ thuật. Ngay cả một chỉnh sửa nhỏ, chẳng hạn như điều chỉnh thời điểm bước xem xét xuất hiện, cũng có nghĩa là chỉnh sửa thành phần, cập nhật xác thực, mở yêu cầu kéo, chờ xem xét và triển khai lại. Phần 2: Dựa trên lược đồ (SurveyJS) Bây giờ hãy xây dựng cùng một quy trình bằng cách sử dụng lược đồ. Cài đặt npm cài đặt Survey-core Survey-Reac-ui @tanstack/Reac-query
Survey-coreCông cụ thời gian chạy độc lập với nền tảng được MIT cấp phép hỗ trợ kết xuất biểu mẫu của SurveyJS - phần chúng tôi quan tâm ở đây. Nó lấy một lược đồ JSON, xây dựng một mô hình nội bộ từ nó và xử lý mọi thứ lẽ ra có trong thành phần React của bạn: đánh giá các biểu thức hiển thị, tính toán các giá trị dẫn xuất, quản lý trạng thái trang, theo dõi xác thực và quyết định xem “hoàn thành” nghĩa là gì đối với những trang thực sự được hiển thị.
khảo sát-Reac-ui Lớp giao diện người dùng / kết xuất kết nối mô hình đó với React. Về cơ bản, đây là thành phần
Cùng nhau, chúng cung cấp cho bạn thời gian chạy biểu mẫu nhiều trang, đầy đủ chức năng mà không cần viết một dòng điều khiển nào. Bản thân định dạng lược đồ, như đã nói trước đây, chỉ là JSON - không có DSL hay bất kỳ thứ gì độc quyền. Bạn có thể nội tuyến nó, nhập nó từ một tệp, tìm nạp nó từ API hoặc lưu trữ nó trong cột cơ sở dữ liệu và hydrat hóa nó khi chạy. Biểu mẫu giống nhau, như dữ liệu Đây là dạng tương tự, lần này được biểu thị dưới dạng đối tượng JSON. Lược đồ xác định mọi thứ: cấu trúc, xác thực, quy tắc hiển thị, tính toán dẫn xuất, điều hướng trang — và giao nó cho Mô hình để đánh giá lược đồ đó trong thời gian chạy. Đây là những gì trông giống như đầy đủ:
xuất const khảo sátSchema = { title: "Order Flow", showProgressBar: "top", pages: [ { name: "details", Elements: [ { type: "text", name: "firstName", isRequired: true }, { type: "text", name: "email", inputType: "email", isRequired: true, validators: [{ type: "email", text: "Email không hợp lệ" }] } ] }, { name: "order", phần tử: [ { type: "text", name: "price", inputType: "number", defaultValue: 0 }, { type: "text", name: "quantity", inputType: "number", defaultValue: 1 }, { type: "dropdown",tên: "taxRate", defaultValue: 0,1, các lựa chọn: [ { value: 0,05, text: "5%" }, { value: 0,1, text: "10%" }, { value: 0,15, text: "15%" } ] }, { type: "biểu thức", tên: "tổng phụ", biểu thức: "{price} {quantity}" }, { type: "biểu thức", tên: "thuế", biểu thức: "{subtotal} {taxRate}" }, { type: "biểu thức", tên: "total", biểu thức: "{subtotal} + {tax}" } ] }, { name: "account", các thành phần: [ { type: "radiogroup", name: "hasAccount", các lựa chọn: ["Có", "Không"] }, { type: "text", tên: "tên người dùng",visibleIf: "{hasAccount} = 'Có'", isBắt buộc: true }, { type: "text", name: "password", inputType: "password",visibleIf: "{hasAccount} = 'Yes'", isRequired: true, validators: [{ type: "text", minLength: 6, text: "Min 6 character" }] }, { type: "rated", name: "satisfaction", rateMin: 1, rateMax: 5 }, { type: "comment", name: " PositiveFeedback",visibleIf: "{satisfaction} >= 4" }, { type: "comment", name: "improvementFeedback",visibleIf: "{satisfaction} <= 2" } ] }, { name: "review",visibleIf: "{total} >= 100", các phần tử: [] } ]};
Hãy so sánh phiên bản này với phiên bản RHF một lát.
Khối superRefine yêu cầu tên người dùng và mật khẩu có điều kiện đã biến mất. visibleIf: "{hasAccount} = 'Yes'" kết hợp với isRequired: true xử lý cả hai mối quan tâm cùng nhau, trên chính trường đó, nơi bạn mong muốn tìm thấy chúng. Chuỗi useWatch + useMemo tính tổng phụ, thuế và tổng được thay thế bằng ba trường biểu thức tham chiếu lẫn nhau theo tên. Điều kiện trang đánh giá, trong phiên bản RHF chỉ có thể được xây dựng lại bằng cách truy tìm thông qua showSubmit, nhánh kết xuất bước 3. Và cuối cùng, logic của nút điều hướng là một thuộc tínhvisibleIf duy nhất trên đối tượng trang.
Logic tương tự là có. Chỉ là lược đồ cung cấp cho nó một nơi để tồn tại ở nơi nó có thể nhìn thấy một cách biệt lập, thay vì trải rộng khắp thành phần. Ngoài ra, hãy lưu ý rằng lược đồ sử dụng loại: 'biểu thức' cho tổng phụ, thuế và tổng. Biểu thức ở dạng chỉ đọc và được sử dụng chủ yếu để hiển thị các giá trị được tính toán. SurveyJS cũng hỗ trợ type: 'html' cho nội dung tĩnh, nhưng đối với các giá trị được tính toán, biểu thức là lựa chọn phù hợp. Bây giờ là về phía React. Hiển thị và gửi Rất đơn giản. Kết nối onComplete với API của bạn theo cách tương tự - thông qua useMutation hoặc tìm nạp đơn giản:
nhập { useState, useEffect, useRef } từ "react";import { useMutation } từ "@tanstack/react-query";import { Model } từ "survey-core";import { Survey } từ "survey-react-ui";import "survey-core/survey-core.css";
hàm xuất SurveyForm() { const [model] = useState(() => new Model(surveySchema));
đột biến const = useMutation({ đột biếnFn: không đồng bộ (dữ liệu) => { 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(dữ liệu), }); if (!res.ok) ném Lỗi mới("Không thể gửi"); trả về res.json(); }, });
const MutationRef = useRef(đột biến); đột biếnRef.current = đột biến; useEffect(() => { const handler = (sender) => MutationRef.current.mutate(sender.data); model.onComplete.add(handler); return () => model.onComplete.remove(handler); }, [model]); // ref tránh đăng ký lại trình xử lý mỗi lần kết xuất (thay đổi nhận dạng đối tượng đột biến)
trở lại (
<>
Xem Pen SurveyJS-03-SurveyJS [phân nhánh] bởi Sixextinction.
onComplete kích hoạt khi người dùng đến cuối trang hiển thị cuối cùng. Vì vậy, nếu tổng số không bao giờ vượt quá 100 và trang đánh giá bị bỏ qua, nó vẫn kích hoạt chính xác vì SurveyJS đánh giá khả năng hiển thị trước khi quyết định “trang cuối cùng” nghĩa là gì. Sau đó, sender.data chứa tất cả các câu trả lời cùng với các giá trị được tính toán (tổng phụ, thuế, tổng) dưới dạng các trường hạng nhất, do đó tải trọng API giống hệt với tải trọng mà phiên bản RHF được tập hợp thủ công trong onSubmit. cácMẫu MutantRef giống với mẫu mà bạn sẽ tiếp cận ở bất kỳ nơi nào bạn cần một trình xử lý sự kiện ổn định đối với một giá trị thay đổi trên mỗi lần hiển thị - không có gì cụ thể về SurveyJS về nó.
Thành phần React không còn chứa bất kỳ logic nghiệp vụ nào nữa. Không có useWatch, không có JSX có điều kiện, không có bộ đếm bước, không có chuỗi useMemo, không có superRefine. React đang làm những gì nó thực sự giỏi: hiển thị một thành phần và kết nối nó với lệnh gọi API. Điều gì đã thay đổi khỏi React?
Mối quan tâm ngăn xếp RHF Khảo sátJS khả năng hiển thị Các nhánh JSX hiển thịNếu Giá trị dẫn xuất useWatch / useMemo biểu hiện Quy tắc chéo sân siêu Tinh chỉnh Điều kiện lược đồ Điều hướng trạng thái bước Trang hiển thịNếu Vị trí quy tắc Phân phối trên các tập tin Tập trung trong lược đồ
Những gì còn lại trong React là bố cục, kiểu dáng, hệ thống gửi và tích hợp ứng dụng, nghĩa là những thứ mà React thực sự được thiết kế cho. Mọi thứ khác đều được chuyển vào lược đồ và vì lược đồ chỉ là một đối tượng JSON nên lược đồ này có thể được lưu trữ trong cơ sở dữ liệu, được tạo phiên bản độc lập với mã ứng dụng của bạn hoặc được chỉnh sửa thông qua công cụ nội bộ mà không cần triển khai. Người quản lý sản phẩm cần thay đổi ngưỡng kích hoạt trang đánh giá có thể thực hiện việc đó mà không cần chạm vào thành phần. Đó là sự khác biệt có ý nghĩa về mặt vận hành đối với các nhóm có hành vi biểu mẫu phát triển thường xuyên và không phải lúc nào cũng do các kỹ sư thúc đẩy. Khi nào nên sử dụng mỗi phương pháp? Đây là một nguyên tắc nhỏ phù hợp với tôi: hãy tưởng tượng việc xóa hoàn toàn biểu mẫu. Bạn sẽ mất gì?
Nếu đó là màn hình, bạn muốn các biểu mẫu hướng đến thành phần. Nếu đó là logic nghiệp vụ, chẳng hạn như ngưỡng, quy tắc phân nhánh và yêu cầu có điều kiện mã hóa các quyết định thực tế, thì bạn cần có một công cụ lược đồ.
Tương tự, nếu những thay đổi sắp tới của bạn chủ yếu liên quan đến nhãn, trường và bố cục, RHF sẽ phục vụ tốt cho bạn. Nếu chúng liên quan đến các điều kiện, kết quả và quy tắc mà nhóm điều hành hoặc pháp lý của bạn có thể cần điều chỉnh vào chiều Thứ Ba mà không cần gửi yêu cầu, thì mô hình lược đồ với SurveyJS sẽ phù hợp hơn. Hai cách tiếp cận này không thực sự cạnh tranh với nhau. Chúng giải quyết các loại vấn đề khác nhau và sai lầm đáng tránh là không khớp tính trừu tượng với trọng số của logic — coi hệ thống quy tắc giống như một thành phần vì đó là công cụ quen thuộc hoặc tiếp cận công cụ chính sách vì biểu mẫu đã tăng lên ba bước và có được trường có điều kiện. Hình thức chúng tôi xây dựng ở đây nằm gần ranh giới một cách có chủ ý, đủ phức tạp để bộc lộ sự khác biệt nhưng không quá cực đoan đến mức khiến việc so sánh có cảm giác gian lận. Hầu hết các dạng thực đã trở nên khó sử dụng trong cơ sở mã của bạn có thể nằm gần cùng một ranh giới đó và câu hỏi thường chỉ là liệu có ai đã đặt tên cho chúng thực sự là gì hay không. Sử dụng React Hook Form + Zod khi:
Các biểu mẫu được định hướng CRUD; Logic nông và dựa trên giao diện người dùng; Kỹ sư làm chủ mọi hành vi; Phần cuối vẫn là nguồn gốc của sự thật.
Sử dụng SurveyJS khi:
Các biểu mẫu mã hóa các quyết định kinh doanh; Các quy tắc phát triển độc lập với giao diện người dùng; Logic phải hiển thị, kiểm tra được hoặc được phiên bản; Những người không phải là kỹ sư ảnh hưởng đến hành vi; Biểu mẫu tương tự phải chạy trên nhiều giao diện người dùng.