Ten artykuł jest sponsorowany przez SurveyJS Istnieje model mentalny, którym podziela się większość programistów React, nigdy nie omawiając go na głos. Że formy zawsze mają być komponentami. Oznacza to stos taki jak:
Reaguj na formularz Hook dla stanu lokalnego (minimalne ponowne renderowanie, ergonomiczna rejestracja w terenie, konieczna interakcja). Zod do walidacji (poprawność danych wejściowych, walidacja granic, parsowanie bezpieczne dla typu). Reaguj na zapytanie dotyczące zaplecza: przesyłanie, ponowne próby, buforowanie, synchronizacja serwera i tak dalej.
W przypadku zdecydowanej większości formularzy – ekranów logowania, stron ustawień, modułów CRUD – działa to naprawdę dobrze. Każdy element spełnia swoje zadanie, komponuje się przejrzyście i możesz przejść do tych części aplikacji, które faktycznie wyróżniają Twój produkt. Jednak od czasu do czasu w formularzu zaczynają się gromadzić takie elementy, jak reguły widoczności zależne od wcześniejszych odpowiedzi lub wartości pochodne, które przechodzą kaskadą przez trzy pola. Może nawet całe strony, które należy pominąć lub wyświetlić na podstawie sumy bieżącej. Pierwszy warunek obsługujesz za pomocą useWatch i wbudowanej gałęzi, co jest w porządku. Potem kolejny. Następnie sięgasz po superRefine, aby zakodować reguły międzypolowe, których Twój schemat Zoda nie może wyrazić w normalny sposób. Następnie nawigacja krokowa zaczyna przeciekać logikę biznesową. W pewnym momencie patrzysz na to, co zbudowałeś i zdajesz sobie sprawę, że formularz nie jest już tak naprawdę interfejsem użytkownika. To raczej proces decyzyjny, a drzewo komponentów to miejsce, w którym je zapisałeś. Myślę, że w tym miejscu mentalny model formularzy w Reactie się załamuje i tak naprawdę nie jest to niczyja wina. Stos RHF + Zod jest doskonały w tym, do czego został zaprojektowany. Problem w tym, że mamy tendencję do używania go aż do momentu, w którym jego abstrakcje odpowiadają problemowi, ponieważ alternatywa wymaga zupełnie innego sposobu myślenia o formach. Ten artykuł dotyczy tej alternatywy. Aby to pokazać, dwukrotnie zbudujemy dokładnie ten sam wieloetapowy formularz:
Z formularzem React Hook + Zod podłączonym do zapytania React w celu przesłania, Z SurveyJS, który traktuje formularz jako dane — prosty schemat JSON — a nie drzewo komponentów.
Te same wymagania, ta sama logika warunkowa, to samo wywołanie API na końcu. Następnie dokładnie zmapujemy, co się przeniosło, a co pozostało, a także przedstawimy praktyczny sposób decydowania, jakiego modelu należy użyć i kiedy. Formularz, który budujemy:
W tym formularzu zastosowano 4-etapowy proces: Krok 1: Szczegóły
Imię (wymagane), Adres e-mail (wymagany, prawidłowy format).
Krok 2: Zamów
Cena jednostkowa, Ilość, stawka podatku, Pochodne: Suma częściowa, podatek, Razem.
Krok 3: Konto i opinia
Czy masz konto? (Tak/Nie) Jeśli Tak → nazwa użytkownika + hasło, oba wymagane. Jeśli Nie → e-mail już zebrany w kroku 1.
Ocena satysfakcji (1–5) Jeśli ≥ 4 → zapytaj „Co Ci się podobało?” Jeśli ≤ 2 → zapytaj „Co możemy ulepszyć?”
Krok 4: Przegląd
Pojawia się tylko wtedy, gdy suma >= 100 Ostateczne złożenie.
To nie jest ekstremalne. Ale to wystarczy, aby wyeksponować różnice architektoniczne. Część 1: Sterowanie komponentami (forma haka React + Zod) Instalacja npm zainstaluj formularz reakcji-hook zod @hookform/resolvers @tanstack/react-query
Schemat Zoda Zacznijmy od schematu Zoda, bo to tam zazwyczaj ustalany jest kształt formy. W pierwszych dwóch krokach — danych osobowych i danych wejściowych zamówienia — wszystko jest proste: wymagane ciągi znaków, liczby z wartościami minimalnymi i wyliczenie. Interesująca część zaczyna się, gdy próbujesz wyrazić reguły warunkowe.
importuj { z } z "zod";
eksport const formSchema = z.object({ imię: z.string().min(1, "Wymagane"), e-mail: z.string().email("Nieprawidłowy adres e-mail"), cena: z.number().min(0), ilość: z.number().min(1), stawka podatku: z.number(), hasAccount: z.enum(["Tak", "Nie"]), nazwa użytkownika: z.string().opcjonalne(), hasło: z.string().opcjonalne(), satysfakcja: z.number().min(1).max(5), pozytywne sprzężenie zwrotne: z.string().opcjonalne(), ulepszenieOpinia zwrotna: z.string().opcjonalne(),}).superRefine((dane, ctx) => { if (data.hasAccount === "Tak") { if (!data.username) { ctx.addIssue({ kod: "niestandardowe", ścieżka: ["nazwa użytkownika"], wiadomość: "Wymagane" }); if (!data.hasło || data.hasło.długość < 6) { ctx.addIssue({ kod: "niestandardowe", ścieżka: ["hasło"], wiadomość: "Min. 6 znaków" } } }
if (data.satisfaction >= 4 && !data.positiveFeedback) { ctx.addIssue({ kod: "custom", ścieżka: ["positiveFeedback"], wiadomość: "Udostępnij, co Ci się podobało" }); }
if (data.satisfaction <= 2 && !data.improvementFeedback) { ctx.addIssue({ code: "custom", ścieżka:["improvementFeedback"], wiadomość: "Proszę nam powiedzieć, co mamy poprawić" }); }});
typ eksportu FormData = z.infer
Zwróć uwagę, że nazwa użytkownika i hasło są wpisywane jako opcjonalne(), mimo że są wymagane warunkowo, ponieważ schemat poziomu typu Zoda opisuje kształt obiektu, a nie reguły rządzące tym, kiedy pola mają znaczenie. Warunkowe wymaganie musi znajdować się w superRefine, które jest uruchamiane po sprawdzeniu kształtu i uzyskuje dostęp do całego obiektu. To oddzielenie nie jest wadą; właśnie do tego zaprojektowano to narzędzie: superRefine to miejsce, w którym logika międzypolowa trafia tam, gdzie nie można jej wyrazić w samej strukturze schematu. Godne uwagi jest to, czego ten schemat nie wyraża. Nie ma koncepcji stron, nie ma pojęcia, które pola są widoczne w którym momencie, ani nie ma koncepcji nawigacji. To wszystko będzie żyło gdzie indziej. Komponent formularza
import { useForm, useWatch } z "react-hook-form"; import { zodResolver } z "@hookform/resolvers/zod"; import { useMutation } z "@tanstack/react-query"; import { useState, useMemo } z "react"; import { formSchema, wpisz FormData } z "./schema";
const KROKI = ["szczegóły", "zamówienie", "konto", "recenzja"];
wpisz OrderPayload = FormData & { suma częściowa: liczba; podatek: liczba; łącznie: liczba };
funkcja eksportu RHFMultiStepForm() { const [krok, setStep] = useState(0);
stała mutacja = useMutation({ mutacjaFn: asynchroniczny (ładunek: OrderPayload) => { const res = oczekiwanie na pobranie("/api/zamówienia", { metoda: „POST”, nagłówki: { "Content-Type": "application/json" }, treść: JSON.stringify(ładunek), }); if (!res.ok) wyrzuć nowy błąd("Nie udało się przesłać"); zwróć res.json(); }, });
const { rejestr, kontrola, handleSubmit, formState: { błędy }, } = useForm
return (
);}Zobacz Pen SurveyJS-03-RHF [rozwidlony] przez sixthextinction. Dzieje się tu całkiem sporo i warto zwolnić, żeby zauważyć, jak to wszystko się skończyło.
Wyprowadzone wartości — suma częściowa, podatek, suma — są obliczane w komponencie za pomocą useWatch i useMemo, ponieważ zależą od bieżących wartości pól i nie ma dla nich innego naturalnego miejsca. Reguły widoczności nazwy użytkownika, hasła, pozytywnej opinii i ulepszenia opinii są dostępne w JSX jako wbudowane warunki warunkowe. Logika pomijania kroków — strona recenzji pojawia się tylko wtedy, gdy suma >= 100 — jest osadzona w zmiennej showSubmit i warunku renderowania w kroku 3. Sama nawigacja to po prostu licznik stanu użycia, który zwiększamy ręcznie. React Query obsługuje ponowne próby, buforowanie i unieważnianie. Formularz po prostu wywołuje mutację.mutate ze zweryfikowanymi danymi.
Nic z tego nie jest złe samo w sobie. To wciąż idiomatyczny React, a komponent jest dość wydajny dzięki temu, jak RHF izoluje ponowne renderowanie. Gdyby jednak przekazać to komuś, kto tego nie napisał i poprosić o wyjaśnienie, w jakich warunkach pojawia się strona z recenzją, osoba ta musiałaby prześledzić showSubmit, warunek renderowania z kroku 3 i logikę przycisku nawigacyjnego — w trzech oddzielnych miejscach — aby zrekonstruować regułę, którą można zapisać w jednym wierszu. Formularz działa, to prawda, ale zachowania jako systemu nie da się tak naprawdę sprawdzić. Trzeba to wykonać mentalnie. Co ważniejsze, zmiana tego wymaga zaangażowania inżynierów. Nawet niewielka zmiana, np. dostosowanie momentu pojawienia się etapu przeglądu, oznacza edycję komponentu, aktualizację walidacji, otwarcie żądania ściągnięcia, oczekiwanie na sprawdzenie i ponowne wdrożenie. Część 2: Oparta na schemacie (SurveyJS) Teraz zbudujmy ten sam przepływ, korzystając ze schematu. Instalacja npm zainstaluj ankietę-core ankietę-reaguj-ui @tanstack/react-query
Survey-coreNiezależny od platformy silnik wykonawczy na licencji MIT, który obsługuje renderowanie formularzy w SurveyJS — to część, na której nam zależy. Pobiera schemat JSON, buduje na jego podstawie model wewnętrzny i obsługuje wszystko, co w przeciwnym razie istniałoby w komponencie React: ocenianie wyrażeń widoczności, obliczanie pochodnych wartości, zarządzanie stanem strony, śledzenie sprawdzania poprawności i decydowanie, co oznacza „kompletny”, biorąc pod uwagę, które strony zostały faktycznie wyświetlone.
Survey-React-ui Interfejs użytkownika/warstwa renderowania, która łączy ten model z React. Zasadniczo jest to komponent
Razem zapewniają w pełni funkcjonalne, wielostronicowe środowisko wykonawcze formularzy bez konieczności pisania ani jednej linii przepływu sterowania. Sam format schematu jest, jak powiedziano wcześniej, po prostu JSON – bez DSL ani niczego zastrzeżonego. Można go wstawić, zaimportować z pliku, pobrać z interfejsu API lub zapisać w kolumnie bazy danych i nawodnić w czasie wykonywania. Ta sama forma, co dane Oto ten sam formularz, tym razem wyrażony jako obiekt JSON. Schemat definiuje wszystko: strukturę, walidację, reguły widoczności, obliczenia pochodne, nawigację po stronach — i przekazuje to Modelowi, który ocenia to w czasie wykonywania. Oto jak to wygląda w całości:
eksport const SurveySchema = { tytuł: "Przepływ zamówienia", showProgressBar: "top", strony: [ { name: "details", elementy: [ { type: "text", name: "firstName", isRequired: true }, { type: "text", name: "email", inputType: "email", isRequired: true, validators: [{ type: "email", tekst: "Nieprawidłowy adres e-mail" }] } ] }, { nazwa: "zamówienie", elementy: [ { typ: "tekst", nazwa: "cena", typ wejścia: "liczba", wartość domyślna: 0 }, { typ: "tekst", nazwa: "ilość", typ wejścia: "liczba", wartość domyślna: 1 }, { typ: "dropdown",nazwa: "taxRate", wartość domyślna: 0,1, wybory: [ { wartość: 0,05, tekst: "5%" }, { wartość: 0,1, tekst: "10%" }, { wartość: 0,15, tekst: "15%" } ] }, { typ: "wyrażenie", nazwa: "suma częściowa", wyrażenie: "{cena} {ilość}" }, { typ: "wyrażenie", nazwa: "podatek", wyrażenie: "{suma częściowa} {podatekRate}" }, { typ: "wyrażenie", nazwa: "suma", wyrażenie: "{suma częściowa} + {podatek}" } ] }, { nazwa: "konto", elementy: [ { typ: "grupa radiowa", nazwa: "hasAccount", opcje: ["Tak", "Nie"] }, { typ: "tekst", nazwa: "nazwa użytkownika", widocznyIf: "{hasAccount} = „Tak”, isRequired: true }, { wpisz: „tekst”, nazwa: „hasło”, inputType: „hasło”, widocznyIf: „{hasAccount} = 'Tak'”, isRequired: true, walidatory: [{ typ: „tekst”, minLength: 6, tekst: „Min 6 znaków” }] }, { wpisz: „ocena”, nazwa: „satysfakcja”, stawkaMin: 1, stawkaMax: 5 }, { wpisz: "komentarz", nazwa: "pozytywneFeedback", widocznyIf: "{satysfakcja} >= 4" }, { wpisz: "komentarz", nazwa: "poprawaOpinia", widocznyIf: "{satisfaction} <= 2" } ] }, { nazwa: "recenzja", widocznyIf: "{total} >= 100", elementy: [] } ]};
Porównaj to przez chwilę z wersją RHF.
Blok superRefine, który warunkowo wymagał nazwy użytkownika i hasła, zniknął. widoczneIf: „{hasAccount} = 'Tak'” w połączeniu z isRequired: true obsługuje oba problemy razem, w samym polu, w którym można je znaleźć. Łańcuch useWatch + useMemo, który obliczał sumę częściową, podatek i sumę, został zastąpiony trzema polami wyrażeń, które odwołują się do siebie po nazwie. Stan strony recenzji, który w wersji RHF można było odtworzyć jedynie poprzez śledzenie poprzez showSubmit, gałąź renderowania kroku 3. I na koniec, logika przycisku nawigacyjnego to pojedyncza właściwość widoczna w obiekcie strony.
Jest tam ta sama logika. Tyle, że schemat zapewnia mu miejsce do życia, gdzie jest widoczny oddzielnie, a nie rozproszony po całym komponencie. Należy również pamiętać, że schemat używa typu: „expression” dla sumy częściowej, podatku i sumy. Wyrażenie jest tylko do odczytu i używane głównie do wyświetlania obliczonych wartości. SurveyJS obsługuje również typ: 'html' dla treści statycznych, ale w przypadku wartości obliczonych właściwym wyborem jest wyrażenie. Teraz o stronie Reagowania. Renderowanie i przesyłanie Bardzo proste. Podłącz onComplete do swojego API w ten sam sposób — poprzez useMutation lub zwykłe pobieranie:
importuj { useState, useEffect, useRef } z „react”; importuj { useMutation } z „@tanstack/react-query”; importuj { Model } z „survey-core”; importuj { Ankieta } z „survey-react-ui”; importuj „survey-core/survey-core.css”;
funkcja eksportu SurveyForm() { const [model] = useState(() => new Model(surveySchema));
stała mutacja = useMutation({ mutacjaFn: asynchroniczny (dane) => { const res = oczekiwanie na pobranie("/api/zamówienia", { metoda: „POST”, nagłówki: { "Content-Type": "application/json" }, treść: JSON.stringify(dane), }); if (!res.ok) wyrzuć nowy błąd("Nie udało się przesłać"); zwróć res.json(); }, });
const mutacjaRef = useRef(mutacja); mutacjaRef.prąd = mutacja; useEffect(() => { const handler = (nadawca) => mutacjaRef.current.mutate(sender.data); model.onComplete.add(handler); return () => model.onComplete.remove(handler); }, [model]); // ref pozwala uniknąć ponownej rejestracji modułu obsługi przy każdym renderowaniu (zmiana tożsamości obiektu mutacji)
powrót (
<>
Zobacz ankietę Pen SurveyJS-03-SurveyJS [rozwidloną] przez sixthextinction.
onComplete uruchamia się, gdy użytkownik dotrze do końca ostatniej widocznej strony. Jeśli więc suma nigdy nie przekroczy 100, a strona recenzji zostanie pominięta, nadal będzie uruchamiana poprawnie, ponieważ SurveyJS ocenia widoczność przed podjęciem decyzji, co oznacza „ostatnia strona”. Następnie sender.data zawiera wszystkie odpowiedzi wraz z obliczonymi wartościami (suma częściowa, podatek, suma) jako pola najwyższej klasy, dzięki czemu ładunek API jest identyczny z tym, który wersja RHF zebrała ręcznie w onSubmit. TheWzorzec mutacjiRef jest tym samym, po który można sięgnąć wszędzie tam, gdzie potrzebna jest stabilna obsługa zdarzeń z wartością zmieniającą się przy każdym renderowaniu — nie ma w nim nic specyficznego dla SurveyJS.
Komponent React nie zawiera już żadnej logiki biznesowej. Nie ma useWatch, nie ma warunkowego JSX, nie ma licznika kroków, nie ma łańcucha useMemo, nie ma superRefine. React robi to, w czym jest naprawdę dobry: renderuje komponent i łączy go z wywołaniem API. Co wyszło z React?
Troska Stos RHF AnkietaJS Widoczność Oddziały JSX widoczneJeśli Wartości pochodne użyj Obejrzyj / użyj Memo wyrażenie Zasady międzypolowe superrafinuj Warunki schematu Nawigacja stan krokowy Strona widocznaJeśli Lokalizacja reguły Rozproszone w plikach Scentralizowane w schemacie
To, co pozostaje w React, to układ, styl, okablowanie do przesyłania i integracja aplikacji, czyli rzeczy, do których React jest właściwie zaprojektowany. Wszystko inne zostało przeniesione do schematu, a ponieważ schemat jest po prostu obiektem JSON, można go przechowywać w bazie danych, wersjonować niezależnie od kodu aplikacji lub edytować za pomocą wewnętrznych narzędzi bez konieczności wdrażania. Menedżer produktu, który musi zmienić próg uruchamiający stronę recenzji, może to zrobić bez dotykania komponentu. To znacząca różnica operacyjna dla zespołów, w których zachowanie formy często ewoluuje i nie zawsze jest sterowane przez inżynierów. Kiedy stosować każde podejście? Oto dobra zasada, która się u mnie sprawdza: wyobraź sobie, że całkowicie usuwasz formularz. Co byś stracił?
Jeśli są to ekrany, potrzebujesz formularzy opartych na komponentach. Jeśli chodzi o logikę biznesową, taką jak progi, reguły rozgałęzień i wymagania warunkowe, które kodują rzeczywiste decyzje, potrzebujesz silnika schematu.
Podobnie, jeśli nadchodzące zmiany dotyczą głównie etykiet, pól i układu, RHF będzie Ci dobrze służyć. Jeśli dotyczą warunków, wyników i zasad, które Twój zespół operacyjny lub zespół prawny może potrzebować dostosować we wtorkowe popołudnie bez składania zgłoszenia, model schematu z SurveyJS będzie bardziej uczciwy. Te dwa podejścia tak naprawdę nie konkurują ze sobą. Odnoszą się do różnych klas problemów, a błędem, którego warto unikać, jest niedopasowanie abstrakcji do wagi logiki – traktowanie systemu reguł jak komponentu, ponieważ jest to znane narzędzie, lub sięganie po silnik polityki, ponieważ formularz rozrósł się do trzech kroków i uzyskał pole warunkowe. Forma, którą tutaj zbudowaliśmy, celowo mieści się blisko granicy, na tyle złożona, aby ukazać różnicę, ale nie tak ekstremalna, aby porównanie wydawało się sfałszowane. Większość prawdziwych formularzy, które stały się nieporęczne w twoim kodzie, prawdopodobnie znajduje się w pobliżu tej samej granicy i zwykle pojawia się pytanie, czy ktoś nazwał, czym one właściwie są. Użyj React Hook Form + Zod, gdy:
Formularze są zorientowane na CRUD; Logika jest płytka i opiera się na interfejsie użytkownika; Inżynierowie są właścicielami wszystkich zachowań; Backend pozostaje źródłem prawdy.
Użyj SurveyJS, gdy:
Formularze kodują decyzje biznesowe; Reguły ewoluują niezależnie od interfejsu użytkownika; Logika musi być widoczna, możliwa do kontrolowania lub wersjonowana; Osoby niebędące inżynierami wpływają na zachowanie; Ten sam formularz musi działać na wielu interfejsach.