Strona główna / Artykuły

Ręcznie budowane formularze w React: kontrolowane pola wejściowe, walidacja i stany wysyłania

Dowiedz się, jak działają formularze w React – od kontrolowanych pól wejściowych i obsługiwaczy różnych typów po walidację, stany wysyłania, pola dynamiczne, przesyłanie plików oraz sytuacje, w których biblioteki formularzy są przydatne.

Wydawca: REACTAPP.TOP13 min czytania2992 słów

Źródła: React Forms: A Complete Guide from Basics to Production Patterns

Logowanie, zakupy, wyszukiwanie, ustawienia profilu, ankiety – niemal każde aplikacja opiera się na formularzach. W React formularz oznacza zarządzanie wartościami, walidacją, komunikatami o błędach, wysyłką danych, stanem ładowania i błędami serwera, polami dodawanymi w czasie wykonywania aplikacji, możliwością sformatowania formularza oraz optymalizacją wydajności. Ten przewodnik przedstawia ten proces krok po kroku, od kontrolowanych pól wejściowych po kompletny formularz rejestracyjny bez użycia bibliotek, pokazując również, kiedy warto zastosować React Hook Form i Zod.

Jak formularz funkcjonuje w aplikacji React

Formularz zbiera informacje od użytkownika, jak w tym ekranie rejestracji:

┌──────────────────────────────┐
│       Create Account         │
│                              │
│ Name     [______________]    │
│ Email    [______________]    │
│ Password [______________]    │
│                              │
│         [ Register ]         │
└──────────────────────────────┘

React łączy te pola wejściowe z logiką aplikacji, a prawie każdy formularz przechodzi przez ten sam cykl życia:

User Input
    ↓
Form State
    ↓
Validation
    ↓
Submit
    ↓
API Request
    ↓
Success / Error

Komponenty kontrolowane: stan jako źródło prawdy

W komponencie kontrolowanym value pola wejściowego pochodzi ze stanu React, a każda zmiana jest zapisywana z powrotem za pomocą onChange.

import { useState } from "react";

function LoginForm() {
  const [email, setEmail] = useState("");

  return (
    <input
      value={email}
      onChange={(event) => {
        setEmail(event.target.value);
      }}
    />
  );
}

Ustawienie value={email} oznacza, że to React decyduje, co ma być wyświetlone. Każdy nacisk klawisza przechodzi przez ten cykl:

User types
    ↓
onChange fires
    ↓
setEmail()
    ↓
React state updates
    ↓
Component renders
    ↓
Input receives new value

Ponieważ wyświetlana wartość zawsze pochodzi ze stanu, stan jest jedynym źródłem prawdy.

Nazwa „kontrolowany” odzwierciedla fakt, że pole wejściowe nie posiada własnej wartości:

<input
  value={email}
  onChange={handleChange}
/>

Dane przepływają ze stanu do elementu, poprzez wpisywanie tekstu przez użytkownika i obsługę zdarzeń, a następnie z powrotem do stanu:

             React State
                  │
                  ▼
              input value
                  │
                  ▼
              User types
                  │
                  ▼
              onChange
                  │
                  ▼
             React State

Jeden obiekt stanu dla wielu pól

Korzystanie z osobnego wywołania useState dla każdego pola powoduje słabą skalowalność:

const [name, setName] = useState("");
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");

Jedno obiekt z kluczem opartym na nazwie pola jest łatwiejszy w utrzymaniu. Ten format przechowuje trzy pola w jednym obiekcie, dzieli się jedną funkcją handleChange i zapisuje dane po wysłaniu formularza.

import { useState } from "react";

function RegisterForm() {
  const [form, setForm] = useState({
    name: "",
    email: "",
    password: "",
  });

  const handleChange = (event) => {
    const { name, value } = event.target;

    setForm((previous) => ({
      ...previous,
      [name]: value,
    }));
  };

  const handleSubmit = (event) => {
    event.preventDefault();

    console.log(form);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        name="name"
        value={form.name}
        onChange={handleChange}
      />

      <input
        name="email"
        value={form.email}
        onChange={handleChange}
      />

      <input
        name="password"
        type="password"
        value={form.password}
        onChange={handleChange}
      />

      <button type="submit">
        Register
      </button>
    </form>
  );
}

