Галоўная / Артыкулы

Рэакт-формы, створаныя вручную: кераваныя элементы вводу, перакантроле і статусы адправкі

Дазнайцеся, як працуюць формы у React — ад кантроліруемых вводаў і обработчыкаў па типу до верыфікацыі, статусаў адправкі, дынамічных поль, заваносу дадзеных і таго, калі бібліятэка форм становіцца корыстной.

Выдавец: REACTAPP.TOP13 хв чытання2992 слоў

Крыніцы: React Forms: A Complete Guide from Basics to Production Patterns

Уражанне, выкарыстоўванне, пошук, настройкі прафіля, анкеты: практычна кожны дапрыемлік ствараецца на адной з форм. У React форма означае кераванне значэннямі, перакананне ў правамільнасці дадзеных, паведамленнях пра аберанні, адправцы дадзеных, стане завантажэння і абераннях на серверы, полях, якія дадаюцца пад час выканання, скасаванні настройкаў і прабоце. Шырокія інструкцыі пашагоўна паказваюць гэты процес, ад кераваных вхідных дадзеных да цэлай формы рэгістрацыі без викорыстання бібліятэкаў, і адзначаюць, калі варта выкарыстоўваць React Hook Form і Zod.

Як форма функцыонуе ў дапрыемліку на React

Форма збірае інформацыю ад корыстувальніка, як у гэтым экране рэгістрацыі:

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

React спаявае гэтыя вхідныя элементы з логікай дапрыемліка, і практычна кожная форма прыходзіць за аднаковым жыцёвым циклам:

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

Кераваныя компоненты: стан як аднаковы источнік дадзеных

У кантроліруемым элементе значэнне value вводу падае з стану React, а кожныя змены запісваюцца праз onChange.

import { useState } from "react";

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

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

Установка value={email} означае, што React сам адначыта, што паказваць у поле. Кожны натиск клавішы праходзіць через гэты цикл:

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

Паколькі паказванае значэнне завжды вылучваецца зі стану, стан ёсць едынай адрадным джэрам.

Назва „кантроліруемы“ адраджае тое, што ввод не мае сабе значэння:

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

Даныя пераходзяць зі стану да элемента, через натыканне корыстніка і обрабоўчык, і знову ў стан:

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

Адна аб’ект стану для многах поль

Выклік useState для кожнага поля паслабляе эфектыўнасць:

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

Адміністрування адзэнтна, калі ўсё застаўляецца ў аднам об’екте, ключам да якога ёсць назва полья. У такім формате тры полья знаходзяцца ў аднам об’екте, вони дзеліцься адным методам handleChange, а даны фіксуюцца праз заданне формы.

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>
  );
}

Ключоўай лініяй ёсць вычысленая властасць [name]: value. Атрыбут name кожнага элемента адпавядае ключу стану, таму адны метод обрабоцы выконваецца для таго полья, яке было актываўанае.

Для элементаў тексту метод чытае даны з элемента, які вызвалі захад:

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

event.target — це сам элемент вводу, таму event.target.value — це яго текущы текст:

User types: Salim

event.target.value
       ↓
"Salim"

Правильная обработка кожнага типу вводу

Формы мішчу элементы для вводу тексту, пароля, цифр, прапускных поліцаў, радыё-поліцаў, выбору, текстовых полей, файлоў, дат і дзіапазонаў. Большасць з іх працюе за аднаковым прынцыпам, з калькама важлівых разлікаў.

Тэкст, пароль і цифры

Поле для тексту прыязджае value да свайго ключа:

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

Поле для пароля разлічаецца толькі за type:

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

То ж самае стосуецца і поля для числа:

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

Проблема: event.target.value ўсё равна являе сабою строку, нават калі type="number". У прыкладе нижэй сумешаны код і адказ, але суть у тым, што вы отрымаеце "25", а не 25, і для пераканварэння патрэбны Number().

const age = event.target.value;

may produce:

"25" rather than: 25

If you need a number:

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

Зверніце увагу на порожнія значэння: Number("") вяртае 0, таму багаты формы заставаюць первісную строку і пераканваруюць яе толькі пад верыфікацыёй або надсылкай.

Поле для пазначэння викорыстоўвае checked

Для поля для пазначэння прыязджае checked да булевага значэння і чытаецца event.target.checked:

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

event.target.value у прапорцы вяртае ўласнасць value (за замовчаннем "on"), а не інфармацыю пра тое, чы розмечана яна.

Групы select, textarea і radio

Элемент select керуецца за дапамою ўласнасці value; порожній першы варыянт выступае як месца для заповнення.

<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 спрацоўвае з textarea як з элементам уводу, выкарыстоўваючы проп value заместо тексту ўнутры:

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

Прапорцы радіо ў групе дзеляцца аднаковым значэнням name. Кожны з іх розмечаецца, калі яго value падпадае пад стан, а спяльнік обробкі зберагае выбор у 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>

Перэйманне процеса адправкі

Па звычайным чынам прыграма-браузер сама адправляе форму і можа перейсці на іншую сторону чыстаць екран. Аплікацыі на React адрабатваюць адправку дадзеных у JavaScript, таму функцыя для адправкі пачынаецца з event.preventDefault(), як у прыкладзе рэгістрацыі. Пасля гэтага логіка контролюецца вам:

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

