Αυτό το άρθρο χρηματοδοτείται από την SurveyJS Υπάρχει ένα νοητικό μοντέλο που μοιράζονται οι περισσότεροι προγραμματιστές του React χωρίς να το συζητούν ποτέ δυνατά. Ότι οι φόρμες υποτίθεται ότι είναι πάντα συστατικά. Αυτό σημαίνει μια στοίβα όπως:

React Hook Form για τοπική κατάσταση (ελάχιστες αναπαραγωγές, εργονομική καταχώρηση πεδίου, επιτακτική αλληλεπίδραση). Zod για επικύρωση (ορθότητα εισαγωγής, επικύρωση ορίων, ανάλυση ασφαλούς τύπου). React Query για backend: υποβολή, επαναλήψεις, προσωρινή αποθήκευση, συγχρονισμός διακομιστή και ούτω καθεξής.

Και για τη συντριπτική πλειονότητα των φορμών — τις οθόνες σύνδεσής σας, τις σελίδες ρυθμίσεών σας, τα μοντέλα CRUD σας — αυτό λειτουργεί πολύ καλά. Κάθε κομμάτι κάνει τη δουλειά του, συνθέτουν καθαρά και μπορείτε να προχωρήσετε στα μέρη της εφαρμογής σας που πραγματικά διαφοροποιούν το προϊόν σας. Αλλά κάθε τόσο, μια φόρμα αρχίζει να συγκεντρώνει πράγματα όπως κανόνες ορατότητας που εξαρτώνται από προηγούμενες απαντήσεις ή παράγωγες τιμές που διαιρούνται σε τρία πεδία. Ίσως ακόμη και ολόκληρες σελίδες που θα πρέπει να παραβλεφθούν ή να εμφανιστούν με βάση το τρέχον σύνολο. Μπορείτε να χειριστείτε την πρώτη υπό όρους με ένα useWatch και έναν ενσωματωμένο κλάδο, κάτι που είναι εντάξει. Μετά άλλο. Στη συνέχεια, προσεγγίζετε το superRefine για να κωδικοποιήσετε κανόνες μεταξύ πεδίων που το σχήμα Zod σας δεν μπορεί να εκφράσει με τον κανονικό τρόπο. Στη συνέχεια, η πλοήγηση στα βήματα αρχίζει να διαρρέει επιχειρηματική λογική. Κάποια στιγμή, κοιτάτε τι έχετε δημιουργήσει και συνειδητοποιείτε ότι η φόρμα δεν είναι πια πραγματικά διεπαφή χρήστη. Είναι περισσότερο μια διαδικασία απόφασης και το δέντρο συστατικών είναι ακριβώς εκεί που έτυχε να το αποθηκεύσετε. Εδώ νομίζω ότι καταρρέει το νοητικό μοντέλο για τις φόρμες στο React και πραγματικά κανείς δεν φταίει. Η στοίβα RHF + Zod είναι εξαιρετική σε αυτό για το οποίο σχεδιάστηκε. Το θέμα είναι ότι τείνουμε να συνεχίσουμε να το χρησιμοποιούμε πέρα ​​από το σημείο όπου οι αφαιρέσεις του ταιριάζουν με το πρόβλημα, επειδή η εναλλακτική απαιτεί εντελώς διαφορετικό τρόπο σκέψης για τις μορφές. Αυτό το άρθρο αφορά αυτήν την εναλλακτική. Για να το δείξουμε αυτό, θα δημιουργήσουμε την ίδια ακριβώς φόρμα πολλαπλών βημάτων δύο φορές:

Με το React Hook Form + Zod συνδεδεμένο στο React Query για υποβολή, Με το SurveyJS, το οποίο αντιμετωπίζει μια φόρμα ως δεδομένα - ένα απλό σχήμα JSON - αντί ως δέντρο συνιστωσών.

Ίδιες απαιτήσεις, ίδια λογική υπό όρους, ίδια κλήση API στο τέλος. Στη συνέχεια, θα χαρτογραφήσουμε ακριβώς τι μετακινήθηκε και τι παρέμεινε και θα παρουσιάσουμε έναν πρακτικό τρόπο για να αποφασίσετε ποιο μοντέλο θα χρησιμοποιήσετε και πότε. Η φόρμα που φτιάχνουμε:

Αυτή η φόρμα θα χρησιμοποιεί μια ροή 4 βημάτων: Βήμα 1: Λεπτομέρειες

Όνομα (απαιτείται), Email (απαιτείται, έγκυρη μορφή).

Βήμα 2: Παραγγελία

Τιμή μονάδας, Ποσότητα, φορολογικός συντελεστής, Προκύπτουν: Μερικό σύνολο, Φόρος, Σύνολο.

Βήμα 3: Λογαριασμός και σχόλια

Έχετε λογαριασμό; (Ναι/Όχι) Εάν Ναι → όνομα χρήστη + κωδικός πρόσβασης, απαιτούνται και τα δύο. Εάν Όχι, → email έχει ήδη συλλεχθεί στο βήμα 1.

Βαθμολογία ικανοποίησης (1–5) Αν ≥ 4 → ρωτήσετε "Τι σας άρεσε;" Εάν ≤ 2 → ρωτήσετε "Τι μπορούμε να βελτιώσουμε;"

Βήμα 4: Επανεξέταση

Εμφανίζεται μόνο εάν σύνολο >= 100 Τελική υποβολή.

Αυτό δεν είναι ακραίο. Αλλά είναι αρκετό για να αποκαλύψει τις αρχιτεκτονικές διαφορές. Μέρος 1: Βασισμένο σε στοιχεία (React Hook Form + Zod) Εγκατάσταση npm εγκατάσταση react-hook-form zod @hookform/resolvers @tanstack/react-query