Kluczową linią jest obliczana właściwość [name]: value. Atrybut name każdego pola wprowadzania danych odpowiada kluczu stanu, więc jedna funkcja aktualizuje to pole, które zostało aktywowane.

Dla pól tekstowych funkcja ta odczytuje treść elementu z zdarzenia:

<input
  value={name}
  onChange={(event) => {
    setName(event.target.value);
  }}
/>

event.target to element wprowadzania danych, więc event.target.value to jego aktualna treść tekstowa:

User types: Salim

event.target.value
       ↓
"Salim"

Prawidłowe obsługiwanie każdego typu pola

Formularze zawierają pola tekstowe, hasła, liczbowe, checkboxy, radio, select, textarea, pliki, daty oraz kontrolery zakresu. Większość z nich follows ten sam schemat, z kilkoma istotnymi różnicami.

Tekst, hasło i liczby

Pole tekstowe łączy value z jego kluczem:

<input
  type="text"
  value={form.name}
  onChange={handleChange}
/>

Pole na hasło różni się tylko parametrem type:

<input
  type="password"
  value={form.password}
  onChange={handleChange}
/>

Tak samo jest z polem numerycznym:

<input
  type="number"
  value={form.age}
  onChange={handleChange}
/>

Problem polega na tym, że event.target.value jest łańcuchem nawet w przypadku ustawienia type="number". Poniższy fragment łączy kod z wyjaśnieniem – chodzi o to, że otrzymujemy wartość "25" zamiast 25, więc konieczna jest funkcja Number() do jej przekształcenia.

const age = event.target.value;

may produce:

"25" rather than: 25

If you need a number:

const age = Number(event.target.value);

Uwaga na puste wartości: Number("") zwraca 0, dlatego wiele form zachowuje surowy łańcuch i przekształca go dopiero podczas weryfikacji lub wysyłki.

Pole wyboru używa atrybutu checked

Dla pola wyboru należy powiązać atrybut checked z wartością logiczną i odczytywać event.target.checked:

<input
  type="checkbox"
  checked={form.agree}
  onChange={(event) => {
    setForm((previous) => ({
      ...previous,
      agree: event.target.checked,
    }));
  }}
/>

event.target.value w polu typu checkbox zwraca jego atrybut value (domyślnie "on"), a nie informację o tym, czy jest zaznaczone.

Grupy select, textarea i radio

Element select jest kontrolowany za pomocą atrybutu value znajdującego się w tym elemencie; pusta pierwsza opcja służy jako miejsce zastępcze.

<select
  name="role"
  value={form.role}
  onChange={handleChange}
>
  <option value="">
    Select role
  </option>

  <option value="frontend">
    Frontend Developer
  </option>

  <option value="backend">
    Backend Developer
  </option>
</select>

React traktuje textarea jak zwykłe pole wejścia, używając do niego właściwości value zamiast tekstu zawartego wewnątrz.

<textarea
  name="message"
  value={form.message}
  onChange={handleChange}
/>

Przyciski radio w grupie dzielą się tą samą wartością name. Każdy z nich jest sprawdzany, gdy jego wartość value odpowiada aktualnemu stanowi, a wspólny obsługiwacz zapisuje wybraną opcję do form.gender.

<label>
  <input
    type="radio"
    name="gender"
    value="male"
    checked={form.gender === "male"}
    onChange={handleChange}
  />
  Male
</label>

<label>
  <input
    type="radio"
    name="gender"
    value="female"
    checked={form.gender === "female"}
    onChange={handleChange}
  />
  Female
</label>

Przejmowanie procesu wysyłania

Domyślnie przeglądarka sama wysyła formularz i może przenieść się do innej strony lub ponownie ją załadować. Aplikacje React obsługują wysyłanie danych w JavaScript, więc funkcja obsługująca wysyłkę zaczyna się od event.preventDefault(), tak jak w przykładzie rejestracji. Następnie tok działania zależy od Ciebie:

Submit
  ↓
preventDefault()
  ↓
Validate
  ↓
API
  ↓
Update UI

Walidacja przed żądaniem

Walidacja sprawdza, czy dane wejściowe są dopuszczalne. Pierwsza wersja gromadzi błędy opisane pod kątem poszczególnych pól:

