Рэакт-формы, створаныя вручную: кераваныя элементы вводу, перакантроле і статусы адправкі
Дазнайцеся, як працуюць формы у React — ад кантроліруемых вводаў і обработчыкаў па типу до верыфікацыі, статусаў адправкі, дынамічных поль, заваносу дадзеных і таго, калі бібліятэка форм становіцца корыстной.
Крыніцы: 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("@") фіксае толькі явныя прабелы; справжнім падтверджэнням таго, што адреса працюе, ёсьць підтверджувальны электронны лист.
Парадакты кліента і сервера выпалююць разныя заведаменні
Парадакты з боку кліента выконваюцца ў браузеры і зазвычай включаюць:
- Обавязковыя полья
- Фармат электроннага пасворта
- Мінімальная дужына пасворта
- Падтверджэнне пасворта
Гэта дае шырокі адказ, кращыя умовы вядароўства і менша колькасць зайвых запытак, але гэта не ёсць меры безпекі. Сервер должен парадактаваць перакананні і адпаведзець на запыткі, якія браузер не можа:
- Чы рэўіструвана ўжо гэтая электронная адреса?
- Чы ўжо ёсць права у пользователя?
- Чы ціна ўжо дзейсная?
- Чы можа гэты пользователь выконваць гэту операцыю?
Безпечныя прыкладніцы викорыстоўваюць аба падходы. Ніколі не пішыце такой логіки:
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
Корыстныя заходы протыкампаніі:
- Зберагчы стан локальна, а не глобальна без прычыны.
- Раздзеліце форму на часткі, каб кожная змяна перарысовывала менш разоў:
Form
├── PersonalDetails
├── Address
├── Education
├── Experience
└── Documents
- Іспользуйце падобную бібліятэку, такую як React Hook Form.
- Утримвайцеся ад сложных вычыслаў чы ўтрыманнях API на кожны симвал.
- Зменшыце частоту выканання тых задач, якія неабходны, напрыклад, пераканальвання наявнасці імені паўтарэння:
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, калі колькасць полей, спяльныя правілы аб выконанне коду робяць ручнае керування станам складным.