Перакананне перад запытам

Перакананне пераключаецца на тое, чы роўнаважныя даны ўзьмліваны. Першая версія скупляеі бяглы недакладнасці, пазначаныя па полях:

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;
};

Функцыя для адправкі спачатку пераканае даны, зберагчы бяглы недакладнасці і завершвае роботу, як толькі такія з’яўляюцца:

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

  const validationErrors = validate();

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

  // Submit form
};

Бяглы недакладнасці зберагаюцца ў сваёй сабе становасці:

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

У паводзе часу ён можа мячыць:

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

Кожна полька адрасавае свойя паведамлення за умовам:

{errors.email && (

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

)}

Кращая версія функцыі validate прыймае форму як аргумент, чынячы ёю «чыстай» і праблематычную для тэставання, і пераканае формат чысціна дадзеных калі полька ўжо є:

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;
}

Адміністратар выклікае яго так сама:

const errors = validate(form);

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

Тэст includes("@") фіксае толькі явныя прабелы; справжнім падтверджэнням таго, што адреса працюе, ёсьць підтверджувальны электронны лист.

Парадакты кліента і сервера выпалююць разныя заведаменні

Парадакты з боку кліента выконваюцца ў браузеры і зазвычай включаюць:

  1. Обавязковыя полья
  2. Фармат электроннага пасворта
  3. Мінімальная дужына пасворта
  4. Падтверджэнне пасворта

Гэта дае шырокі адказ, кращыя умовы вядароўства і менша колькасць зайвых запытак, але гэта не ёсць меры безпекі. Сервер должен парадактаваць перакананні і адпаведзець на запыткі, якія браузер не можа:

  1. Чы рэўіструвана ўжо гэтая электронная адреса?
  2. Чы ўжо ёсць права у пользователя?
  3. Чы ціна ўжо дзейсная?
  4. Чы можа гэты пользователь выконваць гэту операцыю?

Безпечныя прыкладніцы викорыстоўваюць аба падходы. Ніколі не пішыце такой логіки:

if (clientValidationPassed) {
  // Data is safe
}

Перакрыць пераглядчыковыя перакантрольванні можна пацверджэнням сторунки або безпасячым вызовам API, таму бэкенд незалежна перакантрольвае кожны запит:

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

Моделюванне стаўак адправкі

Форма для прымення пераходзіць праз разныя стаўкі, якія павінны быць адбіваемыя UI:

Idle
 ↓
Submitting
 ↓
Success

or

Idle
 ↓
Submitting
 ↓
Error

Два элемента стану адстежуюць іх:

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

Асінхронны працоўнік падымае флаг, чыстыць старыя памылкі сервера, чакае на запит, зафіксавае дружэльскую паведамленне пры невясці і перазначае флаг у блоку finally незалежна ад рэзультатаў.

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

  setIsSubmitting(true);
  setServerError("");

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

Аблікаванне і перэназначэнне кнопкі пад час запита запобегае дублікацыям адправкі:

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

Жыцёвы цикл кнопкі падобны да жыцёвага цикла запита:

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

Контролюемыя проты неконтролюемых вводаў

За дапамою кантроліруемага вводу, стан React зберагае значэнне. Нижэйшы блакет паказвае скрацаную версію, цэлы компонент і трыкт дадзейнаў, раздзеленыя лініямі з звёздак.

<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

За дапамою некантроліруемага вводу, DOM зберагае значэнне, а React чытае яго через ref, зазвычай пад выкананнем функціі submit, не зберагаючы кожны натиск клавішы:

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

Выбірайце кантроліруемыя вводы для негараздзейчанага верыфікацыі, умовнай UI, выведзеных значэнняў, форматавання, стану, керованага React, або спецыяльных взаімадзейнаў. Паколькі React знае значэнне, UI можа мгновенна рэагаваць:

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

Выберыце неконтролюемыя элементы вводу для простых форм, калі стан пасля кожнага натыку не дапамагае, калі вы хочаце менш частых апдэйтаў, або калі бібліятэка сама карыстаецца рэфамі. Аперацыі форм у React 19 таксама будуюцца на гэтым модэле; адзін раз паглядзіце асынхронныя формы з useActionState і useOptimistic.

Калі бібліятэка форм заслуговае на свое месца

React Hook Form

Ручна наладка дзесяткаў поль ёсць клопачліва. Функцыя register у React Hook Form паў’язвае элемент вводу і задае яго правілы, handleSubmit пераканальвае данні пры выклікі вашага калебу, а formState.errors паказвае прычыны будзь-якіх адмов.

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>
  );
}

Ён спрощае рэгістрацыю, верыфікацыю, адзінакоўкі, адправку даных, скасаванне настройкаў, дынамічныя поль і стан формы. Пакалі ён рэгіструе элементы вводу, якія за замовчаннем не контролююцца, ён ухілваецца ад перыядычнага перакаштоввання цэлай формы з кожным нажатым клавішам, як гэта робяць простыя формы з контролем.