const validate = () => {
  const errors = {};

  if (!form.name.trim()) {
    errors.name = "Name is required";
  }

  if (!form.email.trim()) {
    errors.email = "Email is required";
  }

  if (!form.password) {
    errors.password = "Password is required";
  }

  return errors;
};

Funkcja obsługująca wysyłkę najpierw waliduje dane, przechowuje błędy i wychodzi wcześniej, jeśli jakiekolwiek istnieją:

const handleSubmit = (event) => {
  event.preventDefault();

  const validationErrors = validate();

  if (Object.keys(validationErrors).length > 0) {
    setErrors(validationErrors);
    return;
  }

  // Submit form
};

Błędy znajdują się w osobnym stanie:

const [errors, setErrors] = useState({});

W danym momencie może on zawierać:

{
  email: "Invalid email",
  password: "Password must contain at least 8 characters"
}

Każde pole wyświetla swoją informację warunkowo:

{errors.email && (

  <p>{errors.email}</p>

)}

Lepsza funkcja validate przyjmuje formularz jako argument, co sprawia, że jest czysta i poddawalna testom, oraz sprawdza format i długość po pojawieniu się danego pola:

function validate(form) {
  const errors = {};

  if (!form.name.trim()) {
    errors.name = "Name is required";
  }

  if (!form.email.trim()) {
    errors.email = "Email is required";
  } else if (!form.email.includes("@")) {
    errors.email = "Invalid email";
  }

  if (!form.password) {
    errors.password = "Password is required";
  } else if (form.password.length < 8) {
    errors.password =
      "Password must contain at least 8 characters";
  }

  return errors;
}

Obsługa wywołuje to w ten sam sposób:

const errors = validate(form);

if (Object.keys(errors).length > 0) {
  setErrors(errors);
  return;
}

Test includes("@") łapie tylko oczywiste błędy pisowni; prawdziwym dowodem na poprawność adresu jest e-mail z potwierdzeniem.

Walidacja po stronie klienta odbywa się w przeglądarce i zazwyczaj obejmuje:

  1. Pole obowiązkowe
  2. Format adresu e-mail
  3. Minimalna długość hasła
  4. Potwierdzenie hasła

Daje szybką informację zwrotną, lepsze doświadczenie użytkownika oraz mniej niepotrzebnych żądań, ale nie stanowi bariery bezpieczeństwa. Serwer musi powtórzyć te sprawdzenia i odpowiedzieć na pytania, których przeglądarka nie może zadać:

  1. Czy adres e-mail jest już zarejestrowany?
  2. Czy użytkownik ma uprawnienia?
  3. Czy cena jest ważna?
  4. Czy ten użytkownik może wykonać tę operację?

Aplikacje bezpieczne wykorzystują obie formy walidacji. Nigdy nie pisz takiej logiki:

if (clientValidationPassed) {
  // Data is safe
}

Weryfikacje przeglądarki można obejść poprzez edycję strony lub bezpośrednie wywołanie API, dlatego serwer weryfikuje każdą prośbę niezależnie:

Request
  ↓
Authentication
  ↓
Authorization
  ↓
Validation
  ↓
Business Logic
  ↓
Database

Modelowanie stanów wysyłania

Formularz produkcyjny przechodzi przez różne stany, które powinien odzwierciedlać interfejs użytkownika:

Idle
 ↓
Submitting
 ↓
Success

or

Idle
 ↓
Submitting
 ↓
Error

Dwa elementy stanu je śledzą:

const [isSubmitting, setIsSubmitting] = useState(false);
const [serverError, setServerError] = useState("");

Obsługa asynchroniczna ustawia flagę, usuwa stare błędy serwera, czeka na prośbę, zapisuje przyjazną wiadomość w przypadku niepowodzenia i w każdym razie resetuje flagę w bloku finally.

const handleSubmit = async (event) => {
  event.preventDefault();

  setIsSubmitting(true);
  setServerError("");

  try {
    await createUser(form);
  } catch (error) {
    setServerError("Unable to create account");
  } finally {
    setIsSubmitting(false);
  }
};

Wyłączanie i przemianowywanie nazwy przycisku podczas wysyłania prośby zapobiega dublowanym wysyłkom:

<button
  type="submit"
  disabled={isSubmitting}
>
  {isSubmitting
    ? "Creating..."
    : "Create Account"}
</button>

Życiowy cykl przycisku odpowiada procesowi wysyłania prośby:

User clicks Submit
      ↓
Request starts
      ↓
Button disabled
      ↓
Request finishes
      ↓
Button enabled

Wprowadzanie danych kontrolowane versus niekontrolowane

Przy wejściu kontrolowanym stan React przechowuje wartość. Poniższy blok pokazuje zwięzłą wersję, pełny komponent oraz przepływ danych, oddzielone liniami z gwiazdkami.

<input
  value={email}
  onChange={handleChange}
/>

********************************************

function Form() {
  const [email, setEmail] = useState("");

  return (
    <input
      value={email}
      onChange={(event) =>
        setEmail(event.target.value)
      }
    />
  );
}

********************************************



DOM event
   ↓
React state
   ↓
Render
   ↓
DOM value

Przy wejściu niekontrolowanym DOM przechowuje wartość, a React odczytuje ją za pomocą ref, zwykle przy wysyłaniu formularza, bez przechowywania każdego naciśnięcia klawisza:

const emailRef = useRef();

<input ref={emailRef} />

********************************************

function Form() {
  const emailRef = useRef(null);

  const handleSubmit = (event) => {
    event.preventDefault();

    console.log(emailRef.current.value);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input ref={emailRef} />

      <button type="submit">
        Submit
      </button>
    </form>
  );
}

********************************************

DOM
 ↓
Input value

Wybieraj wejścia kontrolowane do natychmiastowej walidacji, warunkowego interfejsu użytkownika, wartości pochodnych, formatowania, stanu sterowanego przez React lub niestandardowych interakcji. Ponieważ React zna wartość, interfejs może reagować natychmiastowo:

{password.length < 8 && (
  <p>Password is too short</p>
)}

Wybierz niekontrolowane pola wejściowe dla prostych form, gdy stan po każdym naciśnięciu klawisza nic nie dodaje, gdy chcesz mniej aktualizacji lub gdy biblioteka zarządza referencjami za ciebie. Działania form w React 19 również opierają się na tym modelu; zobacz asynchroniczne formy z useActionState i useOptimistic.

Kiedy biblioteka do form zasługuje na uwagę

React Hook Form

Ręczne podłączanie dziesiątek pól jest uciążliwe. Funkcja register w React Hook Form łączy pole wejściowe i określa jego zasady, handleSubmit weryfikuje dane przed wywołaniem funkcji zwrotnej, a formState.errors pokazuje komunikaty błędów.

import { useForm } from "react-hook-form";

function LoginForm() {
  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm();

  const onSubmit = (data) => {
    console.log(data);
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input
        {...register("email", {
          required: "Email is required",
        })}
      />

      {errors.email && (
        <p>{errors.email.message}</p>
      )}

      <input
        type="password"
        {...register("password", {
          required: "Password is required",
          minLength: {
            value: 8,
            message:
              "Password must be at least 8 characters",
          },
        })}
      />

      {errors.password && (
        <p>{errors.password.message}</p>
      )}

      <button type="submit">
        Login
      </button>
    </form>
  );
}

Uproszcza rejestrację, walidację, obsługę błędów, wysyłanie danych, resetowanie formularza, a także obsługę pól dynamicznych i stanu formularza. Ponieważ domyślnie rejestruje pola niekontrolowane, unika ponownego renderowania całego formularza przy każdym naciśnięciu klawisza, w odróżnieniu od prostych formularzy kontrolowanych.

Walidacja schematu za pomocą Zod

Gdy liczba reguł rośnie, rozproszone instrukcje if stają się trudne do zarządzania. Zod opisuje oczekiwany kształt danych w jednej deklaracji:

import { z } from "zod";

const userSchema = z.object({
  name: z
    .string()
    .min(2, "Name must be at least 2 characters"),

  email: z
    .string()
    .email("Invalid email"),

  password: z
    .string()
    .min(8, "Password must be at least 8 characters"),
});

Powszechnym połączeniem w produkcji jest używanie biblioteki do zarządzania stanem oraz schematu do definiowania reguł:

React Hook Form
       +
      Zod
       ↓
Form state + validation

Schemat może być tak zwięzły:

const formSchema = z.object({
  email: z.string().email(),
  password: z.string().min(8),
});

React Hook Form umożliwia włączenie schematu poprzez mechanizm resolvera, co jest przydatne w przypadku dużych formularzy, których reguły muszą pozostać spójne.