Σχήμα Zod Ας ξεκινήσουμε με το σχήμα Zod, γιατί συνήθως εκεί εδραιώνεται το σχήμα της φόρμας. Για τα δύο πρώτα βήματα — προσωπικά στοιχεία και εισαγωγές παραγγελιών — όλα είναι απλά: απαιτούμενες συμβολοσειρές, αριθμοί με ελάχιστα και ένα πλήθος. Το ενδιαφέρον μέρος ξεκινά όταν προσπαθείτε να εκφράσετε τους υπό όρους κανόνες.

εισαγωγή { z } από "zod";

εξαγωγή const formSchema = z.object({ firstName: z.string().min(1, "Απαιτείται"), email: z.string().email("Invalid email"), τιμή: z.number().min(0), quantity: z.number().min(1), taxRate: z.number(), has Account:[name:"]enum. z.string().optional(), κωδικός πρόσβασης: z.string().optional(), ικανοποίηση: z.number().min(1).max(5), positiveFeedback: z.string().optional(), improvementFeedback: z.string().optional(),}).superRefine((data, {Afsda) = "Ifsda)=ha"=>> (!data.username) { ctx.addIssue({ code: "custom", path: ["username"], message: "Required" } } if (!data.password || data.password.length < 6) { ctx.addIssue({ code: "custom", path:"}); }

if (data.satisfaction >= 4 && !data.positiveFeedback) { ctx.addIssue({ code: "custom", path: ["positiveFeedback"], message: "Please share that you like" }); }

if (data.satisfaction <= 2 && !data.improvementFeedback) { ctx.addIssue({ code: "custom", path:["improvementFeedback"], μήνυμα: "Πες μας τι να βελτιώσουμε" }); }});

τύπος εξαγωγής FormData = z.infer;

Σημειώστε ότι το όνομα χρήστη και ο κωδικός πρόσβασης πληκτρολογούνται ως optional() παρόλο που απαιτούνται υπό όρους, επειδή το σχήμα επιπέδου τύπου του Zod περιγράφει το σχήμα του αντικειμένου και όχι τους κανόνες που διέπουν πότε τα πεδία έχουν σημασία. Η υπό όρους απαίτηση πρέπει να βρίσκεται μέσα στο superRefine, το οποίο εκτελείται μετά την επικύρωση του σχήματος και έχει πρόσβαση στο πλήρες αντικείμενο. Αυτός ο διαχωρισμός δεν είναι ελάττωμα. είναι ακριβώς αυτό για το οποίο έχει σχεδιαστεί το εργαλείο: το superRefine είναι το σημείο όπου η λογική μεταξύ πεδίων πηγαίνει όταν δεν μπορεί να εκφραστεί στην ίδια τη δομή του σχήματος. Αυτό που είναι επίσης αξιοσημείωτο εδώ είναι τι δεν εκφράζει αυτό το σχήμα. Δεν έχει έννοια σελίδων, δεν έχει ιδέα για το ποια πεδία είναι ορατά σε ποιο σημείο και δεν έχει έννοια πλοήγησης. Όλα αυτά θα ζήσουν κάπου αλλού. Στοιχείο φόρμας

εισαγωγή { useForm, useWatch } από "react-hook-form";εισαγωγή { zodResolver } από "@hookform/resolvers/zod";εισαγωγή { useMutation } από "@tanstack/react-query";εισαγωγή {useState, useMemo } από "react", εισαγωγή τύπος {formSche,

const STEPS = ["λεπτομέρειες", "παραγγελία", "λογαριασμός", "αναθεώρηση"];

πληκτρολογήστε OrderPayload = FormData & { subtotal: number; φόρος: αριθμός; σύνολο: αριθμός };

συνάρτηση εξαγωγής RHFMultiStepForm() { const [βήμα, setStep] = useState(0);

const mutation = useMutation({ mutationFn: async (ωφέλιμο φορτίο: OrderPayload) => { const res = await fetch("/api/orders", { μέθοδος: "POST", κεφαλίδες: { "Content-Type": "application/json" }, σώμα: JSON.stringify(ωφέλιμο φορτίο), }); εάν (!res.ok) ρίχνει νέο Σφάλμα("Αποτυχία υποβολής"); return res.json(); }, });

const { register, control, handleSubmit, formState: { errors }, } = useForm({ solver: zodResolver(formSchema), defaultValues: { price: 0, quantity: 1, taxRate: 0.1, satisfaction: 3, hasAccount: "No}", }); const price = useWatch({ control, name: "price" }); const quantity = useWatch({ control, name: "quantity" }); const taxRate = useWatch({ control, name: "taxRate" }); const hasAccount = useWatch({ control, name: "hasAccount" }); const satisfaction = useWatch({ control, name: "satisfaction" }); const subtotal = useMemo(() => (τιμή ?? 0) * (ποσότητα ?? 1), [τιμή, ποσότητα]); const tax = useMemo(() => subtotal * (taxRate ?? 0), [subtotal, taxRate]); const total = useMemo(() => υποσύνολο + φόρος, [υποσύνολο, φόρος]); const onSubmit = (δεδομένα: FormData) => mutation.mutate({ ...data, subtotal, tax, total }); const showSubmit = (βήμα === 2 && σύνολο < 100) || (βήμα === 3 && σύνολο >= 100)

επιστροφή (

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

{step === 1 && ( <>

Υποσύνολο: {subtotal}
Φόρος: {tax}
Σύνολο: {total}
)}

{step === 2 && ( <>

{hasAccount === "Ναι" && ( <> )}

{satisfaction >= 4 && (