Верыфікацыя схэмы з Zod

Калі колькасць правілаў зростае, розбросаныя оператары if стаюць менш чыткімі. Zod описвае патрэбную структуру даных у адной заяве:

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"),
});

У стандартных рэштрыктурах разработкі бібліятэка выкарыстоўваецца для керування станам, а схэма — для правілаў:

React Hook Form
       +
      Zod
       ↓
Form state + validation

Схэма можа быць такой кампактной:

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

React Hook Form падключае схэму через распалювач, што падходзіць для большых форм, дзе правілы должны застаўцца адносова стабільнымі.

Дынамічныя, умовныя і залежныя поля

Павтараючыся групы

Корыстувачы часта дадаюць новыя елементы за патрэбай, напрыклад, інформацію пра выхаванне:

Education

Degree  [________]
College [________]

[ + Add Education ]

Храніце іх у якосты арэйку об’ектаў:

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

Пры дадаванні новай строкі додаецца пусты об’ект:

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

Атрыбуты адрасуюцца через арэйку:

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

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

Індекс key ўтримваецца толькі тады, калі строкі ніколі не выкарыстоўваюцца або не перакантролююцца; іншым чынам трэба выкарыстоўваць стабільныя ідэнтыфікаторы, бо текставыя даны могу быць прыўязаны да неправильнай строкі.

Умовныя поля

Дзеякія поля залежаць ад паканальнага выбору, напрыклад, дакументы компаніі для аднаго з рахункоў:

Account Type

○ Individual
○ Business

If the user chooses:

Business

show:
Company Name
GST Number
Company Address

Умовны выраз керуе гэтым:

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

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

Якщо корыстнік вернёцца назад, неабходна ачысціць або выключыць схованыя значэння, каб не адправляліся застарелыя даны.

Каскадныя выборы

Страна, штат і горад утвараюць ланцуг:

Country
   ↓
State
   ↓
City

Змена ў верхній часты ланцуга скасоўвае всё, што знаходзіцца нижэй, перш чым запрашваюцца новыя варыянты:

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

Паколькі варыянты надаюцца з сервера, TanStack Query чырпаецца з бібліятэкай форм для кешавання та адрабаткі станоў.

Залічванне файлаў

Элементы уводу файлаў нельга кантролюваць; прачытайце файл з event.target.files:

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

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

Ёнкабельна, адправіце тэла з FormData:

const formData = new FormData();

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

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

Не задавайце Content-Type: multipart/form-data самі за дапамой fetch та FormData; браузер генеруе гэты заголовак з неабходным межам.

Зберагчы вялікія формы швайнае

Вялікія, складныя формы ствараюць напругу для простай кантролюемай формы, якая перарысовываецца пасля кожнага натискання клавішы:

User types
   ↓
setState
   ↓
Render
   ↓
Large form tree

Корыстныя заходы протыкампаніі:

  1. Зберагчы стан локальна, а не глобальна без прычыны.
  2. Раздзеліце форму на часткі, каб кожная змяна перарысовывала менш разоў:
Form
├── PersonalDetails
├── Address
├── Education
├── Experience
└── Documents
  1. Іспользуйце падобную бібліятэку, такую як React Hook Form.
  2. Утримвайцеся ад сложных вычыслаў чы ўтрыманнях API на кожны симвал.
  3. Зменшыце частоту выканання тых задач, якія неабходны, напрыклад, пераканальвання наявнасці імені паўтарэння:
Username input
     ↓
Wait 300ms
     ↓
Check availability

Полны форма без бібліятэкі

У пачатковым прыкладзе выкарыстоўваецца сталяя initialForm для адканальвання формы, окремы статус для памылак у полах, памылак падачы даных і на сервере, функцыя validate, а таксама асінхронная падача даных у формате JSON; адпаведны адказ, які не ў спадзечнасці, супакоўваецца як памылка, форма адканальваная на успех і завжды ацэнюецца статус загрузкі. Атрыбуты htmlFor і 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 для некоректных полаў і з’яёднаць паведамленні пра памылкі за дапамогою атрыбута aria-describedby.

Весь процес у кароткаму пераказе

Усе вышэйзгаданае паслужыць аднам дрэву рашэння: ад вхідных дадзеных, працы з іх паверканнем, пераходу да памылак у полях, да вызова API з галузямі успеху і памылак:

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

Галоўныя выводы

  • Контролюемыя вхідныя даны робяць стан аднаго з источнаў правды; адна аб’екта плюс працоўнік [name]: value пакрывае большасць полей.
  • Чысла прыходзяць у вигляде страк, палічкі з прапускамі выкарыстоўваюць атрыбут checked, выбіранне значэння керуецца элементам select, а вхідныя поля для файлоў завжды застаюцца без керування.
  • Паверканне на стороне кліента служыць корыстніку; толькі паверканне на сервере заходзіць дадзеныя.
  • Стан адправкі модэлю паказваецца ясна, і адправка блокуецца пад час запытоў.
  • Даўайце React Hook Form і Zod, калі колькасць полей, спяльныя правілы аб выконанне коду робяць ручнае керування станам складным.