Pola dynamiczne, warunkowe i zależne

Powtarzające się grupy

Użytkownicy często dodają nowe wpisy w razie potrzeby, na przykład informacje o wykształceniu:

Education

Degree  [________]
College [________]

[ + Add Education ]

Zachowaj je jako tablicę obiektów:

const [education, setEducation] = useState([
  {
    degree: "",
    college: "",
  },
]);

Dodawanie wiersza dodaje pusty obiekt:

setEducation((previous) => [
  ...previous,
  {
    degree: "",
    college: "",
  },
]);

Wyświetlanie odbywa się poprzez przeglądanie tablicy:

{education.map((item, index) => (
  <div key={index}>
    <input
      value={item.degree}
      onChange={(event) => {
        // update degree
      }}
    />

    <input
      value={item.college}
      onChange={(event) => {
        // update college
      }}
    />
  </div>
))}

Indeks key jest bezpieczny tylko wtedy, gdy wiersze nigdy nie są usuwane ani przestawiane; w przeciwnym razie należy używać stabilnych identyfikatorów, ponieważ tekst o określonym typie może zostać przypisany do niewłaściwego wiersza.

Pola warunkowe

Część pól zależy od wcześniejszego wyboru, na przykład dane firmy w kontach biznesowych:

Account Type

○ Individual
○ Business

If the user chooses:

Business

show:
Company Name
GST Number
Company Address

Zarządza tym wyrażenie warunkowe:

{form.accountType === "business" && (
  <>
    <input
      name="companyName"
      value={form.companyName}
      onChange={handleChange}
    />

    <input
      name="gstNumber"
      value={form.gstNumber}
      onChange={handleChange}
    />
  </>
)}

Jeśli użytkownik wróci, usuń lub wyklucz wartości ukryte, aby nie zostały wysłane przestarzałe dane.

Wybory kaskadowe

Kraj, stan i miasto tworzą łańcuch:

Country
   ↓
State
   ↓
City

Zmiana na wyższym poziomie sformatowuje wszystko poniżej przed pobraniem nowych opcji:

Country changes
      ↓
Clear State
      ↓
Fetch states
      ↓
User selects State
      ↓
Fetch cities

Ponieważ opcje pochodzą z serwera, TanStack Query dobrze współpracuje z biblioteką formularzy w zakresie cacheowania i zarządzania stanami ładowania.

Zapisy plików

Pole wejścia na pliki nie można kontrolować; odczytaj plik z event.target.files:

<input
  type="file"
  onChange={(event) => {
    const file = event.target.files?.[0];

    if (file) {
      console.log(file);
    }
  }}
/>

Aby go przesłać, wyślij ciało typu FormData:

const formData = new FormData();

formData.append("name", form.name);
formData.append("file", file);

await fetch("/api/upload", {
  method: "POST",
  body: formData,
});

Nie ustawiaj samodzielnie Content-Type: multipart/form-data za pomocą fetch i FormData; przeglądarka generuje ten nagłówek wraz z wymaganym rozgraniczeniem.

Zachowanie szybkości dużych formularzy

Duże, złożone formularze obciążają prosty formularz kontrolowany, który przerysowuje się przy każdym naciśnięciu klawisza:

User types
   ↓
setState
   ↓
Render
   ↓
Large form tree

Potrzebne środki zaradcze:

  1. Zachowuj stan lokalnie, a nie globalnie bez konieczności.
  2. Rozdziel formularz na sekcje, aby każda zmiana powodowała mniej przerysowań:
Form
├── PersonalDetails
├── Address
├── Education
├── Experience
└── Documents
  1. Użyj odpowiedniej biblioteki, takiej jak React Hook Form.
  2. , np. sprawdzania dostępności nazwy użytkownika:
Username input
     ↓
Wait 300ms
     ↓
Check availability

Ostateczny przykład łączy stałą initialForm służącą do resetowania, oddzielny stan dla błędów pól, błędów podania i serwera, funkcję validate oraz asynchroniczne wysyłanie danych w formacie JSON, traktujące odpowiedź inną niż OK jako niepowodzenie, resetujące formę przy sukcesie i zawsze usuwające flagę ładowania. Etykiety łączą się z polami za pomocą htmlFor i id.

import { useState } from "react";

const initialForm = {
  name: "",
  email: "",
  password: "",
};

function RegisterForm() {
  const [form, setForm] = useState(initialForm);
  const [errors, setErrors] = useState({});
  const [isSubmitting, setIsSubmitting] =
    useState(false);
  const [serverError, setServerError] =
    useState("");

  const handleChange = (event) => {
    const { name, value } = event.target;

    setForm((previous) => ({
      ...previous,
      [name]: value,
    }));
  };

  const validate = () => {
    const errors = {};

    if (!form.name.trim()) {
      errors.name = "Name is required";
    }

    if (!form.email.includes("@")) {
      errors.email = "Enter a valid email";
    }

    if (form.password.length < 8) {
      errors.password =
        "Password must be at least 8 characters";
    }

    return errors;
  };

  const handleSubmit = async (event) => {
    event.preventDefault();

    const validationErrors = validate();

    if (Object.keys(validationErrors).length > 0) {
      setErrors(validationErrors);
      return;
    }

    setErrors({});
    setServerError("");
    setIsSubmitting(true);

    try {
      const response = await fetch("/api/register", {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify(form),
      });

      if (!response.ok) {
        throw new Error("Registration failed");
      }

      setForm(initialForm);
    } catch (error) {
      setServerError(
        "Unable to create your account."
      );
    } finally {
      setIsSubmitting(false);
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <div>
        <label htmlFor="name">
          Name
        </label>

        <input
          id="name"
          name="name"
          value={form.name}
          onChange={handleChange}
        />

        {errors.name && (
          <p>{errors.name}</p>
        )}
      </div>

      <div>
        <label htmlFor="email">
          Email
        </label>

        <input
          id="email"
          name="email"
          type="email"
          value={form.email}
          onChange={handleChange}
        />

        {errors.email && (
          <p>{errors.email}</p>
        )}
      </div>

      <div>
        <label htmlFor="password">
          Password
        </label>

        <input
          id="password"
          name="password"
          type="password"
          value={form.password}
          onChange={handleChange}
        />

        {errors.password && (
          <p>{errors.password}</p>
        )}
      </div>

      {serverError && (
        <p>{serverError}</p>
      )}

      <button
        type="submit"
        disabled={isSubmitting}
      >
        {isSubmitting
          ? "Creating..."
          : "Create Account"}
      </button>
    </form>
  );
}

export default RegisterForm;

aria-invalid na nieprawidłowe pola i połącz komunikaty za pomocą aria-describedby.

Wszystko powyżej mieści się w jednym drzewie decyzji, od danych wejściowych przez walidację po błędy pól lub wywołanie API z gałęziami dla sukcesu i błędu:

                    React Form
                        │
                        ▼
                   User Input
                        │
                        ▼
                    Form State
                        │
                        ▼
                    Validation
                   /          \
                Valid        Invalid
                  │              │
                  ▼              ▼
               Submit        Show Errors
                  │
                  ▼
               API Call
              /        \
          Success      Error
             │           │
             ▼           ▼
        Reset/Form    Show Server
        Success UI       Error

Główne wnioski

  • Kontrolowane dane wejściowe sprawiają, że stan staje się źródłem prawdy; jeden obiekt wraz z obsługą typu [name]: value obejmuje większość pól.
  • Liczby przychodzą jako łańcuchy, pola typu checkbox używają atrybutu checked, elementy wyboru są kontrolowane za pomocą atrybutu select, a pola do wprowadzania plików zawsze są niekontrolowane.
  • Walidacja po stronie klienta służy użytkownikowi; tylko walidacja po stronie serwera chroni dane.
  • Stan wysyłki modelu jest określany wyraźnie, a przycisk wysyłania jest dezaktywowany podczas wysyłki żądań.
  • Dodaj React Hook Form i Zod, gdy liczba pól, wspólne reguły lub koszt renderowania sprawiają, że ręczna obsługa stanu jest uciążliwa.

Pozycje pokrewne

  • Formularz daty urodzenia weryfikowany w Next.js z kontrolowanymi polami wejściowymi i funkcjami callback — Stwórz niewielki formularz kliencki w Next.js, który śledzi dane wprowadzane za pomocą useState, odrzuca nieprawidłowe daty urodzenia, wyświetla czytelne komunikaty błędów i przekazuje czyste dane do komponentu nadrzędnego.